워밍업 클럽 2기 BE 클린코드&테스트 코드 DAY 4 미션

image

1. 아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.

public boolean validateOrder(Order order) {
    if (order.getItems().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    } else {
        if (order.getTotalPrice() > 0) {
            if (!order.hasCustomerInfo()) {
                log.info("사용자 정보가 없습니다.");
                return false;
            } else {
                return true;
            }
        } else if (!(order.getTotalPrice() > 0)) {
            log.info("올바르지 않은 총 가격입니다.");
            return false;
        }
    }
    return true;
}

섹션 3에서 강조하는 것은 뇌 메모리 적게 쓰기. 즉, 코드를 읽는 사람으로 하여금 생각을 많이하지 않도록 하는 것이다. 그 방법에는 여러가지가 있는데, 순서대로 수정하면서 알아보자.

if (order.getItems().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    }

우선 첫 줄부터 마음에 들지 않는다. 코드의 의도를 보니 주문(Order ) 내의 상품(Item) 컬렉션의 요소 개수가 0개인 지 확인하는 것으로 추측된다. 해당 코드에 Order , Item 객체에 대한 내용은 없으니 해당 클래스도 수정한다는 가정하에 진행하겠다. Items를 꺼내지 말고, Order 클래스에 Item 컬렉션이 비었는 지확인하는 메시지hasNoItems() 을 만들어 다음과 같이 수정해보자.

if (order.hasNoItems()) {
   log.info("주문 항목이 없습니다.");
   return false;
}

좀 더 직관적이고 객체지향적으로 수정되었다. 하지만 아직 마음에 들지 않는 것이 있는데, 바로 다음에 나올 else이다. 주문 항목이 없다면 return false 로 해당 메서드를 빠져나가기 때문에 다음 else가 있을 필요가 없다. 메서드에서 ifelse 가 공존하려면 if-else 뒤에 해당 if문에서 분기된 코드가 if-else 뒤에서 사용되는 경우밖에 없다. Early Return을 해보자. 사실 이미 Early Return이긴 하다.. else만 없애보자.

public boolean validateOrder(Order order) {
    if (order.hasNoItems()) {
        log.info("주문 항목이 없습니다.");
        return false;
    }
    // 주문 항목이 없으면 Early Return!
    if (order.getTotalPrice() > 0) { // (1)
       if (!order.hasCustomerInfo()) {
           log.info("사용자 정보가 없습니다.");
           return false;
       } else {
           return true;
       }
    } else if (!(order.getTotalPrice() > 0)) { // (2)
       log.info("올바르지 않은 총 가격입니다.");
       return false;
    }
    
    return true;
}

else 문이 한 레벨 사라졌기 때문에 전체적인 depth가 1 줄었다. 하지만 depth가 최대 2이므로 직관적인 사고를 하기에는 피곤하다. 남은 코드들도 depth를 줄여보자. 코드(사고)의 depth를 줄이는 데에는 Early Return이 최고다. 위에서도 봤지만, Early Return을 하면 굉장히 직관적으로 코드를 읽을 수 있게 된다. (1), (2) 분기문이 있는데, (1)은 아직 2 depth 이기 때문에 (2) 부터 Early Return 하는 게 쉽겠다.

public boolean validateOrder(Order order) {
    if (order.hasNoItems()) {
        log.info("주문 항목이 없습니다.");
        return false;
    }
    // 주문 항목이 없으면 Early Return!
    if (!(order.getTotalPrice() > 0)) { // (2)
       log.info("올바르지 않은 총 가격입니다.");
       return false;
    }
    if (order.getTotalPrice() > 0) { // (1)
       if (!order.hasCustomerInfo()) {
           log.info("사용자 정보가 없습니다.");
           return false;
       } else {
           return true;
       }
    }
    
    return true;
}

(2)를 Early Return을 하고 보니 분기 조건이 굉장히 거슬린다. 부등호를 굳이 Not 연산을 사용해서 한 것부터 약간 킹받는다. getter로 객체를 조회하여 비교하지말고, 메시지를 보내보자.

    if (order.isTotalPriceNegative()) { // (2)
       log.info("올바르지 않은 총 가격입니다.");
       return false;
    }

주문이 음수인 지 확인하는 메서드(메시지)를 만들어 Order 객체로부터 정보를 얻자. 이제 (1)을 손볼 차례다. 우리가 (2)를 Early Return 하면서 (1)의 가장 최상위 분기는 의미 없어졌다. (2)를 통해 Early Return 되었다면 Order의 Total Price는 0 이상이라는 것이기 때문이다.

       if (!order.hasCustomerInfo()) {
           log.info("사용자 정보가 없습니다.");
           return false;
       } else {
           return true;
       }

이제 (1)도 depth가 1이 되었다. 하지만 여기서도 Early Return으로 else 를 없앨 수 있다. depth를 최소화하면서 Early Return을 적용한 코드를 확인해보자.

public boolean validateOrder(Order order) {
    if (order.hasNoItems()) {
        log.info("주문 항목이 없습니다.");
        return false;
    }
    // 주문 항목이 없으면 Early Return!

    if (order.isTotalPriceNegative()) {
       log.info("올바르지 않은 총 가격입니다.");
       return false;
    }
    // 총 가격이 0 이하이면 Early Return!

    if (!order.hasCustomerInfo()) {
        log.info("사용자 정보가 없습니다.");
        return false;
    }
    // 사용자 정보가 없으면 Early Return!

    return true;
}

정리하면서, 각 if문 사이에 공백을 한 줄씩 추가했다. 이렇게 논리 사이에 공백을 주는 것은 생각의 컨텍스트를 분리시킬 수 있는 좋은 방법이다. 예를들어 주문 항목이 없는 것과 가격이 0 이하인 것은 전혀 다른 문제이기 때문에 굳이 줄을 붙여서 사고를 연결할 필요가 없다.

거의 다 마무리 됐다 싶었는데 부정 연산 `!` 하나가 거슬린다. 부정 연산은 읽는 사람으로 하여금 한 번 더 생각하게 만든다.

    if (!order.hasCustomerInfo()) {
        log.info("사용자 정보가 없습니다.");
        return false;
    }

이 코드의 분기 조건을 읽자하면, 주문에 사용자 정보가 있는 지 확인하고 그것을 뒤집어서 사용자가 없는 지를 확인하는 게 되는데.. 이미 말로만 해도 복잡하고 귀찮다. ! 는 될 수 있으면 최대한 사용하지 않고 메서드명으로 추상화하자.

    if (order.hasNoCustomerInfo()) {
        log.info("사용자 정보가 없습니다.");
        return false;
    }

모든 작업이 완료되었다. 1번 미션의 최종 코드를 확인해보자.

public boolean validateOrder(Order order) {
    if (order.hasNoItems()) {
        log.info("주문 항목이 없습니다.");
        return false;
    }

    if (order.isTotalPriceNegative()) {
       log.info("올바르지 않은 총 가격입니다.");
       return false;
    }

    if (order.hasNoCustomerInfo()) {
        log.info("사용자 정보가 없습니다.");
        return false;
    }

    return true;
}

2. SOLID에 대하여 자기만의 언어로 정리해 봅시다.

Robert.C.Martin의 SOLID는 어쩌면 캡추상다 보다 중요한 객체지향 이론이 되었다. 하나씩 살펴보자.

  • SRP (Single Responsibility Principle)

단일 책임 원칙. 하나의 객체는 하나의 책임만을 가져야한다는 뜻이다. SRP에서 가장 중요한 것은 객체를 변경해야할 사유가 단 하나라는 것이다. 개인적으로 SOLID 원칙 중에 가장 직관적이고 쉬운 원칙이라고 생각한다. 하지만 실제로 개발하다보면 가장 지키기 어려운 것이 또 SRP이다. 객체지향프로그래밍은 여러 객체가 각각의 책임을 가지고 협력하는 시스템이다. 책임을 각각 가져야한다. 내 책임이 아니면 다른 객체에게 이관하자.

  • OCP (Open-Closed Principle)

개방 폐쇄 원칙. 확장에는 열려있고 수정에는 닫혀있다는 원칙이다. OCP는 추상화를 통해 코드의 변경이 아닌 확장이 중요하다. 뒤에 나올 DIP와 OCP가 꽤 맞물려 있다고 생각한다.

  • LSP (Liskov Substitution Principle)

리스코프 치환 원칙. 상속 관계에서 자식클래스는 부모 클래스의 역할을 할 수 있어야 한다. 예를 들어, 부모 클래스의 run()은 스레드를 실행시키는 메서드인데, 자식 클래스의 run()은 육상 경기를 시작하는 메서드이면 안 된다. 그렇게 되면 해당 클래스를 참조하는 클라이언트 코드 입장에서 더 이상 그 클래스의 모든 족보를 신뢰하지 못하고 난처해진다.

  • ISP (Interface Segregation Principle)

인터페이스 분리 원칙. 어떤 인터페이스의 구현체는 사용하지 않는 메서드까지 구현할 필요가 없다. 그 말은 즉, 인터페이스가 쓸 데 없이 너무 광범위한 메서드까지 추상메서드로 가지고 있다는 것이다. 추상화는 구현으로 부터 공통 개념을 뽑아내는 것이다. 그렇다면 구현이 우선일까 추상이 우선일까? 메서드를 누구에게 맞춰야할까?

  • DIP (Dependency Inversion Principle)

의존성 역전 원칙. 구현(저급 모듈)이 아닌 추상(고급 모듈)에 의존하라는 것이다. OCP와 맞물려 Spring 프레임워크에서 매우 중요한 원칙이라고 생각한다. A 객체가 B 객체를 의존할 때, B 객체의 저수준인 구현체를 의존하게 되면 구현체를 바꿀 때 마다 A 객체의 코드를 수정해야 한다. 이 행위는 OCP를 지키지 못하는 모습도 된다. DIP를 위해 B 객체의 추상화된 고수준 모듈(인터페이스, 추상클래스, 부모 클래스)을 의존하고, 외부에서 A 객체를 생성할 때 B 객체의 실제 구현체에 대한 의존성을 주입(DI)하게 되면 A 객체를 B 객체 때문에 변경할 일은 없을 것이다.

댓글을 작성해보세요.

채널톡 아이콘