티스토리 뷰

Opinions

리팩토링을 하는 이유

Voyager Woo 2018. 11. 27. 01:05
반응형

회사에서 마틴 파울러의 리팩토링 책으로 스터디를 시작했다. 첫번째 맛보기 예제를 내가 맡아서 발표를 하게 되었다. 발표를 준비하고 발표를 하면서 역시 공부가 많이 되었다. 이번 포스팅에서는 리팩토링 책 1장에 대한 나의 생각을 적어보려 한다.

발표 자료 : https://github.com/voyagerwoo/refactoring-first-example/wiki

예제 소개

이 예제는 비디오 대여점에서 사용할 만한 간단한 프로그램이다. 고객별 고객이 빌린 영화와 영화의 대여료, 적립 포인트를 출력해주는 기능을 가지고 있다.
영화의 타입에 따라서 대여료와 적립포인트가 다르게 계산된다. 현재 영화의 타입은 최신작, 어린이용, 일반 이렇게 세가지로 분류되어 있다.
Customer, Rental, Movie 세 가지 클래스가 정의되어 있고 계산의 모든 로직은 Customer 클래스의 statement라는 메서드에 정의되어 있다.
statement 메서드가 복잡하지만 현재 프로그램은 잘 돌아가고 있다.

리팩토링을 하게 된 이유는 두 가지 수정 요청 때문이다. 첫번째는 영화의 대여료, 적립포인트를 HTML로 출력도록 기능을 추가하는 것이다. 즉 htmlStatement라는 메서드를 추가하는 것이다. 두번째는 대여료 정책의 수정이다. 확실하게 정해지지는 않았지만, 영화의 타입이 추가되거나 타입별로 가격 정책이 변경될 수 있음을 의미한다. 이 두가지 변경사항에 좀 더 효율적으로 대응하기 위해서 리팩토링을 시작한다.

[PART 1] - 코드 정리하기

[PART 1]에서는 Customer 클래스의 statement 메서드를 정리한다. 우선 논리적 덩어리인 대여료를 계산하는 swtich 구문을 메서드로 추출한다. 그리고 그 메서드가 Customer의 필드를 참조하지 않고 Rental의 필드만을 참조하는 것을 확인하고 Rental의 메서드로 이동시킨다. 동일한 방법으로 적립포인트 계산하는 메서드도 추출하고 Rental로 이동시킨다.

그 다음에는 임시변수들을 제거하면서 루프 한번에 세 가지 일(대여료 계산, 적립포인트 계산, 출력 문구 만들기)을 하던 것을 각각 메서드로 추출하여 루프를 세 번 돌도록 수정한다. 이 루프 세번은 최적화 단계에서 다시 한번 고민해보기로 하고 우선 최대한 사람이 보기 좋은 형태로 리팩토링한다.

이를 통해서 여러가지 이득이 있다. 첫째, statement 메서드가 매우 짧아졌다. 당연히 읽기 쉬워졌다.

AS-IS

public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        String result = _name + " 고객님의 대여 기록\n";

        for(Rental each : rentals) {
            double thisAmount = 0;

            // 비디오 종류별로 대여료 계산
            switch (each.getMovie().getPriceCode()) {
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (each.getDaysRented() > 2)
                        thisAmount += (each.getDaysRented() - 2) * 1.5;
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += each.getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (each.getDaysRented() > 3)
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                    break;
            }

            // 적립 포인트를 1 포인트 증가
            frequentRenterPoints ++;

            // 최신물을 이틀 이상 대여하면 보너스 포인트 지급
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints ++;

            // 이번에 대여하는 비디오 정보와 대여료를 출력
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }

        //푸터 행 추가
        result += "누적 대여료: " + String.valueOf(totalAmount) + "\n";
        result += "적립 포인트: " + String.valueOf(frequentRenterPoints);
        return result;
    }

TO-BE

public String statement() {
    String result = _name + " 고객님의 대여 기록\n";

    for(Rental each : rentals) {
        result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
    }

    result += "누적 대여료: " + String.valueOf(getTotalCharge()) + "\n";
    result += "적립 포인트: " + String.valueOf(getTotalFrequentRenterPoints());
    return result;
}

두번째, 메서드 내의 코드들이 제법 추상화 수준이 맞아져서 더욱 읽기가 쉬워졌다. 예를 들어 statement 메서드는 말 그대로 대여 기록을 문장으로 표현하는 기능(메시지)을 가지고 있는데 그 구현(메서드)를 살펴보면 header 행을 추가하고 각 랜탈을 순회하면서 필요한 정보들을 차곡 차곡 쌓은 다음에 footer 행을 추가한다. 비디오의 대여료를 계산하는 부분이나 총 대여료를 계산하는 부분 등은 메서드의 이름으로 추상화 되어있다. 필요하다면 해당 메서드를 확인해서 그 구현을 확인할 수 있다.

세번째, 자연스럽게 객체지향적인 구조가 되었다. 기존에는 Customer 객체에서 Rental이나 Movie 객체의 필드 값만 참조하여 계산했다면 이제는 Rental이라는 대여의 주체가 대여료와 적립금을 계산하고 있다. 객체지향적인 구조의 장점은 주어, 동사 구조의 표현으로 좀 더 가독성이 높아지는 것과 기능이 응집되어 있어 수정의 영향이 작아진다고 이해하고 있다.

결론적으로 [PART 1]에서 긴 메서드를 논리적인 단위로 추출하고, 추출한 메서드를 적절한 위치로 이동시켰다. 또한 임시변수를 제거했다. 훨씬 보기 좋은 코드와 식별하기 좋은 구조가 되었다. 이 구조 덕분에 쉽게 htmlStatement라는 새로운 기능을 쉽게 추가할 수 있게 되었다.

[PART 2] - 변경이 잦은 부분에 대응하기

개인적으로 내가 살고 있는 현실세계에서 [PART 1] 만으로도 충분히 좋은 리팩토링이라고 생각한다. 그러나 이 책은 가격 정책 변경이라는 새로운 기능 추가를 위해서 다시 한번 리팩토링을 한다.

요구사항 분석을 통해서 영화의 타입에 따라서 가격이 변경될 가능성이 높다고 판단한다. 그래서 가격 계산하는 기능 곧 메서드를 Rental에서 Movie로 이동한다. Movie 클래스를 상속하여 NewReleaseMovie, RegularMovie, ChildrensMovie를 만들고 가격 계산하는 메서드를 재정의 할까 생각해보지만, 영화의 생명주기 상 언제든 영화 타입이 변경될 수 있기 때문에 그 방법은 하지 않기로 한다. (이런 상속 구조를 하게 되면 영화 타입을 변경할 때 마다 서버를 재시작해야 한다.) 그래서 디자인 패턴 중에 하나인 상태 패턴으로 Movie 클래스에 가격(Price)이라는 상태(필드)를 추가하기로 한다. 가격이라는 상태는 Price라는 추상 클래스와 그를 상속 및 구현한 NewReleasePrice, RegularPrice, ChildrensPrice로 표현한다. 그리고 Movie를 생성할 때 가격 코드를 통해서 알맞는 가격 상태가 설정되도록 구현한다. 가격 계산을 Price가 하도록 역할을 넘기고 대여료 계산하는 메서드를 각 Price 구현체에서 재정의하면 리팩토링은 끝이다. (적립금 계산동 동일하게 재정의 한다.)

그런데 이번 리팩토링은 좀 많이 어려웠다.

AS-IS

TO-BE

구조도 한눈에 들어오지 않고 인다이렉션도 많아서 흐름은 더 복잡해졌다. 왜 이렇게 까지 리팩토링을 했을까? 정답은 요구사항에 대응하기 위해서이다. 가격 정책을 변경해보는 코드를 작성해보면 느낌이 온다. 예를 들어서 고전(Old Movie)이라는 영화 타입이 추가되었고 가격은 하루에 100원이라고 하자. 그리고 신작(New Release Movie)의 경우에는 대여일이 7일이 지나면 대여료가 20%씩 복리로 상승하도록 수정되었다고 해보자. 어떻게 수정하면 될까? 우선 첫번째로 OldPrice 라는 상태 클래스를 만들고 대여료 계산 메서드를 재정의한다. Movie 생성할 때 고전 영화 가격 코드일 경우에 OldPrice 상태로 설정되도록 코드만 구현해주면 첫번째 요구사항 구현은 끝이다. 또한 신작의 가격 정책 변경도 NewReleasePrice 클래스의 대여료 계산 메서드만 수정해주면 그만이다. 만약 이렇게 리팩토링 하지 않았다면 이렇게 쉽게 요구사항에 대응할 수 있었을까? 기존에 복잡한 switch 문이 더 복잡해지고 결국에는 수정하기 더 어려워졌을 것이다. (결국 야근이라는 소리다.)

[PART 2]에서는 요구사항을 분석해서 자주 변경될 부분을 확인하고 그에 맞는 적절한 디자인 패턴을 적용해서 요구사항에 쉽게 대응할 수 있도록 했다.

결론

리팩토링 책의 첫 맛보기 예제를 내가 이해한 대로 두 부분으로 나눠서 다뤄 보았다. 첫번째는 일반적인 코드 정리였다면 두번째는 특정 상황에 맞는 특별한 처방이었다. 둘다 궁극적으로는 요구사항에 대응하기 위해서였다.

최근에 지인이 매일 리팩토링 하면서 메서드를 추출하고 역할을 분리하는데 이게 맞는 것인지 고민이 된다고 했다. 그 리팩토링이 맞는지 맞지 않는지 여부는 결국 요구사항에 달려있다. 리팩토링 책에서도 언급이 되어있지만 리팩토링을 하는 이유는 단순히 코드에 대한 심미적인 기준을 지키는 것이 아니라 요구사항에 최소한의 비용으로 대응하기 위해서이다. 그러고 보니 클린코드, 디자인 패턴도 결국 같은 맥락인 것 같다.

반응형

'Opinions' 카테고리의 다른 글

서비스 분리시 고민할 점 - 이름 정하기  (0) 2019.12.21
좋은 개발자의 덕목 V2  (1) 2019.12.16
리팩토링을 하는 이유  (0) 2018.11.27
TDD 참관 후기  (0) 2018.10.24
좋은 개발자의 덕목 세가지  (0) 2018.07.18
개발입문자 코드읽기 제안  (0) 2018.07.04
댓글
댓글쓰기 폼