워밍업 클럽 4기 백엔드 Day 4 미션
아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.
as-is
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;
}
to-be
public boolean isValidOrder(Order order) {
if (order.hasNoItems()) {
log.info("주문 항목이 없습니다.");
return false;
}
if (order.hasInvalidTotalPrice()) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (order.hasNoCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
early return
추상화 레벨 맞추기
부정어구 제거
위 세 가지 개념을 도입해서 리팩토링 해봤습니다.
개인적으로 validate라는 이름보다는 is, has 등이 boolean 타입을 반환하는 메소드 이름으로 좀 더 잘 어울리지 않나 싶어서 메소드 이름도 수정해봤습니다.
위 메소드에서 검증 실패한 케이스가 결과적으로 주문 처리가 불가능한 상황이라면, 정상 흐름으로 처리할 수 없는 문제상황인 만큼 boolean을 반환해 처리 방식을 자율에 맡기는 것보다는 좀 더 명시적으로 처리가 불가능하다는 의미를 담아 예외를 던지는 편이 좀 더 낫지 않나 싶은 생각이 들었습니다.
주문이 아닌 다른 도메인이라고 가정하고, 검증 실패 시 특정한 처리를 거쳐 정상 처리가 가능한 경우를 상정하더라도, 로그만 찍고 false를 넘겨버리면 호출하는 쪽에서는 무슨 문제 때문에 검증에 실패한 건지 구분할 수 없어서 불명확한 감이 있다는 생각이 들었습니다. 예외를 던지는 경우라면 사용자 정의 예외를 통해 각 케이스를 구별 가능해서 좀 더 유연한 오류 처리가 가능할 듯 합니다.
아래는 섹션 3의 내용을 적용해보니 좀 더 욕심이 생겨서 객체지향 패러다임에 대한 내용을 떠올리면서 바꿔본 내용입니다.
아래 코드도 마찬가지로 사용자 정의 예외를 이용해 원인을 구분해 처리할 수 있도록 개선하면 더 좋을 것 같습니다.
public class OrderItems {
private final List<item> items;
public OrderItems(List<Items> items) {
this.items = items;
validateOrderItems();
}
public validateOrderItems() {
if (isNullOrEmpty()) {
throw new IllegalStateException("주문 항목이 없습니다.");
}
if (hasInvalidTotalPrice()) {
throw new IllegalStateException("올바르지 않은 총 가격입니다.");
}
}
public boolean isNullOrEmpty() {
return items == null || items.isEmpty();
}
public boolean hasValidTotalPrice() {
return calculateTotalPrice() > 0;
}
public boolean hasInvalidTotalPrice() {
return !hasValidTotalPrice();
}
public int calculateTotalPrice() {
return items.stream().mapToInt(Item::getPrice).sum();
}
// 할인 적용이나 사은품 추가 등 주문 항목의 변동 가능성이 있는 경우 이를 처리할 수 있는 메소드 제공
}
public class Customer {
// id, name 등 필요한 필드 선언
}
public class Order {
private final OrderItems items;
private final Customer customer
public Order(List<Item> itemList, Customer customer) {
if (customer == null) {
throw new IllegalStateException("사용자 정보가 없습니다.");
}
this.items = new OrderItems(itemList);
this.customer = customer;
}
}
2. SOLID에 대하여 자기만의 언어로 정리해 봅시다.
SOLID는 객체지향적 설계를 위한 다섯가지 기본 원칙이다.
Single Responsibility Principle: 단일 책임 원칙
객체는 프로그램의 목적을 달성하기 위해 다른 객체와 협력하며 주어진 책임을 수행한다. 이때 하나의 객체에는 하나의 책임만을 부여해야 한다. 객체란 하나의 관심사를 위한 데이터와 행위를 모아둔 집합으로, 서로 다른 관심사를 하나의 객체에 두는 것은 객체의 본질적인 목적에 어긋난다. 한 가지 이유로 객체를 수정하는 것이 여러 가지 이유로 객체를 수정하는 것보다 명료하다.Open-Closed Principle: 개방-폐쇄 원칙
개방-폐쇄 원칙은 객체가 확장에는 개방적이되 확장으로 인한 코드의 수정에는 폐쇄적이어야 한다는 원칙이다. 자유롭게 확장을 할 수 있으면서도 확장 때문에 기존에 작성된 코드를 고쳐야 하는 일은 없어야 한다는 의미다.Liskov Substitution Principle: 리스코프 치환 원칙
리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체할 수 있어야 한다는 원칙이다. 부모 클래스의 메소드에 대해 어떠한 기대를 가지고 있는 상태에서 동작의 수행 주체를 자식 클래스로 변경하는 경우, 자식 클래스가 그 기대를 배신해서는 안 된다. 즉, 자식 클래스는 부모 클래스에 정해져 있던 행동의 원칙을 위반해서는 안 된다. 부모 클래스가discount()라는 이름의 메소드로 할인액을 계산해 제공해 주었다면, 자식 클래스에서discount()가 할인이 적용된 가격을 반환하도록 재정의해서는 안 된다는 의미다.Interface Segregation Principle: 인터페이스 분리 원칙
인터페이스 분리 원칙은 객체가 필요 없는 인터페이스에 의존해서는 안 된다는 원칙이다. 인터페이스를 구현할 때, 구현체에 불필요한 메소드가 존재한다면 그 메소드는 다른 인터페이스로 분리해야 한다. 생각해 보면 필요도 없는 메소드를 억지로 정의해 아무 의미도 없는 메소드를 만든다는 건 애초에 말이 안 된다.Dependency Inversion Principle: 의존성 역전 원칙
의존성 역전 원칙은 구현이 아니라 추상에 의존해야 함을 의미한다. 헷갈릴 때는 자료구조 공부할 때 배웠던 ADT를 연상하면 될 것 같다. List ADT와 배열 리스트, 연결 리스트를 연상하자. 의존성 역전 원칙의 가장 큰 장점은 상위 모듈이 하위 모듈의 세부사항을 몰라도 상관 없다는 것이다. 클래스(구현)가 아니라 인터페이스, 추상 클래스(추상)에 의존하자.
댓글을 작성해보세요.