티스토리 뷰

반응형

GoogleTechTalks의 "The Clean Code Talks -- Inheritance, Polymorphism, & Testing" 을 참고하여 정리하고 각색하였습니다. https://www.youtube.com/watch?v=4F72VULWFvc


if문을 제거합시다.

왜 if문을 제거해야 할까요? (이 포스팅에서는 switch문을 포함한 모든 조건문을 if문이라고 칭하겠습니다.)

 

  1. if 문은 코드의 가독성을 저해시킵니다.
  2. if 문은 테스트를 불편하게 만듭니다.
  3. if 문은 유지보수하기 어렵게 만듭니다.

모든 if문을 제거할 수는 없습니다. 다만, 대부분의 if문은 다형성을 이용하여서 제거 될 수 있습니다.

 

다형성을 활용하여 제거 할 수 있는 if문은 2가지 경우가 있습니다.

 

  1. 객체가 다른 상태에 따라 다른 행동을 해야하는 경우
  2. 같은 조건문을 반복적으로 사용하고 있는 경우

(Java 같은) 객체지향 프로그래밍 언어에서 다형성은 상속을 통해서 구현합니다.

(여담으로 상속을 코드 중복을 제거하기 위한 수단으로 사용하지 마세요! 상속은 '다형성'을 구현하기 위한 도구입니다. 상속을 사용하여 코드의 중복을 없애고자 한다면 반드시 유지보수의 어려움을 겪게 될 것입니다.)

 

 

예제1 (객체가 다른 상태에 따라 다른 행동을 해야하는 경우)

위의 코드는 조건문을 사용하여서 다른 상태에 따라 다른 행동을 기대하는 전형적인 코드입니다.

 

하지만 상속을 이용한 다형성을 사용하면 아래와 같은 설계가 가능합니다.

다른 상태에 따라 다른 행동을 수행하는 하위 클래스를 만들어서 분리한다면, 코드에 조건문은 제거되고 무엇을 하고자 하는지에 대해서 집중할 수 있게됩니다.

 

예제2 (객체가 다른 상태에 따라 다른 행동을 해야하는 경우)

1 + 2 * 3 을 연산하는 과정을 구현하기 위해서, 숫자와 연산들을 각각의 노드로 구현하고 트리로 표현해보겠습니다.

 

조건문을 사용한다면 다른 상태(operation)에 따른 다른 행동을 직접 명시하게 됩니다. 

 

(실제 제품 코드에서는 더 길고 복잡한 형태의 모습을 갖는 경우가 많습니다. 유지보수하는 입장에서 생각한다면 수정하고자 하는 기능이 어떤 코드인지 파악하는데 많은 시간을 할애해야 할 것입니다.)

 

 

다형성을 사용하여서 분리하기 위해 각 상태들을 분석해보겠습니다.

숫자(#)와 연산자(+, *)들은 Node라는 특징은 공유하지만 차이가 분명하게 존재합니다. 숫자는 값(value)만 존재하는데, 연산자들은 수행하여야하는 기능(function)과 양쪽(left, right)에 다른 노드들을 갖습니다.

 

상속을 통한 '다형성'으로 숫자(value)노드와 연산(operation)노드로 분리해보겠습니다. 숫자노드(ValueNode)와 연산자노드(OpNode)간의 다른 필드는 각자의 클래스에만 할당됩니다.

 

 

여전히 연산노드(OpNode)에는 +와 *을 구분하기 위한 조건문이 존재합니다.

 

연산자노드(OpNode)를 더하기노드(AdditionNode)와 곱하기노드(MultiplicationNode)로 분리해봅시다. 더하기노드와 곱하기노드의 다른 연산의 차이는 evaluate() 메서드를 오버라이딩하여서 달리 구현됩니다.

이처럼 다형성을 이용하면 조건문은 완벽하게 제거되고, 더 나은 설계와 클린한 코드를 얻게 됩니다. 더 이상 숫자 노드들이 무의미한 null을 다루어 NullPointException을 던질 위험도 사라지게 되었네요.

 

만약 새로운 연산기능(-, /)이 추가된다 하더라도 OpNode를 상속받은 새로운 클래스를 선언하는 방식으로 구현 가능합니다. 조건문을 다시 추가하여서 코드를 장황하게 만들지 않아도 되고, 자연스럽게 응집도가 오르게 되니 설계적으로 이점이 있습니다. (SOLID의 개방폐쇄원칙과 의존역전원칙도 준수하게 되었어요!)

 

각 기능들은 다른 파일(클래스)로 분리되니 테스트하기에도 용이해지고, 코드를 쉽게 이해할 수 있게 됩니다.

 

예제3 (같은 조건문을 반복적으로 사용하고 있는 경우)

아래 코드는 'FLAG_i18n_ENABLED'라는 같은 조건문을 반복적으로 사용하여서 같은 구조의 분기처리를 수행하고 있습니다.

위의 코드들을 테스트 하기위해서는 'FLAG_i18n_ENABLED'가 true인 경우와 false인 경우를 분리하여서 테스트해야합니다. 이는 테스트 목적을 알기 어렵게 만듭니다.

 

하지만 상속을 통한 다형성으로 Update를 상위클래스로 만들고 이를 상속받은 2개의 클래스로 분리한다면 if문을 제거할 수 있습니다. 각각의 구현 클래스를 테스트하게 되니 테스트에 목적을 쉽게 알아차릴 수 있습니다.

 

if문은 진짜로 사라졌을까? 

그렇다면 다른 상황들을 구분하기 위한 처리는 어떻게 해야 할까요? 예제3의 Update 객체를 사용하게 되는 사용자 입장에서 2개의 다른 객체를 분명히 구분하여야 할 것입니다.

 

이를 위해서 다른  상황들을 구분해주는 생성(Construction)객체를 만들어야 합니다. 조건문코드는 이 생성객체가 갖게 합니다.

 

??? : 그러면 결국 if문이 제거된게 아니지 않습니까?!

 

맞습니다. 결국 if문은 진짜 제거된게 아닙니다. '적절한'곳으로 이동 되었을 뿐입니다. 

 

이를 통해서 중요한 [비지니스 로직]과 [객체의 생성과 연관 주입을 담당하는 로직]을 분리할 수 있습니다.

 

예제로써 다시 말씀드리면 Update를 사용하는 Consumer 객체는 어떤 Update를 사용해야 하는지 확인할 필요없이 '비즈니스 로직'에만 집중해야 합니다.

 

대신 Factory객체(생성객체)가 어떤 상황에서 어떤 Update객체를 사용해야 하는지 판단합니다. 

 

반복되는 조건문은 없어졌고 분리된 책임만을 갖게 됩니다. 분리된 책임들로 인해서 코드 가독성이 오르고 응집도가 생겼으며, 프로젝트 구조를 이해하기에도 쉽고 유지보수하기에 좋은 구조가 되었습니다.

(혹시 스프링 프레임워크 같은 DI(Dependency Injection - 의존성 주입) 컨테이너를 사용해보신 분들이라면 익숙한 향기(?)를 느끼실 수 있습니다. 결국 DI의 목적도 다형성을 사용한 책임의 분리와 유지보수성 향상이기 때문입니다.)

 

반응형
댓글