본문 바로가기
독서에서 한걸음

[오브젝트] 책임 중심 설계와 데이터 중심 설계 (2)

by oncerun 2023. 2. 26.
반응형

 

2023.02.25 - [독서에서 한걸음] - [오브젝트] 책임 중심 설계와 데이터 중심 설계 (1)

 

[오브젝트] 책임 중심 설계와 데이터 중심 설계 (1)

조영호 님의 Object라는 책을 통해 객체지향 설계와 데이터 중심 설계에 대한 이야기를 읽었다. 객체지향 설계에 대한 가장 중요한 요소는 책임이라는 설명과 이를 객체의 상태에 맞춰 설계를 하

chinggin.tistory.com

 

책임 중심의 설계를 진행했다면 비교를 위해 데이터 중심 설계를 겪을 차례다. 

 

 

여기선 데이터가 무엇인가로 시작한다.  이번 예제는 github에 올라와 있는 예제를 쓴다.

 

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;

    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;
 }

 

책임 중심 설계와 다른 점은 객체의 의존성이 더 추가되었다는 점이다. 

 

이 예제에서 영화는 제목, 상영시간, 금액, 할인조건들, 영화할인정책타입, 할인금액, 할인퍼센트라는 데이터를 전부 가지고 있다.

 

Movie가 할인 금액을 계산을 직접 하기 때문에 필요한 데이터를 모두 알고 있어야 한다고 설명한다. 

 

여기서 DiscountCondition을 구현하는 과정을 생각해 보자. 

 

할인조건을 구현하기 위해 어떤 데이터가 필요한지 생각한다. 

 

1. 종류를 저장할 데이터가 필요하다.

2. 순번 조건에서만 사용될 상영 순번 데이터

3. 기간 조건에서만 사용될 요일, 시작 시간, 종료 시간

 

 

이를 구현한 데이터 클래스를 조합해 영화 애매 절차를 구현하는 클래스를 보자. 

이를 보면 조금은 소름 돋을지 모른다.  과거의 코드에서 본적이 있는 것 같다

 

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);
}

 

예약을 구현하기 위해 Movie 객체를 가져온다. 

이 후 할인 조건이 존재하는지 여부를 검사하고 이에 대한 결괏값을 가지고 어떤 할인 정책을 선택해야 하는지 결정하여 예약하는 구조다. 

 

난 여기서 잠시 멈추고 리팩토링 책, 강의, 클린 코드에서 제시했던 조언을 생각해보면 이 코드는 냄새덩어리로 표현했던 것 같다. 

 

 

객체지향의 좋은 설계를 판단할 수 있는 여러 기준 중 대표를 뽑자면 캡슐화, 응집도, 결합도이고 이 세 단어는 여러 강의나 책에서도 자주 등장했던 단어이다. 왜 이러한 단어가 척도가 된 것인지 의미를 알아보자.

 

 

캡슐화

 

캡슐화는 객체를 자율적으로 만들어주는 기준이다.  상태와 행동을 전부 객체 안에 모으기에 내부 구현을 외부로부터 감출 수 있기 때문이다.

 

여기서 구현은 변경 가능성을 내포한 것을 가리킨다. 

객체지향이라는 패러다임이 과거부터 현재까지 사용되는 강력한 이유 중 하나는 한 곳의 변경이 시스템 전체에 영향을 끼치지 않도록 조절할 수 있는 장치를 제공하기 때문이다. 

 

캡슐화를 통하면 변경 가능성이 높은 부분은 숨길 수 있다. 그리고 외부에는 상대적으로 변경 가능성이 적은 부분만 공개함으로 써 변경을 조절할 수 있다. 

추가적으로 행동과 상태가 한 곳에 모여있다는 것은 오류가 발생했을 때 그 지점을 특정짓기가 매우 쉽다는 특징이 있다. 

 

그리고 인터페이스라는 단어는 상대적으로 변경 가능성이 적은 부분이라는 의미를 가진다.  그 역은 구현이라는 단어로 사용된다. 

 

여기서 다시 한번 설계의 중요성을 깨닫는다.

 

좋은 프레임워크, 객체지향 언어, 좋은 프로그래밍 기술을 적용해 캡슐화를 적용할 수는 있다. 혹은 향상할 수 있다.

하지만 객체지향 프로그래밍을 통해 전반적으로 얻을 수 있는 장점은 오직 설계 과정 동안 캡슐화를 목표로 인식할 때 달성될 수 있다 [워프스 브룩]

 

 

다음 글은 꼭 기억하자. 

 

설계가 필요한 이유는 요구사항이 변경되기 때문이고, 캡슐화가 중요한 이유는 변경의 영향을 통제할 수 있기 때문이다. 

이는 변경의 관점에서 설계의 품질을 판단할 때 캡슐화를 기준으로 삼을 수 있다. 

 

"객체는 캡슐화가 되어야해 그래야 자율적인 객체가 될 수 있어" 도 틀린 말은 아니지만 이를 좀 더 다듬어서 말하면 

"객체의 변경 가능성이 높은 구현을 숨기고 상대적으로 안전한 인터페이스를 노출시키도록 객체를 설계하면, 즉 캡슐화를 하면 변경의 영향도를 조절할 수 있다는 점에서 객체는 자율성을 보장받고 좋은 설계의 다가갈 수 있다. 캡슐화는 구현을 내부로 숨기는 추상화 기법이다."

 

라고 정리할 수 있겠다. (밑줄쓰)

 

응집도와 결합도

 

응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.

 

결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다.

 

 

두 개념 모두 설계와 관련이 있다는 사실을 이해하자. 

 

우리는 높은 응집도, 낮은 결합도 라는 말을 상당히 자주 접하게 된다. 이를 지키면 좋은 설계라고 할 수 있다고 말하기 때문이다. 

 

그런데 여기서 왜? 높은 응집도, 낮은 결합도를 어떻게 판단하는데?라는 질문을 하면 꿀을 찾는다.(그래서 내가 살이 쪘다)

 

좋은 설계라고 하면 변경을 쉽게 수용할 수 있는지로 판단하는 것 같다. 기존의 기능은 문제없이 동작하고 변경에 대한 요구사항을 얼마나 빠르게 수용할 수 있는지에 대해서다. 

 

그렇다면 다시 생각해 본다. 높은 응집도, 낮은 결합도는 좋은 설계다. 그렇기에 높은 응집도, 낮은 결합도는 변경과 관련성이 있다.

 

필자는 변경의 관점에서 응집도는 모듈 내부에서 발생하는 변경의 정도라고 한다.  하나의 변경을 수용하기 위해 여러 요소가 영향을 받으면 응집도가 낮은 것이고, 모듈의 일부만 변경된다면 응집도가 높은 것이다.

혹은 모듈 전체가 함께 변경되면 높은 응집도, 모듈의 일부만 변경된다면 낮은 응집도이다!

 

이에 따라서 결합도도 설명할 수 있다. 결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다. 

 

뭐지? 뭐가 다른 걸까?

 

응집도와 결합도의 차이점은 응집도는 한 모듈 안의 요소들 간의 관련성을 나타내는 반면, 결합도는 다른 모듈과의 관련성을 나타낸다는 점입니다. 응집도는 모듈 내부의 설계에 관한 문제를 다루고, 결합도는 모듈 간의 인터페이스와 관련된 문제를 다룹니다. 따라서 응집도와 결합도는 서로 독립적인 개념이지만 모듈의 설계에 모두 영향을 미치므로 모듈의 품질을 높이기 위해서는 응집도와 결합도 모두 고려해야 합니다.

 

 

그렇다. 결국 응집도와 결합도도 변경과 매우 밀접한 관계를 가지며 이를 변경의 관점으로 바라보는 시각이 필요하다. 

더불어 캡슐화도 응집도와 결합도에 영향을 줄 수 있다. 

 

캡슐화를 지키면 응집도는 높아지고 결합도는 낮아진다. 그렇기에 응집도와 결합도를 고민하기 전에 캡슐화에 대해 고민하는 순서를 가져야 한다.

 

Movie 클래스를 다시 한번 보자.

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;

    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;

    public Movie(String title, Duration runningTime, Money fee, double discountPercent, DiscountCondition... discountConditions) {
        this(MovieType.PERCENT_DISCOUNT, title, runningTime, fee, Money.ZERO, discountPercent, discountConditions);
    }

    public Movie(String title, Duration runningTime, Money fee, Money discountAmount, DiscountCondition... discountConditions) {
        this(MovieType.AMOUNT_DISCOUNT, title, runningTime, fee, discountAmount, 0, discountConditions);
    }

    public Movie(String title, Duration runningTime, Money fee) {
        this(MovieType.NONE_DISCOUNT, title, runningTime, fee, Money.ZERO, 0);
    }

    private Movie(MovieType movieType, String title, Duration runningTime, Money fee, Money discountAmount, double discountPercent,
                  DiscountCondition... discountConditions) {
        this.movieType = movieType;
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountAmount = discountAmount;
        this.discountPercent = discountPercent;
        this.discountConditions = Arrays.asList(discountConditions);
    }

    public MovieType getMovieType() {
        return movieType;
    }

    public void setMovieType(MovieType movieType) {
        this.movieType = movieType;
    }

    public Money getFee() {
        return fee;
    }

    public void setFee(Money fee) {
        this.fee = fee;
    }

    public List<DiscountCondition> getDiscountConditions() {
        return Collections.unmodifiableList(discountConditions);
    }

    public void setDiscountConditions(List<DiscountCondition> discountConditions) {
        this.discountConditions = discountConditions;
    }

    public Money getDiscountAmount() {
        return discountAmount;
    }

    public void setDiscountAmount(Money discountAmount) {
        this.discountAmount = discountAmount;
    }

    public double getDiscountPercent() {
        return discountPercent;
    }

    public void setDiscountPercent(double discountPercent) {
        this.discountPercent = discountPercent;
    }
}

 

 

데이터 중심 설계와 책임 중심 설계는 기능적인 측면은 동일하지만 캡슐화를 다루는 방식이 다르다

위 코드는 구현을 위해 내부 구현을 인터페이스의 일부로 만들었다. 

 

내부 구현을 노출함으로써 캡슐화를 위반했다. 이전에서도 알게 되었듯이 캡슐화를 위반하게 되면 자연스레 높은 결합도와 낮은 응집도를 가지게 된다. 

 

 

다음에..

 

정리하다 보니 글이 길어지게 된다. 잊어먹고 싶지 않은 부분이 생각보다 많아서 그리고 정말 좋은 책이라서 공유하고 싶은 내용도 많은 것 같다. 

 

상당히 배울 점도 많고 예제 코드를 보고 예시를 만들어 보는 과정을 거치면서 책을 읽다 보면 빠르게 이해가 된다.  과거의 코드가 주마등처럼 지나가기도 한다. 

 

다음 시리즈는 어떤 부분이 캡슐화가 위반되었는지, 어디가 높은 결합도를 만들고 낮은 응집도를 만들었는지 알아볼 것이다. 

 

 

 

반응형

댓글