강의

멘토링

로드맵

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

백엔드해야지님의 프로필 이미지
백엔드해야지

작성한 질문수

오브젝트 - 기초편

2-2. 변경과 의존성 / 데이터 내부 변경 시 setter가 변경의 여파를 흡수하는 방식은 어떤가요?

해결된 질문

작성

·

13

1

안녕하세요, 강사님.

 

강의 '2-2. 변경과 의존성'에서 DiscountPolicystartTimeendTimeTimeInteval로 통합하면 외부에서 getter를 통해 startTime, endTime을 참조하는 코드가 변경되어야함을 보여주셨습니다.

condtion.getStartTime() -> condition.getInterval().getStartTime();

 

위 예제를 getStartTimegetEndTime 메서드가 변화를 여파를 흡수하는 방식으로 변경을 할 수도 있었을텐데 강의의 예제처럼 ReservationService가 변경되어야하도록 코드를 변경하신 관점이 궁금합니다.

// interval을 노출시키는 대신 endTime, startTime의 getter에서 변경의 여파를 흡수하고 속성없는 프로퍼티로 동작

public class DiscountCondition {
    public enum ConditionType { PERIOD_CONDITION, SEQUENCE_CONDITION }
    ...
    private TimeInterval interval;
    ...

    public LocalTime getStartTime() {
        return interval.getStartTime();
    }

    ...

    public LocalTime getEndTime() {
        return interval.getEndTime();
    }
}

 

데이터 변경의 여파가 클라이언트에 전달되는 것은 데이터를 다루는 로직이 데이터 외부에 있기 때문이라는 점은 강의에서 충분히 전달해주셨기에 어느정도 이해했다고 생각합니다. 그리고 위에 질문드린 내용은 일부 속성들이 통합되면서도 기존에 데이터 외부로 제공하던 속성(startTime, endTime)을 어떻게든 그대로 제공할 수 있는 상황(interval에서 꺼내줄 수 있음)에 한정된, 숲 보다는 나무에 관한 질문일텐데... 그래도 자꾸 생각이 나서 질문을 드리게됬습니다.

어제에 이어 또 질문을 드리지만 좋은 연휴 되시길 바라는 마음은 정말입니다.

감사합니다.

답변 2

0

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

백엔드해야지님 안녕하세요.

추석 연휴 때 평소에 하지 못했던 공부를 하고 계실거라 부담 갖지 마시고 궁금한 부분 있으면 편하게 질문 남겨주셔도 됩니다. 🙂

강의에 질문이 많이 올라오면 기존과는 다른 각도로 코드를 바라보시데 제 강의가 도움이 되는 것처럼 느껴져서 제 입장에서도 기분 좋은 일이에요.

 

질문으로 돌아와서 DiscountCondition의 getStartTime과 getEndTime으로 변경을 캡슐화하는 부분에 대해 제 의견을 말씀드릴게요.

두 개의 LocalTime 타입 변수인 startTime과 endTime을 TimeInterval로 묶은 이유는 많은 로직에서 startTime과 endTime이 함께 사용되기 때문일거에요.

예를 들어 DiscountCondition을 사용하는 두 개의 클래스 A와 B가 다음과 같이 코딩되어 있다고 가정해 보겠습니다.

class A {
  private LocalTime timeA;
  
  public void a(DiscountCondition condition) {
    if(timeA.compareTo(condition.getStartTime()) >= 0 &&
       timeA.compareTo(condition.getEndTime()) <= 0) {
       ...
    }
  }
}
	
class B {
  private LocalTime timeB;
  
  public void b(DiscountCondition condition) {
    if(timeB.compareTo(condition.getStartTime()) >= 0 &&
       timeB.compareTo(condition.getEndTime()) <= 0) {
       ...
    } 
  }
}

위 코드에는 두 가지 문제가 있습니다.

  1. 중복 코드
    클래스 A의 a() 메서드와 클래스 B의 b() 메서드에는 특정한 시간이 startTime과 endTime 사이에 포함되어 있는지 여부를 체크하는 코드가 중복되어 있습니다. 이 중복 코드를 제거할 필요가 있습니다.

  2. 시간 간격이라는 암시적인 개념
    startTime과 endTime은 함께 사용되어 시간 간격이라는 개념으로 사용되지만 이 개념이 암시적으로 표현되어 있습니다. 따라서 코드를 읽는 사람을 LocalTime을 비교하는 로직을 시간 간격이라는 개념으로 변환해야 하는 부담을 안게 됩니다.

 

이 문제를 해결할 수 있는 좋은 방법은 startTime과 endTime을 하나로 묶은 TimeInterval을 추가하고 include() 메서드를 구현하는 것입니다.

class TimeInterval {
  private LocalTime startTime;
  private LocalTime endTime;
  
  public boolean include(LocalTime time) {
	  return time.compareTo(condition.getStartTime()) >= 0 &&
         time.compareTo(condition.getEndTime()) <= 0
  }
}

다음으로 DiscountCondition이 TimeInterval을 수정하게 만듭니다.

class DiscountCondition {
  private TimeInterval interval;
  
  public TimeInterval getInterval() {
    return interval;
  }
}

이제 class A와 classB를 다음과 같이 수정할 수 있습니다.

class A {
  private LocalTime timeA;
  
  public void a(DiscountCondition condition) {
    if(condition.getInterval().include(timeA)) {
      ...
    }
  }
}
	
class B {
  private LocalTime timeB;
  
  public void b(DiscountCondition condition) {
    if(condition.getInterval().include(timeB)) {
      ...
    }
  }
}

위 코드는 중복이 제거되었을 뿐만 아니라 특정 시간의 포함 여부를 확인한다는 의미도 명시적으로 표현하고 있습니다.

 

그럼 이번에는 DiscountCondition이 getStartTime()과 getEndTime()을 공개하는 방식으로 변경을 캡슐화해 보겠습니다.

class DiscountCondition {
  private TimeInterval interval;
  
  public LocalTime getStartTime() {
    return interval.getStartTime();
  }
  
  public LocalTime getEndTime() {
    return interval.getEndTime();
  }
}

이 경우 클래스 A와 클래스 B의 문제점인 중복 코드와 암시적인 개념 문제는 여전히 남아 있게 됩니다.

여기서 더 안 좋은 점은 이 비교 로직이 TimeInterval 안에 이미 포함되어 있다는 것이죠.

결과적으로 중복 코드가 클래스 A와 클래스 B 두 군데에만 존재하던 기존 코드 대비 TimeInterval을 사용한 코드는 클래스 A, 클래스 B, TimeInterval 세 군데로 중복 코드가 늘어나게 됩니다.

class A {
  private LocalTime timeA;
  
	public void a(DiscountCondition condition) {
    if(timeA.compareTo(condition.getStartTime()) >= 0 &&
       timeA.compareTo(condition.getEndTime()) <= 0) {
       ...
    }
  }
}
	
class B {
  private LocalTime timeB;
  
  public void b(DiscountCondition condition) {
    if(timeB.compareTo(condition.getStartTime()) >= 0 &&
       timeB.compareTo(condition.getEndTime()) <= 0) {
       ...
    } 
  }
}

class TimeInterval {
  private LocalTime startTime;
  private LocalTime endTime;
  
  public boolean include(LocalTime time) {
	  return time.compareTo(condition.getStartTime()) >= 0 &&
         time.compareTo(condition.getEndTime()) <= 0
  }
}

이 예는 속성들을 묶은 새로운 타입을 추가하는 이유가 단순히 속성을 묶기 위해서가 아니라 속성들을 이용하는 어떤 행위를 추가하기 위한 것이라는 점을 잘 보여줍니다.

그리고 그 결과 기존 getter를 사용하던 코드들은 수정되거나 중복 코드를 제거하기 위해 새로운 타입 내부로 옮겨지게 됩니다.

다시 말해서 의존성으로 인해 수정되게 되는 것이죠,.

 

따라서 어떤 변수들을 하나의 클래스로 캡슐화했다면 내부에 캡슐화한 속성 단위로 getter를 제공하는 것은 좋지 않습니다.

DiscountCondition 입장에서는 getInterval() 메서드만 제공하고 getStartTime()과 getEndTime()은 제공하지 않는 것이 좋기 때문에 결과적으로 ReservationService는 어떤 방식으로든 getInterval()을 사용하도록 수정할 수 밖에 없습니다.

 

물론 더 좋은 방법은 DiscountCondition이 TimeInterval 조차도 외부로 제공하지 않는 것입니다.

이를 위해서는 getInterval() 사용하는 판단 로직을 DiscountCondition으로 이동시키는게 좋을 거에요.

이렇게 리팩터링하는게 DiscountCondition을 객체지향적인 설계로 개선하는 첫 단계라고 보시면 됩니다.

 

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

궁금한 부분이 있으시면 언제라도 추가 질문 남겨주세요. 😊

행복한 연휴 보내시구요.

0

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

백엔드해야지님께서 질문하신 부분은 데이터 캡슐화와 변경의 여파를 줄이는 전략에 관한 것입니다. 강의에서는 TimeInteval로 통합하여 getInterval()을 통해 startTime과 endTime을 가져오는 방식으로 코드가 변경되었습니다. 이에 대해 getStartTime()getEndTime() 메서드로 변경의 여파를 흡수하는 방법을 제시하셨습니다.

핵심 요약

  1. 캡슐화와 책임 분리: 데이터 변경의 영향이 클라이언트 코드로 전파되지 않도록 하는 것은 객체지향 설계에서 매우 중요합니다. startTimeendTimeTimeInteval로 묶은 것은 이 데이터들이 논리적으로 하나의 개념으로 묶이기 때문입니다. 따라서 이를 TimeInteval로 관리하여 응집도를 높일 수 있습니다.

  2. 여파 흡수 방안: 백엔드해야지님이 제안하신, 기존의 getStartTime()getEndTime() 메서드를 유지하며 내부적으로 interval로부터 데이터를 가져오는 방식은 클라이언트 코드 변화의 영향을 최소화하는 좋은 방법입니다. 이는 클라이언트 코드의 변경을 최소화하고, API의 안정성을 유지하는데 기여할 수 있습니다.

ReservationService가 변경되도록 했는가?

강의에서는 ReservationService가 변경되도록 코드를 수정한 이유는 아마도 데이터가 어떻게 변화하고 있으며, 이러한 변화에 클라이언트 코드는 어떻게 적응해야 하는지를 학습하기 위한 예로서의 목적도 있었을 것입니다. 때때로 데이터 구조 변화에 클라이언트 코드가 적응하도록 요구하는 것이 불가피하거나 더 명확한 경우가 있습니다.

무엇보다도 클라이언트 코드가 해당 데이터의 내부 구조에 대한 세부 사항에 덜 의존하도록 설계하는 것이 바람직합니다. 따라서 두 접근 방식 모두 실무에서 쓰일 수 있으며, 목적과 상황에 따라 결정될 것입니다.

관련 질문

  • 이와 유사한 고민을 하셨던 분들의 게시물을 통해, 객체지향 설계에서 결합도와 캡슐화에 대한 더 자세한 논의를 확인하실 수 있습니다.

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

백엔드해야지님의 프로필 이미지
백엔드해야지

작성한 질문수

질문하기