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

[오브젝트] 데이터 중심 설계(3)

by oncerun 2023. 2. 26.
반응형

 

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

 

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

2023.02.25 - [독서에서 한걸음] - [오브젝트] 책임 중심 설계와 데이터 중심 설계 (1) [오브젝트] 책임 중심 설계와 데이터 중심 설계 (1) 조영호 님의 Object라는 책을 통해 객체지향 설계와 데이터 중

chinggin.tistory.com

 

 

설계의 과정을 되돌아본다면 데이터 중심 설계, 책임 주도 설계인지 모르고 과거의 배운 대로 생각 없이 만드는 경우가 있다. 

 

이러한 과정이 반복되면 어느샌가 스파게티 코드가 탄생하고 유연하게 변경사항을 반영할 수 없게 되는 경우를 겪어보았다. 

 

그렇다면 지금 코드를 점검하기 시작하여 리팩토링을 해보는 것은 어떨까?  

 

만약 내 코드가 데이터 중심 설계를 기반으로 작성되어 있다면 다음과 같은 특징을 띄게될 것이다. 

 

캡슐화 위반

 

데이터 중심 설계라 해도 인스턴스의 변수에 접근하기 위해선 getter/setter를 사용하여 접근한다. 

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 Money getFee() {
        return fee;
    }

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

 

이 코드는 직접 객체 상태에 접근할 수 없기 때문에 캡슐화를 지키는 것 처럼 보이지만 접근자와 수정 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다. 

 

그 이유는 명확하다. 인터페이스의 이름을 통해 객체 내부의 어떠한 상태가 존재하는지 노골적으로 드러낸다. 

 

데이터에 초점을 맞추고 설계를 진행하다보면 어느 순간 객체의 책임이 무엇인지 까먹는다. 

마치 데이터를 보관하고 있는 저장소처럼 느껴지며 필요할 때마다 인터페이스가 무한하게 증식하는 걸 확인할 수 있다.

 

아마 책에서 가장 많이 나오는 단어를 뽑으라고 하면 책임이다.  저자가 가장 중요하다고 생각하기 때문이다. 

 

객체의 책임은 중요하다. 그렇다면 책임을 어떻게 고려해야 하는 것일까?

그것은 협력이다. 적절한 책임을 할당하게 하기 위해선 협력이라는 문맥을 고려할 때만 가장 적절한 책임을 부여할 수 있다.

 

설계를 할 때 협력이라는 문맥을 고려하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자 메서드가 많이 만들어진다. 

객체가 사용될 문맥이 정확하지 않기 때문에 어떠한 경우에도 객체가 사용될 수 있게 만들려고 하기 때문이다. 

 

"어떠한 상황에도 객체가 사용될 수 있게 많은 접근자와 수정자가 추가된다 "

 

이 문장을 보고 또 한번에 코드 주마등이 지나간다.

 

무언가를 행동을 해야한다. -> 관련 데이터를 가진 클래스를 찾는다. -> 코드를 작성한다.  이런 식으로 가볍게 생각했던 과거의 나를 반성한다.

 

이를  개선하려면 다음과 같이 생각을 해보자.

협력 안에서 어떠한 행동(책임)을 해야 하는지 고민한다. ->  적절한 책임이 정해졌다면 이를 처리할 역할을 찾는다.

적절한 역할을 찾았다는 것은 이 책임을 처리하기 위한 데이터도 있고 자신이 해결하지 못하는 부분에 있어 메시지를 보낼 의존성도 가지고 있다는 것을 말한다.  

그리고 해당 책임을 자율적으로 구현한 이후 안정적인 인터페이스를 공개한다. 이러한 과정을 의식하면서 코드를 작성해 보자.  

 

Allen Holub은 이렇게 접근자와 수정자를 과도하게 의존하는 설계를 추측에 의한 설계 전략이라고 부른다.

(design-by-guessing strategy 단어는 외우자. 가끔 사용할 때가 있더라)

 

이 전략은 객체가 사용될 협력을 고려하지 않고 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행하기에 어떠한 압박감에 시달려 프로그래머는 내부 구현을 인터페이스로 노출할 수밖에 없다 

 

이는 캡슐화의 위반으로 이어지면서 이는 아주 작은 스노볼이 된다. 

 

 

전체적인 설계를 검토하거나 코드 리뷰를 진행할 때 시니어 개발자나, 클린코드를 지향하는 개발자들은 무엇을 우선적으로 볼까?  이들도 각자가 검토하는 순서가 있을 것 같은데 정말 궁금하다. 

 

 

높은 결합도

 

다시 한 번 상기해 보자 캡슐화를 위반하면 자연스레 높은 결합도를 가지고 낮은 응집도를 갖는다.

 

내부 구현을 인터페이스로 노출시키기 때문에 이를 사용하는 클라이언트가 내부 구현에 강하게 결합된다는 것을 의미한다.  

 

이는 내부 구현의 변경이 인터페이스를 사용하는 모든 클라이언트의 변경이 발생한다는 것을 의미한다. 

 

이를 사용하는 클라이언트 코드를 살펴보자.

 

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

   		...

        Money fee;
        if (discountable) {
     		...

            fee = movie.getFee().minus(discountAmount).times(audienceCount);
        } else {
            fee = movie.getFee().times(audienceCount);
        }

        return new Reservation(customer, screening, fee, audienceCount);
    }
}

 

 

Movie에서 Money 타입을 getFee() 메서드를 통해 가져오는 부분을 살펴보자. 

 

Movie의 Money 타입이 변경되었다. 이제 이 영향은 getFee() 메서드를 타고 getFee() 메서드를 사용하는 클라이언트의 코드 변경에 영향을 미친다. 

 

또한 여러 데이터 객체를 사용하는 제어로직이 특정 객체 안에 집중되어 하나의 객체가 다수의 객체에 강하게 결합된다는 것이다. 이 결합도롤 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수밖에 없다고 한다. 

 

제어 객체를 클라이언트로 대체해 보자. 기능을 사용하는 입장에서 여러 객체를 사용해야 한다. 

필요한 데이터를 가진 객체를 가져와야하고 이는 접근자 메서드로 가져온다. 

이 과정에서 클라이언트는 매우 높은 결합도를 갖게 된다. 

 

이 상황에서 요구 사항이 들어온다고 가정하자. 클라이언트와 강결합되어 있는 객체의 변경이 필수불가결이면 해당 요구사항을 반영하기 위해 많은 두려움과 많은 시간과 노력이 들어간다. ( 겪어본 적 있다. )

 

내가 생각하기에 데이터 중심 설계의 높은 결합도가 주는 치명적인 문제점으로 보인다. 

 

낮은 응집도

 

응집도가 낮다라는 의미는 서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때  응집도가 낮다고 한다.

(역으로  같은 이유로 변경되는 코드가 하나의 모듈 안에 모여있을 때는 응집도가 높다고 하겠지?) 

 


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

 

이 코드가 변경되는 이유를 나열해보자. 

 

우선 getMovieType()은 할인 정책을 가져오는 코드이며 이후 swtich문을 통해 할인 금액을 설정한다. 

이는 할인 정책이 추가되면 이 코드는 수정돼야 함을 의미한다. 

 

DiscountCondition은 할인 조건을 의미한다. 할인 조건을 순회하며 각 할인 조건에 대한 조건문을 통해 할인 여부를 판단하는 로직이 있는데 만약 할인 조건이 추가되면 코드가 변경되어야 한다.

 

낮은 응집도는 두 가지 측면에서 설계에 문제를 발생시킨다. 

 

1. 서로 다른 이유로 변경되는 코드들을 하나의 모듈 안에 뭉쳐놓았기에 변경과 아무런 상관이 없는 코드들이 영향을 받게 된다. 위 코드를 예시를 들면 할인 정책관련된 코드와 할인 조건에 관련된 코드가 뭉쳐있다. 

따라서 할인 정책의 작업이 할인 조건에도 영향을 미칠 수 있다. 

만약 어떤 코드를 수정한 이후 아무런 상관이 없는 코드가 영향을 미치는 경우가 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.

 

2. 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮은 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하기 때문이다. 

 

즉 현재는 새로운 할인정책이나 할인조건을 추가하는 경우 하나 이상의 클래스를 동시에 수정해야 한다. 

이러한 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 것이다. 

 

 

여기서 단일 책임의 원칙을 생각해보자. 클래스는 단 한 가지의 변경 이유만을 가져야 한다는 것, 

이는 응집도과 변경과 관련이 있다는 것을 강조하기 위해 로버트 마틴이 제시한 원칙이다. 

이때 책임을 기존의 객체의 책임 할당하는 그 책임이 아니라 좀 더 큰 범위의 "변경의 이유"라는 의미로 사용된다고 한다. 

 

 

데이터 중심 설계가 변경에 취약한 이유는 두 가지다.

 

1. 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다. 

 

2. 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다. 

 

 

데이터도 구현의 일부이다. 데이터 중심의 설계의 첫 질문은 이 객체가 포함해야 하는 데이터가 무엇인가?라는 질문으로 시작한다. 

 

데이터 중심 설계는 시작부터 데이터에 관해 결정하도록 강요하기에 내부 구현에 초점을 맞추게 된다. 

 

잘 생각해보면 데이터 중심 설계에 대한 객체는 단순한 데이터의 집합체로 바라본다. 그렇기에 데이터와 기능이 자연스레 분리되기 때문에 캡슐화를 위반하게 된다. 

 

이로 인해 과도한 접근자와 수정자가 추가되고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체로 구현하게 된다. 

 

또한 데이터와 데이터를 처리하는 작업을 동일한 객체 안에 두더라도 데이터에 초점이 맞춰있다면 캡슐화를 만족하기 어렵다. 

 

즉 데이터 결정 -> 데이터를 처리하는 오퍼레이션 결정하는 것은 인터페이스에 데이터에 관한 지식이 자연스레 녹아든다. 

 

단순히 내부 상태를 감추는 것이 캡슐화가 아니라 제공하는 인터페이스에 구현의 냄새가 난다면 이도 캡슐화를 위반했다고 하는 것 같다. 

 

이렇게 인터페이스에 내부 구현에 대한 지식이 녹아들면서 캡슐화를 실패하고 변경에 취약해지는 결과로 이어지는 것이다. 

 

 

다시 한 번 기억해 보자. 객체지향 설계는 협력이라는 문맥에서 필요한 책임을 결정하고 적절한 역할의 객체를 찾아 결정하는 것이 매우 중요하다. 

 

객체의 내부의 어떠한 데이터를 가지고 그 데이터를 어떻게 동작하고 관리하는지는 부가적인 문제로 더 중요한 것은 다른 객체와 협력하는 것이다.

 

 

 

반응형

댓글