본문 바로가기

리팩터링 스터디 2주차 (Ch.2 ~ Ch.3)

 

 

본 포스트는 마틴 파울러의 리팩터링 2판(2020, 한빛미디어)를 기반으로 진행한 사내 스터디에서 배운 점과 기억하고 싶은 점을 개인적으로 정리한 글입니다.

진행한 챕터

02 리팩터링 원칙

03 코드에서 나는 악취

 

챕터 2 리팩터링 원칙

챕터 2는 왜 리팩터링을 해야 하며, 리팩터링을 하면 좋은점과 언제 리팩터링을 해야 하는지에 대해 다루고 있었다.

챕터 2의 내용이 글이 많아서 좀 정리가 어려웠는데 개인적으로 꼭 이해하고 싶은 면은 다음과 같다.

 

리팩터링은 언제 해야 할까?

1. 일단 복붙하더라도 (혹은 함수를 매개변수화하여 사용) 기능을 쉽게 추가할 수 있도록 한다.

2. 코드를 이해하기 쉽게 만든다.

3. 원래 하려던 일을 하던 도중에 리팩터링할 로직을 발견하면 간단한거라면 즉시 수정하고 복잡하다면 메모만 해두고 원래 할 일을 마치고 나서 수행한다. 

 

보기 싫은 코드를 발견하면 즉시 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.

무언가 수정하려 할 때는 먼저 수정하기 슆게 정돈하고 그런 다음 쉽게 수정하자.

 

리팩터링할 때 주의할 점

리팩터링의 각각의 단계들이 코드를 깨트리지 않게 작업해야 한다. (이걸 잘 어기고 하고 있었다 ㅠㅠ)

 

챕터 3 악취가 나는 코드

챕터 3은 앞으로 리팩터링 기법을 어떤 상황에서 적용할지에 대해 다룬 챕터이므로 표로 만들어보았다.

 

앞으로의 챕터에서 각 기법들을 학습하게 될텐데 어느 순간에 사용할 기법인지 쉽게 인지하기 위해서이다. 스터디를 진행하면서 각 기법에 링크를 추가할 예정이다. (책의 부록에도 있지만 스스로 정리해보기로 하였다)

문제상황 상황 설명 해결 기법
기이한 이름 함수, 모듈, 변수, 클래스명만 보고 무슨 역할을 하는지 모를 때 6.5 함수 선언 바꾸기
6.7 변수 이름 바꾸기
9.2 필드 이름 바꾸기
중복 코드 똑같은 코드 구조가 여러 곳에서 반복
비슷하지만 완전히 똑같지는 않은 코드
같은 부모로부터 파생된 서브 클래스에 코드가 중복되어 있음
6.1 함수 추출하기
8.6 문장 슬라이스하기
12.1 메서드 올리기
긴 함수 함수의 길이보다는 함수의 목적(의도)과 구현 함수 간의 괴리로 판단 6.1 함수 추출하기
7.4 임시함수를 질의함수로 바꾸기
6.8 매개변수 객체 만들기
11.4 객체 통째로 넘기기
11.9 함수를 명령으로 바꾸기
10.1 조건문 분해하기
10.4 조건문을 다형성으로 바꾸기
8.7 반복문 쪼개기
긴 매개변수 목록 함수의 매개변수 목록이 길어짐 11.5 매개변수를 질의함수로 바꾸기
11.4 객체 통째로 넘기기
6.8 매개변수 객체 만들기
11.3 플래그 인수 제거하기
6.9 여러 함수를 클래스로 묶기
전역 데이터 클래스 변수 6.6 변수 캡슐화하기
가변 데이터 무분별한 데이터 수정에 따른 사이드 이펙트 발생 위험이 높아질 때

구조체의 내부 필드에 변수가 있는 경우
6.6 변수 캡슐화하기
9.1 변수 쪼개기

갱신 로직 분리
8.6 문장 슬라이스하기
6.1 함수 추출하기

11.1 질의함수와 변경함수 분리하기
11.7 세터 제거하기
9.3 파생변수를 질의함수로 바꾸기

변수의 유효범위 축소
6.9 여러함수를 클래스로 묶기
6.10 여러함수를 변환함수로 묶기

9.4 참조를 값으로 바꾸기
뒤엉킨 변경 단일 책임 원칙에 위배될 때
하나의 모듈이 여러 이유들로 인해 변경되는 일이 많을 때
6.11 단계 쪼개기
8.1 함수 옮기기 (맥락에 맞는 곳으로)
6.1 함수 추출하기 (여러 맥락에 관여 시)
7.5 클래스 추출하기
산탄총 수술 하나의 맥락이 여러 코드에 산발적으로 존재 한 모듈로 묶기
8.1 함수 옮기기

8.2 필드 옮기기
6.9 여러함수를 클래스로 묶기
6.10 여러함수를 변환함수로 묶기

6.2 함수 인라인하기
7.6 클래스 인라인하기
기능 편애 자신이 속한 모듈보다 다른 모듈과 상호작용할 일이 잦음 8.1 함수 옮기기
6.1 함수 추출하기 (독립함수로 부리)
데이터 뭉치 데이터 여러개가 항상 함께 다닐 때 7.5 클래스 추출하기
6.8 매개변수 객체 만들기
11.4 객체 통째로 넘기기
기본형 집착 기초 타입을 그저 기본형 타입으로 계산 7.3 기본형을 객체로 만들기
12.6 타입코드를 서브클래스로 바꾸기
10.4 조건부 로직을 다형성으로 바꾸기
7.5 클래스 추출하기
6.8 매개변수 객체 만들기
반복되는 switch문 중복된 switch문 10.4 조건부 로직을 다형성으로 바꾸기
반복문 일급 함수 사용 권장 8.8 반복문을 파이프라인으로 바꾸기
성의 없는 요소 메서드가 하나뿐인 클래스처럼 굳이 그 구조를 사용할 필요가 없을 때 6.2 함수 인라인하기
7.6 클래스 인라인하기
12.9 계층 합치기 (클래스)
추측성 일반화 나중에 필요할거라는 생각으로 작성한 코드
(특이 케이스 처리 로직 등)
12.9 계층 합치기

위임하는 함수 (무쓸모)
6.2 함수 인라인하기
7.6 클래스 인라인하기

6.5 함수 선언 바꾸기
8.9 죽은 코드 제거하기
임시 필드 특정 상황에서만 설정되는 필드를 가진 클래스 7.5 클래스 추출하기
8.1 함수 옮기기
10.5 특이 케이스 추가하기
메시지 체인 중첩 객체의 depth가 깊은 경우 7.7 위임 숨기기
6.1 함수 추출하기
8.1 함수 옮기기
중개자 다른 모듈(클래스)에 위임만을 하는 경우 7.8 중개자 제거하기
내부자 거래 모듈 간의 결합도가 높은 경우 8.1 함수 옮기기
8.2 필드 옮기기
7.7 위임 숨기기

클래스 상속 구조
12.10 서브클래스를 위임으로 바꾸기
12.11 슈퍼클래스를 위임으로 바꾸기
거대한 클래스 한 클래스가 너무 많은 일을 할 때 7.5 클래스 추출하기
12.8 슈퍼클래스 추출하기
12.6 타입 코드를 서브클래스로 바꾸기
서로 다른 인터페이스의 대안 클래스들 클래스 교체 시 (인터페이스가 동일해야 함) 6.5 함수 선언 바꾸기
8.1 함수 옮기기
12.8 슈퍼클래스 추출하기 (대안 클래스 사이에 중복코드 발생 시)
데이터 클래스 getter/setter로만 구성된 클래스 7.1 레코드 캡슐화하기 (public)
11.7 세터 제거하기
8.1 함수 옮기기
6.1 함수 추출하기

* 불변 데이터는 필드 자체를 공개하기
상속 포기 부모의 메서드는 필요한데 인터페이스는 따르고 싶지 않을 때 12.10 서브클래스를 위임으로 바꾸기
12.11 슈퍼클래스를 위임으로 바꾸기
주석 장황한 주석 6.1 함수 추출하기
6.5 함수 선언 바꾸기
10.6 어서션 추가하기 (선행 조건 명시)

* 주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요없는 코드로 리팩터링해본다.

 

아래는 책에서 중점적으로 얘기했던 대화와 현재 사내 개발 프로세스나 코드 상황에 대해 얘기한 대화를 기록하였다.

글쓴이 포함 총 5명이서 나눈 대화로, 지금부터 개발자 A, B, C, D, 글쓴이라고 지칭합니다 😊

 

1. 두 개의 모자

개발자 A, B. 책에서 저자는 개발 목적에 따라 '기능 추가'인지 '리팩터링'인지 명확히 구분해서 작업을 한다고 한다. PR단위로 이를 구분하기는 어렵지만 커밋 단위로 '리팩토링'인지 '기능 추가'인지 모자를 바꿔쓰면서 작업하는 게 좋아보입니다. 다만 PR을 올릴 때는 이를 구분하지 않고 원래처럼 이슈 단위로 생성합니다.

 

개발자 B. 팀 내 개발자 E님(스터티 인원은 아니심)은 Emoji로 이 커밋이 무엇을 하는 커밋인지를 마크하시는 걸 보았습니다. 좋아보여서 저도 시도하고 있습니다.

[참고] gitmoji 사이트에서 확인할 수 있었다. (사이트 링크

 

개발자 A. 이전 회사에서 어떤 개발자님은 실제 스위치를 책상에 두고 토글하면서 기능추가 모드, 리팩토링 모드 명확히 인지하면서 일하는 경우도 있었습니다.

 

개발자 B. 지금은 개발할 때 기능 추가하다가 리팩토링하는 등 왔다갔다 하는 경우가 많았는데, 앞으로는 명확하게 인지하고 해야겠습니다.

 

개발자 C 이슈 하나당 pr 하나를 올리는데, 기능추가 이슈더라도 도중에 리패토링이 일어날 시 커밋에서 명확하게 하자는게 와닿았습니다.

 

개발자 A. 커밋 단위를 크게 가져가지 않고 더 세밀하게 나누어서 커밋해야겠습니다.

 

"항상 내가 쓰고 있는 모자가 무엇인지와

그에 따른 미묘한 작업 방식의 차이를 분명하게 인식해야 한다."

 

2. 지속적 통합 (CI)

지속적 통합이란? 기능별 브랜치의 통합 주기를 2~3일 단위로 짧게 관리하여 지속적으로 통합하는 방식으로 머지의 복잡도를 줄이기 위함. 책에서는 CI가 리팩터링과 궁합이 좋다고 말하는데 그 이유는 함수 이름 바꾸기와 같은 자잘한 리팩터링이 발생할 때 충돌이 발생할 위험을 줄여줄 수 있기 때문이다.

 

 

개발자 A. 책에서 말하는 지속적 통합의 의미대로라면 feature 플래그 관련하여, 지속적 통합을 위해서는 develop 브랜치에 매일매일 feature들을 merge해야 하는데 현재 팀 내에서 수많은 서비스들 중 당장 배포되서는 안되는 서비스들 같은 경우도 있어서 최신화가 어렵습니다. develop -> epic으로는 자주 하는데 epic -> develop는 자주 못하고 있는 상황입니다.

 

글쓴이. 이런 상황으로 인해 여러 서비스에서 사용되는 공통 컴포넌트를 수정하고 싶을 때 어려움이 발생했습니다.

 

개발자 C. 공통 컴포넌트 수정 시, props로 defaultValue를 활용하여 마치 인터페이스처럼 작성하였습니다.

 

개발자 D. 따라서 지금 프로젝트에서는 공통 컴포넌트를 사용하기 위해 상단에 하나의 레이어(Wrapper 컴포넌트)를 추가로 사용하였습니다. 공통 컴포넌트 자체를 수정하는 것은 신중하게 작업해야 할 것 같습니다.

💬 이렇게 적고 보니까 결국 모든 서비스가 하나의 repo를 사용하는데 이제는 서비스가 너무 커져서 힘든거 같다

개발자 A. 저번에도 엄청 오래 얘기했던 내용이지만 수월한 리팩터링을 위해서는 테스트 코드를 만들고 수정할 때마다 자가 테스트하는게 중요하다고 생각합니다.

[참고] 책에서도 자가 테스트 코드는 브랜치 통합 과정에서 발생하는 의미 충돌을 잡는 매커니즘으로도 활용할 수 있어 CI와도 밀접하다고 함

"자가 테스트 코드는 리팩터링을 할 수 있게 해줄 뿐만 아니라,

새 기능 추가도 훨씬 안전하게 진행할 수 있도록 도와준다."

 

3. 클래스 활용

개발자 A. 책에서는 클래스를 활용하는 것을 다루고 있습니다. 리액트 프로젝트에서는 대부분 함수 기반으로 작성하고 있는데 클래스를 적극 활용하는 것도 좋아보입니다. 유틸성 함수는 클래스로 작성해서 팩토리함수만 export하면 깔끔한 구조로 구현할 수 있습니다.

 

글쓴이. 저번에 클래스로 다형성을 구현하고 싶은 경우가 있었는데 lint 룰 중 한 모듈에 두 개 이상의 클래스를 정의하지 못하게 되어있었습니다. 왜 그런걸까요?

 

개발자 D. lint 룰에 따르면 여러 클래스를 포함하는 파일은 탐색하기 어렵고 구조화하지 않은 코드베이스가 될 수 있어 단일 책임 원칙에 따라 이를 금하고 있다고 합니다.

 

4. 너무 큰 스토어

(함수 길이를 제한하는 lint 룰에 대해 얘기하다가...)

 

개발자 B. 지금 mobX 스토어를 각 서비스별로 하나씩 사용하다보니 너무 큰 스토어가 많습니다.

 

개발자 A. 스토어가 객체에 걸맞는 단위가 되어야 할 거 같습니다. 예를 들어 한 서비스 내에서도 여러 세부 기능이 존재하는데 이를 스토어로 분할하여 관리하는게 적합해보입니다.

 

개발자 B. 컴포넌트 뷰에서 스토어 객체를 사용할 때 어렵지는 않을까요?

 

개발자 A. 스토어는 기능에 맞게 쪼개되, 합치는게 필요하다면 합치는 기능을 모은 스토어를 두는 것으로 해결할 수 있지 않을까요?

 

개발자 D. 이전 회사에서 개발할 때 여러 서비스 별로 다수의 스토어를 가지고 있었으며 중각 지점에서 각 스토어 객체를 가져와 이를 적절하게 융합하는 스토어를 구현한 적이 있었습니다.

 

글쓴이. 지금 저와 D님이 담당하고 있는 ○○ 서비스에서 세부서비스별로 스토어를 모은 객체로 분리하였습니다. 나중에 기획이 fix되면 이 객체를 모듈로 분리해보겠습니다.

현재 스토어 구조