변화 예측

유지보수하기 좋은 코드를 만들려면 변경 사항에 대해 어느 정도 예측이 가능해야 합니다. 물론 이해하기 쉬운 코드를 작성하면 상대적으로 어떤 변화든 쉽게 대응할 수 있는 것이 사실이지만, 모든 종류의 변경 사항에 유연하게 대처할 수 있는 코드를 미리 작성하는 건 사실상 불가능하고 또 시간 낭비일 수도 있기 때문입니다.

리팩토링에서 코드 스멜이라고 이야기하는 것 중에 하나가 타입 혹은 enum 값을 체크하는 조건문이 반복되는 것입니다. Replace Conditional with Polymorphism은 이런 코드를 리팩토링해서 각 타입을 서브클래스를 만들고 다형성으로 해결하라고 이야기합니다.

예를 들어, 다음 코드는 _type에 따라 getSpeed() 메소드의 행동이 달라집니다.

class Bird {
   ...
   double getSpeed() {
       switch (_type) {
           case EUROPEAN:
              return getBaseSpeed();
           case AFRICAN:
              return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts;
           case NORWEGIAN_BLUE:
              return (_isNailed) ? 0 : getBaseSpeed(_voltage);
       }
       throw new RuntimeException ("Should be unreachable");
   }
}

객체지향 프로그래밍의 일반적인 조언은 Bird 클래스의 getSpeed() 메소드를 abstract로 만들고, EuropeanBird, AmericanBird, NorwegianBlueBird 클래스를 만들어서 각각 getSpeed() 메소드를 오버라이드하라는 겁니다.

이렇게 했을 때 뭐가 더 좋아질까요? 일단 각 Bird 고유의 코드는 해당 클래스에 캡슐화(encapsulation) 된다는 장점이 있습니다. AmericanBird의 속도를 계산 방법이 바뀌었을 때 다른 코드에 영향을 주지 않고 구현을 변경할 수 있습니다. 또핸 새로운 Bird 종류가 추가되었을 때도 기존 Bird 구현을 수정하지 않고 추가할 수 있다는 장점이 있습니다.

여기서 가정한 변화의 방향은 Bird의 추가 혹은 특정 Bird의 구현 변경입니다. 하지만 변화의 방향이 다르면 예상과 달랐다면 어떻게 될까요?

Bird가 추가되는 대신 각 Bird에 계속 새로운 속성 혹은 행동이 추가되어야 한다면? 더 이상 변경 사항이 하나의 Bird에 국한되지 않고 모든 Bird 클래스를 수정해야 하는 상황이 됩니다. 예를 들어, 전반적인 속도 계산 알고리즘이 변경되어 모든 BirdgetSpeed() 메소드에 수정이 필요하다면, 오히려 리팩토링 전의 코드가 더 캡슐화가 잘 된 코드입니다. getSpeed() 메소드 하나만 수정하면 해결할 수 있고, 각 Bird의 속도 구현을 한 눈에 파악할 수 있기 때문입니다. 따라서 무조건 다형성을 추구할 것이 아니라 처음 Bird 클래스를 설계할 때 어떤 변경 사항이 많을지 예측을 하고 적절한 방식으로 구조를 잡아야 합니다.

Bird 추가나 특정 Bird 알고리즘의 수정이 동전의 앞면, 메소드 추가나 전체 Bird 알고리즘 수정이 동전이 뒷면이라면 객체지향 프로그래밍은 동전의 앞면에 베팅을 하고 있습니다. 하지만 우리가 이미 동전이 뒷면이 나올 확률이 더 높다고 예측을 했다면 이 경우 객체지향 방법론을 무조건적으로 적용하는 건 정답이 아니게 됩니다.

함수 언어는 보통 반대 방향으로 베팅을 합니다. ADT(Algebraic Data Type)의 Sum Type과 패턴 매칭은 우리가 코드 스멜이라고 생각하는 타입에 따른 조건문 사용을 오히려 장려합니다. 객체지향 프로그래밍과 달리 동전의 뒷면에 베팅을 하기 때문입니다. 마찬가지로 우리가 동전의 앞면이 나올 확률이 더 높다는 사실을 알고 있으면, 함수 프로그래밍을 적용하는 건 정답이 아니게 됩니다.

한 언어가 두 마리 토끼를 모두 잡는 것은 쉽지 않습니다. Philip Wadler 교수가 Expression Problem으로 이름 붙인 문제도 “프로그래밍 언어가 이런 두 가지 변화의 방향에 다 대처할 수 있는가?”입니다. 멀티 메소드나 헤스켈의 타입 클래스(Type Class) 등이 해법으로 나왔지만, 두 가지 모두 우리가 일반적으로 사용하는 프로그래밍 언어에 존재하지 않는 기능이므로 우리는 여전히 변화의 방향을 예측하고 베팅을 해야만 합니다.

객체지향 프로그래밍 원리나 리팩토링을 가르칠 때 가장 어려운 것도 내용이 아니라 맥락에 있습니다. 생각 없이 따르기만 하면 되는 프로토콜은 존재하지 않기 때문입니다. 같은 조언이라도 득이 될 때가 있고 실이 될 때가 있고, 결국 상황에 따라 판단이 필요합니다. 맥락 없이 “switch나 if로 타입 체크하는 코드는 무조건 나빠”라고 이야기하는 사람이 있다면 왜 그런 판단을 내리는지 물어보고 그 이유가 정말로 합당한지 한 번 더 고민해 보시기 바랍니다.

“조언자의 딜레마”라는 말이 있습니다. 세상에 좋은 조언을 널렸지만, 결국 어떤 조언을 취사선택할지의 문제는 나에게 있기 때문입니다. 디자인 패턴, 클린 코드, 리팩토링 등을 아무리 읽고 적용해도 결국 스스로 판단 없이 기계적으로 적용해서는 좋은 결과를 얻기 어렵습니다. 더 중요한 건 스스로 생각하는 훈련입니다.

Advertisements