[워밍업 클럽 2기 BE 클린코드 & 테스트코드] BE 2기
첫 번째 미션 : 아래 코드를 읽기 좋은 코드로 변경하기
아래의 코드는 물론 기능 구현상에서 오작동을 일으키는 코드는 아니겠지만 이번 섹션에서 배운 내용을 토대로 읽는 사람으로
하여금 과부하까진 아니더라도 부하를 겪을 수 있습니다. 아래의 코드는 그럼 어떤 문제를 가지고 있을까요?
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;
}
첫 번째는 캡슐화가 잘 지켜지지 않고 있습니다. 객체의 값을 지속적으로 getter 메서드를 가져오면서 확인하고 있습니다.
이는 TDA(Tell, Don't Ask) 원칙을 지켜지지 않고 있고 객체가 그 자체의 역할을 하게끔 만들지 않았습니다.
두 번째는 읽는 사람으로 하여금 머리를 많이 쓰게 하고 있습니다. 머리를 많이 쓰게 한다는 것은 다음과 같은 이유에서 입니다.
모든 조건문이 한 호흡에 이어져 있다.
공백으로 각 작업간의 STEP을 구분하지 않았다.
Depth가 깊다.
부정 연산자 사용으로 인해서 조건을 한 번 더 이해해야 한다.
이를 리팩토링 한다면 어떻게 할 수 있을까요?
public boolean validateOrder(Order order) {
// 주문 항목 확인
if (order.hasNoItems()) {
log.info("주문 항목이 없습니다.");
return false;
}
// 총 가격 확인
if (order.isTotalPriceInvalid()) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
// 사용자 정보 확인
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
리팩토링한 코드는 위와 같습니다.
Order 객체 스스로 판단할 수 있는 메서드로 조건문을 처리하여 TDA 원칙을 따랐습니다.
공백을 둚으로서 각 작업의 STEP을 구분했습니다.
3-Depth 까지 타고 들어가야하는 코드를 전부 1-Depth 이내에 처리했습니다.
부정 연산자를 사용하기보단 더 의미있는 이름의 메소드를 만들어 직관적으로 조건문을 판단할 수 있도록 했습니다.
과제를 수행하면서 느꼈던 것은 좋은 코드란 좋은 글을 작성하는 것과 마찬가지라는 느낌이었습니다. 좋은 글을 쓰는 첫 출발점은 단문 쓰기를 통해 호흡을 짧게하여 읽는 사람으로 하여금 부하를 느끼지 않게 하는 것이 중요했습니다. 문장의 마침표가 문장의 시작으로부터 너무 멀리 있게되면 이는 읽는 사람을 배려하지 않은 글쓰기이기 때문입니다. 클린 코드는 나만의 만족을 위한 것이 아니라 이타적인 마음을 다시 한번 갖게 되는 좋은 원칙인 것 같습니다.
두 번째 미션 : SOLID를 자신의 언어로 정리하기
SRP : Single Responsibility Principle
단일 책임 원칙은 클래스 혹은 메서드, 함수는 한 가지 책임만 가져야 한다는 것입니다. 자동차를 예로 들어볼까요? 자동차 클래스를 작성한다면, 우리는 이 원칙을 지키기 위해 엔진, 연료, 네비게이션, 에어컨 조절, 음악 재생과 관련된 변수와 메서드를 전부 넣어놓으면 안됩니다. 자동차는 그저 여러 가지 요소의 조립이 끝난 채로 존재해야합니다. 그래서 우리는 엔진, 연료 시스템, 네비게이션, 오디오 클래스를 별도로 분리하고 그에 맞는 기능들을 작성해야 합니다.
OCP - Open/Closed Principle
OCP는 소프트웨어는 구성에 있어서 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙입니다. 솔직히 많은 사람들이 이 이야기를 처음에 들었을 때 의구심을 품습니다. '어떻게 확장을 하는데 수정을 안할 수가 있어?' 라고요. 하지만 적어도 소프트웨어 세상에서는 가능합니다. 앞서 말했던 SRP를 지킨다면 말이죠. 만약 자동차 클래스에 오디오 관련 기능들을 전부 다 작성했다면 어떻게 되었을까요? 우리의 자동차에는 보스의 것도, 뱅앤올룹슨의 것도, 하만 카돈의 것도 들어갈 수 있습니다. 하지만 자동차 클래스에 오디오 관련 코드를 전부 작성했다면 자동차 클래스는 늘어나는 오디오 업체의 코드를 계속해서 생산해내야 합니다. 그러나 이전에 별도의 객체로 만들고 추상화까지 더했다면? 확장에 있어서 기존 코드를 수정하는 일은 없을 것이고 우리는 단순히 '추가'만 하면 될 것입니다.
LSP - Liskov Substitution Principle
리스코프 치환 원칙은 자식이 언제든지 부모를 대체할 수 있어야 한다는 원칙입니다. 다시 차량의 오디오 기능을 생각해봅시다. 오디오 인터페이스가 있고 오디오 인터페이스에 명세되어 있는 기능은 볼륨 켜키, 볼륨 끄기 입니다. 하지만 어느 한 회사의 특정 제품 클래스는 볼륨 켜키 기능이나 볼륨 끄기 기능이 기대한대로 동작하지 않는다면 어떻게 될까요? 예를 들어서 볼륨 끄기를 1씩 끄는게 아니라 한번에 0으로 만들어버린다거나 말이죠. 이렇게 되면 프로그램이 균일하게 동작하지 않을 것이고 서로가 서로를 완벽하게 대체할 수 없을 것입니다. 따라서 인터페이스를 준수하는 것이 LSP가 전달하고자 하는 메세지입니다.
ISP - Interface Segregation Principle
ISP는 클라이언트가 사용하지 않는 인터페이스에 의존하지 않도록 하는 것입니다. 모든 자동차들은 구매할 때 고객이 원하는 옵션을 가지고 있습니다. 하지만 내 자동차에 내가 고르지 않은 옵션을 동작하는 버튼이 자리만 차지하고 있다면 어떻게 될까요? 이는 운전자에게 혼동을 줄 수 있고 불필요한 공간 낭비를 발생시킬 수 있습니다. 코드에서는 어떤가요? 특정 클래스가 구현하지 않아도 되는 부분을 구현해야할 것처럼 개발자에게 혼동을 줄 수 있고 이는 장애를 유발하거나 불필요한 용량 차지로 이어질 수 있습니다. 인터페이스는 가능한 범용적이 아닌 세부적으로 분리해야한다는 것이 ISP가 전하고자 하는 메세지입니다.
DIP - Dependency Inversion Principle
DIP는 고수준 모듈은 저수준 모듈에 의존해서는 안되고, 고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 한다는 원칙입니다.
이 역시 말이 어렵습니다. 저도 처음에 OCP 다음으로 어려웠던 것 같습니다. 하지만 위에서 자동차 예시로 다 말씀을 드렸습니다.
자동차 클래스는 여러 인터페이스의 조합으로 이뤄져야 한다고 했습니다. 자동차 클래스는 가솔링 엔진, 전기 엔진과 같은 구체 클래스에 의존해서는 안됩니다. Engine 인터페이스가 있고 이를 구현한 가솔린 엔진과 전기 엔진이 있어야 합니다. 오디오 역시 오디오라는 인터페이스만 있을 뿐, 이를 구현한 각 제조사의 오디오가 있을 뿐이죠. 자동차 클래스는 따라서 어떤 특정 엔진이나 특정 오디오의 정보는 알 수 없습니다. 그저 인터페이스만 갖고 있을 뿐입니다.
댓글을 작성해보세요.