인프런 워밍업 스터디 클럽 3기 백엔드-code 2주 차 발자국

글은 박우빈님의 readable-code강의 참조하여 작성한 글입니다.

정신없이 진도표를 따라 강의를 수강하고 미션을 진행하였더니 일주일이 순식간에 지나가버렸다.
처음에는 할 만하다고 느꼈었는데,,,강의 내용을 습득하여 스스로의 힘으로 코드에 적용까지 하려니 우빈님이 당부하신 대로 쉽지 않았던 나날들이었던 것 같다,,🥹

특히 이번 스터디카페 리팩토링 미션에 뻥 안치고 10시간 이상은 투자한 것 같은데,,,ㅋㅋㅋㅋ(미션 당일 + 자고 읽어나서 맑은 정신으로 한 번 더 도전)
이후에 리팩토링 강의를 수강하며 내가 전혀 고려하지 못했던 부분을 리팩토링하신 것을 보고 놀랐다. 뿐만 아니라, 리팩토링을 뚝딱 해내시는 모습에서도 감탄했다.
원래 누군가가 뭔가를 쉽게 해내는 것처럼 보이면 진짜 고수라는 말이 딱 맞는 것 같다...!
강의 속에서 리팩토링 하시는 모습은 굉장히 쉬워보였는데...혼자서 해내려니 정말 막막했다 ㅋㅋㅋㅋ
그래도 나도 경험을 더 쌓고 나면, 지금보다 더 짧은 시간 안에 더 객체지향적으로 리팩토링을 해낼 수 있겠지!!
이를 위해 다른 분들이 리팩토링 하신 코드도 많이 읽으면서 여러 번 리팩토링 해봐야겠다.

 

학습 내용 요약

 

주석의 양면성

  • 주석

    • 의사 결정의 히스토리도저히 코드로 표현할 수 없을 때, 주석으로 상세하게 설명하자

      • 주석이 많다 == 비지니스 요구사항을 코드에 잘못 녹였다 는 의미가 성립됨

    • 주석을 작성할 때, 자주 변하는 정보는 최대한 지양해서 작성하자 (그렇지 않으면, 주석도 신경써서 계속 업데이트 해줘야 하는 단점 존재)

변수와 메서드의 나열 순서

  • 상태 변경 > 불리언 등 판별 >= 조회 메서드 순으로 메서드 순서 나열

     

패키지 나누기

  • 패키지: 문맥으로써의 정보를 제공할 수 있음

  • 대규모 패키지 변경은 팀원과의 합의를 이룬 후 하자

    • 본인만 사용하는 부분이면 괜찮지만, 여러 사람과 공통으로 사용하는 클래스들의 패키지를 한번에 변경하면 충돌이 발생할 수 있음!

       

은탄환은 없다

  • 클린코드가 은탄환은 아니다 (클린코드가 무조건적인 정답은 아니다)

    • 요구사항이 변경될 일이 없는 코드는 절차지향적인 코드가 오히려 정답일 수 있음 (e.g., 체스는 500년동안 규칙이 변하지 않았음)

  • 실무는 지속가능한 소프트웨어의 품질 vs 기술부채를 안고 가는 빠른 결과물 사이의 줄다리기

    • 무조건적으로 클린 코드를 추구하기보다는 주어진 기간을 최대한 맞추게끔 결과물을 내놓고, 이후 미래에 잘 고치도록 할 수 있는 코드 센스가 필요 (e.g., 주석으로 리팩토링 할 부분 남겨 놓기 등)

 

미션

 

Day 4 미션 피드백

  • 기존 코드 (return 타입 - boolean)

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;
}

 

public class Order {

    private long id;
    private List<Item> items;
    private Customer customer;

    public Order(final List<Item> items, final Customer customer) {
        validateOrder(items, customer);
        this.items = items;
        this.customer = customer;
    }

    private void validateOrder(final List<Item> items, final Customer customer) {
        if (doesNotHaveItems(items)) {
            throw new RuntimeException("주문 항목이 없습니다.");
        }
        if (doesNotHaveCustomerInfo(customer)) {
            throw new RuntimeException("사용자 정보가 없습니다.");
        }
    }

    public boolean doesNotHaveItems(final List<Item> items) {
        return items.isEmpty();
    }

    public boolean doesNotHaveCustomerInfo(final Customer customer) {
        return customer == null;
    }
}

 

=> boolean을 return하고 있는 기존 메서드에 대한 리팩토링으로 예외를 던지는 것으로 변경하는 것은 좋을 수도, 나쁠 수도 있다.
(자칫하면 오버엔지니어링이 될 수 있음)

📌 예외를 던지는 것은 비싸기 때문에, 신중하게 메서드의 사용현황을 파악 후 상황에 맞게 리팩토링 할 것!!

 

public class Item {

    private static final int MINIMUM_VALUE = 1;

    private long id;
    private String name;
    private int price;

    public Item(final long id, final String name, final int price) {
        validatePositivePrice(price);
        this.id = id;
        this.name = name;
        this.price = price;
    }

    private void validatePositivePrice(final int price) {
            if (price < MINIMUM_VALUE) {
                 throw new RuntimeException("올바르지 않은 가격입니다.");
            }
    }

}

고민이었던 부분: Order 클래스 내에 Item 클래스를 새로 만들어, 이를 리스트 형태로 Order에 의존성 주입하도록 코드 리팩토링을 진행하였다.
이 때 Item 객체를 생성할 때 마다 금액이 양수인지 검증하고 있는데, Order 클래스에서 전체 총 금액이 양수인지 검증을 다시 한번 해주는 게 좋을지 고민이 됐었다.

=>  우빈님 답변: 이미Item 객체를 생성할 때 금액의 유효성을 보장하고 있으니, Order 클래스는 굳이 하지 않아도 될 것 같다.
물론 비지니스 로직이 복잡해지면, 오히려 필요하다고 느끼는 순간이 올 수 있으니, 그 때 추가해도 늦지 않다!

 

Day 7 미션

  • 미션 한 줄 소개: 스터디 카페 이용권 선택 시스템 리팩토링 하기

     

    리팩토링 한 부분

     

  • 코드 중복 제거 및 메서드 추출

  • StudyCafePassMachine의 의존성 config에서 주입: StudyCafePassMachine은 필요한 의존성을 외부에서 주입받기만 하고, 내부에서 어떻게 사용하는지는 외부에 노출하지 않을 수 있다!

  • 일급 컬렉션 활용: 일급 컬렉션으로 분리함으로써, 원래는 private 메서드라 테스트하지 못했던 로직도 테스트 가능해짐!

  • 무분별한 getter 사용이 아닌, 객체에 메세지 보내기

public boolean isSamePassTypeWith(final StudyCafePassType studyCafePassType) {
        return passType == studyCafePassType;
    }

passType를 비교해야 하는 곳에서 passType를 게터를 통해 비교해주는 게 아닌, isSamePassTypeWith 메서드와 같이 객체에 메세지를 전달하자!

  • 스터디 카페 이용권 인터페이스 적용: 이부분은 리팩토링을 잘 하였는지 감이 안온다..시도에 의의를 두자 ㅎ헤ㅔㅎ

public interface StudyCafePassHandler {

    boolean isAppliable(final StudyCafePassType studyCafePassType);

    StudyCafePasses findCandidateStudyCafePasses(final StudyCafePasses studyCafePasses);

}

=========

# StudyCafePassMachine

    private void processUserSelection(final StudyCafePassType studyCafePassType) {
        final StudyCafePasses availablePasses = getAvailablePasses(studyCafePassType);

        outputHandler.showPassListForSelection(availablePasses);
        final StudyCafePass selectedPass = inputHandler.getSelectPass(availablePasses);
        if (studyCafePassType == StudyCafePassType.FIXED) {
            checkLockerPass(selectedPass);
            return;
        }
        outputHandler.showPassOrderSummary(selectedPass, null);
    }

Hourly, Weekly, Fixed라는 3종류의 카페 이용권이 존재하기 때문에, 인터페이스를 정의하여 if문 사용을 자제하고, 상황에 맞는 이용권을 가져왔다.

 

리팩토링 놓친 부분

  • FileHandler: 데이터를 어디로부터 어떻게 가져올 것인가에만 초점이 맞춰져 있음
    -> File관련 로직이 들어나면 FileHandler 가 방대해질 것

    • 개선 방향: provider를 통해 어떤 데이터를 필요로 하는가에 초점을 맞출 것

      • SeatPassProvider

      • LockerPassProvider

         

  • domain영역에 view관련 로직 침투

public enum StudyCafePassType {

    HOURLY("1", "시간 단위 이용권"),
    WEEKLY("2", "주 단위 이용권"),
    FIXED("3", "1인 고정석");

    private final String command;
    private final String description;

    StudyCafePassType(final String command, final String description) {
        this.command = command;
        this.description = description;
    }

    public static StudyCafePassType from(final String userInput) {
        return Arrays.stream(StudyCafePassType.values())
                .filter(studyCafePassType -> studyCafePassType.command.equals(userInput))
                .findAny()
                .orElseThrow(() -> new AppException("잘못된 입력입니다."));
    }

}

 PassType은 중요한 도메인 모델인데, Input과 관련된 의미를 지닌 command가 침투되었다.
passType을 선택하는 command가 변경된다면, 단순히 입력 방식을 바꿨을 뿐인데 도메인 모델을 수정해야 하는 좋지 않은 상황이 발생한다.

  • StudyCafePassOrder 도메인 추출

    • 스터디 카페 좌석 이용권 + 사물함 이용권을 합친 Order 도메인을 새로 추출할 수 있다.

    • 이로 인해 FIxedPassType에만 적용되는 사물함 로직 분기문을 간단하게 처리할 수 있었다!

  • StudyCafePass 내 LOCKER_TYPES 상수 선언

public enum StudyCafePassType {

    HOURLY("시간 단위 이용권"),
    WEEKLY("주 단위 이용권"),
    FIXED("1인 고정석");

    private static final Set<StudyCafePassType> LOCKER_TYPES = Set.of(FIXED);

    private final String description;

    StudyCafePassType(String description) {
        this.description = description;
    }

    public boolean isLockerType() {
        return LOCKER_TYPES.contains(this);
    }

    public boolean isNotLockerType() {
        return !isLockerType();
    }

}

전혀 생각지도 못했지만, 알아두면 참 좋은 객체에 메세지를 보내는 방법에 대해 배웠다.

LOCKER_TYPES를 StudyCafePassType enum내에 적용하여 처리할 수 있다니

도메인 지식이 부족해서 그랬나, 나는 생각지도 못했던 방법이다.

나는 상위 도메인에서 if문을 통해 매번 확인해주었는데, 우빈님이 하신 방법이 더 책임 분리가 잘되어있고 객체에 메세지를 보내는 좋은 방법인 것 같다!!
또한 Locker type이 늘어나도, set에 내용만 추가해주면 된다는 점에서 유지보수도 훨씬 쉬울 것 같다!

 

마무리

강의를 굉장히 빠른 시간 안에 완강했다!!!

그렇지만 강의 내용을 완전히 내 것으로 만들었다기엔 부족하다,,,,

강의 볼 때는,,'이럴 때 조합, 일급 컬렉션, VO 등을 적용 하는구나~!' 를 배우면서, 앞으로 스스로 잘 판단해 낼 줄 알았는데,,,
혼자서 해 보려니까 너무나도 막막했다.

그래도 강의 내에서 주신 미션을 통해 내가 어느 부분이 부족한지 파악할 수 있었던 것 같다.

무엇보다도 너무 재밌었다,,,시간 가는 줄 모르고 했던 것 같다.

이제 강의는 끝이 났지만 지뢰찾기랑 스터디카페 코드에 대해 복습할 것이다. 리팩토링 적용하기 어려웠던 부분을 반복 작성해 보면서 체득시킬 생각이다. 이렇게 반복하다보면 우빈님의 사고법을 체득할 수 있겠지요...?

다음 주부터는 테스트 코드에 대해 배우는데,,평소 테스트 코드에 대해 공부를 많이 하지 않았어서 새로 배우는 양이 어마어마 할 것 같다.

강의 내용을 잘 습득할 수 있도록 메타인지 열심히 해야겠다!

나 자신 아자아자 화이팅이다💪🏻

 

[출처]

댓글을 작성해보세요.


채널톡 아이콘