Skip to content

Instantly share code, notes, and snippets.

@ksundong
Last active September 9, 2023 07:00
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ksundong/0ac48bb80d49813235ec789cb6afea58 to your computer and use it in GitHub Desktop.
Save ksundong/0ac48bb80d49813235ec789cb6afea58 to your computer and use it in GitHub Desktop.
우아한 객체지향 정리

우아한 객체지향

의존성을 이용해 설계 진화시키기 ⇒ 설계의 핵심은 의존성, 의존성을 어떻게 잡느냐에 따라서 설계가 달라지게 된다.

객체지향: 역할, 책임 ⇒ 의존성을 어떻게 관리하느냐가 핵심

어떻게 의존성을 관리하는게 좋은 의존성이고, 의존성을 관리하는 방법에 따라서 설계가 어떻게 바뀌는지를 보여드리려고 함.

의존성에 따라서 설계가 어떻게 바뀌는지를 단계단계 보여드리려고 함.

의존성(Dependency)

설계가 뭔가요? 설계란, 코드를 어떻게 배치할 것인가에 대한 의사결정

어디에 어떤 코드를 넣느냐에 따라서 설계가 달라질 수 있음.

그럼, 어디다 어떤 코드를 넣어야 할까요? ⇒ 핵심은 변경에 초점을 맞추는 것입니다.

같이 변경되면 같이 두어야 하고, 같이 변경되지 않는 코드는 따로 넣어야 합니다.

의존성이 있다는 것의 의미: B가 변경될 때, A도 같이 변경될 있어요.

뭐가 되었던, B가 바뀌었을 때, A가 변경될 가능성이 있어요.

의존성이 있다고 해서 무조건 변경되는 것이 아닐 수 있다.(설계를 잘 하면...)

Dependency의 구분

  • 클래스 사이의 의존성
  • 패키지 사이의 의존성

클래스 의존성의 종류

  1. Association 연관 관계

    A에서 B로 이동할 수 있어요. 코드 상에서 객체 참조가 있다. (영구적으로 갈 수 있는 경로)

  2. Dependency 의존 관계

    코드상에서 파라미터에 그 타입이 나오거나, 리턴 타입이 그 타입이거나, 메소드 안에서 그 타입의 인스턴스를 생성한다. (협력하는 시점에 일시적으로 협력하는 관계)

  3. Inheritance 상속 관계

    실질적으로 B라고 하는 클래스의 구현을 A라는 클래스가 계승을 받는다. B가 변경되면, A가 변경된다.

  4. Realization 실체화 관계

    인터페이스를 implement하는 관계, 상속 관계는 구현이 바뀌면 영향이 바뀌는 것, 실체화 관계는 인터페이스의 오퍼레이션 시그니쳐가 바뀌어야만 영향을 받는 것.

클래스 의존성

패키지 의존성

패키지에 포함된 클래스 사이의 의존성(어떤 패키지 안에 있는 클래스가, 다른 패키지에 있는 클래스에 의존성을 가지면 패키지 의존성이 있다로 생각 하면됨)

코드로 봤을 때, 상단에 다른 클래스를 import한다면 그 패키지끼리 의존성이 있다고 볼 수 있음.

좋은 의존성을 관리하기 위한 몇 가지 규칙

  1. 양방향 의존성을 피하라

    A가 바뀌면 B도 바뀌고, B가 바뀌면 A도 바뀐다는 것은, 하나의 클래스로 볼 수 있는 것을 어거지로 찢어놓은 것으로 볼 수 있다. 또한, A와 B의 상태를 동기화 시켜주어야 하는 문제점이 생길 수 있다.(성능, 버그(싱크를 맞추는 과정에서 발생))

    가급적이면, 양방향 연관관계를 피할 수 있으면 피한다.

  2. 다중성이 적은 방향을 선택하라

    A에서 B의 Collection을 인스턴스 변수로 잡거나, Collection에 대해서 Dependency를 가지게 하는 것 보다는 반대 방향의 Dependency를 가지도록 하는 것이 훨씬 좋다.

    이를 유지하기 위해서 다양한 이슈가 발생한다. 성능이슈, 객체들의 관계를 유지하기 위한 노력들이 필요함.

    가급적이면 다중성이 적은 방향으로 객체를 설계한다.

  3. 의존성이 필요없다면 제거하라

    정말 불필요하다면 제거하는 것이 좋다.

  4. 패키지 사이의 의존성 사이클을 제거하라

    패키지 사이의 양방향 의존성은 반드시 피해야 한다.

설계의 원칙

무조건, 변경이다.

내가 코드 배치를 할 때, 어떻게 바뀔거야에 대해서 포커스를 맞추면 된다.

이게 가장 기본적인 원칙이다.

관계의 방향 = 협력의 방향 = 의존성의 방향

관계의 종류 결정 순서

연관관계 (협력을 위한 영구적인 탐색 구조): 되게 빈번하게 접근함 탐색가능성(navigability): A를 알면 B를 찾아갈 수 있어요. 협력이 영구적으로 유지되어야 해. 일반적으로 연관관계를 구현하는 방법: 객체 참조, 다른 방법도 있다.

의존관계 (협력을 위해 일시적으로 필요한 의존성)

코드로 구현할 때 유의할 점

  1. 메소드를 만들고 메시지를 결정하는 것이 아니라, 메시지를 만들고 메시지를 바탕으로 메소드를 만들어야 한다.(37:20)

  2. 이 설계가 정말 괜찮은 가를 알기 위해서는 툴로 그림을 그리는 것이 아니라 손으로 그림을 그려가면서 의존성의 연관관계를 확인한다.

    자바에서는 패키지로 레이어를 구분한다.(서비스, 도메인 같은...)

  3. 양방향 연관관계가 있다면 뭔가 이상하다고 생각해야한다.

    이를 해결하는 방법

    1. 중간 객체를 이용해서 의존성 사이클 끊기

      중간에 변환이 가능한 클래스를 만들어서 연관관계를 끊는다.(이상해 보일 수 있지만 의존 역전 원칙(DIP)을 사용한 것임) 구체적인 것에 의존하지 않고 추상적인 것에 의존을 하도록 함.

개발에서 추상화의 진정한 의미

  • 사람들의 추상화에 대한 선입견: 추상 클래스나 인터페이스만 추상화의 대상이 될 수 있다고 생각함. But, 구체클래스도 추상화의 대상이 될 수 있음.
  • 개발에서 추상화의 의미는 어떤 것에 비해서, 어떤 것이 잘 변하지 않는 것을 의미한다.
  • 이를 사용하면 재사용성이 증가하게 됨.
  • 이를 꼭 해야한다는 것은 아니고 의존성을 보면서, 설계의 개선 방향을 생각할 수 있는 것이 중요하다.

연관관계 다시 보기

연관관계의 코드 구현중 하나인 객체참조에는 문제점이 있음.

  1. 두 객체 사이의 결합도가 높아짐

  2. 성능 문제 - 어디까지 조회할 것인가? (Lazy Loding 문제가 나옴)

  3. 수정할 때, 도메인 규칙을 함께 적용해야 하는 객체의 범위가 모호함.

    트랜잭션의 경계도 모호해짐. (DB, DataMapping에서 이슈가 발생함)

  4. 이렇게 되면 결국에 트랜잭션 경합이 일어나서 성능이 떨어지는 이슈가 발생함.

객체 참조가 정말 필요한가?

객체 참조의 문제점

모든게 다 연결 되어있다는 것이 문제임.

어떤 객체라도 다 접근 가능하고, 어떤 객체라도 함께 수정 가능함.

객체 참조는 영구적인 결합이기 때문에 결합도가 가장 높은 의존성임.

그러므로, 필요한 경우에는 객체 참조를 다 끊어주는 게 좋다.

객체 참조를 끊는 방법

  1. Repository를 통한 탐색(약한 결합도) ⇒ Shop이 아니라 Id를 들도록 함.

    실제로 Repository에 들어갈 인터페이스는 연관 관계를 구현할 수 있는 Operation이 들어가 주어야 함.

    파라미터로 받은 타입을 가지고 이 객체를 찾을 수 있다는 의미임.

    이것이 깨지는 이유는 조회 로직이나 어드민 로직이 들어가기 때문임. (도메인 자체로는 문제가 없다.)

    비즈니스 로직 측면에서는 다 연관관계를 구현하기 위한 것이 들어가야 함.

어떤 객체들을 묶고 어떤 객체들을 분리할 것인가?

간단한 규칙

  1. 함께 생성되고함께 삭제되는 객체들을 함께 묶어라
  2. 도메인 제약사항을 공유하는 객체들을 함께 묶어라
  3. 가능하면 분리하라

이걸 결정하는 것이 도메인 룰임. (도메인 관점에서 어떤 데이터를 같이 처리해야할 지 결정을 해주어야 한다.)

경계 안의 객체는 같이 묶어주는 편이 좋다.

경계 밖의 객체는 ID를 통해 접근 할 수 있도록 한다.

왜 책이나 강의에선 객체 참조로 구현되어 있는가?

객체 참조로 설명하는 편이 객체간의 메시지를 통한 협력을 표현하기 좋기 때문,

실무에서는 성능 이슈 등의 현실적인 제약으로 분리를 하는 것이 더 좋다.

Lazy Loding과 Eager Loding은 실질적으로 이 경계에서 결정하면 된다.

mongoDB에 저장하는 단위가 바로 이 경계이다.

그룹은 트랜잭션/조회/비즈니스 제약의 단위이다.

하지만 컴파일 에러가 발생한다.

이를 해결하는 방법은 바로 객체를 직접 참조하는 로직을 다른 객체로 옮기는 것이다.

이렇게 하면 좋은점

  1. 여러 객체를 오가면서 로직을 파악하지 않아도 된다.

    처음엔 로직을 파악하기 쉬웠음. 하지만? 다른 사람이 볼 때에는 여러 객체를 오가야 함.

  2. 낮은 응집도의 객체를 높은 응집도의 객체로 변경할 수 있음.

    응집도는 관련된 책임의 집합. 응집도는 같이 변경되는 것이 같이 있을 때 높아지고, 같이 변경되지 않는 것이 같이 있을 때 낮아짐. 변경은 객체의 상태가 바뀌는 것이 아니라, 코드의 수정을 의미함.

때로는 절차지향이 객체지향보다 좋을 때가 있다.

전체 Flow를 한 눈에 볼 수 있는 장점이 있음. (물론 단위테스트가 힘들어지긴 함.)

하지만 사람들이 일반적으로 가지는 강박관념 중에 하나가 validation 로직을 객체가 가지고 있어야 한다는 생각임.

안가져도 무방하다! (단순 객체 자체를 validation하는 것은 그 객체가 가지고 있어야하는게 맞지만, 하위 객체를 validation한다면 따로 빼주는 편이 좋다.)

결합도는 높이지만 응집도를 낮추는 코딩이 될 수 있음을 명심하자.

객체지향이 다 정답은 아니다. 이에 대한 Trade-off를 잘 해야한다.

또 컴파일 에러가 발생한다.

본질: 도메인 로직의 순차적 실행

해결방법

  1. 서비스를 사용하는 것.

    절차지향 로직으로 변경한다.

    어떤 비즈니스 플로우 자체가 한 눈에 보이게 된다.

    객체 참조는 줄여서 객체간의 결합도는 낮추고, 로직의 결합도를 높이고 싶다.

  2. 도메인 이벤트(Domain Event) 사용

    로직간의 A가 끝났을 때, B, C가 실행되었으면 좋겠어, 근데, 최대한 이 순서를 느슨하게 만들고 싶어.

    그래서, 이벤트를 발행해서 누군간 받아주겠지 라고 생각한다.

이게 정말 잘 된걸까?

이를 검증하는 방법은 그려서 확인하는 법이다.

그려보면 의존성 역전을 적용할 부분이 보일 수도 있다.

패키지 간의 의존성 사이클이 돌 때, 추상화를 적용한다.

  1. 좀 더 추상적인 중간 객체를 만들고 이것으로 변환하는 로직을 사용한다.

  2. 인터페이스를 만들고 이를 구현하는 것으로 변경한다.

  3. 패키지를 분리해버린다.

    패키지를 분리하면 얻는 이점은 도메인 로직이 확실히 분리가 됨.

이 영상에서 전달하고픈 점

의존성을 보면서 내가 뭘 봐야 되는지를 쫓아가면 좋겠다.

정리(패키지 의존성 사이크를 제거하는 3가지 방법)

  1. 중간 객체 만들기(새로운 객체로 변환)

    1st

  2. 의존성을 인터페이스나 추상 클래스를 통해서 의존성 역전

    2nd

  3. 새로운 패키지 추가

    3rd

3가지 중 어떤 것을 고를 것이냐는 판단에 따라 다름(Trade-off 가 필요)

의존성과 시스템 분리

도메인과 서비스로 분리하는 기술적인 레이어 단위로 패키지를 쪼갰었음.

왜냐하면 도메인으로 분리하면 의존성 사이클이 존재하기 때문임.

그리고 이렇게 하는 편이 관리하기도 편함.(왜냐하면 별 다른 신경을 안써도 그 안에서 돌기 때문임)

하지만, 도메인 이벤트를 사용해서 도메인 단위를 분리하게 된다면, 의존성 사이클이 제거됨.

systemseperation

각각은 도메인 이벤트를 통해서 협력을 하게됨.

이렇게 분리한다면, 시스템을 분리하기 쉬워짐. (Domain과 Service 패키지로 묶여있을 때보다 쉬워짐)

systemsep

배민은 이런식으로 분리를 해서 시스템끼리 비동기적인 메시지 통신으로 커뮤니케이션은 함. (트래픽이 많아서 연쇄적으로 죽어서)

시스템을 쪼갤 때 무턱대고 쪼개지 말고 의존성을 확인한 다음에 의존성 사이클을 분리하는 기법을 통해 분리하고 쪼개자

어떤 타이밍에 어떻게 쪼개야 할 지 고민하자.

의존성에 따라 시스템을 진화시켜라

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment