인프런 워밍업 스터디 클럽 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;
}
내가 리팩토링 한 코드 중 일부 (1)
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하고 있는 기존 메서드에 대한 리팩토링으로 예외를 던지는 것으로 변경하는 것은 좋을 수도, 나쁠 수도 있다.
(자칫하면 오버엔지니어링이 될 수 있음)
📌 예외를 던지는 것은 비싸기 때문에, 신중하게 메서드의 사용현황을 파악 후 상황에 맞게 리팩토링 할 것!!
내가 리팩토링 한 코드 중 일부 (2)
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 등을 적용 하는구나~!' 를 배우면서, 앞으로 스스로 잘 판단해 낼 줄 알았는데,,,
혼자서 해 보려니까 너무나도 막막했다.
그래도 강의 내에서 주신 미션을 통해 내가 어느 부분이 부족한지 파악할 수 있었던 것 같다.
무엇보다도 너무 재밌었다,,,시간 가는 줄 모르고 했던 것 같다.
이제 강의는 끝이 났지만 지뢰찾기랑 스터디카페 코드에 대해 복습할 것이다. 리팩토링 적용하기 어려웠던 부분을 반복 작성해 보면서 체득시킬 생각이다. 이렇게 반복하다보면 우빈님의 사고법을 체득할 수 있겠지요...?
다음 주부터는 테스트 코드에 대해 배우는데,,평소 테스트 코드에 대해 공부를 많이 하지 않았어서 새로 배우는 양이 어마어마 할 것 같다.
강의 내용을 잘 습득할 수 있도록 메타인지 열심히 해야겠다!
나 자신 아자아자 화이팅이다💪🏻
[출처]
댓글을 작성해보세요.