[개발 서적] Object(코드로 이해하는 객체지향 설계)

5 분 소요

🔗 들어가며

객체지향의 사실과 오해를 읽고 나서 Object를 연속해서 읽고 있다. 왜 지인이 객체지향의 사실과 오해 -> Object순으로 읽으라고 했는지 확실히 이해할 수 있었다. 객체지향의 사실과 오해에서 개념적으로 배운 내용을 토대로 Object 속 코드들을 바라보니 코드를 통해 개념을 명확히 이해할 수 있었다. 이번 게시글에서는 책 속에 수 많은 내용 중에서도 좋은 인터페이스에 대해서 다뤄보고자 한다.


🔗 좋은 인터페이스가 객체의 품질을 결정한다!

객체지향에서의 핵심은 협력을 기준으로 객체를 바라보는 것이다. 객체지향에서의 객체들은 메시지를 통해 상호작용을 하며, 메시지가 적절한 객체를 선택함으로써 내부 구현이 인터페이스에 노출되지 않게 된다.

여기서 어떠한 메시지를 전달받냐에 따라 객체의 interface가 정해진다. 결국 객체지향에서는 인터페이스를 잘 설계하는 것이 객체의 품질을 결정하게 된다.


🔗 좋은 인터페이스?

그렇다면 좋은 인터페이스의 기준이 뭘까??

좋은 interface는 다음 두 가지 조건을 만족시켜야 한다.

  1. 최소한의 인터페이스
    • 꼭 필요한 operation만을 인터페이스에 포함한다.
  2. 추상적인 인터페이스
    • how가 아닌 what을 표현한다.

이 책에서는 다음과 같은 인터페이스를 설계하기 위해 4가지 원칙과 기법을 소개한다.

  1. 디미터 법칙
  2. 묻지 말고 시켜라
  3. 의도를 드러내는 인터페이스
  4. 명령-쿼리 분리

그렇다면 지금부터 위 네가지 원칙에 대해 조금 더 자세히 알아보는 시간을 가지자!! :>


🔗 디미터 법칙

디머터 법칙은 캡슐화를 또 다른 관점에서 표현한 것이다.

  • 캡슐화 : 클래스 내부 구현을 감춰야 한다는 것을 강조
  • 디미터 : 협력하는 클래스의 캡슐화를 지키기 위해 접근해야 하는 요소 제한

즉, 디미터는 다음의 대상에게만 메시지를 전송하도록 한다.

  1. class의 method의 인자로 전달된 클래스(class 자신 포함)
  2. class의 인스턴스 변수 클래스

그렇다면 다음의 코드를 살펴보자.

(이러한 형태를 기차 충돌이라고 한다.)
screening.getMovie().getDiscountConditions();

우선 코드만 보아도 연쇄적으로 메세지를 요청하는 것을 알 수 있다. 조금 더 자세히 살펴보자!

  • getMovie()
    • 수신자의 내부 구조를 물어본다.
    • screening 속 Movie 객체에 직접 접근을 한다.
  • getDiscountConditions()
    • 반환에 대해 연쇄적으로 메시지를 요청한다.

따라서 위의 코드는 디미터 법칙의 위반이다. 이를 그럼 어떻게 수정할 수 있을까?

screening.calculateFee(audienceCount);

아마 관련된 코드 맥락이 없어 해당 코드가 정확히 무슨 일을 수행하는지 파악하는 것은 어려울 것이다. 하지만 여기서 핵심은 수신자의 내부 구조에 대해 묻지 말고 무엇을 원하는지를 단순히 명시하자!

이 내용은 아래 "묻지 말고 시켜라" 원칙과도 연결되는 내용이다.


🔗 묻지 말고 시켜라!

“묻지 말고 시켜라” 원칙은 디미터 법칙을 준수하는 협력을 만들기 위한 스타일을 제시한다. 위에서 설명했듯이 객체의 내부 구조를 묻는 것은 캡슐화를 위반시키는 행위이다.

인스턴스를 private로 설정하더라도 getter/setter를 통해 외부에서 접근이 가능하다면 이는 캡슐화 위반이다.

다음의 경우 “묻지 말고 시켜라” 원칙을 고려해보자!!

  1. 내부 상태를 묻는 오퍼레이션을 인터페이스에 포함시키고 있다면?
    • 캡슐화 위반인지 확인하자!
  2. 내부 결정을 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재한다면?
    • 해당 객체의 책임 누수를 의심하자!


🔗 의도를 드러내는 interface

interface는 객체의 책임에 대한 how가 아니라 what을 서술한다.

  • how : 협력 설계 시기에 내부 구현을 고민하게 한다.
  • what : 객체의 메시지 전송 목적에 초점

만약 기간에 의해 할인 가격이 결정되는 영화, 순번에 의해 할인 가격이 결정되는 영화가 있다 해보자! 두 객체는 영화라는 abstrac class를 상속받고 있다. 여기서 두 영화 객체가 다음과 같이 할이 여부를 판단하는 인터페이스를 가질 때 문제점이 무엇일까?

isSatisfiedByPeriod / isSatisfiedBySequence
  1. 서로 동일하게 할인 여뷰를 판단하는 책임을 수행하지만 이름만 확인했을 때는 동일한 작업을 수행한다는 사실을 알아채기 어렵다.
  2. 클라이언트가 협력하는 객체의 종류를 알게 된다.

그렇다면 해당 interface가 의도를 명확히 드러낼 수 있도록 다음과 같이 수정해보자!

isSatisfiedBy

이렇게 된다면 위의 두 가지 문제가 모두 해결이 된다.

  1. 동일한 인터페이스 이름을 통해서 두 메서드가 동일한 목적을 가진다는 것을 단번에 확인할 수 있다.
  2. 클라이언트가 협력하는 객체의 종류를 알 필요가 없어진다.

따라서 메서드의 이름을 지을 때는 어떻게보다는 무엇이 드러나게 하자! 메시지 수신자의 입장이 아니라 송신자(클라이언트)의 입장에서 의도가 분명하게 드러나도록 하자! 하나의 예시를 더 확인해보자!

theater -(티켓판매)-> TicketSeller:setTicket

여기서 setTicket를 통해서 해당 메소드가 무슨 책임을 수행하는지 알겠는가? 맥락을 모른다면 무슨 작업을 수행하는지 명확히 이해하기 어렵다. 클라이언트 의도가 드러나도록 naming을 바꿔보자!

theater -(티켓판매)-> TicketSeller:sellTo(티켓판매의 의도 드러냄)


🔗 원칙에는 함정이 존재한다!

마지막 명령-쿼리 분리 원칙을 보기 전에 앞의 세 원칙에 대해 잠깐 짚고 넘어갈 것이 있다. 그렇다면 위의 세 원칙을 무조건적으로 따르는 것이 과연 좋을까??

소프트웨어 설계에는 법칙이란 없으며, 원칙에는 예외가 존재한다. 설계란 트레이드오프의 산물이라는 점을 명심 또 명심하자! 따라서 우리는 경우에 따라 적절한 원칙을 적용해야 한다.

🔗 디미터 법칙의 함정

다음과 같이 기차 충돌이 디미터 법칙을 위반한다는 것을 위에서 확인했다.

screening.getMovie().getDiscountConditions()

해당 코드가 디미터 법칙을 위반하는 이유는 수신자의 내부 요소에 직접 접근해 메시지를 연쇄적으로 요청하기 때문이다. 그렇다면 디미터 법칙은 (.)dot을 하나만 강제하는 것일까? 다음의 예를 보자!

IntStream.of(1, 2, 3).filter(x -> x > 2).distinct().count();

해당 코드도 위와 같이 마치 기차 충돌을 일으키는 코드처럼 보인다. 하지만 코드를 살펴본다면 해당 코드는 단지 IntStream을 반환할 뿐, 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다. 따라서 디미터 법칙을 위반하는 코드가 아니다!!

해당 예제를 통해서 알 수 있듯이 디미터 법칙은 하나의 (.)dot을 강제하지 않는다. 기차 충돌처럼 보인다고 해서 항상 디미터 법칙 위반이 아니다!!

⭐️ 기차 충돌처럼 보인다면 다음을 살펴보자! 과연 여러 개의 dot을 사용하는 코드가 객체의 내부 구조를 노출하고 있는가?


🔗 묻지 말고 시켜라 원칙의 함정

“묻지 말고 시켜라” 또한 함정이 존재한다. 위에서 살펴봤던 예시를 다시 가져와보자!

(변경 )
screening.getMovie().getDiscountConditions();

(변경 )
screening.calculateFee(audienceCount);

객체의 내부 구조를 묻는 getMovie() 메서드를 없애고 클라이언트의 의도를 드러내는 메소드 calculateFee를 사용한 것을 위에서 확인했다. 결론적으로는 하나의 위임 메서드를 추가한 것이다.

그렇다면 이렇게 위임 메서드를 추가하는 것이 항상 좋은 방식일까? 이를 확인하기 위해서 책의 예시를 가져와보겠다.

public class PeriodCondition implements DiscountCondition {
  public boolean isSatisfiedBy(Screening screening) {
    return screening.getStartTime().getDayOfWee().equals(dayOfWeek) &&
      startTime.compareTo(screening.getStartTime().toLocalTime())
      ....
  }
}

다음 코드에서 PeriodCondition이 screening 객체의 내부 구조를 묻는다는 것을 알 수 있다. 그렇다면 “묻지 말고 시켜라” 원칙을 만족하기 위해 위의 코드를 Screening으로 옮겨보자!!

public class Screening {
  public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTme) {
    return whenScreened.getDayOfWeek().equals(dayOfWeek) &&
      startTime.compareTo(whenScreened.toLocalTime())
      ...
  }
}

public class PeriodCondition implements DiscountCondition {
  public boolean isSatisfiedBy(Screening screening) {
    return screening.isDiscountable(dayOfWeek, startTime, endTime);
  }
}

우선 이렇게 수정했을 경우, Screening의 캡슐화를 향상시킬 수 있다. 하지만 다음의 문제가 추가적으로 발생한다.

  1. Screening의 응집도 감소
    • 영화 할인 조건을 판단하는 것이 과연 Screening의 책임일까?
    • 아니다! 이는 PeriodCondition의 본질적인 책임이다.
    • 따라서 Screening의 응집도는 감소한다.
  2. Screening과 PeriodCondition의 결합도 증가
    • Screening:isDiscountable에서 PeriodCondition의 인스턴스 변수를 인자로 받고 있다.
    • PeriodCondition의 인스턴스 변수 목록이 변경된다면 Screening도 영향을 받게 된다.

따라서 이러한 측면에서는 Screening의 캡슐화 향상이라는 결과보다는 Screening의 응집도를 높이고, Screening과 PeriodCondition의 결합도를 낮추는 것이 전체적으로 더 좋은 방법이다.

➕ 가끔은 묻는 것 외에 다른 방법이 존재하지 않을 수도 있다! 묻는 대상이 객체인지, 자료 구조인지 파악하자.

  • 객체라면 내부 구조를 숨겨야 하므로 디미터 법칙을 따라야 한다.
  • 자료구조라면 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.


🔗 명령-쿼리 분리 원칙

가끔 묻는 것이 정답일 경우가 있는 사례를 살펴보았다. 그렇다면 이제 마지막 원칙인 명령-쿼리 분리 원칙을 살펴보자. 이를 참조해 퍼브릭 인터페이스에 올바른 인터페이스를 설계하자!

이 원칙의 핵심은 오퍼레이션은 명령 “Or” 쿼리여야 한다는 점이다.

그렇다면 명령/쿼리란 정확히 무엇을 말하는 것일까?

  • 명령
    • 객체의 상태를 수정한다.
    • 부수효과는 발생할 수 있지만 값을 반환할 수 없다.
    • = 프로시저
  • 쿼리
    • 값을 반환한다.
    • 부수효과가 발생하지 않는다.
    • = 함수

결국에는 오퍼레이션은 상태를 변경하거나 or 값을 반환 해야 한다는 것이다. 두 작업을 모두 수행해서는 안된다.

그 이유는 간단하다. 명령과 쿼리를 뒤섞으면 실행결과를 예측하기가 어렵기 때문이다. isSatisfiedBy 메서드처럼 겉에서는 쿼리처럼 보이지만 안에서 부수효과가 일어난다면 그 결과를 아무도 예측하지 못할 것이다.


🔗 마무리하며

요즘 객체지향에 대해 딥다이브를 하며 내가 짰던 코드에 대해 다시 한 번 생각하는 시간을 가질 수 있었다. 왜 domain모듈에 인터페이스들이 존재하는지를 소프트웨어 변경의 관점에서 명확히 이해할 수 있었고, 개념적으로만 이해했던 결합도/응집도에 대해서도 내가 짠 코드를 바탕으로 깊이 고민할 수 있었다.

이 책을 다 읽고 나서는 “함수형 프로그래밍”, “디자인 패턴”도 천천히 읽어봐야겠다. 단순히 코드를 짜고 기능을 구현하는 개발자보다는 소프트웨어에 대해 깊이있는 이해를 가진 개발자로 성장하자!!✨

댓글남기기