대부분의 프로그래머들이 객체지향을 이해하면서 SOLID 원칙은 많이 들어 보았겠지만, DRY같은 원칙은 들어본적은 있지만 제대로 된 이해를 못하는 경우나 YANGI 같은 원칙은 들어본적이 없을 수도 있다. 이번기회에 이 글과 함께 여러가지 다른 설계원칙들은 어떤것들이 있는지 한번 알아보자. 전 보다 코드를 보는 눈이 한층 더 넓어지는 것을 느낄수 있을 것이다.

KISS 원칙

KISS 가능한 단순히 유지하라라는 포괄적인 설계원칙이다. 이에 실제로 KISS약어의 해석은 ‘Keep It Simple and Stupid’, ‘’Keep It Short and Simple’, ‘’Keep It Simple and Straight foraward’와 같이 여러해석이 존재한다.

KISS원칙은 코드의 가독성과 유지보수성을 높여주는 중요한 원칙이지만, 이 원칙은 코드를 단순하게 하라 라고 말할뿐 구체적인 방법을 제시하지는 않는다. 따라 KISS원칙의 원리는 간단하지만 구현하기가 말처럼 쉽지는 않다.

적은 줄 수의 코드가 항상 간단하지많은 않다

  1. 정규표현식
func isValidIpAddressV1(ipAddress: String) -> Bool {
      let regex = "^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"

    return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: ipAddress)
}


2, 기성 클래스

func isValidIpAddressV3(ipAddress: String) -> Bool {
    let ipAddressComponents = ipAddress.split(separator: ".")
    
    if ipAddressComponents.count != 4 {
        return false
    }
    
    if ipAddressComponents < 0 || ipAddressCompnenets > 255 {
        return false
    }
    
    for component in ipAddressComponents {
        let number = Int(component) ?? -1
        if number < 0 || number > 255 {
            return false
        }
        
        let isSpecialIP = component == "0.0.0.0" || component == "255.255.255.255"
        if isSpecialIP {
            return false
        }
    }
    return true
}


어떤 코드가 KISS원칙에 더 부합하는 코드일까? 그렇다 1번 코드가 비록 코드의 줄은 적을지 몰라도 특정 정규표현식을 모르면 이해하기 어려울 수 있기 때문에 가독성과 유지보수성이 좋다고 말하기는 어렵다. 반대로 2번째 코드는 Swift내에 내장되어있는 유틸함수를 써 더 명확하고 이해하기 쉬운 코드를 제공한다. 즉, 2번 코드가 더 KISS원칙을 따른다고 말할 수 있겠다.

이와 비슷한 예로 복잡한 코드가 항상 KISS원칙을 위반하는건 아니다. 에를 들어, 알고리즘 구현부를 작성할때 처럼, 알고리즘 자체가 논리가 복잡하고 가독성이 떨어지는 특성을 가지고 있을수 있지만, 복잡한 알고리즘을 사용하여 복잡한 문제를 해결하는 것은 KISS를 위반한다고 볼 수 없다.


그렇다면 평소에 KISS원칙을 고수하며 코드를 작성하려면 어떤 생각을 가지고 있으면 좋을까?

  1. 복잡한 정규표현식과 같은 복잡성이 심한 기술이나 코드를 사용하여 코드를 구현 하지 않는다
  2. 바퀴를 다시 발명하는 대신 기존것을 잘 활용하여 코드를 작성해볼 것을 먼저 고려해본다.
  3. 과도한 최적화를 조심하자. 최적화를 위해 비트연산을 사용하는 등과 같은 일을 최소화 시킨다.


DRY 원칙

개인적으로는 DRY 원칙은 가장 오해하기 쉬운 원칙이라고 생각한다. ‘don’t repeat yourself’ 라는 뜻으로 흔히 중복 코드를 작성하지 말라는 뜻으로 번역된다. 하지만 많은 사람들이 중복이라는 단어를 동일한 코드가 여러개 존재하는 것으로 착각하고는 한다. 실제로 두개 이상의 동일한 중복코드가 항상 DRY원칙을 위배하는 것은 아니다. 아니 애초에 코드 구현 자체는 DRY원칙의 고려대상이 아니다.

예시를 통해 살펴보자.


  1. 유저이름
    func isValidUsername(username: String) -> Bool {
     if username.count < 4 || username.count > 64 {
         return false
     }
     for ch in username {
         if ch == " " || ch == "!" || ch == "@" || ch == "#" || ch == "$" || ch == "%" || ch == "^" || ch == "&" || ch == "*" || ch == "(" || ch == ")" || ch == "-" || ch == "_" || ch == "+" || ch == "=" || ch == "{" || ch == "}" || ch == "[" || ch == "]" || ch == "|" || ch == ":" || ch == ";" || ch == "'" || ch == "" || ch == "," || ch == "." || ch == "<" || ch == ">" || ch == "?" {
             return false
         }
     }
     return true
    }
    


  2. 비밀번호
    func isValidPassword(password: String) -> Bool {
     if password.count < 4 || password.count > 64 {
         return false
     }
     for ch in password {
         if ch == " " || ch == "!" || ch == "@" || ch == "#" || ch == "$" || ch == "%" || ch == "^" || ch == "&" || ch == "*" || ch == "(" || ch == ")" || ch == "-" || ch == "_" || ch == "+" || ch == "=" || ch == "{" || ch == "}" || ch == "[" || ch == "]" || ch == "|" || ch == ":" || ch == ";" || ch == "'" || ch == "" || ch == "," || ch == "." || ch == "<" || ch == ">" || ch == "?" {
             return false
         }
     }
     return true
    }
    


    이 코드들에서는 중복되보이는 코드가 많이 있으며, 이는 마치 DRY원칙을 위배하는것처럼 보인다. DRY원칙을 준수하기 위해 리팩토링을 해보자

func isValidUsenameOrPassword(usernameOrPassword: String) -> Bool {

    // 위의 코드
    return true
}


이게 과연 올바른 행동일까? 이 리팩토링은 근본적으로 잘못되었다.

isValidUsername과 isValidPassword의 함수의 코드 구현은 중복되지만, 의미적으로 유저 이름과 유저 비밀번호의 유효성을 검사하는것은 완전히 다른 함수이다. 이처럼 리팩토링을 실시하게 되면 둘 중하나의 함수의 요구사항이 바뀌는순간 다시 전면 리팩토링을 실시해야하는 잠재적인 문제점이 생긴다.


이와 같이 앞선 코드의 구현은 동일하지만 의미가 다르기 때문에 DRY원칙에 위배되지 않는다.


코드실행의 중복

class UserService {
    
    private let userRepo: UserRepo
    
    init(userRepo: UserRepo) {
        self.userRepo = userRepo
    }
    
    func login(email: String, password: String) -> User {
        let isExisted = userRepo.checkIfUserExisted(email: email, password: password)
        if !isExisted {
            // Exception
        }
        
        let user = userRepo.getUserByEmail(email: email)
        return user
    }
}

class UserRepo {
    
    func checkIfUserExisted(email: String, password: String) {
        if !EmailValidation.validate(email) {
            // Exception
        }
        
        if !PasswordValidation.validate(password) {
            // Exception
        }
    }
    
    func getUserByEmail(email: String) -> User {
        if (!EmailValidation.validate(email)) {
            // Exception
        }
    }
}


다음 코드에서는 로그인을 하기위해 login함수에서 checkIfUserExisted()를 호출하고 getUserByEmail()을 호출한다. 하지만 여기서 (!EmailValidation.validate(email))가 중복적으로 불리게 된다. 이 코드는 DRY 원칙을 위반하는 걸까?

정답은 ‘YES’이다. 이 코드는 논리적 중복이나 의미적 중복은 없지만 코드에 ‘실행 중복’이 존재하기 때문에 DRY원칙에 위배된다. 똑같은 코드를 의미없이 중복실행하는 곳이 있다면 얼른 제거하고 정리해주자.

cc. 디자인시스템의 아름다움

잘못된 내용이 있을 수도 있습니다. 알려주시면 수정하도록 하겠습니다.