본문 바로가기
기타

리팩토링 스터디 #4.5 - 내용 미리보기

by 유세지 2020. 11. 4.

2주 동안의 휴식기를 두고 리팩토링 스터디가 재개되었습니다. 오랜만의 스터디 준비가 어색하게 느껴지지만 차근히 읽어가다보면 내용을 이해하는데에는 그리 오랜 시간이 걸리지 않을 것 같습니다.

 

이번 주 스터디 범위는 6.10 여러 함수를 변환 함수로 묶기부터 7.2 컬렉션 캡슐화하기까지 약 40페이지 가량입니다. 많지 않은 양이니 가벼운 마음으로 시작해보겠습니다.

 

여러 함수를 변환 함수로 묶기 Combine Functions into Transform

공통 데이터를 중심으로 엮여서 작동하는 함수들은 하나의 클래스로 묶는 것이 편합니다. 이렇게 하면 각 함수들이 공유하는 공통된 환경을 표현하기가 명확하고, 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결히 만들 수 있습니다. 이런 객체를 시스템의 다른 부분에 전달하기 위한 참조를 제공하기도 용이해집니다.

 

위 내용은 지난 6.9절 - 여러 함수를 클래스로 묶기에 나왔던 내용인데, 함수를 묶는 방법으로는 클래스로 묶기 말고도 변환 함수로 묶기도 있습니다. 이번 장에서 다루는 내용이 바로 이것입니다.

 

변환 함수는 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환합니다. 이렇게 해두면 도출 과정을 검토할 일이 생겼을때 이 변환 함수만 살펴보면 되기 때문에 코드를 관리하기 편리해집니다. 이런식으로 코드를 묶는 가장 큰 이유는 무엇보다도 도출 로직이 중복되는 것을 피하기 위해서입니다.

이 기법을 적용하는 과정을 살펴보겠습니다.

 

1. 우선 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만듭니다.

2. 묶을 함수 중 하나를 골라 본문 코드를 변환 함수로 옮깁니다.

3. 그 결과를 레코드에 새 필드로 기록합니다.

4. 그리고 함수를 요청한 쪽에서 이 필드에 접근하여 사용할 수 있도록 합니다.

5. 테스트 및 컴파일.

 

전체적인 흐름은 다른 리팩토링 기법과 똑같습니다. 계산 등의 과정을 함수로 따로 빼주는 기법들처럼 적용시켜주시면 됩니다.

 

이 방법은 원본 데이터가 코드 내에서 갱신될 때는 사용하지 않는 것을 권장합니다. 변환 함수쪽에서 가공된 데이터를 새 레코드에 저장하기 때문에 전체 레코드의 일관성을 해칠 수 있기 때문입니다. 이럴때는 변환 함수 대신 클래스로 묶기를 사용해주시는 것이 바람직 하다고 합니다.

 

 

단계 쪼개기 Split Phase

단계 쪼개기는 서로 다른 두 대상을 한꺼번에 다루는 코드에 적용하기 적합합니다. 단계 쪼개기를 적용하는 가장 간단한 방법 중 하나는 어떠한 작업이 들어왔을때, 동작을 연이은 두 단계로 나누어 주는 것입니다. 아니면, 순차적인 단계에 따라서 나누어주어도 좋습니다. 이때 이 단계들은 반드시 서로 다른 일을 수행하고 있어야 합니다.

위 기법이 적용되는 가장 대표적인 예가 바로 컴파일러(compiler)입니다.

 

컴파일러의 작업 순서를 보면, 텍스트를 토큰화하고, 토큰을 파싱하여 구문 트리를 만들고, 구문 트리를 변환하고, 목적 코드를 생성하는... 우리가 실행할 수 있는 프로그램을 생성하기까지 다양한 단계를 거치는 것을 알 수 있습니다.

 

작업을 단계별로 진행하기

 

이런 단계 쪼개기는 보통 사이즈가 큰 프로그램에 적용되는데, 저자는 규모에 관계 없이 분리할만한 건덕지가 보이는 코드들은 한다고 하네요. 함수들을 별도의 모듈로 분리하면 그 차이들을 코드에서 훨씬 분명하게 드러낼 수 있다고 합니다.

단계 쪼개기의 절차를 간소화해서 나타내보면 아래와 같습니다.

 

1. 어떤 단계의 다음 단계에 해당하는 코드를 추출합니다.

2. 중간 데이터 구조를 만들어 앞에서 추출한 함수의 인수로 추가해줍니다.

3. 추출한 다음 단계 함수의 매개변수를 검토합니다. 여기서 이전 단계에 사용되는 변수는 중간 데이터 구조로 옮겨줍니다.

4. 이전 단계 코드를 함수로 추출하며 중간 데이터 구조를 반환하게 만듭니다.

 

여기까지가 기본적인 리팩토링 기법에 대한 설명이었습니다. 다음 챕터부터는 캡슐화에 대해 본격적으로 다룹니다.

캡슐화는 객체지향 프로그래밍에서 빠질 수 없는 개념인만큼 많은 개발자들이 그 중요성을 알고 있습니다. 본 책에서는 캡슐화를 어디에 어떤 방식으로 활용할 수 있는지 구체적인 예시와 함께 알아보았습니다.

 

 

레코드 캡슐화하기 Encapsulate Record

대부분의 프로그래밍 언어는 데이터 레코드를 표현하는 구조를 제공합니다. 레코드는 여러 데이터들을 직관적으로 묶어 의미있는 단위로 전달할 수 있게 해줍니다. 단순하기 때문에 한 눈에 구조를 파악하기엔 좋지만, 때론 그런 단순함이 단점이 되기도 합니다.

 

불변값이 아닌 이상에야, 데이터는 끊임없이 변화할 수 있습니다. 어쩌면 불과 몇 분전에 저장했던 값이 바뀌는 경우라거나, 아예 새로운 필드 값이 추가되고 기존 필드의 이름이 바뀌는 등 외부의 요구에 따라 수 많은 변동 사항들이 있을 수 있습니다. 우리는 이런 데이터들을 가변 데이터라고 부릅니다.

이러한 가변 데이터들을 저장하는 데에는 1차원적인 레코드보다는 객체로 저장하는 편이 훨씬 유리합니다.

객체를 사용해서 데이터들을 저장하게 되면, 어떻게 저장했는지는 숨긴 채로 값들을 각각의 메서드로 제공할 수 있습니다. 사용하는 입장에서 이 데이터가 무엇인지, 단순히 입력된 값인지 아니면 객체 내부에서 따로 계산된 값인지 따위를 알 필요가 없습니다.

 

이름이 바뀌는 경우에도 편리합니다. 레코드를 찾아서 바꿔주는 것보다, 새로운 이름의 메서드를 제공하기만 하면 됩니다. 이 경우엔 기존에 제공하던 메서드도 그대로 유지할 수 있기 때문에 점진적인 수정 또한 가능합니다.

본문에는 레코드 구조에 대해서 조금 더 상세히 서술되어 있는데, 레코드 구조는 필드 이름의 노출 여부에 따라 두 가지로 구분됩니다. 외부로부터 이름을 숨기는 경우엔 프로그래머가 원하는 이름을 쓸 수 있습니다. 이 경우가 주로 라이브러리에서 해시(hash), 맵(map), 해시맵(hashmap), 딕셔너리(dictionary), 연관 배열(associative array)과 같은 이름으로 제공합니다.

 

특히 해시맵은 유용하게 사용하지만 필드를 명확히 알려주지 않는다는 단점이 있습니다. 프로그램 여기저기서 많이 사용되면 사용될수록 불분명함으로 인해 생기는 문제가 발생할 확률이 커지게됩니다. 이럴 경우 레코드 대신 클래스를 사용하는게 나은 선택입니다.

 

 

Encapsulate

 

아래 절차에 따라 캡슐화를 진행해주시면 되겠습니다.

 

1. 레코드를 담은 변수를 캡슐화합니다.

2. 해당 변수의 내용을 레코드를 감싸는 단순한 클래스로 교체합니다.

3. 해당 클래스에 원본 레코드를 반환하는 접근자를 정의해주고, 함수들이 이 접근자를 사용하도록 수정해줍니다.

4. 원본 레코드 대신 새로 정의한 클래스를 반환하는 함수를 만들어줍니다.

5. 새로 만든 함수를 레코드를 반환하는 예전 함수 자리에 넣어줍니다.

6. 기존 함수를 제거합니다.

 

컬렉션 캡슐화하기 Encapsulate Collection

가변 데이터들을 캡슐화하고나서, 컬렉션들을 다룰때 가장 많이 하는 실수 중 하나가 게터가 컬렉션 자체를 반환해버리는 것입니다. 이렇게 되면 컬렉션을 다루며 의도치 않게 원본 원소들을 조작해버려 값이 변하는 상황이 초래될 수 있습니다. 저자는 이런 문제들을 방지하기 위해 따로 컬렉션 변경자 메서드를 만든다고 하네요. (보통 add()와 remove() 같은 이름이라고 합니다!)

 

물론 이런 방법은 모든 팀원들이 원본 모듈 밖에선 컬렉션을 수정하지 않고, 언제나 컬렉션이 바뀔 수 있음을 인지하고 있는 상황일때나 문제 없이 동작하지, 작은 실수 한 번이 자칫하면 찾기 어려운 버그로 이어질 수 있어서 이런 습관에만 의존하는 방식은 바람직 하지 않습니다.

 

내부 컬렉션을 수정하지 못하게 막는 방법으로, 애초에 컬렉션 값을 반환하지 않도록 할 수도 있습니다.

예를 들어 customer.orders.size() 와 같은 접근 방식을 customer.numberOfOrders() 처럼 바꾸는 것입니다.

 

다만 저자는 이 방법에 동의하지 않습니다. 최신 언어는 다양한 컬렉션 클래스들을 표준화된 인터페이스로 제공하여 다채롭게 조합할 수 있도록 지원하는데 반해, 이러한 전용 메서드들을 사용하게 되면 부가적인 코드가 늘어남과 동시에 언어가 지원하는 컬렉션 연산을 사용하기도 어려워지기 때문입니다.

 

다른 방법으로는 컬렉션을 제공하되, 읽기 전용으로만 제한을 두는 것입니다.

내부 컬렉션을 읽어오는 연산은 그대로 전달하고, 쓰기 요청은 예외를 던지는 방식으로 모두 막아버리는 것입니다. 이터레이터(iterator)나 열거형 객체를 기반으로 컬렉션을 조합하는 라이브러리들은 이러한 방식을 사용합니다.

 

가장 흔한 방식은 게터를 제공하되, 원본이 아닌 컬렉션의 복제본을 반환하는 방법입니다. 프로그래머가 원본을 수정할 목적으로 반환된 컬렉션을 조작해도 원본은 변하지 않습니다. 이 방법의 단점으로는 컬렉션의 크기가 많이 크다면 복제본을 생성하는 과정에서 성능 문제가 발생할 수 있다는 점인데, 실제로 그런 경우는 별로 없으니 웬만하면 그냥 사용하는 것을 추천합니다.

 

어떠한 방법을 사용할지는 프로그래머의 마음이지만, 위 방법들 중 하나를 골라 통일하는 편이 좋습니다. 여러개를 혼용할 경우 팀원들에게 혼란을 줄 수 있으니 주의해야합니다.

 

 

아래의 절차에 따라 컬렉션을 캡슐화하면 되겠습니다.

 

1. 컬렉션에 원소를 추가/제거하는 함수를 추가합니다.

2. 정적 검사(실제 실행 없이 코드를 분석하는 것, static program analysis)를 수행합니다.

3. 컬렉션을 참조하는 부분을 모두 찾아 새로 만든 함수로 변경해줍니다.

4. 정상적으로 작동하는지 테스트 합니다.

 

 

마무리

 

여기까지 기본적인 리팩토링 기법 후반부 내용과 캡슐화에 관련된 내용 일부를 마쳤습니다. 어렵지 않은 내용이다보니 (설명이 자세한 덕분이기도 하고) 막힘없이 읽혔던 것 같습니다. 그럼 본 스터디 포스팅으로 다시 오겠습니다.

반응형

댓글