객체지향 - 데이터 주도설계에서 책임주도설계로

dragonappear·2021년 12월 20일
0

책임과 객체 사이에서 방황할 때 돌파구를 찾기 위해 선택하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성해보자.

아무것도 없는 상태에서 책임과 협력에 관해 고민하기보다는 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키는 것이다.

주의점: 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌어서는 안된다. 캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야 하지만 동작은 그대로 유지해야한다.

이처럼 이해하귀 쉽고 구성하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동작은 바꾸지 않은 채 내부 구조를 변경하는 것을 리팩토링이라고 부른다.

데이터 중심으로 설계된 영화 예매 시스템에서 도메인 객체들은 단지 데이터의 집합일 뿐이며 영화 예매를 처리하는 모든 절차는 ReservationAgency에 집중돼 있다.

따라서 ReservationAgency에 포함된 로직들을 적절한 객체의 책임으로 분배하면 책임주도설계와 거의 유사한 결과를 얻을 수 있다.

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        Movie movie = screening.getMovie();
        boolean discountable = false;

        for (DiscountCondition condition : movie.getDiscountConditions()) {
            if (condition.getType() == DiscountConditionType.PERIOD) {
                discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek()) &&
                        condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <=0 &&
                        condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >=0;
            }     else{
                discountable = condition.getSequence() == screening.getSequence();
            }

            if (discountable) {
                break;
            }
        }


        Money fee;
        if (discountable) {
            Money discountAmount = Money.ZERO;
            switch (movie.getMovieType()) {
                case AMOUNT_DISCOUNT:
                    discountAmount = movie.getDiscountAmount();
                    break;
                case PERCENT_DISCOUNT:
                    discountAmount = movie.getFee().times(movie.getDiscountPercent());
                    break;
                case NONE_DISCOUNT:
                    discountAmount = Money.ZERO;
                    break;
            }
            fee = movie.getFee().minus(discountAmount).times(audienceCount);
        } else {
            fee = movie.getFee().times(audienceCount);
        }
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

위 코드의 문제점

  • 어떤 일을 수행하는지 하눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는데 너무 많은 시간이 걸린다.
  • 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다.
  • 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.
  • 로직의 일부만 재사용하는 것이 불가능하다.
  • 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이르모 코드 중복을 초래하기 쉽다.

한마디로 긴 메서드는 응집도가 낮기 때문에 이해하기도 어렵고 재사용하기 어루며 변경하기도 어렵다.

메서드가 명령문들의 그룹으로 구성되고 각 그룹에 주석을 달아야 할 필요가 있다면 그 메서드의 응집도는 낮은것이다.

클래스의 응집도와 마찬가지로 메서드의 응집도를 높이는 이유도 변경과 관련이 깊다.

응집도 높은 메서드는 변경되는 이유가 단 하나여야 한다.

클래스가 작고, 목적이 명확한 메서드들로 구성돼 있다면 변경을 처리하기 위해 어떤 메서드를 수정해야 하는지를 쉽게 판단할 수 있다. 또한 메서드의 크기가 작고 목적이 분명하기 대문에 재사용하기도 쉽다.

짧고, 이해하기 쉬운 이름의 메서드를 만들자.
1. 메서드가 잘게 나눠져 있을 때 다른 메서드에서 사용될 확률이 높아진다.
2. 고수준의 메서드를 볼때 일련의 주석을 읽는 것 같은 느낌이 들게 할 수 있다. 또한 메서드가 잘게 나눠져있을때 오버라이딩하는 것도 훨씬 쉽다.

객체로 책임분배 할때 가장 먼저 할 일은 메서드를 응집도 있는 수준으로 분해하는 것이다.
긴 메서드를 작고 응집도 높은 메서드로 분리하면 각 메서드를 적절한 클래스로 이동하기가 더 수월해지기 때문이다.

다음은 위 ReservationAgency를 응집도 높은 메서드들로 잘게 분해한것이다.


public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        Movie movie = screening.getMovie();
        boolean discountable = checkDiscountable(screening);
        Money fee = calculateFee(screening, discountable, audienceCount);
        return createReservation(customer, screening, fee, audienceCount);
    }

    private Money calculateFee(Screening screening, boolean discountable, int audienceCount) {
        if (discountable) {
            return screening.getMovie().getFee().minus(calculateDiscountFee(screening, screening.getMovie()))
                    .times(audienceCount);
        }
        return screening.getMovie().getFee().times(audienceCount);
    }

    private boolean checkDiscountable(Screening screening) {
        return screening.getMovie().getDiscountConditions().stream()
                .anyMatch(condition -> isDiscountable(condition, screening));
    }

    private boolean isDiscountable(DiscountCondition condition, Screening screening) {
        if (condition.getType() == DiscountConditionType.PERIOD) {
            return isSatisfiedByPeriod(condition, screening);
        }
        return isSatisfiedBySequence(condition, screening);
    }

    private boolean isSatisfiedByPeriod(DiscountCondition condition, Screening screening) {
        return screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek()) &&
                condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <=0 &&
                condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >=0;
    }

    private boolean isSatisfiedBySequence(DiscountCondition condition, Screening screening) {
        return condition.getSequence() == screening.getSequence();
    }

    private Money calculateDiscountFee(Screening screening, Movie movie) {
        switch (movie.getMovieType()) {
            case AMOUNT_DISCOUNT:
                return calculateAmountDiscountFee(movie);
            case PERCENT_DISCOUNT:
                return calculatePercentDiscountFee(movie);
            case NONE_DISCOUNT:
                return calculateNoneDiscountFee(movie);
        }

        throw new IllegalArgumentException();
    }

    private Money calculateNoneDiscountFee(Movie movie) {
        return Money.ZERO;
        
    }

    private Money calculatePercentDiscountFee(Movie movie) {
        return movie.getFee().times(movie.getDiscountPercent());
    }

    private Money calculateAmountDiscountFee(Movie movie) {
        return movie.getDiscountAmount();
    }

    private Reservation createReservation(Customer customer, Screening screening, Money fee, int audienceCount) {
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

이제 ReservationAgency 클래스는 오직 하나의 작업만 수행하고, 하나의 변경이유만 가지는 작고,명확하고, 응집도가 높은 메서드들록 구성돼 있다.

메서드들의 응집도 자체는 높아졌지만 이 메서드들을 담고 있는 ReservationAgency의 응집도는 여전히 낮다. ReservationAgency의 응집도를 높이기 위해서는 변경의 이유가 다른 메서드들은 적절한 위치로 분배해야 한다.


출처

조용호, 『[eBook] 오브젝트』, 위키북스(2019), p196~201.

0개의 댓글