본문 바로가기

Clean Code

[리팩터링 2판 - 실전편] 07장 캡슐화

 


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

 

 

모듈을 분리하는 가장 중요한 기준은 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 '얼마나 잘 숨기느냐'에 있을 것이다. 

 

7.1 레코드 캡슐화하기 Encapsulate Record

대부분의 프로그래밍 언어는 데이터 레코드를 표현하는 구조를 제공한다.

레코드는 연관된 데이터를 묶을 수 있어서 훨씬 의미 있는 단위로 전달할 수 있게 해준다. 하지만 단순한 레코드는 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다. 가령 값의 범위를 표현하려면 어떤 식으로 저장하든 ‘시작’과 ‘끝’과 ‘길이’를 알 수 있어야 한다.

객체를 사용하면 어떻게 저장했는지를 숨긴 채 세 가지 값을 각각의 메서드로 제공할 수 있다. 사용자는 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없다. 캡슐화하면 이름을 바꿀 때도 좋다.

레코드 구조는 두 가지로 구분할 수 있다. 하나는 필드 이름을 노출하는 형태고, 다른 하나는 (필드를 외부로부터 숨겨서) 내가 원하는 이름을 쓸 수 있는 형태다.

코드를 작성하다 보면 중첩된 리스트를 받아서 JSON 같은 포맷으로 직렬화할 때가 많다. 이런 구조 역시 캡슐화할 수 있는데, 그러면 나중에 포맷을 바꾸거나 추적하기 어려운 데이터를 수정하기가 수월해진다.

덩치 큰 데이터 구조를 다룰수록 쓰기 부분에 집중한다. 캡슐화에서는 값을 수정하느 ㄴ부분을 명확하게 드러내고 한 곳에 모아는 일이 굉장히 중요하다.

깊은 복사는 lodash 라이브러리의 cloneDeep로 처리하는 방법, 읽기전용 프락시를 반환하는 방법 등이 있다.

클라이언트가 데이터 구조를 요청할 때 실제 데이터를 제공하면 클라이언트가 데이터를 직접 수정하지 못하게 막을 방법이 없어서 '모든 쓰기를 함수 안에서 처리한다'는 캡슐화의 핵심 원칙이 깨진다.

 

7.2 컬렉션 캡슐화하기 Encapsulate Collection

가변 데이터를 모두 캡슐화하면 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워지기 때문이다.

컬렉션 변수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀌어버릴 수 있다.

나는 이런 문제를 방지하기 위해 컬렉션을 감싼 클래스에 흔히 add()와 remove()라는 이름의 컬렉션 변경자 메서드를 만든다. 이렇게 항상 컬렉션을 소유한 클래스를 통해서만 원소를 변경 하도록 하면 프로그램을 개선하면서 컬렉션 변경 방식도 원하는 대로 수정할 수 있다.

컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는 게 낫다.

가장 흔히 사용하는 방식은 아마도 컬렉션 게터를 제공하되 내부 컬렉션의 복제본을 반환하는 것이다. 복제본을 수정해도 캡슐화된 원본 컬렉션에는 아무런 영향을 주지 않는다. 반환된 컬렉션을 수정하면 원본도 수정될 거라 기대한 프로그래머는 좀 당황할 수 있지만, 이미 여러 코드베이스에서 많은 프로그래가 널리 사용하는 방식이라 크게 문제되지는 않을 것이다. 

여기서 중요한 점은 코드베이스에서 일관성을 주는 것이다. 한 가지만 적용해서 컬렉션 접근 함수의 동작 방식을 통일해야 한다.

컬렉션에 대해서는 어느 정도 강박증을 갖고 불필요한 복제본을 만드는 편이, 예상치 못한 수정이 촉발한 오류를 디버깅하는 것보다 낫다. 

다른 언어들은 컬렉션을 수정하는 연산들이 기본적으로 복제본을 만들어 처리하지만, 자바스크립트에서는 배열을 정렬할 때 원본을 수정한다. 컬렉션 관리를 책임지는 클래스라면 항상 복제본을 제공해야 한다.


7.3 기본형을 객체로 바꾸기 Replace Primitive with Object

개발 초기에는 단순한 정보를 숫자나 문자열 같은 간단한 데이터 항목으로 표현할 때가 많다. 그러다 개발이 진행되면서 간단했던 이 정보들이 더 이상 간단하지 않게 변한다. 금세 중복 코드가 늘어나서 사용할 때마다 드는 노력도 늘어나게 된다.

단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하는 편이다. 시작은 기본형 데이터를 단순히 감싼 것과 큰 차이가 없을 것이라 효과가 미미하지만 나중에 특별한 동작이 필요해지면 이 클래스에 추가하면 되니 프로그램이 커질수록 점점 유용한 도구가 된다. 

 

7.4 임시 변수를 질의 함수로 바꾸기 Replace Temp with Query

함수 안에서 어떤 코드의 결과값을 뒤에서 다시 참조할 목적으로 임시변수를 쓰기도 한다. 임시변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 변수 이름을 통해 값의 의미를 설명할 수도 있어서 유용하다.

그런데 아예 함수로 만들어 사용하는 편이 나을 때가 많다.

이 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다. 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문이다. 클래스 바깥의 함수로 추출하면 매개변수가 너무 많아져서 함수를 사용하는 장점이 줄어든다. 

임시변수를 질의함수로 바꾼다고 다 좋아지는 건 아니다. 자고로 변수는 값을 한 번만 계산하고, 그 뒤로는 읽기만 해야한다. 변수에 값을 한 번 대입한 뒤 더 복잡한 코드 덩어리에서 여러 차례 다시 대입하는 경우는 모두 질의 함수로 추출해야 한다. 

 

 

7.5 클래스 추출하기 Extract Class

클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리애야 한다. 하지만 실무에서는 몇 가지 연산을 추가하고 데이터도 보강하면서 클래스가 점점 비대해지곤 한다.

메서드와 데이터가 너무 많은 클래스는 이해하기 가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다. 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다. 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다. 제거해도 다른 필드나 메서드 들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.

 

7.6 클래스 인라인하기 Inline Class

클래스 추출하기를 거꾸로 돌리는 리팩터링이다. 리팩터링을 하고 나니 특정 클래스에 남은 역할이 거의 ㅇ벗을 때 이런 현상이 자주 생긴다. 이 불쌍한 클래스를 가장 많이 사용하는 클래스로 흡수시키자.

두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다. 클래스를 인라인해서 하나로 합친 다음 새로운 클래스를 추출하는게 쉬울 수도 있기 때문이다.

 

7.7 위임 숨기기 Hide Delegation

모듈화 설계 핵심은 캡슐화다. 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다. 캡슐화가 잘되어 있다면 무언가를 변경해야할 때 고려해야할 모듈 수가 적어져서 코드를 변경하기 쉬워진다.

의존성을 없애려면 서버 자체에 위임 메서드를 만들어서 위임 객체의 존재를 숨기면 된다. 그러면 위임 객체가 수정되더라도 서버 코드만 고치면 되며, 클라이언트는 아무런 영향을 받지 않는다.

 

7.8 중개자 제거하기 Remove Middle Man

캡슐화 이점이 거저 주워지는 건 아니다. 클라이언트가 위임 객체의 또 다른 기능을 사용하고 싶을 때마다 서버에 위임 메서드를 추가해야하는데, 이렇게 기능을 추가하다 보면 서버 클래스는 그저 중개자 역할로 전락하여, 차라리 클라이언트가 위임 객체를 직접 호출하는게 나을 수 있다.

데메테르의 법칙(Law of Demeter)
최소 지식 원칙(Principle of Least Knowledge)라고도 부르며, 내부 정보를 가능한 한 숨기고 밀접한 모듈과만 상호작용하여 결합도를 낮추자는 원칙이다. 자칫하면 이 과정에서 위임 혹은 래퍼 메서드가 너무 늘어나는 등의 부작용이 있을 수 있으니 상황에 맞게 응용하는 게 좋다.

 

7.9 알고리즘 교체하기 Substitute Algorithm

어떤 목적을 달성하는 방법은 여러가지가 있게 마련이다. 그중에서도 다른 것보다 더 쉬운 방법이 분명히 존재한다. 알고리즘도 마찬가지다.