출처 : 박우빈 강사님의 『Readable Code: 읽기 좋은 코드를 작성하는 사고법』
github 링크 : readable-code
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;
}
✔️ 사용자가 생성한 '주문'이 유효한지를 검증하는 메서드.
✔️ Order는 주문 객체이고, 필요하다면 Order에 추가적인 메서드를 만들어도 된다. (Order 내부의 구현을 구체적으로 할 필요는 없다.)
✔️ 필요하다면 메서드를 추출할 수 있다.
리팩토링을 진행하기 이전에, Order 객체를 유추하여 구현해보았다.
setter 사용은 지양하기 위해 따로 추가하지않았다.
package cleancode.mission.order.model;
import java.util.ArrayList;
import java.util.List;
/**
* 주문 항목 정보 Dto
* @since 2025-03-06
* */
public class Order {
private List<String> items;
private int totalPrice;
private String customerInfo;
public Order(List<String> items, int totalPrice, String customerInfo) {
this.items = items == null ? new ArrayList<>() : items;
this.totalPrice = totalPrice;
this.customerInfo = customerInfo == null ? "" : customerInfo;
}
public List<String> getItems() {
return items == null ? new ArrayList<>() : items;
}
public int getTotalPrice() {
return totalPrice;
}
public String getCustomerInfo() {
return customerInfo;
}
public boolean hasCustomerInfo(){
return this.customerInfo != null && !this.customerInfo.isEmpty();
}
}
- 객체의 Item이 존재하지 않는다면, return false
- 1의 로직이 아니면서 총 가격이 0보다 크면서
2-1. 사용자 정보가 없다면, return false
2-2. 2-1의 로직이 아니라면, return true- 1의 로직이 아니면서 총 가격이 0보다 크지 않다면, return false
- 그 외, return true
인지적 사고를 적게하기 위해 빠른 리턴을 사용하여 불필요한 부분을 삭제해보자.
사실 이미 소스상 return은 되어져 있기 때문에,
다음 로직에서 이전 단계의 조건을 생각할 필요가 없다.
- 객체의 Item이 존재하지 않는다면, return false
1의 로직이 아니면서총 가격이 0보다 크지 않다면, return false1의 로직이 아니면서 총 가격이 0보다 크면서
2-1. 사용자 정보가 없다면, return false
2-2.2-1의 로직이 아니라면, return true- 그 외, return true
참고로 3번은 2번의 논리적 부정이기에 else와 동일하다고 보면 된다.
if (order.getTotalPrice() > 0) // 2번
else if (!(order.getTotalPrice() > 0)) // 3번
단순화시키기 위해 2번과 3번 순서를 바꿔서 먼저 유효하지 않은 가격을 먼저 처리해주었다.
소스로 정리해본다면 다음과 같다.
public boolean validateOrder(Order order) {
if (order.getItems().size() == 0) {
log.info("주문 항목이 없습니다.");
return false;
}
if (order.getTotalPrice() <= 0) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
공백으로 의미 단위를 나눠보자.
public boolean validateOrder(Order order) {
if (order.getItems().size() == 0) {
log.info("주문 항목이 없습니다.");
return false;
}
if (order.getTotalPrice() <= 0) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
불필요한 부정 연산자 제거하고 추상화 레벨을 동등하게 맞추자.
public boolean validateOrder(Order order) {
if (hasNoOrderItems(order)) {
log.info("주문 항목이 없습니다.");
return false;
}
if (hasInValidTotalPrice(order)) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (hasNoCustomerInfo(order)) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
private static boolean hasNoOrderItems(Order order) {
return order.getItems().size() == 0;
}
private static boolean hasInValidTotalPrice(Order order) {
return order.getTotalPrice() > 0;
}
private static boolean hasNoCustomerInfo(Order order) {
return !order.hasCustomerInfo();
}
// OrderService.java
public boolean validateOrder(Order order) {
if (order.hasNoOrderItems()) {
log.info("주문 항목이 없습니다.");
return false;
}
if (order.hasInValidTotalPrice()) {
log.info("올바르지 않은 총 가격입니다.");
return true;
}
if (order.hasNoCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
// Order.java
...
public boolean hasNoOrderItems() {
return this.items.size() == 0;
}
public boolean hasInValidTotalPrice() {
return this.totalPrice <= 0;
}
public boolean hasNoCustomerInfo() {
return !this.hasCustomerInfo();
}
...
의도된 예외처리를 통해 예외 메세지를 출력할 수 있도록 변경하자.
사전에 처리되지 않은 예외도 잡아서 개발자가 확인할 수 있도록 한다.
public boolean validateOrder(Order order) {
try{
if (order.hasNoOrderItems()) {
throw new OrderException("주문 항목이 없습니다.");
}
if (order.hasInValidTotalPrice()) {
throw new OrderException("올바르지 않은 총 가격입니다.");
}
if (order.hasNoCustomerInfo()) {
throw new OrderException("사용자 정보가 없습니다.");
}
} catch (OrderException e){
log.info(e.getMessage());
return false;
} catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}
단일 책임의 원칙은 하나의 클래스가 하나의 책임만 지도록 설계해야한다는 원칙이다. 객체 단위의 역할을 명확히 정의함으로써, 클래스 간의 결합도를 낮추고 응집도를 높일 수 있다.
개방 폐쇄 원칙은 시스템이 확장에는 열려있고, 수정에는 닫혀있어야한다는 원칙이다. 이는 새로운 기능 추가 등 기존 시스템을 확장할 때 기존 코드를 최소한으로 수정할 수 있다.
리스코프 치환 원칙은 부모 클래스의 행동을 변경하지 않고, 자식 클래스가 이를 그대로 상속받아야 한다는 원칙을 말한다. 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체할 수 있어야한다.
인터페이스 분리 원칙은 인터페이스에 사용하지 않는 메소드가 포함되어 필요하지 않음에도 구현체에서 이를 구현하는 상황을 방지하기 위해, 인터페이스를 기능별로 분리하여 불필요한 의존성을 줄여야 한다는 원칙이다.
의존성 역전의 원칙은 저수준 모듈과 고수준 모듈이 인터페이스를 의존하도록 변경하는 원칙이다. 구체에 가까운 모듈은 변경 가능성이 높기 때문에 상위의 인터페이스 타입의 객체로 통신하라는 원칙이다.