개요
최근에 당근에서 Martin fowler의 리팩터링 2판을 굉장히 싼 가격에 주워왔습니다.
그러던 중, 타이밍 좋게도 토비의 스프링의 저자 토비님이 스터디를 진행한다고 해서 좋은 스터디 경험이 될 것 같아 참여하게 되었습니다.
스터디에 임하기 전에 ‘어떠한 마음가짐으로 스터디를 하면 좋을까?‘를 고민하다가 예전에 토비님이 인프콘을 마치고 쓰신 글이 기억났습니다.
다음은 블로그에서 발췌한 내용입니다.
클린 코드는 그저 이름 잘 짓고, 함수 작게 만들고, 주석 달지말고, 스타일 통일하고, 디미터 법칙 지키자라는 수준의 피상적이고 기계적인 주장을 하는 캐치프레이즈가 아니다.
대부분 앞부분만 보고 6장 넘어서 나오는 내용에 대해서는 얘기도 하지 않는, 책에 나오는 저자가 꽤나 고민하면서 수집하고 애써 만진 코드는 술렁술렁 넘기고 말 책이 아니란 말이다.
책을 읽을 때(예시의 경우는 클린 코드), 명시된 코드를 스킵해버리고 이론만 읽는다면 ‘그저 좋은 코드를 작성하는 책을 읽어서 기분만 좋은 것’일 뿐이라는 말씀을 하셨습니다.
이를 의식하여 코드는 무조건 직접 쳐보고 ‘리팩터링’ 각 챕터에서 말하려고 하는 것이 무엇인지, 내 생각은 어떠한지 정립해나가면 더 좋은 스터디 경험이 되지 않을까 생각이 들었습니다.
글은 코드를 작성하면서 느낀 점과 JavaScript와 Java의 언어 패러다임 상에서 오는 차이점에 초점을 맞춰 작성했습니다.
코드는 Github에서 확인 가능합니다.
초기 코드
챕터 1에서 리팩터링 대상이 되는 코드입니다. 먼저 기능을 살펴보겠습니다.
- 각 공연별 요금 계산
- 비극(tragedy): 기본 $400, 관객 30명 초과시 1인당 $10 추가
- 희극(comedy): 기본 $300, 관객 20명 초과시 기본 $100 추가 및 1인당 $5 추가. 모든 관객당 $3 추가
- 적립 포인트(volume credits) 계산
- 모든 공연: 관객 30명 초과시 초과 인원만큼 포인트 적립
- 희극 추가 보너스: 관객 5명당 1포인트 추가
- 청구서 출력
- 각 공연별 요금과 관객수
- 총 청구 금액
- 총 적립 포인트
리팩터링 작업 전에, 2판의 예제는 JavaScript로 작성되어 있어 제가 사용할 언어인 Java로 변환하는 과정이 필요합니다.
JavaScript는 동적 타이핑(Dynamic typing) 언어로 인터프리터가 그 시점의 변수 값을 기반으로 런타임에 변수에 타입을 할당할 수 있기 때문에 invoice와 plays를 바로 패러미터로 넘길 수 있습니다.
반면 Java는 정적 타이핑(Static typing) 언어이기 때문에 자료형에 대한 검사를 컴파일 타임에 진행되기 때문에 패러미터를 바로 넘길 수 없습니다.
때문에 invoice와 plays를 statement()의 패러미터로 넘기기 위해 클래스의 인스턴스 메서드로 만든 뒤에 생성자에서 play와 instance를 받아두고 멤버 메소드에서 이를 사용하는 식으로 구성할 필요가 있었습니다.
패러미터로 넘기기 위해 필요한 객체를 정의하겠습니다.
설명에서 Performance 및 PlayType, Play는 생략했습니다.
그리고 statement() 메서드의 패러미터로 다시 넘겨 리팩토링 할 준비를 마칩니다.
객체 지향 관점에서 바라본 문제점
리팩터링에서 제시하는 방법은 프로그램의 작동 방식을 쉽게 파악할 수 있도록 코드를 여러 함수와 프로그램 요소로 재구성하는 것입니다.
함수를 분리하는 것은 비단 객체지향만의 문제는 아니지만, 문장을 보고 단일 책임의 원칙(Single Responsibility Principle), 개방 폐쇄 원칙(Open Closed Principle)을 떠올릴수 밖에 없었습니다.
‘모든 클래스는 하나의 책임만 가져야한다’는 것이며, 이를 풀어서 말하면 모듈이 변경되는 이유는 한 가지여야 한다는 것입니다.
코드를 보면 statement() 메서드에 공연료 계산, 공연료 청구서 출력이라는 두 책임을 가지고 있습니다.
이는 공연료 계산의 요구사항이 변경된다면 청구서 관련 로직에도 영향을 줄 수 있는 변경점이 생기게 될 것입니다.
또한 현재는 비극과 희극 두 장르만을 지원하고 있는데, 추가적인 장르가 추가된다면 switch 문의 변경은 피할 수 없습니다.
그래서 구현체의 변경이 발생하더라도 statement() 메서드는 영향받지 않도록, 상속이나 인터페이스로 정의된 계산 메서드를 호출하여 사용하는 방법을 사용할 수 있겠지요.
리팩터링 예제에서는 상속을 사용했는데, 인터페이스를 사용하면 나중에 테스트 작성을 고려했을 때 mocking도 쉬울 뿐더러 구현체를 바꾸는게 훨씬 용이하기 때문에 더 좋은 방법이 아닐까 생각됩니다.
아마 실제 현업에서도 상속을 잘 안쓰는 것이 그런 이유가 아닐까 싶기도..
테스트가 주는 안정감
위는 커밋 전에 무조건 실행한 테스트 코드로, 간단하게 출력으로 출력하는 텍스트가 변경되진 않았는지 확인하는 테스트입니다.
statement() 함수 쪼개기에서 솔직히 나라고 해서 항상 단계를 이처럼 잘게 나누는 것은 아니지만, 그래도 상황이 복잡해지면 단계를 더 작게 나누는 일을 가장 먼저 한다. 특히 리팩터링 중간에 테스트가 실패하고 원인을 바로 찾지 못하면 가장 최근 커밋으로 돌아가서 테스트에 실패한 리팩터링의 단계를 더 작게 나눠 다시 시도한다.
리팩터링에서는 테스트를 강조합니다. 책 예시에는 테스트 코드가 딱히 제시되어 있지 않아 Json 파일을 읽어들이고 청구서 내용을 출력하여 바뀐 점이 없는지 확인했습니다.
컴파일 - 테스트 - 커밋
과정을 거치는데, 리팩터링 뒤에 테스트를 실행했을 때 green bar를 봤을 때 그 안정감, 테스트가 실패하더라도 가장 최근 커밋으로 돌아가 이미 테스트 코드가 성공했기 때문에 보장되어 있다는 안락함(?)이 코드 완성 내내 부적처럼 지켜줬습니다.
책이 99년에 나온 것으로 알고 있는데, 이 때부터 테스트 코드를 중요하게 생각했다니 새삼 ‘고전은 영원하다’라는 생각이 들었습니다.
최종 코드
모든 로직이 응집되어 있는 첫 코드와 다르게 메서드 추출로 시작한 코드 분리, 출력 형식 담당 데이터 처리(StatementData), 요금 계산 로직(Calculator)과 같은 책임 분리, 조건부 로직을 다형성으로 풀어 좀 더 변경하기 용이한 코드로 변모했습니다.
맺음
챕터1부터 꽤나 많은 주제를 다룹니다.
적용해 본 것만 해도 테스트 코드 기반 리팩터링, 함수 추출, 변수를 인라인 선언, 지역변수 제거, 반복문 쪼개기 및 파이프라인으로 변경, 함수 단계나누기, 다형성 활용 등등..
코드를 짜다보면 클래스에서 공통적으로 사용되는 필드는 어떻게 관리되어야 하고, 접근 범위를 어떻게 제한할 수 있는지 이런 고민을 많이 했었는데 제 생각을 정립할 수 있는 내용이 많아서 재미있게 읽을 수 있었습니다.
챕터 마지막 쯔음에 다음이 언급됩니다:
리팩토링을 효과적으로 하는 핵심은 단계를 잘게 나눠야 더 빠르게 처리할 수 있고, 코드는 절대 깨지지 않으며 이러한 작은 단계들이 모여서 상당히 큰 변화를 이룰 수 있다는 사실을 꺠닫는 것이다.
지금이야 주어진 예제에서 코드를 리팩터링하는 작업만 했는데 현업에서는 이미 작성된 코드를 리팩터링 하는 일이 많을텐데, 내 코드를 수정하면서 이 ‘작은 단계’의 기준을 어떻게 정하느냐 대한 고민도 깊어졌습니다.
나중에 Kotlin으로 작성해봐도 재밌을 것 같네요. 앞으로의 스터디가 기대됩니다.