티스토리 뷰

객체 지향 설계

  1. 역할, 책임, 협력 중 가장 중요한 것은 `책임`이다.
  2. 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 연관되어 있다.

객체들의 책임이 적절히 할당되지 못한다면 원활한 협력을 기대할 수 없으며 역할은 책임의 집합이기 때문에 역할 역시 협력을 이루지 못한다.

설계란 변경을 위해 존재하며 훌륭한 설계란 합리적인 비용안에서 변경을 수용할 수 있는 구조를 만드는 것이다.
이를 위해서는 객체의 상태가 아닌 행동에 초점을 맞춰야 한다.

객체를 단순한 데이터의 집합으로만 본다면 내부 구현을 퍼블릭 인터페이스에 노출되어 변경에 취약해진다.
이번장에서는 데이터 중심의 설계를 보고 객체지향적 설계와의 차이점을 살펴본다.

두 가지 시스템 객체 분할 방법

  1. 상태(데이터) 기준 분할 - 데이터를 조작하는데 필요한 오퍼레이션을 정의한다 (데이터 -> 행동)
  2. 책임(행동) 기준 분할 - 다른 객체가 요청하는 오퍼레이션을 위한 데이터를 정의한다. (행동 -> 데이터)
훌륭한 객체지향 설계는 책임(행동)에 초점을 맞춰야 한다.

객체의 상태는 구현에 속하며 구현은 불안정하기 때문에 변하기 쉽다. 상태를 객체 분할의 기준으로 삼으면 세부사항이 인터페이스에 스며들어 캡슐화가 무너지며 의존하는 모든 객체에 영향을 준다. 따라서 변경에 취약할 수 있다.

객체의 책임은 인터페이스에 속한다. 책임을 드러내는 인터페이스 뒤에 책임을 수행하는데 필요한 상태를 숨김(캡슐화)으로써 변경에 대한 파장을 내부로 한정할 수 있다.


데이터 중심의 영화 예매 시스템

이전 글을 먼저 읽고 비교하면서 보면 도움이 될 거 같습니다. :)

2020/01/05 - [독서/오브젝트] - Object Chapter 02. 객체지향 프로그래밍 - 영화 예매 시스템

데이터 중심 설계는 객체 내부의 데이터를 기반으로 시스템을 분할하는 방법이다. 객체 내부에 저장해야 하는 `데이터가 무엇인가`로 시작한다.

/**
 * 영화 정보 관련 클래스
 */
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;
}

기존에 DiscountPolicy에 속해있던 DiscountCondition이 직접 Movie에 객체로 속해있다.
영화 할인은 하나의 할인만 가능하기 때문에 금액 할인 정보인 discountAmount와 비율 할인 정보인 discountPercent가 MovieType별로 각 하나만 사용할 수 있다.

/**
 * 영화 할인 정책 정보
 */
public enum MovieType {
    /** * 가격 할인 */
    AMOUNT_DISCOUNT,
    /** * 비율 할인 */
    PERCENT_DISCOUNT,
    /** * 할인 사용 안함 */
    NONE_DISCOUNT
}

AMOUNT_DISCOUNT의 경우 Movie의 discountAmount, PERCENT_DISCOUNT의 경우 Movie의 discountPercent를 사용한다.

이처럼 데이터 중심 설계에서는 객체가 포함해야 하는 데이터에 집중한다. 특히 객체 종류를 저장하는 인스턴스(MovieType)와 종류에 따라 배타적으로 사용되는 인스턴스 변수(discountAmount, discountPercent)를 한 객체 안에 저장하는 방식은 데이터 중심 설계에서 흔히 볼 수 있다. (헉... 내가 이렇게 했는데..;;)

public class Movie {
    ....
    ....
    
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    public Duration getRunningTime() { return runningTime; }
    public void setRunningTime(Duration runningTime) { this.runningTime = runningTime; }

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

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

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

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

객체지향의 가장 핵심은 캡슐화이므로 가장 간단한 방법인 getter/setter를 생성한다.

/**
 * 영화 할인 조건
 */
public enum DiscountConditionType {
    /** * 상영 순번 조건 */
    SEQUENCE,
    /** * 상영 시간 조건 */
    PERIOD
}
/**
 * 할인 조건 정보 정관 클래스
 */
public class DiscountCondition {
    private DiscountConditionType type;

    /** * 할인 대상 상영 회차 */
    private int sequence;

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public DiscountConditionType getType() { return type; }
    public void setType(DiscountConditionType type) { this.type = type; }

    public int getSequence() { return sequence; }
    public void setSequence(int sequence) { this.sequence = sequence; }

    public DayOfWeek getDayOfWeek() { return dayOfWeek; }
    public void setDayOfWeek(DayOfWeek dayOfWeek) { this.dayOfWeek = dayOfWeek; }

    public LocalTime getStartTime() { return startTime; }
    public void setStartTime(LocalTime startTime) { this.startTime = startTime; }

    public LocalTime getEndTime() { return endTime; }
    public void setEndTime(LocalTime endTime) { this.endTime = endTime; }

이어서 할인조건의 종류를 구분할 DiscountConditionType과 할인 조건을 구하는 DiscountCondition을 만든다. 위와 같이 종류를 나누는 객체(DiscountConditionType)와 배타적으로 사용되는 변수(sequence, dayofWeek, startTime, endTime)가 존재한다.

/**
 * 영화 상영 정보 도메인 클래스
 */
public class Screening {
    /** * 상영될 영화 정보 */
    private Movie movie;
    /** * 상영 회차 정보 */
    private int sequence;
    /** * 상영 시작 시간 */
    private LocalDateTime whenScreened;

    public Movie getMovie() { return movie; }
    public int getSequence() { return sequence; }

    public LocalDateTime getWhenScreened() { return whenScreened; }
    public void setMovie(Movie movie) { this.movie = movie; }

    public void setSequence(int sequence) { this.sequence = sequence; }
    public void setWhenScreened(LocalDateTime whenScreened) { this.whenScreened = whenScreened; }
}
/**
 * 예약 정보 도메인 클래스
 */
public class Reservation {
    /** * 예약 고객정보 */
    private Customer customer;
    /** * 예약 상영 정보 */
    private Screening screening;
    /** * 예약 금액 */
    private Money fee;
    /** * 예약 인원 수 */
    private int audienceCount;

    public Reservation(Customer customer, Screening screening,
        Money fee, int audienceCount) {
        this.customer = customer;
        this.screening = screening;
        this.fee = fee;
        this.audienceCount = audienceCount;
    }

    public Customer getCustomer() { return customer; }
    public void setCustomer(Customer customer) { this.customer = customer; }

    public Screening getScreening() { return screening; }
    public void setScreening(Screening screening) { this.screening = screening; }

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

    public int getAudienceCount() { return audienceCount; }
    public void setAudienceCount(int audienceCount) { this.audienceCount = audienceCount; }
}

 Screening과 Reservation도 데이터를 먼저 만들고 getter/setter를 추가한다.

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

        boolean discountable = false;
        // 영화 금액 할인 조건에 부합한지 loop를 돌며 확인한다.
        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;
                default:
            }

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

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

ReservationAgency는 데이터 클래스들을 조합하여 영화 예매 절차를 구현한 클래스이다.

크게 DiscountCondition을 돌며 할인 여부를 검사한 후에 할인 대상 시 Movie타입별로 분기시켜 할인금액을 계산하여 예매 객체를 반환한다.


세 가지 품질 척도

캡슐화

변경 가능성이 높은 부분을 객체의 내부로 숨기는 기법.

객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 조절할 수 있는 장치를 제공하기 때문이다. 변경 가능성이 높은 부분(구현)은 객체 내부로 숨기고 안정적인 부분(인터페이스)만 공개함으로써 통제할 수 있다.

응집도와 결합도

응집도는 모듈에 포함된 내부 요소들의 연관된 정도를 나타내며, 결합도는 의존성 정도를 나타내며 다른 모듈에 대해 얼마나 많이 알고 있는지를 나타내는 척도이다.

일반적으로 좋은 설계란 높은 응집도와 낮은 결합도를 가지고 있다. 이유는 설계를 변경하기 쉽게 해 주기 때문이다.

변경 관점에서 응집도는 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도이며 응집도가 높을수록 변경의 대상과 범위가 명확하기 때문에 변경이 쉬워진다.

결합도는 한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 내부 구현을 변경하였을 때 이것이 다른 모듈까지 영향을 끼친다면 결합도가 높다고 할 수 있다. 따라서, 클래스의 구현이 아닌 인터페이스에 의존하도록 작성해야 낮은 결합도를 얻을 수 있다.


데이터 중심 영화 예매 시스템의 문제점

캡슐화 위반

public class Movie {

	....
    
    
    /** * 영화 관람 가격 */
    private Money fee;
    
    public Money getFee() { return fee; }
    public void setFee(Money fee) { this.fee = fee; }   
    
    ....
}

위와 같은 설계는 마치 캡슐화를 지키고 있는 것처럼 보인다.
하지만 getFee와 setFee메서드는 파라미터와 반환 타입을 통해 객체 내부에 Money타입이 존재하는 것을 드러내고 있다.

설계를 할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자 및 수정자(getter/setter)를 사용하는 경향이 나타난다. 이를 추측에 의한 설계라고 한다.

높은 결합도

객체 내부의 구현이 객체 인터페이스에 드러나는 것은 클라이언트가 구현에 강하게 결합된다는 의미다. 이는 객체 내부 구현 변경 시 의존하는 클라이언트 모두를 수정해야 하는 상황을 발생시킨다.

또 다른 단점으로 여러 데이터 객체들을 사용하는 로직이 특정 객체(ReservationAgency) 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다.

데이터 중심 설계는 전체 시스템을 하나의 의존성 덩어리로 만들기 때문에 어떤 변경이 일어나도 시스템 전체에 영향이 간다.

낮은 응집도

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

낮은 응집도가 발생시키는 문제점

1. 변경의 이유가 서로 다른 코드를 하나의 모듈에 뭉쳤기 때문에 변경과 아무 상관없는 코드들이 영향을 받게 된다.

ReservationAgency안에 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 공존하기 때문에 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 끼칠 수 있다.

2. 하나의 요구사항을 변경하기 위해 동시에 여러 모듈을 수정해야 한다.

새로운 할인정책을 추가시 MovieType에 새로운 할인 타입의 열거형을 추가해야하고 ReservationAgency의 reserve메서드의 switch구문에 추가해줘야한다. 또 Movie에 할인 계산을 위한 데이터도 넣어줘야 한다. (MovieType, ReservationAgency, Movie 모두 변경이 필요함)


객체 지향적인 코드로 변경

캡슐화는 설계의 제1 원리이다.

낮은 응집도와 높은 결합도의 근본적인 원인은 캡슐화를 위한했기 때문이다.
메서드는 단순히 getter/setter를 의미하는 것이 아닌 객체자 책임져야 하는 무엇인가를 의미한다.

객체는 단순한 데이터 제공자가 아니며 데이터보다 협력에 참여하면서 수행하는 오퍼레이션이 더 중요하다.

public class DiscountCondition {
    private DiscountConditionType type;

    /** * 할인 대상 상영 회차 */
    private int sequence;

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    ....

    /**
     * 기간을 기준으로 할인이 가능한지 판단하여 반환한다.
     * @param dayOfWeek 상영 요일
     * @param time 상영 시간
     * @return 할인 가능 여부
     */
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
        if (this.type != DiscountConditionType.PERIOD) {
            throw new IllegalArgumentException();
        }

        return this.dayOfWeek.equals(dayOfWeek)
            && this.startTime.compareTo(time) <= 0
            && this.endTime.compareTo(time) >= 0;
    }

    /**
     * 상영 회차를 기준으로 할인이 가능한지 판단하여 반환한다.
     * @param sequence 상영 회차
     * @return 할인 가능 여부
     */
    public boolean isDiscountable(int sequence) {
        if (this.type != DiscountConditionType.SEQUENCE) {
            throw new IllegalArgumentException();
        }

        return this.sequence == sequence;
    }
}

DiscountCondition이 할인여부를 스스로 처리할 수 있도록 두개의 isDiscountable 메서드를 추가하였다.

/**
 * 영화 정보 관련 클래스
 * 좀더 명확히 보여주기 위해 lombok 사용안함.
 */
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;

    ....
    
    /**
     * 비용 할인 금액을 제외하고 예매금액을 계산하여 반환한다.
     * @return 할인 예매 금액
     */
    public Money calculateAmountDiscountedFee() {
        if (movieType != MovieType.AMOUNT_DISCOUNT) {
            throw new IllegalArgumentException();
        }

        return this.fee.minus(this.discountAmount);
    }

    /**
     * 비율 기준 할인 금액을 제외하고 예매금액을 계산하여 반환한다.
     * @return 할인 예매 금액
     */
    public Money calculatePercentDiscountedFee() {
        if (movieType != MovieType.PERCENT_DISCOUNT) {
            throw new IllegalArgumentException();
        }

        return this.fee.minus(this.fee.times(this.discountPercent));
    }

    /**
     * 할인 정책이 없으므로 예매금액을 그대로 반환한다.
     * @return 할인 예매 금액
     */
    public Money calculateNoneDiscountedFee() {
        if (movieType != MovieType.NONE_DISCOUNT) {
            throw new IllegalArgumentException();
        }

        return this.fee;
    }
}
public class Movie {

    /**
     * 할인이 가능한 예매인지 판단하여 반환한다.
     * @param whenScreened 예매 상영 시간
     * @param sequence 예매 상영 회차
     * @return 할인 가능 여부
     */
    public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
        for (DiscountCondition discountCondition : this.discountConditions) {
            if (discountCondition.getType() == DiscountConditionType.PERIOD) {
                // 기간 할인 판단
                if (discountCondition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
                    return true;
                }
            } else {
                // 회차 할인 판단
                if (discountCondition.isDiscountable(sequence)) {
                    return true;
                }
            }
        }

        return false;
    }

}

Movie에서 할인 조건을 판단하는 부분 및 할인 타입에 따라 예매금액을 스스로 계산을 진행하도록 수정하였다.

/**
 * 영화 상영 정보 도메인 클래스
 */
public class Screening {
    /** * 상영될 영화 정보 */
    private Movie movie;
    /** * 상영 회차 정보 */
    private int sequence;
    /** * 상영 시작 시간 */
    private LocalDateTime whenScreened;

    public void setMovie(Movie movie) { this.movie = movie; }

    public void setSequence(int sequence) { this.sequence = sequence; }
    public void setWhenScreened(LocalDateTime whenScreened) { this.whenScreened = whenScreened; }

    /**
     * 예매 금액을 계산하여 반환한다.
     * @param audienceCount 예매 인원수
     * @return 총 예매 금액
     */
    public Money calculateFee(int audienceCount) {
        switch (this.movie.getMovieType()) {
            case AMOUNT_DISCOUNT:
                if (this.movie.isDiscountable(this.whenScreened, this.sequence)) {
                    return this.movie.calculateAmountDiscountedFee().times(audienceCount);
                }

            case PERCENT_DISCOUNT:
                if (this.movie.isDiscountable(this.whenScreened, this.sequence)) {
                    return this.movie.calculatePercentDiscountedFee().times(audienceCount);
                }

            case NONE_DISCOUNT:
                return this.movie.calculateNoneDiscountedFee().times(audienceCount);
        }

        return this.movie.calculateNoneDiscountedFee().times(audienceCount);
    }
}

Screening에서는 영화 할인 타입별로 조건을 만족하는지 검사후 각 할인 타입에 알맞는 금액 계산 메서드를 호출하여 예매 금액을 계산하여 반환한다.

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        Money fee = screening.calculateFee(audienceCount);
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

screening의 calculateFee를 호출하여 예매 금액 계산을 위임하여 Reservation객체를 생성한다.

이번 설계에서는 데이터를 처리하는 데 필요한 메서드를 데이터를 가지고 있는 객체가 스스로 처리한다. 따라서, 객체들은 스스로 책임진다고 할 수 있다.


하지만.. 아직도 부족하다.

캡슐화 위반

public class DiscountCondition {
	public DiscountConditionType getType() { return type; }
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {  ...  }

    public boolean isDiscountable(int sequence) {  ...  }
}

DiscountCondition의 isDiscountable메서드의 시그니쳐를 보면 객체 내부에 DayOfWeek, LocalTime, int와 같이 파라미터를 통해 내부 인스턴스 정보를 노출하고 있다. 또한 getType을 통해 DiscountConditionType을 포함하고 있음을 노출하고 있다.

만약 DiscountCondition의 속성을 변경한다면 isDiscountable를 변경해야 하며 isDiscountable를 사용하는 클라이언트들도 수정해야한다.

public class Movie {

    /**
     * 비용 할인 금액을 제외하고 예매금액을 계산하여 반환한다.
     * @return 할인 예매 금액
     */
    public Money calculateAmountDiscountedFee() {  ...  }

    /**
     * 비율 기준 할인 금액을 제외하고 예매금액을 계산하여 반환한다.
     * @return 할인 예매 금액
     */
    public Money calculatePercentDiscountedFee() {  ...  }

    /**
     * 할인 정책이 없으므로 예매금액을 그대로 반환한다.
     * @return 할인 예매 금액
     */
    public Money calculateNoneDiscountedFee() {  ...  }

}

Movie는 파라미터나 반환타입으로 내부 구현을 노출하고 있지는 않으나 메서드의 종류를 통해 세개의 할인정책(금액, 비율, 비할인)이 존재한다는 것을 외부로 노출시키고있다.

만약 할인정책이 추가 또는 삭제된다면 이 메서드에 의존하는 클라이언트 모두를 수정해야한다.

캡슐화의 진정한 의미

캡슐화란 변할수 있는 어떤 것이라도 감추는 것이다.
내부 구현의 변경으로인해 외부 객체가 영향을 받는다면 캡슐화를 위반한것이다.
설계에서 변하는것이 무엇인지 파악하고 캡슐화해야한다.

높은 결합도

DiscountCondition의 내부구현이 외부로 노출되었기 때문에 Movie와 DiscountContidion사이의 결합도는 높을수 밖에 없다.

public class Movie {
    public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
        for (DiscountCondition discountCondition : this.discountConditions) {
            if (discountCondition.getType() == DiscountConditionType.PERIOD) {
                // 기간 할인 판단
                if (discountCondition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
                    return true;
                }
            } else {
                // 회차 할인 판단
                if (discountCondition.isDiscountable(sequence)) {
                    return true;
                }
            }
        }

        return false;
    }
}

 

  1. DiscountCondition의 할인 명칭이 바뀔경우 Movie의 조건문을 수정해야한다.
  2. DiscountCondition의 할인 조건이 추가/삭제될 경우 Movie의 조건문을 추가/삭제 해야한다.
  3. DiscountCondition의 만족여부를 판단하는 정보가 변경된다면 Movie의 isDiscountable의 파라미터도 변경될수 있다.

유연한 설계를 위해서는 캡슐화를 설계의 첫번째 목표로 삼아야한다.

낮은 응집도

public class Screening {
    public Money calculateFee(int audienceCount) {
        switch (this.movie.getMovieType()) {
            case AMOUNT_DISCOUNT:
                if (this.movie.isDiscountable(this.whenScreened, this.sequence)) {
                    return this.movie.calculateAmountDiscountedFee().times(audienceCount);
                }

            case PERCENT_DISCOUNT:
                if (this.movie.isDiscountable(this.whenScreened, this.sequence)) {
                    return this.movie.calculatePercentDiscountedFee().times(audienceCount);
                }

            case NONE_DISCOUNT:
                return this.movie.calculateNoneDiscountedFee().times(audienceCount);
        }

        return this.movie.calculateNoneDiscountedFee().times(audienceCount);
    }
}

위에 말한것 처럼 할인 여부를 판단하는 DiscountCondition의 파라미터가 변경된다면 Movie의 isDiscountable의 파라미터도 변경될 가능성이 높으며 이로인해 Screening의 calculateFee에서 isDiscountable를 호출하는 부분도 변경될 가능성이 크다.

하나의 변경을 위해 여러곳을 동시에 수정해야하는 것은 응집도가 낮다는 증거이다.


데이터 중심 설계의 문제점

  • 너무 이른시기에 데이터에 관해 결정한다.
  • 협력이라는 문맥을 고려하지 않고 객체를 고립시킨채 오퍼레이션을 고려한다.

객체의 행동보다는 상태에 초점을 둔다.

데이터 중심 설계에서 익숙한 방식으로 데이터와 가능을 분리하여 개발하고 이로인해 접근자와 수정자를 과도하게 추가하여 객체를 사용하여 절차를 다른 객체 안에 구현하게된다. 이는 상태와 행동을 캡슐화하는 객체지향 패러다임에 반하는것이다.

접근자와 수정자는 public속성과 큰 차이가 없다!

객체를 고립시킨채 오퍼레이션을 정의한다.

객체지향을 사용한다는 것은 협력하는 객체들의 공동체를 구축한다는 것이다. 따라서, 협력이라는 문맥 안에서 책임을 결정하고 그 책임을 부여받을 객체를 결정하는것이 제일 중요하다!

데이터 중심 설계의 경우 협력의 문맥에 대한 고민없이 세부정보(데이터)를 먼저 결정 후에 다른 객체와의 협력을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다.


 

GIT

2장의 소스와 비교하면서 보면 도움이 될지도...?

https://github.com/oodmumc3/object-data-driven-book-movie-ticketing.git


느낀점

이번장에서는 실제로 객체지향적인 설계가 아닌 데이터 중심의 설계로 2장에서 했던 티켓팅 앱을 구현해보고 비교해보는 내용이었다.

처음부터 객체지향적 설계를 볼때는

그냥 그런가부다.. 이게 객체 지향이구나... 나도 얼핏 비슷하게 하는거 같기도 한데...

라고 생각했었는데 이번장을 읽으면서 완전히 잘못된 생각이었다는걸 깨달았다.

데이터 중심 설계를 보면서 내 코드를 옮겨놓은것 아닌가 라는 생각이 들정도로 너무나 내 코드 스타일과 비슷했다.

내 딴에는 getter/setter를 쓰면 객체지향적 언어이고 캡슐화를 잘하면서 코딩하고 있는거야 라고 생각하면서 했는데 전혀 아니었다니 뒤통수 한대 세게 맞은 기분이었다. 어느 책이나 웹문서를 봐도 getter/setter는 자바에서 필수적 요소이고 이걸 써야 객체지향 언어라고 생각했었는데 무분별한 getter/setter나 속성을 public으로 설정한거나 다를거 없다는 책 내용에서 나는 적지않은 충격을 받았다.

한편으론 궁금하기도 했다. 기계적으로 롬복의 @Data를 사용하면서 이게 진짜 객체지향적인 개발이 맞는건지 진짜 객체지향이 뭔지 궁금했지만 검색을 해봐도 이런 내용은 전혀 찾아볼수 없었다. (물론 내가 못찾아 본걸수도 있다.)

전에도 말했지만 캡슐화, 응집도, 결합도에대한 사전적의미는 알고있었지만 그래서 뭐 어디서 어떻게 쓰라는거지? 라는 생각을 계속해서 가지고 있었는데 이번장에서 정말 깔끔하게 정리를 해주었던거 같다.

이번장의 핵심은 캡슐화, 결합도, 응집도 그리고 협력을 문맥으로 행동을 결정 한 후에 데이터를 고려한다는것이다.

이번장을 보면서 정말 많은것을 깨닫고 느꼈지만 한편으로 걱정도 된다. 머릿속으로는 이해했다고 생각은 하는데 실제 내가 개발 업무에서 이렇게까지 생각하면서 개발을 진행할 수 있을까? 라는 생각에서이다.

지금까지 해온 버릇들과 고정관념이 남아있어서 한번에 적용하긴 어렵겠지만 일단 알고있는 상태와 모르고있는 상태에서 개발은 다를거라 생각한다. 이책을 다 읽을때 쯤 내가 개발했던 소스들을 다시 보면서 수정할 곳들을 수정해 나가야겠다.

이번장은 왠지 정리하기가 너무 힘들었다. 모든 내용이 다 중요해보이긴 하지만 그중에서도 중요한 부분만 추려내서 요약하고 싶었는데 그것도 안되고;; 이해한거 같은데도 다시 읽어보니 모르겠어서 쓰다말고 또 읽고... 지금도 다 쓰긴했지만 그래도 어딘거 어색하고 빼먹은거 같다.

그래도 꾸준히 해나가서 다행이라는 생각이든다.

 

댓글