본문 바로가기
기타

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

by 유세지 2020. 11. 14.

개인적인 사정으로 지난 스터디 당일에 참여하지 못했습니다. 스스로 공부했던 내용을 되짚어보는 것으로 해당 챕터를 마무리 지었고, 따로 리팩토링 스터디 #5는 업로드 하지 않을 예정입니다. 아쉽지만 다음 스터디를 더 알차게 진행하는 것으로 만족해야겠습니다.

 

이번 주제는 지난번에 이어 캡슐화에 관련된 리팩토링 기법들입니다.

 

 

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

 첫 번째로 살펴 볼 기법은 기본형을 객체로 바꾸기입니다. 우리는 보통 단순한 정보를 표기할때 이러한 기본형으로 많이 표현합니다. 그러나 개발이 진행되다보면 처음엔 생각하지 못했던 추가적인 동작을 해야할 일이 생깁니다. 예를 들면 단순한 전화번호를 문자열로 표현했었는데 나중에 보니 지역 번호를 따로 추출해야 한다던지, 문자열이었던 데이터를 특별한 형식으로 포맷팅 해준다거나 하는 동작이 있을 수 있습니다. 이런 경우 높은 확률로 다른 곳에서도 이러한 기능을 사용하거나 방금 했던 작업들처럼 새로운 기능을 추가해야 할 일이 또 생기게 됩니다.

 

 따라서 이러한 데이터 항목을 객체로 바꾸어 표현해 줄 필요가 있습니다. 물론 처음에는 그저 데이터를 한 번 더 감싸주는 모양에 불과하겠지만, 코드가 쌓이면 쌓일수록 객체로 바꿔 준 데이터는 큰 힘을 발휘하게 될 것입니다. 이 과정은 언뜻 직관적이지 않게 보일 수 있습니다. 그렇게 큰 차이는 없어 보이는데 굳이 바꾸어 줄 필요가 있나 싶지만, 필드에서 많은 경험을 쌓았던 개발자들은 가장 유용하게 생각하는 리팩토링 기법 중 하나라고 하네요.

 

기본형을 객체로 바꾸기는 아래 절차에 따라 진행합니다.

 

1. 변수를 먼저 캡슐화 해줍니다.

2. 단순한 값 클래스를 만듭니다. 생성자는 기존 값을 매개변수로 받아 저장합니다.

3. 이 값을 반환해주는 게터를 만듭니다.

4. 값 클래스의 인스턴스를 새로 만들어 필드에 저장하도록 세터를 수정합니다.

5. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 기존의 게터를 수정합니다.

 

캡슐화에 볼드 처리를 한 이유는, 여기서 말하는 캡슐화가 이전 6.6절에서 다루었던 캡슐화의 과정을 그대로 따르기 때문입니다. 따라서 게터와 세터를 캡슐화 과정에서 만들고, 이후 과정에서 이 게터와 세터를 수정해주시면 됩니다.

 

 

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

어떤 코드의 결과값을 다시 참조하는 경우에, 임시 변수를 통해 진행하면 아주 유용합니다. 이때, 임시 변수를 넘어서서 아예 함수로 만들어 사용하면 더욱 좋습니다. 임시 변수를 질의 함수로 바꾸기가 바로 그런 경우입니다. 변수 대신 함수로 만들게되면 비슷한 연산을 하게되는 다른 함수에서도 사용할 수 있어 코드의 중복을 줄일 수 있습니다. 그래서 여러곳에서 똑같은 방식으로 계산되는 변수들을 찾아 이 기법을 적용해줄 수 있는지 살펴보는 것이 좋습니다.

 

물론 이 방법이 항상 좋기만한 것은 아닙니다. 변수의 계산은 한 번만 일어나야하며, 그 뒤에는 읽기만 해야합니다. 변수 하나를 가지고 이러한 연산이 여러 번 반복된다면 개발자에게 혼란을 줄 수 있습니다. 또한 이 계산 로직은 이후에 다시 적용해도 똑같은 결과를 반환해야합니다. 그래서 스냅샷 용도로 사용되는 변수에는 이 기법을 적용하지 않는 것이 좋습니다.

 

임시 변수를 질의 함수로 바꾸기는 아래 절차에 따라 진행합니다.

 

1. 변수가 사용되기 전 값이 확실히 결정되는지, 변수를 사용할때마다 다른 결과를 내지 않는지 확인합니다.

2. 읽기 전용으로 만들 수 있는 변수는 읽기 전용으로 만들어줍니다.

3. 변수 대입문을 함수로 추출합니다.

4. 변수 인라인하기로 임시 변수를 제거한다.

 

위에서 말했던 변수 계산은 항상 같은 결과를 내어야 한다는 원칙이 바로 1번에 있습니다. 개발자에게 혼란을 줄 수 있는 요소는 사전에 차단하여 발생할 수 있는 문제를 최대한으로 줄여주는 것이 중요합니다. 여기서 4번의 변수 인라인하기는 6.4절에 나왔던 내용이니 기억이 잘 안난다면 리팩토링 스터디 #3.5 - 내용 미리보기의 변수 인라인하기를 참고해주세요. 

 

 

 

클래스 추출하기 Extract Class

클래스의 가이드라인 중 클래스는 명확히 추상화 되어야 한다는 말을 들어보셨나요? 모든 클래스들은 처음엔 소수의 주어진 역할만 처리하는 예쁜 모양을 유지합니다. 그러나 시간이 갈수록 기능들은 추가되어 비대해지고, 점점 복잡한 모양이 되어가는걸 경험하신 적이 있으실겁니다. 이럴 때 사용하는 기법이 바로 클래스 추출하기입니다.

 

온갖 메서드와 데이터들로 범벅이 된 클래스는 척 보기에 어떤 기능을 갖고 있는지 파악하기 어렵습니다. 이러한 데이터와 기능들을 나눠주는 것으로 코드의 상태를 훨씬 깔끔하게 유지할 수 있으니 클래스가 감당하기 버거울 정도로 커진다싶으면 클래스 추출하기를 사용할 수 있는지 주기적으로 점검해봅시다.

 

클래스도, 디렉토리도 항상 깔끔하게 정리합시다

 

아래 절차를 따라 진행해주시면 됩니다.

 

1. 클래스의 역할을 분리할 방법과 기준을 정합니다.

2. 분리될 역할을 담당할 클래스를 새로 만들어줍니다.

3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 저장해둡니다.

4. 분리될 역할에 필요한 필드와 메서드들을 새로운 클래스로 옮겨줍니다.

5. 양쪽 클래스를 비교해보며 불필요한 메서드를 제거하고, 이름도 바꾸어줍니다.

6. 새 클래스를 외부로 노출할지 결정합니다. 이때 노출하게 되면 직접적인 참조가 아닌 값을 리턴해줍니다.

 

 

 

클래스 인라인하기 Inline Class

위의 클래스 추출하기와 정확히 반대의 경우입니다. 메인이 되는 클래스가 비대해져 분리해주었지만, 나중에 가니 역할이 사라져버린 불쌍한(본문의 표현입니다) 클래스들에 적용합니다. 이런저런 리팩토링 과정을 거치다보면 의외로 자주 일어난다고 하네요.

 

또는 두 클래스의 기능 배분을 재조정하고 싶을때도 이용합니다. 하나씩 옮겨가며 배분하는 것보다 일단 합쳐놓고 다시 분리하는게 수월한 경우도 있으니까요. 이렇게 하면 그 자리에서 한 둘만 옮기면 되는걸 너무 먼 길을 돌아가는것 같지만, 급할수록 돌아가라는 말이 있듯 천천히 정석을 따르는게 결과적으로 더 빠른 경우를 많이 경험해본지라 공감이 갑니다.

 

절차는 클래스 추출하기의 역순입니다. 마치 분해와 조립처럼요.

 

1. 소스 클래스의 각 public 메서드에 대응하는 메서드를 타깃 메서드에 생성합니다.

2. 소스 클래스의 메서드를 사용하는 코드들을 타깃 클래스의 메서드로 연결시켜줍니다.

3. 남아있는 메서드와 필드를 모두 옮겨줍니다.

4. 소스 클래스를 삭제하고 조의를 표한다 (X...)

 

저자가 이 부분을 쓰면서 작성한 코드들에 애착이 있었던 것 같습니다. 유난히 감성적인 글귀가 많이 보이네요. 우리도 애써 작성한 코드들을 삭제하며 한 번씩 조의를 표하는 시간을 가져보면 좋을 것 같습니다.

 

 

X대신 Delete를 눌러 코드들에게 조의를 표하십시오

 

위의 조의를 표한다는 제가 쓴 농담이 아니라 실제로 책에 있는 문장입니다...

 

 

위임 숨기기 Hide Delegate

모듈화 설계를 제대로 하기 위해선 캡슐화에 신경을 많이 써야 합니다. 캡슐화가 얼마나 잘 되어있는지에 따라 무언가를 변경해야 할 때 고려해야 할 요소의 수가 결정됩니다. 캡슐화는 단순히 필드를 숨기는데 그치는 것이 아니라, 클라이언트와 서버간의 접근을 제어하기도 합니다. 그 예가 바로 위임 객체입니다.

 

클라이언트에서 서버가 가리키는 객체의 메서드를 호출하기 위해서는 위임 객체를 알고 있어야 합니다. 그런데 위임 객체의 인터페이스가 바뀌게 되면 이 인터페이스를 사용하던 모든 클라이언트들이 코드를 수정해야 합니다. 이런 의존성을 없애기 위해서 위임 객체에 접근하는 것이 아닌, 서버에 위임 메서드를 만들어 이를 중간 다리 삼아 접근하는 방법을 사용할 수 있습니다. 이렇게 하면 서버에서 위임 객체의 인터페이스가 수정되더라도 클라이언트는 중간 다리인 위임 메서드를 거쳐가기 때문에 이 메서드를 수정하는 것으로 클라이언트는 위임 객체의 변화에 영향을 받지 않게 됩니다. 아주 바람직한 캡슐화의 예시가 되겠네요.

 

 

 

서버 메서드를 통한 delegate(위임객체)와 클라이언트 간의 통신

 

 

위임 숨기기는 절차도 간단합니다.

 

1. 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성합니다.

2. 클라이언트가 위임 객체가 아닌 서버를 호출하도록 수정합니다.

3. 서버에서 위임 객체를 얻는 접근자를 제거해줍니다.

 

 

굳이 적어주진 않았지만, 매 단계마다 테스트를 해주시면 더할 나위없이 좋습니다.

 

 

중개자 제거하기 Remove Middle Man

위에서 다루었던 위임 숨기기와 반대되는 기법입니다. 위임 숨기기에서 중간에 메서드를 추가해줬다면 이번에는 객체에 직접 연결해주는 식입니다.

 

중간에 메서드를 추가해주면 분명한 이점이 있습니다. 다만 이렇게 계속 반복되다보면 위임 메서드들이 쌓이고 쌓여 서버 클래스는 단순히 요청을 전달하기만 하는 애물단지로 전락하게 됩니다. 이런 경우엔 차라리 위임 객체를 직접 호출하는 편이 나을 수도 있습니다.

 

데메테르의 법칙이라는 것이 있습니다. 최소 지식 원칙(principle of least knowledge)이라고도 부르는 이 원칙은 내부 정보를 가능한 한 숨기고 밀접한 모듈과만 상호작용하여 결합도를 낮추어야 한다는 원칙인데, 이를 너무 신봉하다보면 위와 같은 사례가 생기게 됩니다. 저자의 개인적인 의견에 따르면 "가끔 참고하면 괜찮은 법칙" 정도로 부르고 싶다고 하네요.

 

다음과 같은 절차에 따라 중개자를 제거해주시면 됩니다.

 

1. 위임 객체를 얻는 게터를 만듭니다.

2. 위임 메서드를 호출하는 클라이언트가 모두 이 게터에 접근하도록 수정합니다.

3. 모두 수정했다면, 위임 메서드를 삭제합니다.

 

여담이지만 이번엔 조의를 표하지 않네요. 아무래도 저자는 클래스를 편애하는 것 같습니다.

 

 

알고리즘 교체하기 Subtitute Algorithm

뭔가 거창해보이는 이름이지만, 사실 그리 대단한건 아닙니다. 우리가 BOJ 문제 등을 풀며 메모리 사용을 줄이고, 시간을 줄이는 방법을 고민했던 기억을 되살려 가장 간명하고, 쉬운 코드로 바꿔주는 작업입니다. 예를 들면 수 많은 if문의 향연을 단순한 for문으로 고친다던가, 또는 switch문을 활용해서 짧게 줄인다던가 하는 일입니다.

 

물론 여기서의 목적은 메모리 절감도, 시간 단축도 아닌 보기 편하고 짧은 코드를 만들기 위함이라는게 차이점이겠네요.

 

다만 이 작업에 들어가기 전, 메서드를 가능한 한 잘게 나누었는지 먼저 확인해야 합니다. 거대하고 복잡한 알고리즘을 교체하는 것은 어렵고 귀찮은 일이니까요. 최대한 간소화 시킨 뒤에 교체해주는 것이 좋습니다.

 

1. 교체할 코드를 함수 하나에 모아줍니다.

2. 이 함수만을 이용해 동작을 검증하는 테스트를 마련합니다.

3. 대체할 알고리즘을 준비해 정적 검사를 실행해줍니다.

4. 기존 알고리즘과 새 알고리즘의 결과 값을 비교해보고, 같게 나온다면 교체해줍니다.

 

 

 

 

여기서부터는 챕터 8 : 기능 이동의 내용입니다. 이전까지 프로그램 요소를 생성하거나 제거하는 작업을 했다면 여기서는 요소를 다른 컨텍스트로 옮기는 일을 다룹니다. 같은 코드라도 어디에 위치하느냐에 따라 가독성에 차이가 생기는 만큼 기능 이동 또한 중요한 작업이라고 볼 수 있습니다. 오늘 다룰 내용은 아니지만, 이 챕터의 마지막에는 죽은 코드 제거하기라는 기법이 있는데 필요 없는 문장들을 디지털 화염방사기로 태워버리는 것만큼 짜릿한 일도 없다라고 소개하는군요... 참 많은 생각이 듭니다.

 

 

함수 옮기기 Move Function

모듈화가 얼마나 잘 되어있는지를 뜻하는 모듈성은 좋은 소프트웨어 설계의 핵심 요소입니다. 이 모듈성을 높이기 위해선 연관된 요소들을 함께 묶어주고, 요소 사이사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야합니다. 보통은 이해도가 높아질 수록 소프트웨어 요소들을 어떻게 묶어야 좋은지를 잘 알게 됩니다. 요소들을 이리저리 많이 옮겨봅시다.

 

어떤 함수가 자신이 속해있는 모듈 A의 요소들보다 다른 모듈 B에 있는 요소들을 더 많이 참조한다면 이 함수는 모듈 A가 아니라 모듈 B에 속해있는게 맞습니다. 이렇게 하면 다른 모듈에서 B에 접근해야 할 필요가 하나 줄어듦과 동시에 모듈 B에 대한 의존성 또한 약해지게 됩니다. 따라서 이렇게 할 경우 캡슐화가 좋아지게 되었다고 말할 수 있겠습니다.

 

다른 함수 안에서 도우미 역할을 하고 있으면서 독립적으로도 고유한 가치가 있는 함수라면 접근하기 더 쉬운 쪽으로 옮기는게 나을 수도 있습니다. 또는 다른 클래스로 옮겼을때 사용하기 더 편한 메서드도 존재합니다.

 

이렇게 함수를 옮길지 말지, 옮긴다면 어디로 옮길지 정하는 것은 쉽지 않습니다. 이럴때는 대상 함수의 현재 컨텍스트와 후보 컨텍스트들을 둘러보면 도움이 됩니다. 대상 함수를 호출하는 함수가 무엇인지, 대상 함수가 호출하는 함수는 무엇인지, 사용되는 데이터는 무엇인지등 컨텍스트에는 많은 힌트들이 있습니다. 실제로 어디로 옮겨야할지 정하기 어려울수록 크게 문제가 되는 부분이 아닐 확률이 높으니, 정하지 못하겠다면 그냥 둔 채로 작업하는 것도 괜찮습니다. 이 위치가 적합한 위치인지는 개발이 진행되며 차차 깨달아 갈 것이고, 언제든 옮길 수 있기 때문입니다.

 

 

아래 절차에 따라 함수 옮기기를 적용해주시면 됩니다.

 

1. 선택한 함수가 현재 컨텍스트에서 사용 중인 모든 요소를 살펴봅니다. 이때 함께 옮겨가야 할 것들이 있는지도 찾아봅니다.

2. 선택한 함수가 다형 메서드인지 확인하고, 다른 함수에 미치는 영향이 있는지 확인합니다.

3. 함수를 타깃 컨텍스트로 복사해줍니다. 그 후 옮겨진 컨텍스트에 녹아들 수 있도록 잘 다듬어줍니다.

4. 소스 컨텍스트에서 타깃 함수를 참조시킵니다. 동시에 소스 함수가 타깃 함수의 위임 함수가 되도록 만들어줍니다.

5. 소스 함수를 인라인 해줍니다.

 

5번의 경우 굳이 인라인 해 줄 필요가 없을 수도 있습니다. 하지만 함수 호출에 문제가 없다면 굳이 남겨둘 필요도 없습니다. 제거해주시는걸 추천합니다.

 

 

필드 옮기기 Move Field

프로그램의 진짜 힘은 데이터 구조에서 나온다는 말이 있습니다. 데이터 구조가 적절하게 활용된다면 동작 코드는 자연스레 직관적으로 짜여진채로 따라오게 됩니다. 반대로, 데이터 구조를 잘못 선택하게 된다면 서로 맞물리지 않는 데이터를 다루기 위한 코드가 가득해지게 됩니다. 이렇게 되면 이해하기 어려운 코드가 되는데에서 끝나지 않고, 데이터 구조 자체도 그 프로그램이 무엇을 위한 프로그램인지 파악하기 어려워집니다.

 

몇 번을 강조해도 지나치지 않을 만큼 데이터 구조는 중요합니다. 잘 짜야 합니다. 그렇지만 그게 어려운 일이죠. 많은 경험과 도메인 지식등이 올바른 데이터 구조를 짜는데 도움을 주긴 하지만 초기 설계에서는 실수가 나오기 마련입니다. 어제까지는 괜찮았던 설계가 다음날이 되니 엉망진창인 경우도 부지기수로 일어납니다. 따라서 적절하지 않은 데이터 구조를 발견하였다면 그 즉시 수정해주어야 합니다. 고치지 않은 채로 잘못된 데이터 구조를 남기게 되면 나중에 작성될 코드들은 더 큰 혼란에 빠진채 개발자를 수렁으로 디버그 수렁으로 끌고 갈지도 모릅니다.

 

그렇다면 어떤 상황에서 필드 옮기기를 수행해야 하는 것이 좋을까요? 함수에 어떤 데이터 레코드를 넘길 때마다 다른 레코드의 필드를 넘기게 된다면 데이터의 위치를 다시 잡아줘야 할겁니다. 함께 전해지는 데이터들은 상호 관계가 명확하도록 한 레코드에 담는게 가장 좋습니다. 이건 6.8절의 매개변수 객체 만들기에서 다루었던 것과 비슷한 개념입니다. 또 한 레코드를 변경해야할 때 다른 레코드까지 변경해야만 한다면 이것도 필드의 위치가 잘못되었다는 신호입니다.

 

보통 필드 옮기기는 이동만으로 끝나는게 아니라 더 큰 효과를 불러옵니다. 해당 데이터를 사용하는 곳이 많으면 많을 수록 코드 변경의 폭이 커지게 됩니다. 데이터 호출 코드까지 모두 변경해야하니 그에 맞는 대작업이 이어지게 되겠죠.

 

때로는 옮기려는 데이터가 사용하는 패턴이 있어 당장은 변경하지 못하는 경우가 있을 수 있습니다. 이런 경우엔 사용 패턴을 먼저 리팩토링 한 후에 필드 옮기기를 진행해줍니다.

 

위에서 데이터 레코드라는 표현을 사용하긴 했지만, 사실 클래스든 객체든 어떤 것이 와도 똑같습니다. 모두 건강하게 관리되어야 하는 데에는 다를 바가 없고, 그게 클래스일 경우엔 리팩토링하기 좀 더 쉬워질 뿐입니다. 만약 클래스처럼 어느정도 정제된 데이터가 아니라 날 것 그대로의 데이터 구조를 수정하려고 한다면, 내가 지금 옮기려고 하는게 어디어디에 어떻게 영향을 미치고 있는지 꼭 파악한 뒤 리팩토링을 진행하도록 합시다.

 

 

필드 옮기기는 아래 절차에 따라 진행합니다.

 

1. 소스 필드가 캡슐화 되어있지 않다면 먼저 캡슐화를 진행합니다.

2. 타깃 객체에 필드와 접근자 메서드들을 생성합니다.

3. 소스 객체에서 타깃 객체를 참조할 수 있는지 확인합니다.

4. 접근자들이 타깃 필드를 사용하도록 수정합니다.

5. 원래 있던 소스 필드는 제거해줍니다.

 

자바스크립트의 경우 원래의 소스 필드가 없는 경우도 있습니다. 그럴땐 5번은 따로 진행하지 않아도 되겠네요.

 

 

 

마무리

기분 탓인지 오늘은 평소보다 내용이 많았던 것 같습니다. 중간중간 나오는 저자의 감성적인 부분 덕분에 재밌게 읽었네요.

 

앞으로 이어지는 내용들은 챕터 8을 포함해 당분간 데이터 구조에 관한 내용들이 주로 나오게 됩니다. 그동안 함수의 기능적인 면모를 다듬었다면 이제 좀 더 깊은 곳을 깎아내는 느낌이 듭니다. 그럼 다음 글인 리팩토링 스터디 #6 - 캡슐화 & 기능 이동으로 찾아오겠습니다. 읽어주셔서 감사합니다.

 

 

피드백은 언제나 환영입니다! 아직 아는게 많이 부족하다 보니 잘못 이해한 내용이 혼재되어 있을 수 있습니다. 덧글을 통해 지적해주시면 감사히 받겠습니다.

반응형

댓글