이상적인 객체지향 if else switch 조건문?

*이 포스트는 Robert C. Martin 님의 허락을 받아 blog.cleancoder.com 의 글
if-else-switch“를 번역한 것입니다. 저작권에 유의하시기 바랍니다.

서론

얼마 전 누가 트위터에 올린 글이 있습니다. 아래 PHP 코드 스타일 중에 어떤 것이 최고냐, 아니면 혹시 더 좋은 스타일이 있냐 하는 질문이였죠.

그래서 저는 아래처럼 암호문같은 답변을 남겼습니다.

if/else 문들을 각각의 다형성 객체를 생성하는 팩토리 객체 안으로 옮기세요.
팩토리 객체를 main 함수에서 생성하고, 애플리케이션으로 전달하세요.
그러면 if/else 체인이 딱 한 번만 쓰이도록 보장할 수 있습니다.

그러자 사람들이 예를 보여달라고 하더군요. 트위터는 이런 예를 자세히 보여주기 좋은 플랫폼이 아니죠.

그래서…

객체지향 관점의 if/else/switch 조건문이 가진 문제점

먼저, 프로그래머의 의도가 단지 이렇게 매핑하는거라면:

0->'male', 
1->'female' 
otherwise->'unknown'

제 선택은 이미지의 # Refactoring – 2 입니다.

그렇지만, 저 시스템의 비즈니스 로직이 성별 코드를 정책 결정에 쓰지는 않을 것 같아요. 걱정되는 부분은 질문자가 물어본 if/else/switch 체인이 코드 여기저기 더 많을 것 같다는 점입니다. 그 중 몇 개는 위에처럼 정수 값을 switch 에 쓸테고, 몇 개는 문자열을 switch 에 쓰는 식으로요. 아니면 if 문 하나에는 정수 값을 쓰고 다음엔 문자열을 썼을 경우도 불가능하지는 않죠!

소프트웨어 시스템에서 if/else/switch 조건문이 증식하는건 아주 흔한 문제입니다. 이게 위험한 이유는 어쩔 수 없이 그 조건문들을 변경할 때, 몇 개는 빠트릴 수도 있다는 거죠. 그럼 곧바로 시스템의 버그로 이어집니다.

게다가 조건문들에는 더 큰 문제가 있습니다. 바로 의존성 구조의 문제입니다.

이런 조건문들은 주로 더 낮은 레벨 모듈들을 가리키는 케이스들을 가지는 경향이 있습니다. 그말은 즉, 이 조건문을 가지고있는 모듈이 더 낮은 레벨 모듈들의 소스 코드에 의존하게 된다는 말이죠.

이건 마음에 들지 않습니다. 우린 높은 레벨에서 낮은 레벨로 흘러가는 의존성을 좋아하지 않죠. 독립적으로 배포할 수 있는 컴포넌트들의 조합으로 구조를 잡는 데 훼방을 놓기 때문입니다.

그런데 위의 그림은 더 나쁜 상황을 보여줍니다. 또 다른 높은 레벨의 모듈이 if/else/switch 조건문을 가지는 모듈에 의존하고 있습니다. 그러면 이 높은 레벨의 모듈은 더 낮은 레벨의 모듈에게도 줄줄이 의존하게 됩니다. 이런 상황이 if/else/switch 조건문을 의존성 자석(Dependency magnet)으로 만듭니다. 소스 코드 여기저기에 손을 뻗쳐서는, 시스템이 유연한 컴포넌트 구조를 가질 수 없도록 해 꽉 죄인 모놀리식 구조로 가두는 것이죠.

객체지향 관점의 해결책

이 문제의 해결책은 낮은 레벨 모듈들로 나가는 의존성들을 끊어내는 겁니다. 이건 간단한 다형성으로도 충족됩니다.

위 그림에서 높은 레벨의 모듈이 인터페이스를 사용해 낮은 레벨의 세부 구현을 다형적으로 분산하는 것을 볼 수 있습니다. 조금만 생각해보면 이게 조금 꼬아놓았을 뿐 if/else/switch 조건문과 같은 역할을 한다는 것을 알 수 있습니다. 대신 높은 레벨의 모듈이 인터페이스를 사용하기 전에, 반드시 미리 어떤 케이스를 따를 지 결정되어 있어야 하죠.

언제 그게 결정되는지에 대해서는 조금 있다가 다시 돌아오겠습니다. 우선 지금은, 의존성의 방향이 어떻게 되었는지 한 번 보세요. 이제 더 이상 높은 레벨에서 낮은 레벨로 흘러가는 소스 코드 의존성은 존재하지 않습니다. 이제 두 레벨을 나누는 컴포넌트 경계를 쉽게 만들 수 있죠. 낮은 레벨은 신경쓰지 않고, 높은 레벨 모듈을 독립적으로 배포할 수도 있습니다. 이런 특징은 멋지고 유연한 구조를 만들어줍니다.

또 생각해 볼 점은, if/else/switch 조건문과 다형적 구현 모두 테이블 검색(lookup)을 사용한다는 점입니다. if/else 문의 경우 테이블 검색이 절차적이죠. switch 문의 경우엔 대부분의 컴파일러에서 검색 테이블을 만듭니다. 다형적 디스패치의 경우 인터페이스의 벡터 테이블이 만들어지죠. 따라서 세 경우 모두 비슷한 실행 시간, 메모리 특징을 가집니다. 즉, 하나가 다른 것들보다 월등히 빠른게 아니라는 겁니다.

그래서 결정은 언제 되나요? 인터페이스 구현 클래스의 인스턴스가 생성될 때 결정됩니다. 이상적으로는 main 함수같이 안전하고 좋은 곳에서 만들어지죠. 흔히들 간단한 팩토리 클래스에게 이 역할을 맡깁니다.

팩토리 클래스 (=팩토리 디자인 패턴)

위 그림에서 이제 높은 레벨 모듈이 인터페이스를 사용해 동작하는 것을 볼 수 있습니다. 이전에 if/else/switch 조건문에 의존했던 모든 비즈니스 로직은 이제 모두 각 상황에 맞는 베이스 클래스의 메소드로 호출됩니다. 비즈니스 로직이 어떤 메소드를 호출하면, 알맞은 낮은 레벨 모듈을 호출하게 되는 거죠. 이 낮은 레벨 모듈들은 Factory 인터페이스에서 만들어집니다. 높은 레벨 모듈이 결정에 관련된 값인 x를 인자로 Factory 인터페이스의 make(x) 메소드를 호출합니다. 그러면 if/else/switch 조건문을 가지고 있는 FactoryImpl 인터페이스 구현 클래스가, 알맞은 인스턴스를 생성해 높은 레벨 모듈에게 돌려주는 것이죠.

의존성의 방향을 다시 보세요. 붉은 색 선이 보이나요? 그게 바로 쉽고 편리한 컴포넌트 경계선입니다. 이 선을 지나는 모든 의존성들은 높은 레벨 모듈을 가리킵니다.

결정 관련 값 x에 대해서는 조심하세요. 이걸 enum으로 만든다거나 붉은 색 선 위에 선언해야하는 무언가로 만들려고 하지 마세요. 그냥 정수 값이나 문자열이 더 좋은 선택입니다. 타입 세이프 하지는 않을 수 있어요. 아니, 타입 세이프 할 수가 없어요. 하지만 시스템이 컴포넌트 구조를 가질 수 있게 해줄 겁니다.

객체지향 관점에서 생기는 또 다른 문제

아마도 다른 문제가 걱정될 수도 있습니다. if/else/switch 조건문에 의존했던 비즈니스 로직들은 모두 그에 대응하는 인터페이스 메소드가 필요합니다. 이런 비즈니스 로직이 많아질 수록, 인터페이스에 메소드를 계속 추가해줘야하죠. 그리고 이미 다른 비즈니스 로직들이 이 인터페이스에 의존하고 있기 때문에, 관계 없는 부분이 수정되었다고 해도 모두 다시 컴파일되거나, 다시 배포되어야 합니다.

이 문제를 해결하는 것도 여러 방법이 있습니다. 여기에 2000단어쯤 더 적어서 설명할 수는 있겠지만, 인터페이스 분리 원칙비순환 방문자 패턴에 대해서 직접 찾아보기를 바라요.

그나저나, if/else/switch 조건문 같이 단순한 것에 대해서 이야기하는 것도 이렇게 흥미로울 수 있다니 멋지지 않나요?

다른 Robert C. Martin 시리즈

4.7 3 votes
Article Rating
구독
Notify of
guest
0 Comments
Inline Feedbacks
View all comments