Five Lines of Code - 2. 리팩토링 깊게 들여다보기

주제: 가독성을 통한 의미 전달, 불변속성 지역화, 추가를 통해 변경 가능하게 만들기

🔖 2.1 가독성 및 유지보수성

책의 내용

가독성 - 의도를 전달하기 위한 코드의 성질이며, 가독성이 있다는 말은 코드가 무슨 일을 하는지 파악하기가 매우 쉽다는 뜻입니다.

유지보수성 - 얼마나 많은 후보를 조사해야 하는지를 나타내는 표현입니다. 조사 단계에 시간이 오래걸린다는 것은 유지보수성이 나쁘다는 징후입니다. 만약 무언가를 수정했을 때 관련이 없는 다른 곳에서 문제가 발생한다면, 이를 취약하다고 말합니다. 취약성의 근원은 일반적으로 '전역 상태' 입니다. 여기서 전역은 우리가 고려한 범위(scope)를 벗어난 것을 의미합니다.

나의 생각

돌이켜보면 전역 설정으로 인한 디버깅으로 은근 시간을 많이 쓴 것 같다. 예를 들면, 특정 스타일 전역 설정을 해두었는데 이를 까먹고 지역 설정이 왜 적용이 안되는지 계속 뒤져보았던 적이 있었다. 혹은 라이브러리 설정으로 인해 지역 설정이 적용되지 않았던 적도 있고.. 많은 후보를 조사하느라 (디버깅) 그때의 내 코드는 취약했다는 의미겠지..?

🔖 2.1 불변속성

책의 내용

불변속성 - 데이터가 전역적일 경우, 데이터가 연결된 다른 변수를 통해 누군가가 읽거나 변경할 수 있어 실수로 데이터가 손상될 수 있습니다. 이때, 코드에서 상태(조건)를 명시적으로 확인하지 않는 속성을 불변속성(invariant)이라고 합니다. (e.g. 이 숫자는 절대 음수일 수 없습니다.) 하지만 불변속성이 유효한 상태로 유지되기란 거의 불가능합니다. 이런 경우의 리팩토링은 불변속성을 더욱 쉽게 볼 수 있도록 서로 가깝게 이동시켜 '불변속성의 범위 제한 (localizing invariants)' 이라고 합니다.

나의 생각

불변속성이 유효한 상태로 유지되기가 왜 어려운걸까? 변수가 재할당되지 않도록 설정하면 불변하지 않나? 물론 가깝게 놓으면 불변할 가능성이 높아지긴 하겠으나, 원천적으로 변경을 차단할 수 있는 방법은 없는지 스터디원들에게 이 예시를 물어보아야겠다.

🔖 2.2 상속보다는 컴포지션 사용

책의 내용

상속보다는 컴포지션을 사용합니다. 대부분의 리팩토링 패턴과 규칙은 구체적으로 객체 컴포지션을 돕기 위한 것들입니다. 즉, 객체가 내부에 다른 객체의 참조를 가지는 것입니다.

interface Bird {
  hasBeak(): boolean;
  canFly(): boolean;
}
class CommonBird implements Bird {
  hasBeak() { return true; }
  canFly() { return true; }
}
class penguin extends CommonBird { // 상속
  canFly() { return false; }
}
interface Bird {
  hasBeak(): boolean;
  canFly(): boolean;
}
class CommonBird implements Bird {
  hasBeak() { return true; }
  canFly() { return true; }
}
class penguin implements Bird {
  private bird = new CommonBird(); // 컴포지션
  hasBeak() { return bird.hasBeak(); } // 호출을 직접 명시적으로 전달해야 함
  canFly() { return false; }
  canSwim() { return false; } // 만약 메서드 하나가 더 추가된다면?
}

위의 예시처럼, 컴포지션의 가장 큰 장점은 추가(addition)로 손쉬운 변경이 가능하다는 점입니다. 소프트웨어 구성 요소들은 확장에 대해 열려 있어야 하고, 수정에 대해 닫혀 있어야 한다는 의미입니다. 이를 개방-폐쇄(open-closed) 원칙이라고 합니다.

나의 생각

JavaScript를 학습하다보면, 자연스레 상속 이라는 개념을 배우기에 이게 리팩토링의 대상이 되어야 하는지 생각을 못했다. 그래서 처음엔 왜 상속보다는 다른 무언가를 사용할까? 했지만 예시를 보니 이해가 되었다. 만약 메서드를 하나 더 추가했을 때 penguin이 새로운 메서드 canSwim을 구현하지 않기 때문에 컴파일 오류가 발생한다. 이때, 컴포지션을 사용한다면 hasBeak()처럼 canSwim()도 간단하게 클래스 내에 정의하기만 하면 된다. 하지만 반대로 상속의 상황에서는, canSwim()을 재정의(override)해야 한다는 것을 작업자가 기억해야 하므로 이러한 의존 관계를 기억하기 어려울 수 있을 것 같다.

이렇듯, 상속보다 컴포지션을 사용한다면 더 자유자재로 코드를 레고 블럭을 조립하고 교체하듯이 코딩할 수 있을 것 같다.

Last updated