티스토리 뷰

728x90
반응형

서론


  • 객체지향 설계의 핵심은 책임, 역할, 협력입니다. 협력은 애플리케이션의 기능을 수행하기 위해 다른 객체들과 메시지를 주고받는 상호작용입니다. 책임은 객체가 다른 객체와 협력하기 위해 수행하는 행동이고, 역할은 대체 가능한 책임들의 집합입니다.
  • 객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 만드는 활동입니다.

 

객체지향 설계에 관한 관점 두가지

  • 첫째, 객체지향 설계의 핵심은 책임이다.
  • 둘째, 책임을 할당하는 작업은 응집도와 결합도 같은 설계 품질과 연관되어 있다.

 

결합도와 응집도를 합리적인 수준으로 유지하는 방법

  • 객체의 상태가 아닌 객체의 행동에 초점을 맞춥니다. 객체를 단순한 데이터의 집합으로 바라보는 시각은 객체의 내부 구현을 노출시키는 결과를 초래할 수 있으므로 변경에 취약해집니다. 그렇기 때문에 책임에 초점을 맞춰야합니다.
  • 책임은 객체의 상태에서 행동으로, 나아가 객체와 객체의 상호작용으로 설계 중심을 이동시키고, 결합도가 낮고 응집도가 높으며 구현을 효과적으로 캡슐화하는 객체들을 만들 수 있는 기반을 제공합니다.

 

데이터 중심의 설계


  • 객체지향 설계는 상태(데이터)를 분할의 중심으로 삼는 방법책임을 분활의 중심으로 삼는 방법이 있습니다.
  • 데이터 중심의 관점은 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 작업을 정의합니다. 또한 객체의 상태에 초점을 맞추게 됩니다. 그렇기 때문에 객체를 독립된 데이터 덩어리로 바라보게 됩니다.
  • 책임 중심의 관점은 객체는 다른 객체가 요청할 수 있는 작업을 위해 필요한 상태(데이터)를 보관합니다. 그렇기 때문에 객체를 협력하는 공동체의 일원으로서 바라보게 됩니다.

 

데이터와 책임 중 어떤것을 택해야 할까?

  • 훌륭한 객체지향 설계는 책임에 초점을 맞춰야합니다.
  • 그 이유는 상태(데이터)를 분할의 중심으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너지게 됩니다. 따라서 해당 인터페이스에 변경이 있을 경우 의존하고 있는 모든 객체에 영향이 퍼지게 됩니다.
    반면 책임을 분할의 중심으로 삼으면 책임을 드러내는 인터페이스를 뒤로 한채 책임을 수행하는데 필요한 상태를 캡슐화함으로써 
    구현이 변경되었을 경우 외부로 퍼져나가는 것을 방지할 수 있습니다. 따라서 책임에 초점을 맞추면 상대적으로 변경에 안정적인 설계를 가질 수 있습니다.

 

💡데이터를 준비하자

  • 데이터 중심의 설계란 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할하는 방법입니다.

 

Movie 클래스

더보기
@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 enum MovieType {

    AMOUNT_DISCOUNT,
    PERCENT_DISCOUNT,
    NONE_DISCOUNT;
}

 

DiscountCondition 클래스

더보기
@Getter
@Setter
public class DiscountCondition {

    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
}

public enum DiscountConditionType {

    SEQUENCE,
    PERIOD;
}

 

Screening 클래스

더보기
@Getter
@Setter
public class Screening {

    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;
}

 

Reservation 클래스

더보기
@Getter
@Setter
@AllArgsConstructor
public class Reservation {

    private Customer customer;
    private Screening screening;
    private Money fee;
    private int audienceCount;
}

 

Customer 클래스

더보기
@AllArgsConstructor
public class Customer {

    private String name;
    private String id;
}

 

데이터 클래스 구조

 

 

ReservationAgency 클래스

  • reserve 메서드는 두가지의 부분으로 나눌 수 있습니다. 첫번째는 반복문을 순회하면서 할인 가능 여부를 discountable 변수에 할당하고 있습니다. 두번째는 discountable 값을 확인하여 할인 정책에 따라 요금을 계산하고 있습니다.
더보기
public class ReservationAgency {

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

        boolean discountable = false;
        for (DiscountCondition discountCondition : movie.getDiscountConditions()) {
            if (discountCondition.getType() == DiscountConditionType.PERIOD) {
                discountable = screening.getWhenScreened().getDayOfWeek().equals(discountCondition.getDayOfWeek()) &&
                        discountCondition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                        discountCondition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
            } else {
                discountable = discountCondition.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);
        } else {
            fee = movie.getFee();
        }
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

 

설계 트레이드오프


  • 상태(데이터) 중심의 설계와 책임 중심의 설계의 장단점을 비교하는 방법에 캡슐화, 응집도, 결합도를 사용할 수 있습니다.

 

💡 캡슐화

  • 객체의 내부 구현을 외부로부터 감추는 것입니다. 여기서 내부 구현이란 추후에 변경될 수 있는 모든 것입니다.
  • 객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 인터페이스만 제공하여 어떠한 변경이 전체 시스템에 영향을 끼치지 않도록 하는 것입니다.
  • 변경될 가능성이 높은 부분을 구현이라 부르고 상대적으로 안전한 부분을 인터페이스라 부릅니다. 
  • 객체를 설계하기 위한 가장 기본적인 아이디어는 변경 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스만 의존하도록 관계를 조절하는 것입니다.
  • 이렇게 객체지향에서 가장 중요한 것은 캡슐화입니다. 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류입니다.
  • 캡슐화를 지킨다면 응집도는 높아지고 결합도는 낮아질 수 있습니다.

 

💡 응집도와 결합도

 

응집도

  • 응집도는 모듈에 포함되어 있는 내부 요소들이 연관된 정도를 나타냅니다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 높은 응집도를 가졌다고 할 수 있습니다.
  • 객체지향의 관점에서 응집도는 객체 또는 클래스에게 얼마나 관련된 높은 책임을 할당했는지를 나타냅니다.

결합도

  • 결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 가지고 있는지 나타내는 척도입니다.
  • 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 높은 결합도를 가지고 있다고 할 수 있습니다.
  • 결합도는 한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정할 수 있습니다.
  • 내부 구현을 변경햇을 때 이것이 다른 모듈에 영향을 미치는 경우 두 모듈 사이의 결합도가 높다고 표현합니다. 
    반면 인터페이스를 수정했을 때만 다른 모듈에 영향을 미치는 경우에는 결합도가 낮다고 표현합니다. 따라서 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있습니다.
  • 결합도를 살펴보는 방법은 해당 객체의 import를 보면 확인할 수 있지 않을까? 생각이 듭니다.

 

좋은 설계

  • 일반적으로 좋은 설계란 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계를 의미합니다.
  • 좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 받아드릴 수 있는 설계입니다.
  • 높은 응집도와 낮은 결합도를 추구해야 하는 이유는 설계를 변경하기 쉽게 만들기 때문입니다. 변경의 관점에서 응집도란 변경이 일어났을 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있습니다.

 

문제점


  • 데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만듭니다. 

💡캡슐화 위반

  • Movie 클래스를 보면 getter, setter를 통해 객체의 내부 상태에 접근할 수 있다는 것을 알 수 있습니다.
    getter, setter를 통해 캡슐화를 지키는것 같지만 해당 매서드를 통해 Movie 클래스는 titile, fee 등 여러가지 속성이 있다는 것을 유추할 수 있습니다.
더보기
@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;
}

 

💡높은 결합도

  • ReservationAgency의 reserve 메서드를 보면 모든 제어 로직이 특정 객체 안에 집중되어 있다는 것을 알 수 있습니다.  
    이렇게 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것입니다. 
    이렇게 된다면 특정한 객체가 변경이 발생할 때 해당 제어 로직 객체도 함께 변경이 발생하게 됩니다.
더보기
public class ReservationAgency {

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

        boolean discountable = false;
        for (DiscountCondition discountCondition : movie.getDiscountConditions()) {
            if (discountCondition.getType() == DiscountConditionType.PERIOD) {
                discountable = screening.getWhenScreened().getDayOfWeek().equals(discountCondition.getDayOfWeek()) &&
                        discountCondition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                        discountCondition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
            } else {
                discountable = discountCondition.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);
        } else {
            fee = movie.getFee();
        }
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

 

💡낮은 응집도

  • 서로 다른 이유로 변경되는 코드가 하나의 제어 로직 안에 있을 때 응집도가 낮다고 표현할 수 있습니다.
    예를들어 위의 reserve 메서드에서 할인 정책이 추가되거나 정책별 할인 요금을 계산하는 방업이 변경되는 경우, 할인 조건이 추가되는 경우 등이 발생했을 경우 reserve 메서드에도 변경이 발생하게 됩니다.

낮은 응집도로 발생하는 문제점

  • 제어 로직이 한 곳에 있기 때문에 어떠한 객체의 수정시 다른 코드도 영향을 받게 됩니다. 어떤 코드를 수정한 후 아무런 상관없던 코드에서 문제가 발생하는 것은 모듈의 응집도가 낮을 때 발생하는 증상입니다.
  • 하나의 요구사항 변경을 반영하기 위해서는 동시에 여러 모듈을 수정해야 합니다. 예를들어 MovieType이 추가되었을 경우 case문을 추가해야 합니다.

 

자율적인 객체를 향해


💡캡슐화를 지켜라

  • 객체는 자신이 어떤 데이터를 가지고 있는지를 캡슐화하고 외부에 공개해서는 안됩니다. 객체는 스스로의 상태를 챚임져야 하며 외부에서는 인터페이스를 통해서만 상태에 접근할 수 있어야 합니다.
  • 여기서 말하는 메서드는 단순히 속성값을 반환하거나 수정자 메서드를 의미하는게 아닙니다. 객체에게 의미있는 메서드는 객체가 책임져야 하는 무엇가를 수행하는 메서드 입니다.
  • 속성을 private로 설정했다고해서 getter, setter를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것입니다.

 

캡슐화를 위반하는 예제 

더보기
@Getter
@Setter
public class Rectangle {

    private int left;
    private int right;
    private int top;
    private int bottom;

    public Rectangle(int left, int right, int top, int bottom) {
        this.left = left;
        this.right = right;
        this.top = top;
        this.bottom = bottom;
    }
}

public class Example {

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(10, 10, 10, 10);
        rectangle.setLeft(rectangle.getLeft() + 10); // 캡슐화 위반
    }
}

 

캡슐화를 지키는 예제 

더보기
public class Rectangle {

    private int left;
    private int right;
    private int top;
    private int bottom;

    public Rectangle(int left, int right, int top, int bottom) {
        this.left = left;
        this.right = right;
        this.top = top;
        this.bottom = bottom;
    }

    public void changeWidth(int value) {
        this.top += value;
        this.bottom += value;
    }
}

public class Example {

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(10, 10, 10, 10);
        rectangle.changeWidth(10);
    }
}

 

아직 남은 여러 문제들


💡캡슐화 위반

  • 아래는 DiscountCondition을 수정한 코드입니다. 해당 클래스에는 isDiscountable 메서드가 존재하는데 매개변수로 DiscountCondition에 속성으로 포함되어 있는 정보드를 받아오고 있습니다. 이 메서드는 매개변수를 통해서 해당 객체가 DayOfWeek, LocalTime의 속성값을 가지고 있구나라고 유추할 수 있습니다. 그렇기 때문에 캡슐화를 위반하게 됩니다.
  • 또한 DiscountCondition의 속성을 변경하게 된다면 해당 메서드의 매개변수도 변경이 발생하게 되고, 해당 메서드를 사용하고 있는 객체들도 변경이 발생하게 됩니다. 이러한 증상은 캡슐화가 부족하다는 증거입니다.
더보기
@Getter
public class DiscountCondition {

    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
        if (type != DiscountConditionType.PERIOD) {
            throw new IllegalArgumentException();
        }

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

    public boolean isDiscountable(int sequence) {
        if (type != DiscountConditionType.SEQUENCE) {
            throw new IllegalArgumentException();
        }
        return this.sequence == sequence;
    }
}

 

💡캡슐화의 진정한 의미

  • 내가 알던 캡슐화는 캡슐화가 아니였구나라는 것을 느끼게 되었습니다. 그럼 캡슐화가 뭘까요? 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미합니다. 내부 속성을 외부로 감추는 것은 "데이터 캡슐화" 라고 불리는 캡슐화의 한 종류일 뿐입니다.

 

💡높은 결합도

  • 캡슐화의 위반으로 인해 DiscountCondition의 내부 구현이 외부로 노출되었기 때문에 Movie와 DiscountCondition은 높은 결합도를 가지게 되었습니다. 결합도가 높은 이유는 DiscountCondition에서 정책 할인 조건이 추가 되거나 삭제되면 Movie 클래스의 isDiscountable 메서드 또한 변경이 발생하기 때문입니다.
더보기
@Getter
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 boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
        for (DiscountCondition discountCondition : discountConditions) {
            if (discountCondition.getType() == DiscountConditionType.PERIOD) {
                if (discountCondition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
                    return true;
                }
            } else {
                if (discountCondition.isDiscountable(sequence)) {
                    return true;
                }
            }
        }
        return false;
    }
}

 

💡 낮은 응집도

  • DiscountCondition에 수정이 발생하면 DiscountCondition을 사용하고 있는 Movie에서도 변경이 발생하고 Screening에서 Movie의 isDiscountable 메서드를 호출하는 부분도 변경이 발생하게 됩니다. 
  • 따라서 DiscountCondition의 변경이 발생하면 Movie, Screening에도 변경이 발생하기 때문에 응집도가 낮다고 표현할 수 있습니다.
더보기
@Getter
public class Screening {

    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;

    public Money calculateFee(int audienceCount) {
        switch (movie.getMovieType()) {
            case AMOUNT_DISCOUNT:
                if (movie.isDiscountable(whenScreened, sequence))
                    return movie.calculateAmountDiscountedFee().times(audienceCount);
                break;
            case PERCENT_DISCOUNT:
                if (movie.isDiscountable(whenScreened, sequence))
                    return movie.calculatePercentDiscountedFee().times(audienceCount);
                break;
            case NONE_DISCOUNT:
                return movie.calculateNoneDiscountedFee().times(audienceCount);
        }
        return movie.calculateNoneDiscountedFee().times(audienceCount);
    }
}

 

데이터 중심의 설계의 문제점


  • 데이터 중심의 설계는 변경에 취약한 이유를 가지고 있습니다.
    • 첫째, 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요합니다.
    • 둘째, 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 작업을 결정합니다.

 

💡데이터 중심 설계는 객체의 행동보다 상태에 초점을 맞춥니다.

  • 데이터 설계는 처음 시작시 이 클래스는 어떤 데이터를 담고 있어야하지? 부터 시작하게 됩니다. 이렇게 된다면 너무 이른 시기에 내부 구현에 초점을 맞추게 됩니다.
  • 데이터 중심의 관점은 객체는 단순 데이터를 모으는 집합체일 뿐입니다. 이로 인해 과도한 getter, setter를 사용하게 되고, 데이터 객체를 사용하는 절차를 분리하지 않고 단순히 객체 안에 구현하게 됩니다.

 

💡데이터 중심 설계는 객체를 고립시킨 채 작업을 정의합니다.

  • 객체지향 어플리케이션은 협력하는 객체들의 공동체를 구축하는 것을 의미합니다. 따라서 협력이라는 문맥안에서 적절한 책임을 찾고 이를 적절한 객체에게 주어야하는 것이 가장 중요합니다.
  • 올바른 객체지향 설계의 무게 중심은 항상 객체 내부가 아닌 외부에 맞춰있어야 하는데 데이터 중심의 설계의 무게 중심은 객체 내부에 맞춰있습니다. 이렇게 객체의 내부 구현이 이미 구현된 상태에서 다른 객체와의 협력 방버을 고민하기 때문에 인터페이스를 억지로 끼워맞출 수 밖에 없습니다.

 

 

 

 

 

 

 

728x90
반응형