https://www.youtube.com/watch?v=dJ5C4qRqAgA

조회 로직 != 비즈니스 로직

연사 분이 조회 로직과 비즈니스 로직을 구분해서 말씀하셨다. 엔티티 간의 협력을 만들고 설계를 고민해야하는 부분은 비즈니스 로직이다. 나는 지금까지 조회가 비즈니스 로직에 포함되는 부분인 줄 알았다. 그래서 연습문제 검색하기라는 기능을 협력 위주로 설계해보려고 해도 답이 안나왔던 것이다.

MSA 패턴 공부할 때 읽기와 쓰기를 분리하는 패턴이 있었다. 분리하는 이유는 로직의 라이프사이클이 다르기 때문이라고. 나중에 폴리글랏하게 구성하고 읽기 로직은 조회에 최적화된 MongoDB + nodeJS를 쓰는 경우도 봤는데 같은 맥락인 것 같다.

연관관계

연관관계 구현하기

연관관계는 모듈 간의 의존성을 말한다. 객체 사이에서 연관관계는 다양한 방법으로 구현이 된다. 영상에서는 객체 참조에 의한 강한 결합과 객체 식별자를 사용하는 레포지토리 조회의 약한 결합이 제시됐다. 강한 결합은 라이프사이클이 다른 도메인이나 다른 레이어까지 결합되어 예측하기 어렵게 동작하기 때문에, 객체 참조에 의한 결합을 제한해야한다. 이 둘의 구분은 도메인 그룹의 내부와 경계에서 엔티티를 참조하는 핵심적인 방법이다.

양방향 연관관계는 왜 지양해야하는가?

아 그리고 객체 간 양방향 연관관계를 사용하면 관리 포인트가 늘어나고, 트랜잭션이 길어지는 문제가 발생한다. 양방향 연관관계를 넣기 시작하면 패키지 사이에 의존성 사이클이 금방 생기는데, 사이클이 생긴 객체들은 결합도가 높기 때문에 함께 변경될 가능성이 크다.(꼭 의존성 사이클이 생겨야만 문제가 아니고, 양방향 연관관계 자체가 객체간의 결합도를 높여서 문제다) 이때는 설계에 문제가 있는 것이므로 패키지 분리를 다시 고민해봐야한다. 패키지 의존성 사이클은 설계 점검의 신호이다!

단방향 연관관계 TIP

그러니 끊을 수 있다면 단방향 연관관계를 사용하는게 좋다. 그리고 단방향 연관관계를 설정할 때 컬렉션으로 OneToMany를 넣는 것보다는 ManyToOne을 넣는 것이 좋다.

도메인 관리

도메인 그룹

도메인 그룹은 객체들의 의존성을 관리하기 위해 도메인 객체를 묶는 단위이다. 결합도가 높은, 즉 함께 변경되는 경우가 많은 도메인들은 같은 그룹에 속한다. 변경의 이유가 같기 때문에 객체 참조로 의존해도 문제 되지 않는다. JPA 지연 로딩도 문제없다. 조회, 트랜잭션, 영속성 저장소의 분리 범위도 도메인 그룹으로 정해진다.

만약 트랜잭션으로 묶고 있는 도메인들의 라이프사이클이 다르다면, admin이나 배치 로직이 추가되어 특정 도메인의 상태가 변경됐을 때 트랜잭션으로 묶여 있는 다른 도메인에도 영향을 줄 수 있다. 예를 들어 특정 도메인에 admin으로 대용량 데이터를 업데이트해버리면 락이 걸려서 다른 도메인도 동작하지 않는다. 즉, 트랜잭션 경합으로 인해 성능 저하가 발생한다.

반면 결합도가 낮은, 변경의 이유가 다른 도메인들은 다른 그룹에 속한다. 객체 참조로 의존하면 결합도가 증가하므로 객체의 식별자를 이용해 레포지토리에서 조회해서 사용한다. (이때 기존의 객체 참조를 식별자로 바꾸면 컴파일 에러가 떨어지는데, 해결하는 방법도 함께 나온다)

도메인 그룹은 묶는 기준은 비즈니스 요구사항에 따라 결정된다. 예를 들어 장바구니와 장바구니 안에 담기는 아이템은 서로 라이프사이클이 달라서 함께 묶으면 결합도가 올라간다. 만약 요구사항에 하나의 장바구니 안에 담기는 아이템은 모두 같은 가게의 것이어야 한다는 조건이 있으면 장바구니와 아이템 사이에 도메인 컨텍스트가 생기므로 하나로 묶어야한다. 그리고 함께 생성되고 삭제되는, 라이프사이클이 같은 객체들을 함께 묶어야한다. 가능하다면 분리한다.

이 내용은 DDD 이론을 보면서 항상 이해가 잘 안됐던 부분인데, 의존성으로 생기는 결합도와 연결해서 생각하니 되게 자연스러운 개념이라는 생각이 들었다.

패키지의 사용 목적 - 레이어와 도메인 그룹

실무에서 사용하는 웹 서버는 단순하게 엔티티만의 협력으로 만들 수 없다. 웹을 사용하는 프레젠테이션 계층, DB를 사용하는 데이터베이스 계층과 함께 도메인 로직을 작성해야한다. 특정 도메인 엔티티 안에 속할 수 없는 로직이라면 도메인 서비스 계층을 따로 두어야 한다. 이렇게 애플리케이션은 여러 레이어로 구분되는데, 이 레이어를 자바의 패키지로 분리할 수 있다.

패키지는 결합도가 높은 도메인들의 집합인 도메인 그룹을 묶는 도구로써 기능한다. 레이어 안에 도메인을 둘 수도 있고, 도메인 안에 레이어를 둘 수도 있다. 도메인 안에 레이어를 구성하는 것이 객체 사이의 의존성을 확실히 관리할 수 있는 방법이기에 권장한다. (구현은 레이어 안에 도메인이 더 쉽다)

애플리케이션에 도메인 외에 레이어가 생기면서 객체의 관계를 설정하는 방법이 이상적인 객체 지향과는 달라진다. 완전한 객체 지향에서는 객체 참조로 의존성을 받는다. 하지만 실무에서는 참조가 많아지면 결합도도 올라가고, DB와 엮였을 때 성능에 문제가 있기 때문에 항상 참조로 의존할 수 없다.

🍰 의존성 관리

패키지 의존성 사이클 제거 방법

  1. 중간 클래스

    Untitled

    shop과 order 양방향으로 흐르는 의존성을 order -> shop 단방향으로 바꾸기 위해서 shop 쪽에 OptionGroup, Option 중간 클래스를 둔다.

  2. DIP 제거하려는 의존성이 있는 곳에 인터페이스를 두고, 의존성이 있는 구현은 다른 패키지로 이동한다. 의존성이 하위 구현에서 상위 인터페이스로 흐르게 만들어서, 인터페이스가 있는 패키지와 다른 패키지로의 의존성을 제거할 수 있다. (그림은 아래 DIP 참고)

  3. 새로운 패키지로 분리 의존성을 새로운 패키지로 분리한다. 이 과정에서 도메인 그룹간의 관계가 더 분명해진다.

    Untitled

데이터 의존관계를 식별자 참조로 바꿨을 때 컴파일 에러를 해결하는 방법

  1. 컴파일 에러가 나는 로직을 전부 다른 객체로 옮기기 Order에서 Shop 참조를 shopId로 바꾸면 기존에 validate에서 shop 참조를 사용하던 부분은 컴파일 에러가 난다. 이 코드를 전부 분리해서 OrderValidator로 모은다. 결합도가 올라가지만 Order 엔티티의 응집도가 올라가고, 가독성도 올라간다.
  2. 절차지향으로 해결하기 Order에서 Shop 참조를 shopId로 바꾸면 가게에 주문을 전달하는 부분에서 컴파일 에러가 난다. 이 로직은 오더가 shop에 최소 주문 금액 확인을 하고 delivery에 complete 요청을 날린다. 서비스 클래스를 하나 더 추가하고 Order, Shop, Delivery를 절차지향적으로 사용한다. 때로는 순서를 나타내는 로직에서 절차지향적인 방식이 더 좋을 수도 있다.

그런데 의존성을 그려보면 의존성 사이클이 발생한 것을 볼 수 있다.

Untitled

이럴 때 의존성 역전을 사용하면 의존성이 Delievery -> Shop으로 흐르기 때문에 사이클을 끊을 수 있다. 인터페이스는 도메인 그룹 안에 두고, 그 구현을 다른 패키지에 둔다.

Untitled

  1. 도메인 이벤트 발행 도메인 이벤트를 사용해 어떤 순서로 일어나는 이벤트들의 결합도를 느슨하게 만들 수 있다.

Untitled

그래도 매개변수로 이벤트를 받아서 사이클이 생기더라.. 이벤트를 매개변수로 받는 핸들러를 별도의 패키지로 분리한다. 그리고 Shop 패키지 중에 분리된 핸들러와 관련있는 로직을 분리해서 함께 위치시킨다.

Untitled

타입별 서브타입 만드는 TIP

주문의 상태 필드에 값을 넣어 저장하는 코드를 보고 연습문제의 하위 타입 구현 힌트를 얻었다. 연습문제는 구화와 발음 타입으로 나뉘는데, 필드만 차이나고 행동은 동일해서 상속으로 서브 타입을 만들고 객체 인스턴스 생성이 필요한 클라이언트 쪽에는 팩토리 메소드를 제공하고 타입 정보를 받으면 되겠다!고 생각했다.

⭐️ 잘된 설계인지 판단하는 TIP ⭐️

  1. 패키지와 객체 간의 의존성 그림을 그린다
  2. 패키지 의존성 순환이 발생하는지 확인한다
  3. 그러하다면, 순환을 끊을 수 있도록 중간 클래스를 두거나, 순환을 유발하는 코드를 다른 패키지로 옮기거나, DIP를 사용한다. 어느 방법을 사용할지는 트레이드 오프를 따져야한다.