본문 바로가기
Study/클린코드

TDD와 함께 SRP, OCP, DIP를 만족하도록 코드 개선해보기

by Nahwasa 2023. 1. 11.

스터디 메인 페이지

 

- 스터디 진행할 때 9장에 나온 TDD와 10장에 나온 SRP, OCP, DIP를 묶어서 심플하게 예시를 보여주려고 라이브코딩으로 진행한 내용을 정리했습니다.

 

- 라이브코딩으로 스터디에서 공유하려한 것 : 클린코드 9장에 나온 TDD의 규칙에 따라 진행해서 TDD가 어떻게 하는건지 확인해보고, 클린코드 10장에 나온 SRP, OCP, DIP를 글로만 봐선 이해가 안될 것이니 TDD로 구현한 코드를 리팩토링하면서 해당 규칙들을 만족하는 형태로 한번 바꿔보는 과정 보여주기.

 

- 라이브코딩 주제 : 도시가스 요금을 계산하는 간단한 클래스를 만들어 보려 한다. 단순히 단위 요금 x 사용량으로 요금이 계산된다. 다만 취약계층에겐 할인이 들어가야 하고, 차후 또다른 요금 계산 방식이 추가될 예정이다.

 

 

우선 기본적인 요금 계산 (단위 요금 x 사용량) 부터 해보자.

  • TDD의 세 가지 법칙 중 1 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.

 

  •  TDD의 세 가지 법칙 중 2 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.

 

  •  TDD의 세 가지 법칙 중 3 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

 

 

다음으로 취약계층에게 20%를 할인(소수점 내림하고 정수만 사용)하는 요구사항을 추가해보자. 가장 간단하고 객체지향적인 사고가 부족한 대부분의 개발자가 생각할 수 있는 방법은 CityGasCharge 클래스에 상태값을 두고 일반 요금과 취약계층 요금을 if문으로 구분하는 것이다. 

  • 마찬가지로 테스트 코드부터 작성해야 한다.

 

  • 컴파일이 가능하도록 해준다.

 

  • 테스트를 통과하도록 해준다.

 

 

⚈ 이쯤에서 '테스트 주도 개발 (켄트 백 저)' 책에 나온 '일반적인 TDD 주기'를 보자. 클린코드 책 9장에 나온 TDD의 규칙은 아래 주기의 '1'에만 해당한다.

  1. 테스트를 작성한다. 올바른 답을 얻기 위해 필요한 이야기의 모든 요소를 포함시켜라.
  2. 실행 가능하게 만든다. 빨리 초록 막대를 보는 것은 모든 죄를 사해준다. 하지만 아주 잠시 동안만이다.
  3. 올바르게 만든다. 직전에 저질렀던 죄악을 수습하자.

 

 

따라서 이제 직전에 저질렀던 죄악을 수습해야 한다. 딱히 죄악으로 보이는건 없는 것 같지만, SOLID 관점에서 보면 죄악이 보인다.

  • 우선 현재 코드만 봐도 SRP 위배는 쉽게 찾을 수 있다. 일반 요금과 최약 계층 요금 계산 로직 모두에 대한 책임을 가지고 있다.
  • 요구사항에  '차후 또다른 요금 계산 방식이 추가될 예정이다.'가 있다. 위에서 짠 코드의 경우 새로운 요금 계산 방식이 추가될 때 마다 아래처럼 실제 구현부에 if문에 계속 추가되고, ChargeType도 계속해서 추가되어야 한다. 따라서 OCP 또한 위배된다. 실제 구현 클래스 자체를 사용하고 있으므로 DIP 위배는 당연하다.

 

 

그럼 다음으로 생각해볼만한건 어차피 취약계층 요금은 기본요금에서 할인하면 되는 것이고, 이미 기본요금 클래스는 짜여져 있었으니 상속을 이용해 코드를 재사용(서브클래싱)해보는 방법이다.

  • 우선 테스트 코드를 생각한 바에 맞춰 수정해준다. 이제 ChargeType은 필요없고, CityGasCharge를 상속받아 새로운 클래스를 만들 것이므로 아래처럼 작성해주면 될 것이다.

 

  • 컴파일이 가능하도록 수정한 테스트코드에 맞춰 ChargeType은 필요없어졌으니 삭제하고, CityGasCharge를 상속받은 클래스를 짜보자.

 

  • 테스트를 통과하도록 구현을 짜주자.

 

 

얼핏 이제 SRP를 만족하는 것으로 보인다.

  • CityGasCharge의 calculateCharge가 변경될 시 VulnerableCityGasCharge도 수정되어야 한다. 예를들어 취약계층 제외한 모든 타입에게 10% 할인을 해주겠다고 하면? 취약계층쪽도 당연히 변경이 필요하다. 즉 변경의 이유가 여러가지이므로 여전히 SRP 위배이다.
  • 추가로 부모 클래스의 구현을 알고있어야만 (요금이 어떻게 나오는지) 자식 클래스에서 구현이 가능하므로 캡슐화 실패라고 볼 수 있다.

 

 

그렇다면 이번엔 두 클래스의 공통적인 부분을 모아 또 다른 부모 클래스에 공통된 부분을 모으고, 부모 클래스와 자식 모두가 추상화에 의존하도록 해보자.

  • 일반 요금과 취약계층 요금의 공통된 부분은 calculateCharge이므로 추상화된 부모 클래스로 올려준다. 그리고 자식의 추상화에 의존할 수 있게 calculateEachCharge를 abstract로 만들어준다.

 

  • CityGasCharge는 이렇게 변했다.

 

  • VulnerableGasCharge는 이렇게 변했다.

 

 

테스트코드 변경 없이 상당히 많은 리팩토링이 이루어졌다. 이렇게 마구 변경할 수 있던 이유는 테스트코드가 있으므로 언제든 검증이 가능하기 때문이다.

  • 테스트 코드는 그대로 둔 채 많은 리팩토링을 했지만 여전히 테스트 코드가 검증을 해주니 문제없다! (클린코드 9장 - "테스트 케이스가 없다면 모든 변경이 잠정적인 버그다.")

 

 

추가로 눈에 띄는 부분을 조금 더 리팩토링 해주자.

  • 우선 VulnerableCityGasCharge 내에 할인율 20%가 그대로 넣어져 있다. 이 경우 취약계층 할인율이 변경된다면 실제 구현을 손봐야 한다. 또한 차후 코드를 보게될 개발자가 할인율을 알고싶으면 클래스 코드를 까보는 수밖에 없다(캡슐화 실패). 할인율을 실제 사용하는 곳에서 주입하도록 하자 (클린코드 5장 - "...기대와는 달리 잘 알려진 상수가 적절하지 않은 저차원 함수에 묻힌다. 상수를 알아야 마땅한 함수에서 실제로 사용하는 함수로 상수를 넘겨주는 방법이 더 좋다.", 9쇄 기준 105쪽)

 

  • AbstractCityGasCharge 클래스명도 문제다. Abstract 어쩌구라던지, ICityGas(인터페이스의 경우)처럼 작성하는 경우 인터페이스에 자기 자신을 드러내게 되므로 캡슐화 위배다. 또 이름 자체만 봐도 CityGasCharge의 최상의 부모라는 점이 명확하지 않다. CityGasCharge 또한 일반 요금제임이 명확히 드러나지 않는다. 따라서 AbstractCityGasCharge를 CityGasCharge로 변경하고, CityGasCharge는 RegularCityGasCharge로 이름을 변경해주자.

 

 

여전히 테스트는 잘 동작하고 코드는 처음보다 많이 나아진 것 같다.

 

  • SRP : 각각 하나의 변경 이유만을 가진다. 전반적인 도시가스 계산의 로직이 변경된 경우 CityGasCharge를 변경하면 된다. 예를들어 모든 요금에 VAT를 합쳐서 보여달라는 요구사항이 발생한다면 CityGasCharge의 calculateCharge에 1.1을 곱하기만 하면 된다. 일반 요금 계산 방식이 변경될 경우 RegularCityGasCharge의 계산 로직을 변경하면 된다. 취약계층 요금 게산 방식이 변경될 경우 VulnerableCityGasCharge의 계산 로직을 변경하면 된다.

 

  • OCP : 새로운 요금 계산 방식은 기존 코드 변경 없이 확장 가능하다. 예를들어 고정 금액으로 할인을 해줄 수 있는 요구사항이 추가된다면 아래처럼 클래스만 추가해주면 된다.

 

  • DIP : 부모와 자식 모두 추상화에 의존하고 있으므로 DIP를 만족한다.

 

 

물론 한계는 있다.

  • 상속을 이용한 위 설계는 요금 책정 방식의 종류가가 늘어날 경우 모든 조합의 경우의 수에 해당하는 클래스 생성이 필요하다. 현재는 상속의 depth가 1개라 별 문제가 없지만, depth가 늘어날수록 새로운 기능 추가를 위해 한번에 여러개의 depth에 해당하는 클래스를 추가해줘야 한다. 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발(class explosion) 문제 또는 조합의 폭발(combinational explosion) 문제라고 한다.
  • 따라서 사실 조합이 엄청 많이 늘어날 가능성이 있다면 합성(composition)으로 설계를 변경하는 것이 더 좋을 수 있다. 하지만 현재까지의 요구사항만 본다면 위 정도면 충분한 설계일 것 같다 (트레이드오프 - 일반적으로 설계가 유연해질수록 코드의 복잡성은 올라간다. / YAGNI - You Ain't Gonna Need It)

'Study > 클린코드' 카테고리의 다른 글

[클린코드] 12장. 창발성  (0) 2023.01.21
[클린코드] 11장. 시스템  (0) 2023.01.21
[클린코드] 10장. 클래스  (0) 2023.01.11
[클린코드] 9장. 단위 테스트  (0) 2023.01.11
[클린코드] 8장. 경계  (0) 2023.01.02

댓글