리팩터링 2판 스터디: 8장 '기능 이동'
이 글은 리팩터링 2판 스터디 시리즈 중 하나입니다.
리팩터링 2판 스터디의 네 번째 글입니다. 여러 명이 나눠서 정리하는 스터디였기 때문에 이 글을 마지막으로 스터디는 끝나는데, 나머지 내용도 제 블로그에 정리할지는 좀 고민해 봐야겠어요.
이 장은 말 그대로 코드를 옮겨야 하는 상황과 그 방법에 대해 다룹니다. 절차적 프로그래밍에서 코드를 절차상 적절한™ 위치로 옮기는 건 아주 중요하죠.
목차
정리
8.1 함수 옮기기
모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다. 하지만 프로그램을 얼마나 잘 이해했느냐에 따라 구체적인 방법이 달라질 수 있다. 보통은 이해도가 높아질수록 소프트웨어 요소들을 더 잘 묶는 새로운 방법을 깨우치게 된다. 그래서 높아진 이해를 반영하려면 요소들을 이리저리 옮겨야 할 수 있다. – p.278
절차 (p.278)
- 선택한 함수가 현재 컨택스트에서 사용 중인 모든 프로그램 요소를 살펴본다. 이 요소들 중에도 함께 옮겨야 할 게 있는지 고민해본다.
- 선택한 함수가 다형 메서드인지 확인한다.
- 객체 지향 언어의 다형성(polymorphism)을 구현하는 메서드인지 확인한다는 의미입니다. 슈퍼클래스나 서브클래스에도 선언된 메서드라면 전부 같이 옮겨 줘야 하니까요.
- 선택한 함수를 타깃 컨텍스트로 복사한다(이 때 원래의 함수를 소스 함수라 하고 복사해서 만든 새로운 함수를 타깃 함수라 한다). 타깃 함수가 새로운 터전에 잘 자리잡도록 다듬는다.
- 정적 분석을 수행한다.
- 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
- 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
- 테스트한다.
- 소스 함수를 인라인(6.2절)할지 고민해본다.
요점
- 여러 개의 함수를 함께 옮기는 경우, 호출하는 함수보다 호출되는 함수를 먼저 옮기는 게 낫습니다.
- 원문에 “하위 함수들의 호출자가 고수준 함수 하나뿐이면 먼저 하위 함수들을 고수준 함수에 인라인한 다음...“이라는 내용이 있는데, 여기서 말하는 고수준 함수가 Higher-Order Function을 뜻하는 것인지 아니면 그냥 함수의 호출 관계를 하위/상위로 나눈 것인지 모르겠네요. 원서를 읽어 봐야 이해가 될 것 같습니다...
- 여러 개의 함수를 함께 옮기는 경우, 코드의 다른 곳에 미치는 영향이 적은 함수를 먼저 옮깁시다.
- 옮겨지는 함수가 원래 있던 곳(소스 컨텍스트)의 요소들을 사용한다면 그 요소들을 매개변수로 넘기거나 컨텍스트 자체를 참조(레퍼런스)로 넘겨 줍시다.
- 컨텍스트 자체를 넘겨 준다는 것의 예를 들자면,
SomeClass.method()
를OtherClass
로 옮기고 싶은데method
의 구현에SomeClass
의 필드들을 참조해야 한다면 아예OtherClass.method(someClass)
식으로SomeClass
의 인스턴스 참조를 넘길 수도 있다는 의미입니다.
- 컨텍스트 자체를 넘겨 준다는 것의 예를 들자면,
- 옮겨지는 함수를 호출하는 코드를 모두 한 번에 찾아서 바꾸는 게 아니라, 함수의 구현만 옮기고 기존 선언을 위임 함수(새로운 구현을 호출하는 함수)로 남겨두는 점이 특기할 만합니다. 이 위임 함수를 호출하는 코드를 직접 호출로 바꿀지는 마지막에 고민해도 괜찮으니 한 번에 최소한의 수정만 하자는 것이죠.
8.2 필드 옮기기
필드 옮기기 리팩터링은 대체로 더 큰 변경의 일환으로 수행된다. 예컨대 필드 하나를 잘 옮기면, 그 필드를 사용하던 많은 코드가 원래 위치보다 옮겨진 위치에서 사용하는 게 더 수월할 수 있다. 그렇다면 리팩터링을 마저 진행하여 호출 코드들까지 모두 변경한다. – p.290
절차 (p.290)
- 소스 필드가 캡슐화되어 있지 않다면 캡슐화한다.
- 테스트한다.
- 타깃 객체에 필드(와 접근자 메서드들)를 생성한다.
- 정적 검사를 수행한다.
- 소스 객체에서 타깃 객체를 참조할 수 있는지 확인한다.
- 참조할 수 없으면 참조하게 만듭시다(...)
- 접근자들이 타깃 필드를 사용하도록 수정한다.
- 테스트한다.
- 소스 필드를 제거한다.
- 테스트한다.
요점
- 크게 타깃 필드 만들기, 소스 컨텍스트의 접근자 수정하기, 소스 필드 없애기의 세 단계로 나눌 수 있겠습니다.
- 그런데 소스의 접근자는 그대로 둡니다. 이 시점에서 소스 컨텍스트의 접근자는 일종의 위임 함수가 된 상태인 셈인데, 위임 함수를 인라인할지 고민하는 건 별개의 문제라고 보는 것 같아요.
- 여러 소스에서 같은 타깃을 공유한다면(즉 소스 객체가 타깃 객체를 다대일 관계로 참조한다면), 우선 소스 필드를 남겨둔 상태로 세터가 타깃 필드와 소스 필드 모두를 갱신하게 한 다음 어서션(10.6절)을 통해 일관성을 깨뜨리는 갱신을 검출해 봅시다.
8.3 문장을 함수로 옮기기
중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다. 예컨대 특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에서 똑같은 코드가 추가로 실행되는 모습을 보면, 나는 그 반복되는 부분을 피호출 함수로 합치는 방법을 궁리한다. – p.296
절차 (p.297)
- 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이드하기(8.6절)를 적용해 근처로 옮긴다.
- 타깃 함수를 호출하는 곳이 한 곳뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사하고 테스트한다. 이 경우라면 나머지 단계는 무시한다.
- 호출자가 둘 이상이면 호출자 중 하나에서 '타깃 함수 호출 부분과 그 함수로 옮기려는 문장들을 함께' 다른 함수로 추출(6.1절)한다. 추출한 함수에 기억하기 쉬운 임시 이름을 지어준다.
- 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
- 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인(6.2절)한 후 원래 함수를 제거한다.
- 새로운 함수의 이름을 원래 함수의 이름으로 바꿔 준다(함수 이름 바꾸기, 6.5절).
- 더 나은 이름이 있다면 그 이름을 쓴다.
요점
- 단순히 원래 있던 함수 안으로 코드를 옮기는 게 아니라, 원래 함수와 함수 안으로 옮길 코드를 함께 호출하는 새로운 함수를 만들고, 각각의 호출을 이 새 함수로 바꿔 가며 테스트한 다음, 그제서야 문제가 없으면 원래 함수의 내용을 새 함수 안으로 옮깁니다. 이렇게 하면 반복 코드의 미묘한 차이로 문제가 생기는 상황을 잡아낼 수 있습니다.
- 완전히 하나의 함수로 합칠 수 있는 상황은 아니지만 여전히 해당 함수와 함께 사용되어야만 하는 코드라면 (예를 들어 어떤 코드가 어느 함수를 호출할 때만 사용되지만, 그 함수를 호출할 때마다 그 코드를 사용하는 것은 아니라면) 함수 안으로 옮기는 대신 위 절차의 4번까지만 수행해서 새로운 함수로 만듭니다.
8.4 문장을 호출한 곳으로 옮기기
앞 절의 반대입니다. 앞 절에서는 어떤 함수를 호출할 때 함수 밖에서 항상 동일한 동작을 수행한다면 그 동작을 함수 안으로 옮기는 방식이었는데, 반대로 어떤 함수를 호출할 때 그 함수 안의 한 부분이 경우에 따라 다르게 동작해야 한다면 그 부분을 함수 밖으로 꺼냅니다.
절차 (p.302)
- 호출자가 한두 개뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음(혹은 마지막) 줄(들)을 잘라내어 호출자(들)로 복사해 넣는다(필요하면 적당히 수정한다). 테스트만 통과하면 이번 리팩터링은 여기서 끝이다.
- 더 복잡한 상황에서는, 이동하지 '않길' 원하는 모든 문장을 함수로 추출(6.1절)한 다음 검색하기 쉬운 임시 이름을 지어준다.
- 원래 함수를 인라인(6.2절)한다.
- 추출된 함수의 이름을 원래 함수의 이름으로 변경한다(함수 이름 바꾸기, 6.5절).
- 더 나은 이름이 있다면 그 이름을 쓴다.
요점
- '이동하지 않길 원하는 문장을 추출'하는 상황에서, 대상 함수가 다형 메서드라면 모든 구현에서 남길 부분을 메서드로 추출한 다음 (이 부분은 당연히 모든 구현에서 동일해야 합니다) 슈퍼클래스의 메서드만 남깁니다. 이 때 남길 부분은 모든 구현에서 동일한 부분이어야 합니다.
8.5 인라인 코드를 함수 호출로 바꾸기
이미 존재하는 함수와 똑같은 일을 하는 인라인 코드를 발견하면 보통은 해당 코드를 함수 호출로 대체하길 원할 것이다. 예외가 있다면 [...] 기존 함수의 코드를 수정하더라도 인라인 코드의 동작은 바뀌지 않아야 할 때뿐이다. 이 경우인가를 판단하는 데는 함수 이름이 힌트가 된다. 이름을 잘 지었다면 인라인 코드 대신 함수 이름을 넣어도 말이 된다. – p.308
절차 (p.309)
- 인라인 코드를 함수 호출로 대체한다.
- 테스트한다.
요점
- 이 절은 절차가 수상하게 단순한데, 인라인 코드가 이미 다른 함수에 구현된 일을 한다는 것이 명확한 상황에서 그 코드를 함수로 대체하는 일에 대한 내용이기 때문입니다. 그냥 바퀴를 재발명하지 말자는 내용입니다.
- 자주 사용되는 코드가 있고, 그 코드를 라이브러리 함수나 기존에 구현된 다른 함수로 대체할 수 없다면 함수 추출하기(6.1절)를 사용해서 함수로 만들어 줍시다.
8.6 문장 슬라이드하기
관련 코드끼리 모으는 작업은 다른 리팩터링(주로 함수 추출하기; 6.1절)의 준비 단계로 자주 행해진다. 관련 있는 코드들을 명확히 구분되는 함수로 추출하는 게 그저 문장들을 한데로 모으는 것보다 나은 분리법이다. 하지만 코드들이 모여 있지 않다면 함수 추출은 애초에 수행할 수조차 없다. – p.310
절차 (p.311)
- 코드 조각(문장들)을 이동할 목표 위치를 찾는다. 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 훑어보면서, 조각을 모으고 나면 동작이 달라지는 코드가 있는지 살핀다.
- 다음과 같은 간섭이 있다면 포기한다.
- 코드 조각에서 참조하는 요소를 선언하는 문장 앞으로는 이동할 수 없다.
- 코드 조각을 참조하는 요소의 뒤로는 이동할 수 없다.
- 코드 조각에서 참조하는 요소를 수정하는 문장을 건너뛰어 이동할 수 없다.
- 코드 조각이 수정하는 요소를 참조하는 요소를 건너뛰어 이동할 수 없다.
- 다음과 같은 간섭이 있다면 포기한다.
- 코드 조각을 원래 위치에서 잘라내어 목표 위치에 붙여넣는다.
- 테스트한다.
요점
- 이 책에서 가장 자주 언급되는 리팩터링 기법 중 하나인 것 같은데, 말 그대로 코드 블록을 위아래로 옮기는 것입니다. VS Code에서
Alt+↑
아니면Alt+↓
로 하는 그거죠. - 절차에서 '다음과 같은 간섭'을 언급하는데, 간단히 요약하자면 코드의 인과 관계를 유지하는 선에서 옮겨야 한다는 것입니다.
- 한 문장만 옮길 때는 '문장 교환하기(Swap Statement)'라고 부른다고 하네요.
8.7 반복문 쪼개기
종종 반복문 하나에서 두 가지 일을 수행하는 모습을 보게 된다. 그저 두 일을 한꺼번에 처리할 수 있다는 이유에서 말이다. 하지만 이렇게 하면 반복문을 수정해야 할 때마다 두 가지 일 모두를 잘 이해하고 진행해야 한다. 반대로 각각의 반복문으로 분리해 두면 수정할 동작 하나만 이해하면 된다. – p.316
절차 (p.317)
- 반복문을 복제해 두 개로 만든다.
- 반복문이 중복되어 생기는 부수효과를 파악해서 제거한다.
- 테스트한다.
- 완료됐으면, 각 반복문을 함수로 추출(6.1절)할지 고민해본다.
요점
- 이 리팩터링은 각 반복문을 다른 함수로 추출해야 할지 고민해 보는 것으로 마무리됩니다. 하나의 반복문 안에서 여러 일이 일어나고 있어서 쪼개야 한다면, 그 함수가 여러 기능을 수행하고 있다는 신호일 수 있기 때문입니다.
- 본문에서는 한 문단을 할애하여 최적화를 할 거면 리팩터링을 먼저 하고 하라고 호소하고 있습니다. '반복문 하나에서 처리할 수 있는 일을 왜 굳이 쪼개지?!' 라는 생각이 들 때가 있긴 하지만, 미래의 우리는 그 반복문의 역할을 까먹을 것이므로 유지보수를 위해 역할별로 쪼개어 주는 것이 아무래도 좋겠습니다.
8.8 반복문을 파이프라인으로 바꾸기
논리를 파이프라인으로 표현하면 이해하기 훨씬 쉬워진다. 객체가 파이프라인을 따라 흐르며 어떻게 처리되는지를 읽을 수 있기 때문이다. – p.320
절차 (p.320)
- 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
- 반복문의 첫 줄부터 시작해서, 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다. 이때 컬렉션 파이프라인 연산은 1. 에서 만든 반복문 컬렉션 변수에서 시작하여, 이전 연산의 결과를 기초로 연쇄적으로 수행된다. 하나를 대체할 때마다 테스트한다.
- 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.
요점
filter
,map
,reduce
같은 컬렉션 파이프라인을 쓰자는 내용입니다. 아직 안 쓰고 계셨다면 쓰세요. 두 번 쓰세요.
8.9 죽은 코드 제거하기
코드가 더 이상 사용되지 않게 됐다면 지워야 한다. 혹시 다시 필요해질 날이 오지 않을까 걱정할 필요 없다. 우리에겐 버전 관리 시스템이 있다! – p.327
절차 (p.328)
- 죽은 코드를 외부에서 참조할 수 있는 경우라면(예컨대 함수 하나가 통째로 죽었을 때) 혹시라도 호출하는 곳이 있는지 확인한다.
- 없다면 죽은 코드를 제거한다.
- 테스트한다.
요점
- 안 쓰는 코드를 주석 처리하지 말고 지우라고 해서 좀 찔립니다. 깃을 열심히 써야겠어요.
Dani Soohan Park (@heartade)
Follow this blog at Fediverse: @heartade@blog.heartade.dev
Follow my shorter shoutouts at Fediverse: @heartade@social.silicon.moe
Follow me at Bluesky: @heartade.dev