Beyond SOLID: Go의 KISS, DRY 및 LOD 원칙들
Daniel Hayes
Full-Stack Engineer · Leapcell

SOLID 원칙 외에도 실제로 유용하고 널리 인정받는 설계 원칙이 있습니다. 이 글에서는 이러한 설계 원칙을 소개하며, 주로 다음 세 가지를 포함합니다.
- KISS 원칙;
- DRY 원칙;
- LOD 원칙.
KISS 원칙
KISS 원칙(Keep It Simple, Stupid)은 소프트웨어 개발에서 중요한 원칙입니다. 소프트웨어 시스템의 설계 및 구현에서 과도한 복잡성과 불필요한 설계를 피하고, 단순하고 직관적으로 유지하는 것을 강조합니다.
KISS 원칙에 대한 설명은 다음과 같이 여러 버전이 있습니다.
- Keep It Simple and Stupid;
- Keep It Short and Simple;
- Keep It Simple and Straightforward.
하지만 자세히 살펴보면 그 의미는 본질적으로 동일하며, 중국어로는 “가능한 한 간단하게 유지하라”로 번역할 수 있습니다.
KISS 원칙은 코드 가독성과 유지 관리성을 보장하는 중요한 수단입니다. KISS의 “단순성”은 코드 줄 수로 측정되지 않습니다. 코드 줄 수가 적다고 해서 반드시 더 간단한 코드를 의미하는 것은 아닙니다. 논리적 복잡성, 구현 난이도, 코드 가독성 등도 고려해야 합니다. 또한 문제 자체가 복잡한 경우 복잡한 방법으로 해결하는 것이 KISS 원칙을 위반하는 것은 아닙니다. 또한 동일한 코드가 한 비즈니스 시나리오에서는 KISS 원칙을 만족하지만 다른 시나리오에서는 만족하지 못할 수도 있습니다.
KISS 원칙을 만족하는 코드 작성 지침:
- 코드를 구현할 때 동료가 이해하지 못할 수 있는 기술을 사용하지 마십시오.
- 바퀴를 재발명하지 마십시오. 기존 라이브러리를 잘 활용하십시오.
- 과도하게 최적화하지 마십시오.
다음은 KISS 원칙에 따라 설계된 간단한 계산기 프로그램의 예입니다.
package main import "fmt" // Calculator defines a simple calculator structure type Calculator struct{} // Add method adds two numbers func (c Calculator) Add(a, b int) int { return a + b } // Subtract method subtracts two numbers func (c Calculator) Subtract(a, b int) int { return a - b } func main() { calculator := Calculator{} // Calculate 5 + 3 result1 := calculator.Add(5, 3) fmt.Println("5 + 3 =", result1) // Calculate 8 - 2 result2 := calculator.Subtract(8, 2) fmt.Println("8 - 2 =", result2) }
위의 예에서 덧셈과 뺄셈을 수행하는 Add
및 Subtract
메서드를 포함하는 간단한 계산기 구조체 Calculator
를 정의했습니다. 간단한 설계 및 구현을 통해 이 계산기 프로그램은 명확하고 이해하기 쉬우며 KISS 원칙의 요구 사항을 충족합니다.
DRY 원칙
DRY 원칙(Don’t Repeat Yourself)은 소프트웨어 개발에서 중요한 원칙 중 하나입니다. 중복된 코드 및 기능을 피하고 시스템의 중복성을 최소화하는 것을 강조합니다. DRY 원칙의 핵심 아이디어는 시스템의 모든 정보는 하나뿐이고 모호하지 않은 표현을 가져야 한다는 것입니다. 여러 위치에서 동일한 정보나 논리를 반복적으로 정의하는 것을 피합니다.
DRY 원칙은 매우 간단하고 적용하기 쉽다고 생각할 수 있습니다. 코드가 두 개 이상 똑같아 보이면 DRY를 위반하는 것입니다. 하지만 정말 그럴까요? 대답은 아니오입니다. 이는 원칙에 대한 일반적인 오해입니다. 실제로 코드가 중복되어도 반드시 DRY를 위반하는 것은 아니며, 반복되지 않는 것처럼 보이는 코드도 실제로는 DRY를 위반할 수 있습니다.
일반적으로 코드 반복에는 구현 논리 반복, 기능적 의미 반복, 실행 반복의 세 가지 유형이 있습니다. 이러한 중복 중 일부는 DRY를 위반하는 것처럼 보이지만 실제로는 그렇지 않고, 다른 일부는 괜찮아 보이지만 실제로는 이를 위반합니다.
구현 논리 반복
type UserAuthenticator struct{} func (ua *UserAuthenticator) authenticate(username, password string) { if !ua.isValidUsername(username) { // ... code block 1 } if !ua.isValidPassword(username) { // ... code block 1 } // ... other code omitted ... } func (ua *UserAuthenticator) isValidUsername(username string) bool {} func (ua *UserAuthenticator) isValidPassword(password string) bool {}
isValidUserName()
및 isValidPassword()
함수에 중복된 코드가 포함되어 있다고 가정합니다. 언뜻 보면 이는 DRY를 명백히 위반하는 것처럼 보입니다. 중복을 제거하기 위해 코드를 리팩터링하고 이를 보다 일반적인 함수인 isValidUserNameOrPassword()
로 병합할 수 있습니다.
리팩터링 후 줄 수가 줄어들고 반복되는 코드가 없습니다. 이게 더 나을까요? 대답은 아니오입니다. 함수 이름에서도 알 수 있듯이 병합된 isValidUserNameOrPassword()
함수는 사용자 이름 유효성 검사 및 암호 유효성 검사의 두 가지 작업을 처리합니다. 이는 단일 책임 원칙 및 _인터페이스 분리 원칙_을 위반합니다.
실제로 두 함수를 병합하더라도 문제가 남아 있습니다. isValidUserName()
및 isValidPassword()
가 논리적으로 반복되는 것처럼 보이지만 의미적으로는 그렇지 않습니다. 의미적 비반복은 기능적으로 이 두 메서드가 완전히 다른 작업을 수행한다는 것을 의미합니다. 하나는 사용자 이름의 유효성을 검사하고 다른 하나는 암호의 유효성을 검사합니다. 현재 설계에서는 유효성 검사 논리가 동일하지만 이를 병합하면 잠재적인 문제가 발생합니다. 예를 들어 언젠가 암호 유효성 검사 논리를 변경하면 두 함수의 구현이 다시 달라집니다. 그러면 원래 두 함수로 다시 분할해야 합니다.
코드가 중복된 경우 종종 더 작고 세분화된 함수로 추상화하여 문제를 해결할 수 있습니다.
기능적 의미 반복
동일한 프로젝트에서 isValidIp()
및 checkIfIpValid()
의 두 함수를 고려해 보세요. 이름이 다르고 다른 구현을 사용하지만 기능은 동일합니다. 둘 다 IP 주소가 유효한지 확인합니다.
func isValidIp(ipAddress string) bool { // ... validation using regex } func checkIfIpValid(ipAddress string) bool { // ... validation using string operations }
이 예에서는 구현이 다르지만 기능이 반복됩니다(즉, 의미적 반복). 이는 DRY 원칙을 위반합니다. 이러한 경우 구현을 단일 접근 방식으로 통합해야 하며 IP가 유효한지 확인해야 할 때마다 동일한 함수를 일관되게 호출해야 합니다.
실행 반복
type UserService struct { userRepo UserRepo } func (us *UserService) login(email, password string) { existed := us.userRepo.checkIfUserExisted(email, password) if !existed { // ... } user := us.userRepo.getUserByEmail(email) } type UserRepo struct{} func (ur *UserRepo) checkIfUserExisted(email, password string) bool { if !ur.isValidEmail(email) { // ... } } func (ur *UserRepo) getUserByEmail(email string) User { if !ur.isValidEmail(email) { // ... } }
위의 코드에는 논리적 중복과 의미적 중복이 없지만 여전히 DRY를 위반합니다. 이는 코드에 실행 반복이 포함되어 있기 때문입니다.
수정은 비교적 간단합니다. UserRepo
에서 유효성 검사 논리를 제거하고 UserService
에 중앙 집중화하면 됩니다.
코드 재사용성을 개선하는 방법은 무엇입니까?
- 코드 결합을 줄입니다.
- 단일 책임 원칙을 따릅니다.
- 비즈니스 논리와 비즈니스 외 논리를 분리합니다.
- 일반 코드를 공유 모듈로 푸시합니다.
- 상속, 다형성, 추상화 및 캡슐화를 적용합니다.
- 템플릿과 같은 디자인 패턴을 사용합니다.
다음은 코드의 명확성과 재사용성을 보장하기 위해 DRY 원칙을 적용하는 간단한 인사 관리 시스템의 예입니다.
package main import "fmt" // Person struct represents personal information type Person struct { Name string Age int } // PrintPersonInfo prints personal information func PrintPersonInfo(p Person) { fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age) } func main() { // Create two people person1 := Person{Name: "Alice", Age: 30} person2 := Person{Name: "Bob", Age: 25} // Print personal information PrintPersonInfo(person1) PrintPersonInfo(person2) }
위의 예에서 개인 정보를 나타내는 Person
구조체와 개인 정보를 출력하는 PrintPersonInfo
함수를 정의했습니다. 인쇄 논리를 PrintPersonInfo
에 캡슐화함으로써 DRY 원칙을 준수하고 반복적인 인쇄 논리를 피하며 코드 재사용성 및 유지 관리성을 향상시킵니다.
LOD 원칙
LOD 원칙(Law of Demeter)은 _최소 지식 원칙_이라고도 하며, 객체 간의 결합을 줄이고 시스템의 여러 부분 간의 종속성을 최소화하는 것을 목표로 합니다. LOD 원칙은 객체가 다른 객체에 대해 가능한 한 적게 알아야 하며, 낯선 사람과 직접 통신해서는 안 되며, 대신 자신의 멤버를 통해 작동해야 함을 강조합니다.
Demeter의 법칙은 직접적인 종속성이 없는 클래스는 종속성이 없어야 하고, 종속성이 있는 클래스는 필요한 인터페이스에만 의존해야 한다고 강조합니다. 아이디어는 클래스 간의 결합을 줄이고 가능한 한 독립적으로 만드는 것입니다. 각 클래스는 나머지 시스템에 대해 가능한 한 적게 알아야 합니다. 변경이 발생하면 더 적은 수의 클래스가 이러한 변경 사항을 인식하고 이에 적응해야 합니다.
다음은 LOD 원칙을 사용하여 설계된 간단한 사용자 관리 시스템의 예입니다.
package main import "fmt" // UserService: responsible for user management type UserService struct{} // GetUserByID retrieves user information by user ID func (us UserService) GetUserByID(id int) User { userRepo := UserRepository{} return userRepo.FindByID(id) } // UserRepository: responsible for user data maintenance type UserRepository struct{} // FindByID retrieves user information from database by ID func (ur UserRepository) FindByID(id int) User { // Simulate fetching user from database return User{id, "Alice"} } // User struct type User struct { ID int Name string } func main() { userService := UserService{} user := userService.GetUserByID(1) fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name) }
위의 예에서 UserService
(사용자 서비스)와 UserRepository
(사용자 리포지토리)의 두 부분으로 구성된 간단한 사용자 관리 시스템을 설계했습니다. UserService
는 UserRepository
를 호출하여 사용자 정보를 쿼리합니다. 이는 “직접적인 친구”와만 통신이 이루어지도록 보장하여 LOD 원칙을 준수합니다.
Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 비용을 지불합니다. 요청도 없고 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불합니다.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI。
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합。
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅。
손쉬운 확장성 및 고성능
- 쉬운 확장성으로 높은 동시성을 쉽게 처리할 수 있습니다.
- 운영 오버헤드가 없어 구축에만 집중할 수 있습니다.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ