본문 바로가기

Clean Code

[리팩터링 2판 - 실전편] 11장 API 리팩터링


마틴파울러 리팩터링 2판 정주행 스터디 - 실전편

 

모듈과 함수는 소프트웨어를 구성하는 빌딩 블록이며, API는 이 블록들을 끼워 맞추는 연결부다. 이런 API를 이해하기 쉽고 사용하기 쉽게 만드는 일은 중요한 동시에 어렵기도 하다. 그래서 API를 개선하는 방법을 새로 깨달을 때마다 그에 맞게 리팩터링해야 한다.

 

11.01 질의함수와 변경함수 분리하기 Separate Query from Modifier

우리는 사이드이펙트가 전혀 없이 값을 반환해주는 함수를 추구해야 한다. 사이드이펙트가 있는 함수와 없는 함수는 명확히 구분하는 것이 좋다.

'질의함수(읽기함수)는 모두 사이드이펙트가 없어야 한다.'라는 규칙을 따르자. 이를 명령-질의 분리(command-query separation)이라고 한다.

https://martinfowler.com/bliki/CommandQuerySeparation.html

 

11.02 함수 매개변수화하기 Parameterize Function

두 함수의 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 합쳐서 중복을 없앨 수 있다. 이렇게하면 매개변수 값만 바꿔서 여러 곳에서 쓸 수 있으니 함수의 유용성이 커진다.

 

11.03 플래그 인수 제거하기 Remove Flag Argument

플래그 인수(flag argument)는 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수이다. 플래그 인수는 호출하는 쪽에서 (프로그램에서 사용되는 데이터가 아닌) 리터랄 값을 건넨다. 또한, 호출되는 함수는 그 인수를 (다른 함수가 전달하는 데이터가 아닌) 제어 흐름을 결정하는 데 사용해야 한다.

플래그 인수는 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야하는지를 이해하기가 어렵게 만든다. 플래그 인수가 있으면 함수들의 기능 차이가 잘 드러나지 않는다. 사용할 함수를 선택한 후에도 플래그 인수로 어떤 값을 넘겨야 하는지를 또 알아내야 한다.

Boolean 플래그는 코드를 읽는 이에게 뜻을 온전히 전달하지 못하기 때문에 더욱 좋지 못하다. 함수에 전달한 true의 의미를 해석해야한다. 이보다는 특정한 기능 하나만 수행하는 명시적인 함수를 제공하는 편이 훨씬 깔끔하다. 

플래그 인수가 둘 이상인 경우, 플래그 인수를 제거하려면 조합 수만큼 함수를 만들어야하기 때문에 플래그 인수를 사용하는 것이 나을 수도 있다. 그러나 플래그 인수가 둘 이상이라면 함수 하나가 너무 많은 일을 처리하고 있다는 신호이기도 한다. 그럴 땐 더 간단한 함수를 만들 방법을 고민해봐야 한다.

 

11.04 객체 통째로 넘기기 Preserve Whole Object

객체를 통째로 넘기면 변화에 대응하기 쉽다. 그 함수가 더 다양한 데이터를 사용하도록 바끼어도 매개변수 목록은 수정할 필요가 없다. 그리고 매개변수 목록이 짧아져서 함수 사용법을 이해하기도 쉬워진다.

어떤 객체로부터 값 몇 개를 얻은 후, 그 값들로만 무언가를 하는 로직이 있다면, 그 로직을 객체 안으로 집어넣어야 함을 알려주는 악취로 봐야한다. 그래서 객체 통째로 넘기기는 특히 매개변수 객체 만들기로 산재한 수많은 데이터 더미를 새로운 객체로 묶은 후 적용하곤 한다.

단, 함수가 객체 자체에 의존하지 않기를 원할 때는 이 리팩터링을 수행하지 않는다. 객체와 함수가 서로 다른 모듈에 속한 상황이면 특히 더 그렇다.

 

11.05 매개변수를 질의함수로 바꾸기 Replace Parameter with Query

매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다. 즉 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다.

이 목록에서도 중복은 피하는게 좋으며 짧을수록 이해하기 쉽다. 피호출함수가 스스로 '쉽게' 결정할 수 있는 값을 매개변수로 건네는 것도 일종의 중복이다.

매개변수가 있다면 값을 결정하는 책임 주체가 호출자가 되고, 매개변수가 없다면 피호출 함수가 된다. 책임소재를 피호출 함수로 옮기고 호출하는 곳을 간소하게 만들자.

제거하려는 매개변수의 값을 다른 매개변수에 질의해서 얻을 수 있다면 안심하고 질의함수로 바꿀 수 있다. 다른 매개변수에서 얻을 수 있는 값을 별도 매개변수로 전달하는 것은 아무 의미가 없다.

물론 피호출 함수가 그 역할을 수행하기 적합할 때만 그렇게 한다. 매개변수를 제거하면 피호출 함수에 원치 않는 의존성이 생긴다면 매개변수를 질의함수로 바꾸면 안된다. 해당 함수가 알지 못했으면 하는 프로그램 요소에 접근해야하는 상황을 만들지 않도록 말이다. 

또한 매개변수를 없애는 대신 가변 전역변수를 이용하는 일은 하면 안된다. 대상 함수가 참조 투명해야하기 때문이다. 이런 함수는 동작을 예측하고 테스트하기가 훨씬 쉬우니 이 특성이 사라지지 않도록 주의하자.

*참조 투명(Referential Transparency): 함수에 똑같은 값을 거넨 호출하면 항상 똑같이 동작한다.

'Referential transparency' and 'referential opacity' are properties of parts of computer programs. An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior.
- 위키백과

 

 

11.06 질의함수를 매개변수로 바꾸기 Replace Query with Parameter

코드에서 참조를 풀어내는 책임을 호출자로 옮기고 싶은 상황이 있다. 이런 상황은 대부분 대상 함수가 더 이상 특정 원소에 의존하지 원치 않을 때 일어난다.

참조 투명하지 않은 원소에 접근하는 모든 함수는 참조 투명성을 잃게된다. 해당 원소를 매개변수로 만들면 참조 투명하게 만들 수 있다. 모듈을 참조 투명하게 만들어 얻는 장점은 대체로 아주 크기때문에, 순수함수들을 따로 구분하고, 프로그램의 입출력과 기타 가변 우너소들을 다루는 로직으로 순수함수들의 겉은 감싸는 패턴을 많이 활용한다. 

이 리팩터링을 수행하면 호출하는 쪽 코드는 전보다 다루기 어려워지는게 보통이다. 모든 것을 매개변수로 바꿔 아주 길고 반복적인 매개변수 목록을 만드는 방법과, 함수들끼리 많은 것을 공유해서 수많은 결합을 만들어내는 두 극단 사이에서 적절한 균형을 찾아야 한다. 책임 소재를 프로그램의 어디에 배정하느냐의 문제로, 답을 찾기 쉽지 않으며 항상 정답이 있는 것도 아니다. 한 시점에 내린 결정이 영원히 옳다고 할 수 없는 문제인 것이다. 

 

11.07 세터 제거하기 Remove Setting Method

세터 메서드가 있다는 것은 필드가 수정될 수 있다는 뜻이다. 생성 후 수정되지 않길 원하는 필드라면, 세터를 제거해서 객체가 생성된 후에는 값이 바뀌면 안되는 뜻을 분명히 할 수 있다.

 

 

11.08 생성자를 팩터리 함수로 바꾸기 Replace Constructor with Factory Function

constructor에는 일반 함수에는 없는 이상한 제약이 있지만, 팩터리 함수에는 이런 제약이 없다. 

단, 팩터리 함수를 호출할 때 함수에 문자열 리터럴을 건넨다면 악취로 봐야한다. 함수의 이름에 문자열 리터럴의 내용을 녹이는 방식을 권한다.

const leadEngineer = createEmployee(document.leadEngineer, 'E'); // (X)

const leadEngineer = createEngineer(document.leadEngineer); // (O)

 

 

11.09 함수를 명령으로 바꾸기 Replace Function with Command

함수는 프로그래밍의 기본적인 빌딩 블록 중 하나다.이 함수를 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다. 이런 객체를 가리켜 '명령 객체' 혹은 단순히 '명령(command)'이라고 한다.

이 명령은 '명령 패턴(Command Pattern)'이라 부르는 디자인 패턴에서의 명령과 같은 맥락의 명령이다.

명령 객체는 대부분 메서드 하나로 구성되며, 이 메서드를 요청해 실행하는 것이 이 객체의 목적이다. 복잡한 함수를 잘게 쪼개서 이해하거나 수정하기 쉽게 만들고자 할 때 명령을 사용하는 편이 낫다. 

명령 객체는 복잡한 연산을 다룰 수 있는 강력한 메커니즘을 제공한다. 큰 연산 하나를 여러 개의 작은 메서드로 쪼개고 필드를 이용해 쪼개진 메서드들끼리 정보를 공유할 수 있다. 어떤 메서드를 호출하냐에 따라 다른 효과를 줄 수 있고 각 단계를 거치며 데이터를 조금씩 완성해갈 수 있다.

자바스크립트에서라면 중첩함수가 명령의 합리적 대안이 될 수도 있다. 그래도 나는 여전히 명령 객체를 사용한다. 명령을 사용하면 서브함수들을 테스트와 디버깅에 활용할 수 있기 때문이다.

 

11.10 명령을 함수로 바꾸기 Replace Command with Function

명령 객체는 평범한 함수 메커니즘보다 훨씬 유연하게 함수를 제어하고 표현할 수 있다. 하지만 유연성은 언제나 그렇듯 복잡성을 키우고 얻는 대가임을 잊지 말아야한다.

일급 함수와 명령 중에 선택해야한다면, 나라면 95%는 일급 함수의 손을 들어준다. 명령은 명령보다 더 간단한 방식으로는 얻을 수 없는 기능이 필요할 때만 선택한다.

로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 객체로 바꿔주는 게 낫다.

 

 

11.11 수정된 값 반환하기 Return Modified Value

데이터가 어떻게 수정되는지 추적하는 일은 코드에서 가장 이해하기 어려운 부분 중 하나다. 특히 같은 데이터 블록을 읽고 수정하는 코드가 여러 곳이라면 데이터가 수정되는 흐름과 코드의 흐름을 일치시키기가 상당히 어렵다.

데이터가 수정된다는 사실을 명확히 알려주어서, 어느 함수가 무슨 일을 하는지 쉽게 알 수 있게 해야한다.

변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담자. 이 방식으로 코딩하면 호출자 코드를 읽을 때 변수가 갱신될 것임을 분명하게 인지하게 된다. 해당 변수의 값을 단 한번만 정하면 될때 특히 유용하다.

이 방법은 값 여러 개를 갱신하는 함수에는 효과적이지 않다.

 

11.12 에러 코드를 예외로 바꾸기  Replace Error Code with Exception

예외는 프로그래밍 언어에서 제공하는 독립적인 에러처리 메커니즘이다. 에러가 발견되면 예외를 던진다. 그러면 적절한 에러핸들러를 찾을 때까지 콜스택을 타고 위로 전파된다. 

예외를 사용하면 에러코드를 일일이 검사하거나 에러를 식별해 콜스택 위로 던지는 일을 시경쓰지 않아도 된다. 예외에는 독자적인 흐름이 있어 프로그램의 나머지에서는 에러발생에 따른 복잡한 상황에 대처하는 코드를 작성하거나 읽을 일이 없게 해준다.

예외는 정확히 예상 밖은 동작일 때만 쓰여야 한다. 달리 말하면 프로그램의 '정상 동작' 범주에 들지 않는 에러를 나타낼 때만 쓰여야 한다. 예외를 던지는 코드를 프로그램 '종료 코드'로 바꿔도 프로그램이 여전히 정상 동작할지를 따져보는 것이다. 정상 동작하지 않을 것 같다면 예외를 사용하지 말라는 신호다. 예외 대신 에러를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.

 

11.13 예외를 사전확인으로 바꾸기

예외를 사용하면 에러코드를 연쇄적으로 전파하던 긴 코드를 깔끔히 제거할 수 있지만, '뜻밖의 에러'라는 말 그대로 예외적으로 동작할 떄만 쓰여야 한다.

함수 수행 시 문제가 될 수 있는 조건을 '함수 호출 전'에 검사할 수 있다면 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야한다.