8장 기능이동에 나오는 기법들은 다음과 같다.
- 함수 옮기기
- 필드 옮기기
- 문장을 함수로 옮기기
- 문장을 호출한 곳으로 옮기기
- 문장 슬라이드 하기
- 인라인 코드를 함수 호출로 바꾸기
- 반복문 쪼개기
- 반목문을 파이프 라인으로 바꾸기
- 죽은 코드 제거하기
설명만 봐도 대충 느낌오는 한 함수, 필드 옮기기, 반복문 제거 관련 기법, 죽은 코드 제거하기의 기법들이 있는 반면
문장에 대한 기법중 "문장" 이 의미하는게 뭔지 궁금해진다.
때로는 한 덩어리의 문장들이 기존 함수와 같은 일을 할 때가 있다.
라는 설명을 봤을때 처음 든 생각은 그냥 컨텍스트를 부여하지 않은 코드를 문장이라고 표현하신 듯 하다.
1. 함수 옮기기
[왜 하는가?]
모듈성 (Modularity)
- 모듈화가 얼마나 잘 되어 있느냐
- 프로그램의 어떤 부분을 수정할때 해당 기능과 깊이 관련된 작은 일부만 이해해도 수정이 가능하도록 하는 능력
-> 기능 수정시 하나의 함수 또는 하나의 클래스 또는 하나의 파일 안에 코드만 이해해도 수정을 달성할 수 있는 코드가 모듈성이 높은 코드로 이해했다.
결국 응집성이 높은 코드를 말하는데 리팩토링에서 모듈성을 언급하는 이유는
처음부터 어떤 코드가 해당 기능과 밀접한 관련이 있는지 모르기 때문에
개발하는 과정에서 프로그램에 대한 이해도가 높아지며 높아진 이해를 토대로 관련된 요소를 옮겨야 되기 때문이다.
[절차]
1. 선택한 함수가 현재 컨텍스트에서 사용 중인 모든 프로그램 요소를 살펴보고 같이 옮겨야 하는지 판단한다.
- 호출되는 함수중 같이 옮겨야 할 게 있을경우 그 함수 부터 옮기는게 좋다.
- 얽혀 있는 함수가 여러개라면 다른곳에 미치는 영향이 적은 함수부터 옮긴다.
- 하위 함수들의 호출자가 고수준 함수 하나면, 하위 함수를 없애고 고수준 함수에 해당 내용을 포함한 다음(함수 인라인)
고수준 함수를 옮기고 다시 하위 함수를 그곳에서 추출한다.
2. 선택함 함수가 다형 메서드(오버라이드 오버로드된 함수인지) 확인 한다.
3. 원래의 함수를 source function이라고 하고 새로운 함수를 target function이라고 해서 target function이 새로운 터전에 자리잡도록 (옮기고 빌드 되도록) 다듬는다.
- 소스 컨텍스트의 요소를 사용하면 해당 요소를 매개변수로 넘기거나 컨텍스트 자체를 넘겨준다.
- target function이 맥락에 맞게 새로운 이름을 가질 수도 있다.
4. 정적 분석을 수행한다.
5. 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
6. 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
7. 테스트 한다.
8. 소스 함수를 인라인 할지 고민한다.
- 소스 함수는 위임함수로 남겨둘 수 있는데 왠만하면 중간 단계는 제거하는게 좋다.
절차를 봤을때 리팩토링의 핵심인 리팩토링 과정에서 프로그램이 깨지지 않도록 하는 핵심을 지키며 단계를 수행한다는 생각이 들었다.
나같은 경우 함수를 옮길땐 함수 전체를 잘라내어 원하는 위치에 붙여넣은 다음 필요한 요소들(파라미터, 전역 변수 등등)을 어떻게든 연결 시키는 2가지 스텝으로 수행했는데 결국 함수를 좀더 맞는 맥락에 재배치 하는 것 보다 요소들을 어거지로 참조하는 방법이 적절하지 않아 되돌렸던 기억들이 있다.
책에서는 하위 함수를 고수준 함수에 다 포함시킨뒤 필요한 요소들을 넘겨주고 중간 함수 source function을 통해 중간에도 빌드가 되도록 하는게 필요한 스텝일지는 의문이다. 또한 정적 분석, 린트확인 이런것도 이게 예제코드가 js라서 그런건가..?
예제코드는 중첩 함수 옮기기와 다른 클래스로 옮기기가 있는데
중첩함수 옮기기 예제에서 키포인트는 관련된 하위 메서드를 같이 옮길지에 대한 판단에서 서로에게 의존도가 높기 때문에 같이 옮기는 결정을 했고 중간 함수를 없앨지에 대한 판단에서 호출자가 많지않은 지역화된 함수라는 이유로 대리자 함수(소스함수)를 제거했다.
다른 클래스로 옮기는 예제에서는 기존 은행 이자 계산 기능에 계좌 종류에 따라 다른 이자 계산 알고리즘을 적용할 수 있도록 리팩토링 하는데, 계좌 종류에 따라 영향을 받는 함수들을 계좌 종류 클래스인 AccountType으로 옮기고 영향을 받지 않는 (계좌별로 달라지는 메서드)는 그대로 놔두는 결정을 한다. 또한 메서드에 필요한 변수를 넘길때 계좌 정보를 통째로 넘길지 또는 필요한 데이터만 넘길지에 대한 판단에서 계좌정보에서 하나의 값만 필요하기 때문에 daysOverdrawn만 넘겨주도록 수정했다.
두 예제 전부 지역화된 함수라는 이유로 위임 메서드(소스함수)를 제거했는데
반대로 넘겨지는 파라미터가 많을 경우 위임 메서드를 남겨 둬야 하는지는 의문이다.
대부분의 경우에 제거하는게 맞을 것 같은데 놔둬야 하는 예시가 있을까
2. 필드 옮기기
[왜 하는가?]
데이터 구조
- 프로그램의 진짜 힘은 동작을 나타내는 코드도 있지만 데이터 구조에서 나온다.
- 문제에 대한 적합한 데이터 구조를 활용하면 코드는 단순하고 직관적으로 짜여진다.
-> 결국 적합한 데이터 구조를 차용하는게 중요한데 함수 옮기기 처럼 개발 과정에서 적합한 데이터 구조는 계속 변경되기 때문에
단순하고 직관적인 코드를 위해 데이터 구조를 그때그때 알맞게 리팩토링 하는게 중요하다.
만약 어떤 함수에 레코드를 넘길 때 마다 다른 레코드의 필드 (쓸모없는 데이터)도 넘기고 있다면 그건 데이터의 위치를 옮기라는 뜻이다.
또한 레코드를 변경할 때 다른 레코드까지 변경되어야 할 경우 또한 위치가 잘못되었다는 신호다.
레코드 (또는 클래스, 객체)를 변경하는 작업은 더 큰 변경의 일환으로 수행되는데 당연히 해당 필드를 호출하는 모든 코드에 대해서 수정해야하기 때문이다. 당장 필드를 옮길 수 없을 경우는 사용 패턴을 먼저 리팩터링 한 다음 필드를 옮겨준다.
클래스의 데이터 들은 캡슐화 되어있는게 옮기는 작업을 쉽게 해준다.(접근자만 바꿔주면 클라이언트 코드는 수정이 불필요하기 때문)
[절차]
1. 소스 필드가 캡슐화 되어 있지 않다면 캡슐화 한다. (직접 접근이 아닌 접근자를 사용)
2. 테스트 한다.
3. 타깃 객체에 필드(와 접근자 메서드)를 생성한다.
4. 정적 검사를 수행한다.
5. 소스 객체에서 타깃 객체를 참조할 수 있는지 확인한다.
- 기존 필드나 메서드 중 타깃 객체를 넘겨주는게 있을 수 있는데 만약 없다면 이런 기능이 메서드를 쉽게 만들 수 있는지 고민한다.
- 없다면 타깃 객체를 저장할 새 필드를 만든다. (더 넓은 맥락에서 리팩토링 하면 다시 없앨 수 있을것)
6. 접근자들이 타겟 필드를 사용하도록 수정한다.
- 여러 소스에서 같은 타겟을 공유하면, 먼저 세터를 수정하고, 타겟 필드와 소스필드 모두를 갱신하게 한다.
7. 테스트 한다.
8. 소스 필드를 제거한다.
9. 테스트 한다.
간단한 예시코드를 통해 접해서 그런지 소스필드와 타겟 객체로 나눠서 안전하게 필드를 이사시킨 뒤에 소스 필드에서 제거하는 방식처럼 저렇게 까지 프로그램을 깨지 않으면서 작업을 해야하나라는 생각이 먼저 들었다.
클라이언트 개발자의 입장에서 대부분의 엔티티들은 서버쪽에서 내려오는 응답 구조와 거의 동일하게 사용하고 있는데 물론 서버쪽에서 작업을 할때 도메인에 따라 모델링을 고려했겠지만 api를 붙이다 보면 계속해서 중복되는 필드나 관련없는 필드가 같이 내려오는 등의 문제를 경험했던 것 같다. DAO, DTO로 나눠서 모델을 정의할 수도 있겠지만 그것도 과연 좋은 방식일지는 의문이다.
결국 사용하고 있는 방법은 모델에 프로토콜을 적용하여 추상화를 적용하고 있는데 필드이동과 같은 근본적인 해결책은 아닌 것 같다.
3. 문장을 함수로 옮기기
[왜 하는가?]
DRY한 코드를 위해
- 중복 제거는 코드를 건강하게 관리하는 방법
- 중복을 제거하면 해당 코드만을 수정하면 되지만 중복이 있을경우 모든 중복 케이스에 수정을 해야하기 때문
피호풀 함수의 일부인것 처럼 항상 같이 실행되는 문장들이라면, 다른 말로 함수와 한 몸은 아니지만 여전히 함께 호출돼야 하는 경우라면 단순히 치호출 함수를 통째로 또 하나의 함수로 호출한다.
[절차]
1. 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이드하기를 적용해 근처로 옮긴다.
2. 타깃 함수를 호출하는 곳이 한 곳 뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사하고 테스트 한다.
- 해당되는 경우(호출되는 곳이 한곳) 이면 나머지 단계는 무시한다.
3. 호출자가 둘 이상이면 호출자 중 하나에서 '타깃 함수 호출 부분과 그 함수로 옮기려는 문장들을 함께' 다른 함수로 추출한다. 추출한 함수에 기억하기 쉬운 임시 이름을 지어준다.
4. 다른 호출자가 모두 방금 추출한 함수를 사용하도록 수정한다. 하나씩 수정할 때마다 테스트 한다.
5. 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
6. 새로운 함수의 이름을 원래 함수의 이름으로 바꿔준다.
- 더 나은 이름이 있다면 그걸로 쓴다.
예제에서는 사진 관련 데이터를 HTML로 내보내는 코드를 리팩토링한다.
renderPerson과 photoDiv 함수에서 제목을 출력하는 문장(코드)와 emitPhotoData()가 반복되는데
먼저 제목을 출력하는 코드와 emitPhotoData()가 항상 같이 실행되므로 제목 출력 문장을 emitPhotoData()로 이동시킨다.
위 리팩토링 기법은 따로 기법을 공부하지 않더라도 자연스럽게 행하고 있는 방식인것 같다.
4. 문장을 호출한 곳으로 옮기기
[왜 하는가?]
올바른 추상화의 경계를 위해..?
- 응집도란 추상화의 경계가 올바를때 높은 응집도를 가질 수 있다.
- 추상화의 경계는 기능 범위가 달라짐에 따라 같이 달라질 수 있다.
- 예를들어 여러곳에서 사용하는 기능이 일부 호출자일때는 다르게 동작해야 하는 경우이다.
3번 문장을 함수로 옮기기의 반대 기법이다.
문장을 함수로 옮기기가 반복이 나타나는 코드를 뭉쳐서 함수화 하는 기법었다면
문장을 호출한 곳으로 옮기기는 어떤 이유로(호출자에 따라) 동작이 달라져야 할때 사용한다.
[절차]
1. 호출자가 한두 개 뿐이고 피호출 함수도 간단한 상황이라면, 피호출 함수의 처음( 혹은 마지막) 줄을 잘라내어 호출자로 복사해 넣는다.
- 테스트만 통과하면 리팩터링은 여기서 끝이다.
2. 더 복잡한 상황에서는, 이동하지 않길 원하는 모든 문장을 함수로 추출한 다음 검색하기 쉬운 임시 이름을 지어준다.
3. 원래 함수를 인라인 한다.
4. 추출된 함수의 이름을 원래 함수의 이름으로 변경한다.
- 물론 더 나은 이름이 있다면 그걸로 수정
예제코드로는 두곳에서 호출되는 emitPhotoData() 함수가 있는데 emitPhotoData()의 동작중 일부를 호출하는 곳에 따라 다르게 동작하도록 수정하고 있다. 이전과 같이 빌드가 깨지지 않도록 임시 함수를 만들어 코드를 옮긴다.
내 비슷한 경험으로는 여러곳에서 (대략 한 10곳 이상..?) 사용하는 메서드가 있는데 파라미터를 하나 추가하여 다른 동작을 해야할 경우가 있었다. 이때 파라미터를 추가하면서 기존의 코드는 수정하지 않는 방법이 있었는데 아래와 같이 default 값을 가지는 파라미터를 통해 이미 쓰이고 있는 곳에서는 함수 시그니쳐를 수정할 필요없는 방식으로 작업했다.
//기존 코드
func doSomething(param1: Int, param2: Int) {
}
//수정 코드
func doSomething(param1: Int, param2: Int, really: Bool = false) {
if really {
}
}
doSomething(2,4)
doSomething(2,4,true)
예제 코드에서는 문장을 호출한 곳으로 옮기는게 맞는 것 같은데 10개중 한개의 경우에만 동작이 달라진다면 위의 방식도 나쁘지 않는것 같다..?
5. 인라인 코드를 함수 호출로 바꾸기
[왜 하는가?]
코드를 함수화 하면 장점이 있다.
- 똑같은 일을 하는 코드를 함수 호출로 대체할 수 있다.
- 함수의 이름으로 해당 코드의 역할을 설명할 수 있다.
- 수정이 일어났을때 각각의 코드 수정이 아닌 함수 하나를 수정하면 된다.
결국에 코드 뭉치를 함수화 하면 코드의 가독성이 높아진다.
6.1장의 함수 추출하기와 차이점은 인라인 코두를 대체할 함수의 유무이다.
인라인할 코드를 대체할 함수가 존재하지 않으면 함수 추출하기이고, 있으면 인라인 코드를 함수 호출로 바꾸는 기법을 적용한다.
[절차]
1. 인라인 코드를 함수 호출로 대체한다.
2. 테스트 한다.
6. 문장 슬라이드 하기
[왜 하는가?]
관련된 코드를 모으기 위해
- 다른 리팩터링의 준비단계
반복을 발견하려면 패턴을 발견해야 되는데 일단 관련된 코드를 모아두어야 반복을 발견하기 쉬울 것 같다.
[절차]
1. 코드 조각(문장들)을 이동할 목표 위치를 찾는다. 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 보면서 조각을 모으고 나면 동작이 달라지는 코드가 있는지 살핀다. 아래의 간섭이 있다면 리팩터링을 포기한다.
- 코드 조각에서 참조하는 요소를 선언하는 문장 앞으로는 이동할 수 없다.
- 코드 조각을 참조하는 요소의 뒤로는 이동할 수 없다.
- 코드 조각에서 참조하는 요소를 수정하는 문장을 건너뛰어 이동할 수 없다.
- 코드 조각이 수정하는 요소를 참조하는 요소를 건너뛰어 이동할 수 없다.
2. 코드 조각을 원래 위치에서 잘라내어 목표 위치에 붙여 넣는다.
3. 테스트 한다.
코드의 순서가 프로그램의 동작 방식을 바꿀 수 있는 경우를 조심해야 한다.
예제코드는 위와 관련된 예시를 설명하고 있다.
7. 반복문 쪼개기
[왜 하는가?]
반복문 하나에서 두가지 일을 수행하는 경우
- 반복문을 수정 할 경우 두가지 동작 전부 잘 이해해야 한다.
[절차]
1. 반복문을 복제해 두 개로 만든다.
2. 사이드 이펙트를 제거한다.
3. 테스트 한다.
4. 각 반복문을 함수로 추출할지 고민한다.
성능에 대한 고민이 있을 수 있는데 리팩토링을 한 뒤에 성능에 대한 관점에서 코드를 보는게 더 효과적이다.
성능에 발목을 잡는 병목 코드 일부를 개선하면 훨씬 더 높은 개선을 할 수 있기 때문에
8. 반복문을 파이프라인으로 바꾸기
[왜 하는가?]
처리 과정을 일련의 연산으로 표현할 수 있어서
[절차]
1. 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
2. 반복문의 첫 줄부터 시작해서 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다. 이때 컬렉션 파이프라인연산은 1에서 만든 반복문 컬렉션 변수에서 시작하고, 이전 연산의 결과를 기초로 연쇄적으로 수행한다.
3. 반복문의 모든 동작을 대체했다면 반목문 자체를 지운다.
설명이 어렵지만 map, filter, reduce 등 고차함수를 사용하여 컬렉션의 iteration을 stream으로 처리하는걸로 이해했다.
+ 추가)
고차함수를 사용하면 각각의 쓰레드에서 비동기 실행의 장점으로 성능상의 개선이 있을 수 있다.
(예를들어 filter와 map 이 있는 경우 filter와 map 이 동시에 동작)
하지만 서버 개발자의 경우 api 콜에 대해서 하나의 쓰레드만 할당하는 상황이라면 이러한 파이프라인의 사용시 쓰레드가 핸들링 할 수 없을 정도로 많아 질 수 있어 오히려 성능 저하로 이어질 수 있다고 한다. (물론 최대 사용 쓰레드 수를 제한할 수 있지만 모든 반복문에 대해서 적용하기 어렵기 때문에)
9. 죽은 코드 제거하기
[왜 하는가?]
왜 안하는가
[절차]
'Refactoring' 카테고리의 다른 글
리팩터링 2판 - 02. 리팩터링 원칙 키워드 정리 (0) | 2021.10.14 |
---|---|
리팩터링 2판 - 01. 리팩터링: 첫번째 예시 [Swift] (1) | 2021.09.29 |
댓글