인프런 커뮤니티 질문&답변

icarus8050님의 프로필 이미지
icarus8050

작성한 질문수

오브젝트 - 기초편

5-1. 객체 구현하기

안녕하세요. 기존의 추상화된 역할에 대해서 새로운 협력자가 필요하게 되는 경우는 어떻게 설계해야할까요??

해결된 질문

작성

·

34

·

수정됨

1

안녕하세요. 객체지향의 사실과 오해, 오브젝트부터 이어서 강의까지 영호님의 강의를 즐겁게 보고 있습니다.

강의를 보면서 궁금한 것이 있습니다.

Movie는 DiscountPolicy와 협력하고 있고, DisscountPolicy는 DiscountCondition과 협력하고 있습니다. 여기서 DiscountCondition은 Screen 객체에 의존하여 협력하고 있구요.

보통 현업에서는 이렇게 추상화를 하여 책임을 설계해두어도, 어느날 갑자기 새로운 협력자가 필요하게 되어 그 책임을 온전히 수행하기 힘든 상황이 자주 발생하여 추상화가 깨지는 케이스가 많이 있는 것 같다는 생각이 들었는데요.

만약에 위 예제에서 "고객이 생일인 경우에는 영화 가격의 10%를 할인해준다." 라는 할인 조건이 추가된다면 DiscountCondition 에 협력자로 Customer가 파라미터로 전달이 되어야할 것 같습니다. 이를 위해 BirthdayDiscountCondition이라는 할인 조건을 만들고, isSatisfiedBy() 메서드에 Customer를 인자로 받아야하게 됩니다. 하지만 SequenceCondition과 PeriodCondition은 필요하지도 않는 Customer 협력자를 의존하게 되는 상황이 되는데요.

위 상황에서는 복잡성이 높지 않고, 이 정도의 의존성은 무시해도 된다고 판단되어 그냥 추가할 수도 있지만, 이런식으로 다른 협력자들이 계속해서 추가되어 복잡성이 늘어나면 객체랑은 상관없는 협력자의 추가로 인해 테스트 또한 매번 깨지는 상황도 발생하게 될 것 같습니다.

이렇게 기존에 설계된 추상화가 다른 조건의 추가로 인해 깨지는 경우에는 어떻게 설계를 접근하는 것이 좋을까요??

 

답변 2

1

조영호님의 프로필 이미지
조영호
지식공유자

윤철님 안녕하세요.

좋은 질문 남겨주셔서 감사합니다. 🙂

추상화의 역할은 '현재 알고 있는' 변경을 캡슐화해서 코드 수정으로 인해 받을 수 있는 영향을 최소화하는 것입니다.

여기에서 '현재 알고 있는' 변경이라는 점이 중요한데 어떤 부분이 변경될지 모르는 상황에서 예상에 기반해서 설계할 경우 불필요한 추상화를 도입하게 되고 결과적으로 코드를 수정하게 되기 때문입니다.

현재 알고 있는 변경은 Screening의 데이터에 따라 할인 여부가 달라진다는 사실이기 때문에 Screening에 의존하고 있지만 그 시점에 어떤 데이터가 필요할지 알지 못하기 때문에, 그리고 Screening이 아닌 다른 요소들에 기반해서 할인 여부를 판단하도록 요구사항이 변경될지 알지 못하기 때문에 현재와 같은 클래스 구조를 유지했다고 생각하시면 될 것 같아요.

실제로 Screening 이외의 데이터를 필요로하지 않는다면 현재 코드는 그 사실을 명확하게 보여줍니다.

실제로 코드가 변경되고 새로운 추상화가 필요하다면 그 사실을 코드에 반영하면 됩니다.

말씀하신 경우에는 객체가 아닌 가격 계산에 필요한 항목들을 포함하는 새로운 객체를 추가해서 해결할 수 있을 것 같습니다.

public class PriceFactors {
  private Screening screening;
  private Customer customer;

  public PriceFactors(Screening screening, Customer customer) {
    this.screening = screening;
    this.customer = customer;
  }

  public Money getFixedFee() {
    return screening.getFixedFee();
  }

  public boolean isSequence(int sequence) {
    return screening.isSequence(sequence);
  }
  
  public LocalDate getBirthdate() {
    return customer.getBirthdate();
  }
}

그리고 Movie, DiscountPolicy, DiscountCondition의 오퍼레이션이 PriceFactors를 전달받도록 수정합니다.

public interface DiscountCondition {
  boolean isSatisfiedBy(PriceFactors factors);
}

이제 BirthdateCondition을 다음과 같이 구현할 수 있습니다.

public interface BirthdateCondition implements DiscountCondition {
  @Override 
  public boolean isSatisfiedBy(PriceFactors factors) {
    return factors.getBirthdate().equals(LocalDate.now());
  }
}

새로운 요구사항으로 인해 많은 클래스를 수정해야 하기 때문에 변경 전의 코드는 Screening 이외의 다른 요소를 이용해서 할인 여부 확인에 대해서는 취약한 설계라고 할 수 있습니다.

여기에서 긴장은 PriceFactors를 프로젝트 처음부터 추가할 수 없었느냐인데 새로운 할인 대상이 추가된다는 사실이 확정이 아닌 상태에서 기존 코드에 PriceFactors를 추가했다면 코드가 불필요하게 복잡해 보일것입니다.

다른 관점에서 Screening에 대해 모든 클래스가 의존하기 때문에 PriceFactors를 미리 추가하는 것이 도움이 된다고 생각할 수 있습니다. 이 경우 PriceFactors를 미리 추가하는 것이 더 합리적이라고 볼 수 있습니다.

결국 PriceFactors와 같은 새로운 요소를 미리 도입할지 여부는 해당 변경에 대한 발생가능성과 발생했을 때 코드를 수정하기가 얼마나 어려운지에 따라 달라지게 됩니다.

이제 PriceFactors를 이용해서 리팩터링했기 때문에 현재의 설계는 이런 유형의 변경을 안정적으로 추가할 수 있게 됐습니다. 하지만 현재 예상할 수 없는 새로운 유형이 요구사항이 추가된다면 리팩터링된 코드 역시 다시 수정될 수 밖에 없습니다.

여기에서 눈여겨보실 부분은 Screening, Movie, DiscountPolicy, DiscountCondition의 역할과 협력 방식은 변경되지 않았다는 점입니다. 현재의 설계는 인터페이스는 변경되지만 객체의 역할과 책임 관점에서는 안정적이라고 할 수 있습니다.

다음과 같이 정리할 수 있을것 같아요.

  1. 예상하지 못했던 새로운 유형의 요구사항이 추가됐을 때 변경하지 않고 해당 요구사항을 수용할 수 없습니다. 현재의 요구사항에 적합한 추상화를 선택하세요.

  2. 요구사항이 변경된다면 변경된 요구사항에 적합한 추상화를 이용해서 코드를 수정하세요. 하지만 예측에 기반해서 추상화를 도입하지 말고 실제로 변경이 일어날 때까지 기다렸다가 적합한 추상화를 도입하세요.

  3. 요구사항이 너무 복잡해져서 현재의 추상화에 적합하지 않다면 전체적인 역할, 책임, 협력을 수정해야할 수도 있습니다. 현재의 추상화를 버리고 변경된 요구사항에 적합한 새로운 역할, 책임, 협력을 찾으세요.

질문 중에 테스트가 매번 깨지는 상황이 발생할 수 있다고 하신 부분이 있는데 이건 아마도 테스트가 실패한다는 의미가 아니라 테스트 코드를 수정해야 하는 상황을 말씀하신 것 같아요. PriceFactors를 도입한 후에는 Customer와 Screening과 같은 요소에 기반해서 할인 정책이 변경되는 경우에는 테스트 코드를 수정할 필요가 없을 거에요. 하지만 기존에 알지 못했던 새로운 유형의 요구사항이 추가된다면 당연히 테스트 케이스는 수정될 수 밖에 없을 것입니다.

정말로 코드 수정에 의해 테스트가 매번 수정된다면 3번과 같이 현재의 역할, 책임, 협력을 다시 고민해야 하는 좋은 기회로 삼을 수 있을것 같아요.

요약하면 추상화와 캡슐화는 현재 알고 있는 변경을 드러낼 수 있는 것이면 됩니다. 미래의 요구사항은 알 수 없기 때문에 예상에 기반해서 불필요한 추상화를 미리 도입해서는 안됩니다. 대부분의 경우에 역할과 책임을 납득할만하게 할당하고 클래스와 메서드를 작게 유지한다면 생각보다 코드를 수정하기가 어렵지 않을거에요.

답변이 되었는지 모르겠네요. 😄

 

윤철님의 프로필 이미지
윤철
질문자

고견 감사합니다. 🙂

추상화라는 개념을 제가 잘못 이해하고 있었군요. 추상화는 미래에 어떻게 변경될지 예측할 수 없기 때문에 느슨한 결합을 통해 확장성을 고려해야 하는 것으로 이해하고 있었는데, 그것이 아니라 현재 알고 있는 것에 대한 변경으로 인해 영향을 최소화하는 것이군요!

말씀해주신 부분을 토대로 다시 생각해보니 미래에 어떻게 변경될지 예측하여 추상화 하는건 신의 영역이라는 생각이 드네요.

중요한 것을 알아가는 것 같습니다. 이 강의를 수강하길 정말 잘한 것 같습니다. 답변 감사합니다. 🙇

조영호님의 프로필 이미지
조영호
지식공유자

정확하게 이해하셨습니다

막연한 예측이 아니라 현재 알고 있는 지식을 기반으로 최대한 단순하게 구현하면 더 유연하고 확장성 있는 코드를 얻게 되실거에요!

강의가 도움이 되셨다지 정말 뿌듯하네요. 🙂

눈길 조심하시고 즐거운 하루 보내세요!

0

안녕하세요, 인프런 AI 인턴입니다.

윤철님께서 제기하신 질문은 객체지향 설계에서 발생할 수 있는 주요 문제 중 하나인 추상화의 깨짐에 대한 고민이라고 생각됩니다.

문제 설명

기존의 설계에서는 DiscountPolicy가 여러 DiscountCondition과 협력하는 구조를 가지고 있습니다. 새로운 조건(예: “고객이 생일인 경우 할인을 제공”)이 추가되면서 Customer 객체와의 협력이 필요하게 되어, 기존의 DiscountCondition 계층 구조가 부담해야 할 책임이 늘어나면서 추상화가 깨질 가능성이 생깁니다.

해결 방안

  1. 인터페이스 분리 (Interface Segregation Principle):

    • 각 협력자가 자신의 역할에만 집중할 수 있도록 DiscountCondition 인터페이스를 세분화하세요. 예를 들어 ScreenCondition, CustomerCondition같이 나누어 각 역할에 맞는 책임을 분리합니다.
    • 이렇게 하면 각 조건이 필요한 정보에만 의존하게 되어 불필요한 의존성을 줄일 수 있습니다.
  2. 구성원에 따른 전략 패턴 적용:

    • 새로운 할인 조건이 계속 추가된다면 이를 전략 패턴으로 처리할 수 있습니다. 각 조건을 개별 전략으로 구현하여 필요에 따라 적용할 수 있도록 합니다.
    • 예를 들어 DiscountStrategy 인터페이스를 만들고, 각 조건은 이를 구현하여 각각 다른 계산 로직을 가질 수 있습니다.
  3. 책임 분리:

    • 모든 할인 조건이 하나의 인터페이스에서 다루어지지 않도록 각각의 책임을 더 작은 단위로 나누어 설계합니다.
    • 예를 들어, BirthdayDiscountConditionCustomerBasedDiscountCondition 인터페이스를 따르고, SequenceConditionPeriodConditionScreenBasedDiscountCondition 인터페이스를 따르게 할 수 있습니다.
  4. 데코레이터 패턴 사용:

    • 각 할인 조건을 데코레이터로 감싸 추가적인 기능을 입힐 수 있습니다. 이를 통해 다양하고 복잡한 조건들을 구성해서 사용할 수 있습니다.

문제를 다양한 각도에서 바라보면서 적절한 디자인 패턴이나 설계 원칙을 적용하여 복잡성을 줄여보세요. 추상화가 깨지지 않는 지속 가능한 구조를 만들 수 있을 것입니다.

위 설명과 비슷한 맥락에서 고민한 사례가 있어 관련 질문과 답변 을 추천드립니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해 드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

icarus8050님의 프로필 이미지
icarus8050

작성한 질문수

질문하기