Thief of Wealth
article thumbnail

함수는 프로그램을 작은 부분으로 나누는 주된 수단이다.

함수 선언은 각 부분이 서로 맞물리는 방식을 표현하며, 실질적으로 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다.

건축과 마찬가지로 소프트웨어도 이러한 연결부에 상당히 의존한다.

연결부를 잘 정의하면 시스템에 새로운 부분을 추가하기가 쉬워지는 반면, 잘못 정의하면 지속적인 방해 요인으로 작용하여 소프트웨어 동작을 파악하기 어려워지고 요구사항이 바뀔 때 적절히 수정하기 어렵게 한다.

 

다행히 소프트웨어는 소프트하기 때문에 연결부를 수정할 수 있다.

단 주의해서 해야 한다.

 

이러한 연결부에서 가장 중요한 요소는 함수의 이름이다.

이름이 좋으면 함수의 구현 코드를 살펴볼 필요 없이 호출 문만 보고도 무슨 일을 하는지 파악할 수 있다.

하지만 좋은 이름을 떠올리기란 쉽지 않다.

저자도 적합한 이름을 단번에 지은 적이 거의 없다고 한다.

 

코드를 읽다가 의미가 와닿지 않는 이름을 발견해도 그대로 놔두고 싶은 유혹에 빠진다.

고작 이름일 뿐이지 않은가?

 

하지만 이는 '혼란'이라는 악마의 유혹이다.

프로그램의 영혼을 위해서라도 이러한 달콤한 속삭임에 절대 넘어가면 안 된다.

그래서 나는 이름이 잘못된 함수를 발견하면 더 나은 이름이 떠오르는 즉시 바꾸라는 명령으로 받아들인다.

그래야 나중에 그 코드를 다시 볼 때 무슨 일을 하는지 또 고민하지 않게 된다.

 

* 좋은 이름을 떠올리는데 효과적인 방법 => 주석을 이용해서 함수의 목적을 설명해보는 것이다. 그러면 주석이 멋진 이름으로 바뀌어 돌아올 수 있다.

 

함수의 매개변수도 마찬가지다. 매개변수는 함수가 외부 세계와 어우러지는 방식을 정의한다.

매개변수는 함수를 사용하는 문맥을 설정한다.

예컨대 전화번호 포매팅 함수가 매개변수로 사람을 받는다고 해보자.

그러면 회사 전화번호 포매팅에는 사용할 수 없게 된다.

사람 대신 전화번호 자체를 받도록 정의하면 이 함수의 활용범위를 넓힐 수 있다.

 

이렇게 하면 활용 범위가 넓어질 뿐만 아니라, 다른 모듈과의 결합을 제거할 수도 있다.

예컨대 전화번호 포매팅 로직을 사람 관련 정보를 전혀 모르는 모듈에 둘 수 있다.

동작에 필요한 모듈 수가 줄어들수록 무언가를 수행할 때, 머리에 담아둬야 하는 내용도 적어진다.

그리고 내 머리도 예전만 못하다.

 

매개변수를 올바르게 선택하기란  단순히 규칙 몇 개로 표현할 수 없다.

예컨대 대여한 지 30일이 지났는지를 기준으로 지불 기한이 넘었는지 판단하는 간단한 함수가 있다고 해보자.

이 함수의 매개변수는 지불 객체가 되어야 할까, 아니면 마감일로 해야 할까?

 

지불 객체로 정하면 이 함수는 지불 객체의 인터페이스가 결합되어 버린다.

대신 지불이 제공하는 여러 속성에 쉽게 접근할 수 있어서 내부 로직이 복잡해지더라도 이 함수를 호출하는 코드를 일일이 찾아서 변경할 필요가 없다.

실질적으로 함수의 캡슐화 수준이 높아지는 것이다.

 

이 문제의 정답은 바로 정답이 없다는 것이다. 특히 시간이 흐를수록 더욱더 그렇다.

따라서 어떻게 연결하는 것이 더 나은지 더 잘 이해하게 될 때마다 그에 맞게 코드를 개선할 수 있도록 함수 선언 바꾸기 리팩터링과 친숙해져야만 한다.

 

나는 다른 리팩터링을 지칭할 때도, 대체로 대표 명칭만 사용한다.

하지만 함수 선언 바꾸기에서 '이름 바꾸기'라고 표현해서 확실히 구분할 것이다.

이름을 바꿀 때는 매개변수를 변경할 때든 절차는 똑같다.

 

절차

이 책에서 다른 리팩터링들은 절차를 한 가지만 소개하는데, 방법이 하나뿐이라서가 아니라, 대부분 상황에서 대체로 효과적인 방법이기 때문이다.

하지만 함수 선언 바꾸기는 사정이 다르다.

"간단한 절차"만으로 충분할 때도 있지만, 더 세분화된 "마이그레이션 절차"가 훨씬 적합한 경우도 많기 때문이다.

따라서 이 리팩터링을 할 때는 먼저 변경 사항을 살펴보고 함수 선언과 호출 문을 단번에 고칠 수 있을지 가늠해본다.

가능할 것 같다면 간단한 절차를 따른다.

마이그레이션 절차를 적용하면 호출 문들을 점진적으로 수정할 수 있다.

호출하는 곳이 많거나, 호출 과정이 복잡하거나, 호출 대상이 다형 메서드 거나, 선언을 복잡하게 변경할 때는 이렇게 해야 한다.

 

- 간단한 절차

1. 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.

2. 메서드 선언을 원하는 형태로 바꾼다.

3. 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.

4. 테스트한다.

 

변경할 게 둘 이상이면 나눠서 처리하는 편이 나을 때가 많다. 따라서 이름 변경과 매개변술 추가를 모두하고 싶다면 각각을 독립적으로 처리하자. (그러다 문제가 생기면 작업을 되돌리고 마이그레이션 절차를 따른다.)

 

- 마이그레이션 절차

1. 이어지는 추출 단계를 수월하게 만들어야 한다면, 함수의 본문을 적절히 리팩터링한다.

2. 함수의 본문을 새로운 함수로 추출한다.

3. 추출한 함수에 매개변수를 추가해야 한다면, '간단한 절차'를 따라 추가한다.

4. 테스트한다.

5. 기존 함수를 인라인 한다.

6. 이름을 임시로 붙여뒀다면, 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름을 되돌린다.

7. 테스트 한다.

 

다형성을 구현한 클래스, 즉 상속 구조속에 있는 클래스의 메셔드를 변경할 때는 다형 관계인 다른 클래스에도 변경이 반영되어야 한다.

이때 ,상황이 복잡하기 때문에 간접 호출 방식으로 우회 (혹은 중단 단계로 활용)하는 방법도 쓰인다.

 

먼저 원하는 형태의 메서드를 새로 만들어서,

원래 함수를 호출하는 전달 메서드로 활용하는 것이다.

단일 상속 구조라면 전달 메서드를 슈퍼클래스에 정의하면 된다. (덕 타이핑 처럼)

슈퍼클래스와의 연결을 제공하지 않는 언어라면 전달 메서드를 모든 구현 클래스 각각에 추가해야 한다.

 

공개된 API를 리팩터링할 때에는 새 함수를 추가한 다음 리팩터링을 잠시 멈출 수 있다.

이 상태에서 예전 함수를 폐기 대상으로 지정하고, 모든 클라이언트가 새 함수로 이전할 때까지 기다린다.

클라이언트들이 모두 이전했다는 확신이 들면 예전 함수를 지운다.

 

 

간단한 절차 예제를 한번 살펴보자.

이 함수의 이름을 이해하기 쉽게 바꾸려고 한다.

 

절차대로, 먼저 함수 선언부터 수정하자.

다음으로, cirnum()를 호출한 곳을 모두 찾아서 circumference로 바꿔준다.

 

기존 함수를 참조하는 곳을 얼마나 쉽게 찾을 수 있는 지는 개발 언어에 영향을 받는다.

정적 타입 언어와 뛰어난 IDE의 조합이라면, 함수 이름 바꾸기를 자동으로 처리할 수 있고,

그 과정에서 오류가 날 가능성도 거의 없다.

정적 타입 언어가 아니라면 검색 가능이 뛰어난 도구더라도 잘못찾는 경우가 있어서 일거리가 늘어난다.

 

매개변수 추가나 제거도 똑같이 처리한다.

함수를 호출하는 부분을 모두 찾은 뒤, 선언문을 바꾸고, 호출문도 그에 맞게 바꾼다.

이 각각의 단계를 순서대로 처리하는 편이 대체로 좋다.

함수 이름 바꾸기와 매개변수 추가를 모두 할 때는 이름부터 바꾸고, 테스트하고, 매개변수를 추가하고, 또 테스트하는 식으로 진행한다.

 

간단한 절차의 단점을 호출문과 선언문을 한번에 수정해야 한다는 것이다. 수정할 부분이 몇개 없거나 괜찮은 리팩터링 도구를 사용한다면 그리 어렵지 않다.

 하지만 수정할 부분이 많다면 일이 힘들어진다. 같은 이름이 여러개일 때도 문제다. 예컨데 changeAddress()라는 메서드가 사람 클래스와 계약 클래스 모두에 정의되어 있을 때, 사람 클래스의 메서드란 이름을 바꾸고 싶은 경우 난감해질 수 있다.

 

나는 변경 작업이 복잡할수록 한 번에 진행하기를 꺼린다. 그래서 이러한 상황에 처리한 마이그레이션 절차를 따른다.

마찬가지로 간단한 절차를 따르다가 문제가 생겨도 코드를 가장 최근의 상태로 돌리고나서 마이그레이션 절차에 따라 다시 진행하낟.

 

마이그레이션의 예제를 한번 살펴보자.

위 코드를 마이그레이션 헤보자.

 

일단 함수 본문 전체를 새로운 함수로 추출하고 => 테스트하고 => 예전함수를 인라인한다.

그러면 예전 함수를 호출하는 부분이 모두 새함수를 호출하도록 바꾼다.

하나를 변경할 때마다 테스트하면서 한 번에 하나씩 처리하자.

모두 바꿨다면 기존 함수를 삭제한다.

 

리팩터링 대상을 대부분 직접 수정할 수 있는 코드지만, 함수 선언 바꾸기 만큼을 공개된 API, 다시 말해서 직접 고칠 수 없는 외부 코드가 사용하는 부분을 리팩터링하기에 좋다.

가령 circumference() 함수를 만들고 나서 리팩터링 작업을 멈춘다.

가능하다면 circum()이 폐기 예정임을 표기한다. 그런 다음 circum()의 클라이언트를 모두가 circumference()를 사용하게 바뀔 때까지 기다린다.

모든 클라이언트가 새 함수로 갈아탔다면, circum()을 삭제한다.

 

circum()을 삭제하는 상쾌한 순간을 결코 맞이하지 못할 수도 있지만, 새로 작성되는 코드들은 더 나은 이름의 새로운 함수를 사용하게 될 것이다.

 

 

매개변수 추가하기 예제를 살펴보자.

 

도서 관리 프로그램의 Book 클래스에 예약 기능이 구현되어 있다고 하자.

그런데 예약 시 우선순위 큐를 지원하라는 새로운 요구가 추가되었다.

그래서 addReservation()을 호출할 때 에약 정보를 일반 큐에 넣을지 우선순위 큐에 넣을지를 지정한느 매개변수를 추가하려 한다.

addReservation()을 호출하는 곳을 모두 찾고 고치기가 쉽다면, 곧바로 변경하면 된다.

그렇지 않다면 마이그레이션 절차대로 진행해야 한다.

여기서는 후자의 경우라고 가정해보자.

 

먼저, addReservation()의 본문을 새로운 함수로 추출한다.

새로 추출한 함수의 이름도 addReservation()이어야 하지만,
기존 함수와 이름이 같은 상태로 둘 수는 없으니 우선은 나중에 찾기 쉬운 임시 이름을 붙인다.

 

그런 다음 새 함수의 선언문과 호출문에 원하는 매개변수를 추가한다. (이 작은 간단한 절차로 진행한다.)

 

assert를 추가한 이유는 호출 하는 곳에서 새로추가한 매개변수를 실제로 사용하는지 확인하기 위함이다.

이렇게 해두면, 호출문을 수정하는 과정에서 새 매개벼수를 빠뜨린 부분을 찾는 데 도움이 된다.

 

이제 기존 함수를 인라인하여 호출코드들이 새 함수를 이용하도록 고친다.

호출문을 한번에 하나씩 변경한다.

 

다 고쳤다면 새함수의 이름을 기존 함수의 이름으로 바꾼다.

 

 

이번엔 매개변수를 속성으로 바꾸는 예제를 살펴보자.

 

지금까지는 이름을 바꾸거나 매개변수 하나만 추가하는 단순한 예만 살펴봤다.

하지만 마이그레이션 절차를 따른다면 훨씬 복잡한 상황도 꽤 깔끔하게 처리할 수 있다.

이번에는 좀 더 복잡한 예를 살펴보자.

 

고객이 뉴잉글랜드에 살고 있는지 확인하는 함수가 있다고 하자.

 

위코드는 isNewEngland라는 함수를 밖에서 호출하고 있는 코드이다.

 

나는 함수 선언을 바꿀 때 함수 추출부터 하는 편이다. 하지만 어떤 코드는 함수 본문을 살짝 리팩터링해두면 이후 작업이 더 수월해질 터라 우선 매개변수로 사용할 코드를 변수로 추출해둔다.

 

이제 함수 추출하기로 새 함수로 만든다.

 

새 함수의 이름을 나중에 기존 함수 이름으로 바꾸기 쉽도록 검색하기 좋은 이름을 붙여둔다. 

그럼 다음 기존 함수 안에 변수롤 추출해둔 입력 매개변수를 인라인한다.

함수 인라인하기로 기존 함수의 본문을 호출문들에 집어넣는다.

실질적으로 호출문은 새 함수 호출문으로 교체하는 셈이다.

이 작업은 한 번에 하나씩 처리한다.

 

그리고 바로 이어서, 함수 선언 바꾸기를 통해서 새 함수의 이름을 기존 함수의 이름으로 바꾼다.

 

 

profile on loading

Loading...