워밍업 클럽 2기 BE 클린코드&테스트 코드 DAY 4 미션
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
가 있을 필요가 없다. 메서드에서 if
와 else
가 공존하려면 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 객체 때문에 변경할 일은 없을 것이다.
댓글을 작성해보세요.