Post

ORM @Entity를 Domain layer의 Entity로 썼을 때 단점

ORM @Entity를 Domain Entity로 쓰게 되는 이유

우선 이름이 같아서 같은 개념이라고 착각하기 쉽다.

  • Domain Model의 Entity와 ORM의 @Entity는 둘 다 엔티티로 지칭하기 때문에…
  • layered arch, ORM, DDD 같은 개념을 처음 접하는 사람들은 Domain Model의 Entity와 ORM의 @Entity가 이름만 같고 다른 개념이라는 것을 상정하지 못하는 경우가 많다.
  • 심지어 일부 ORM framework는 ORM @Entity == Domain Entity로 사용하는 것을 의도하고 만들어진 것 같다.
    • Spring data JDBC 내부를 들여다 보면 JdbcAggregateOperations, AggregateRoot 같은 네이밍이 등장하는데, DDD의 Aggregate를 그대로 DB 입출력에 사용하라는 의도로 보인다.
1
2
3
public interface JdbcAggregateOperations {
  <T> void delete(T aggregateRoot);
}

persistence DTO를 없앨 수 있을 것 같다는 착각을 불러일으킨다.

  • ORM @Entity를 Domain Entity로 사용하게 되면, 마치 persistence layer를 아주 얇게, DTO도 없고, 쿼리도 없고, 단지 Repository만 가지고 있어도 문제가 없을 것 같은 착각을 불러일으킨다.
  • 특히 ORM의 @Embedded1, @OneToOne, @OneToMany 같은 기능을 보고 있으면 조인도 자동으로 처리해주고, 도메인 객체를 조작하는 것과 동일한 방식으로 객체를 조작하면 persistence layer가 처리되니… ‘ORM @Entity를 Domain Entity와 병합하고 persistence layer를 없애야겠다!’ 고 생각하기 쉽다.
  • 이런 점 때문에 왠지 ORM을 쓰면, DDD와 궁합이 좋은 것 같은 느낌을 준다. (느낌일 뿐이다)

물론, 장점이 아예 없는 것은 아니다.

  • ORM @Entity와 Domain Entity의 필드가 완전히 동일 하고 메서드도 없어서 굳이 둘을 분리해야 하는 니즈가 크지 않을 때, 이런 상황에서 둘을 반복적으로 나누다보면 ‘굳이 나눠야해?’ 생각 들 수 있다.
  • 실제로 필드가 동일하고 메서드도 없고 persistence 애너테이션으로 인한 domain layer 오염이 크지 않은 경우라면 나누지 않는 것이 실용주의적 관점에서 이득일 수 있다.
  • 주로 개발 초기 단계에서 이득을 볼 수 있는 상황이 자주 발생한다.

ORM @Entity를 Domain Entity로 쓰면 안되는 이유

일단 가장 근본적으로 DDD를 포함한 어떤 비즈니스 레이어의 설계이든지 간에, ORM을 쓰건 안쓰건 가능해야 한다.

  • Domain이 가장 높은 우선순위를 가지며 설계의 중심이 되는 것이 맞다.
  • ORM을 쓰다가, 걷어내고 단순 쿼리 실행기(e.g., JDBC)로 변경해도 Domain 설계가 변경되어서는 안된다.
  • 즉, persistence framework로 ORM을 쓰는지 안쓰는지가 Domain layer에 영향을 주어서는 안된다.

잘못 설계된 table schema를 숨기지 못하고, domain model의 설계가 persistence에 의존하게 된다.

  • 잘 설계되지 않은 못생기고 지저분한 DB layer를 숨기는 것도 persistence layer의 역할 중 하나인데
  • ORM @Entity를 그대로 Domain Entity로 사용하게 되면, 잘못 설계된 table 구조가 그대로 Domain layer에 노출된다.

갱신 이상을 피하려는 DB 설계 특성 상, ORM @Entity 필드와 Domain Entity 필드는 불일치가 발생 할 가능성이 매우 높다.

  • DB는 갱신이상 때문에 컬럼 중복을 최대한 피하려고 하기 때문에, 컬럼 스키마와 필드가 동일한 @Entity를 Domain Entity로 쓰는 것은 무리가 있다.
  • @Entity는 table column과 동일하게 필드를 구성하게 되는데. 도메인 모델에는 column에는 없어야 하는 필드나 더 있어야 하는 필드가 존재할 가능성이 매우 크다.
  • 물론 table에는 없고 비즈니스에서는 필요한 필드에 대해서 @Transient 사용이 가능하지만… 애매하다.
  • 이런 불편이 도메인 모델에 대한 잘못된 변경 압력으로 작용할 가능성이 매우 높다.

persistence 전용 애너테이션으로 Domain Model이 오염된다.

멀티모듈 구조에서 중복이 크게 증가한다.

  • ORM @Entity를 Domain Entity로 쓰면,
    • 각 비즈니스 모듈에서 직접 ORM @Entity를 정의해야 한다. (비즈니스 로직을 가지고 있는 Domain Entity를 하위 공통 모듈로 둘 수는 없으니)
    • 컬럼 변경 시 모든 비즈니스 모듈의 @Entity에 적용해야 한다는 점이 부담되고, 이 과정에서 누락이 발생하기 쉽다.
      1
      2
      3
      4
      
      비즈니스 모듈 1
      ㄴ ORM @Entity(Domain Entity), Dao, Repository
      비즈니스 모듈 2
      ㄴ ORM @Entity(Domain Entity), Dao, Repository
      
  • 반면 ORM @Entity를 따로 두게 되면, ORM @Entity는 별도의 persistence 모듈로 분리 가능하다.
    • 재사용성과 유지보수 측면에서 persistence 모듈은 별도로 두는 것이 좋다.
      1
      2
      3
      4
      5
      6
      
      persistence-module
      ㄴ ORM @Entity, Repository
      비즈니스 모듈 1
      ㄴ Domain Entity, Dao
      비즈니스 모듈 2
      ㄴ Domain Entity, Dao
      

결론) ORM @Entity는 persistence DTO로 간주하고 Domain Entity와는 별개로 관리하는게 유지보수하기에 더 낫다.

결국 언젠가는 ORM @Entity와 Domain Entity를 분리해야 하는 시점이 온다.

  • 개발 초기 단계에서 장점이 있음에도 불구하고 처음부터 분리하는 것을 추천하는데, 시스템을 확장하다 보면 결국 ORM @Entity와 Domain Entity를 분리해야 하는 시점이 오기 때문이다.
  • Q. “개발 초기 단계에서는 실용주의적 관점을 택해서, ORM @Entity와 Domain Entity를 동일시하고, 시스템 확장에 따라 적절한 시점에 분리하면 안되나요?”
    • 적절한 시점에 분리하려면 ‘구성원들이 모두 Domain Model 개념과 ORM에 익숙하며 분리가 필요한 시점을 잘 알고 있다’는 전제조건이 필요하다.
    • 현실적으로 이게 가능한 경우는 잘 못봤다. 다양한 사람들과 함께 개발하는 팀단위 개발에서는… 원칙을 세우는 것이 낫다.
    • 원칙을 세울거라면, 1안 보다 2안이 낫다.
      • 1안. ORM @Entity를 Domain Entity로 사용한다.
      • 2안. ORM @Entity와 Domain Entity를 분리한다.

그러면 쿼리 실행기 대비 ORM의 장점은 무엇인가?

  • 도메인 영역에서의 개발은 도메인 모델인 Domain Entity로 이루어지고, ORM @Entity는 단순 데이터 입출력용으로 사용하게 된다면, ORM의 장점은 무엇인가? -> 쿼리를 직접 작성하지 않아도 된다는 것.
  • 쿼리를 직접 작성하지 않는다는건 단순히 ‘편하다’를 넘어서, ‘persistence DTO를 만들게끔 강제한다’는 의미가 있다.
    • 쿼리를 직접 쓰게되면 도메인 모델 맞춤형으로 쿼리를 작성 할 수 있다보니, 보통 DTO 없이 sql을 바로 Domain Entity에 생성 매핑하게 된다.
    • 하지만, 멀티 모듈 구조에서 sql->Domain Entity 바로 매핑하게 되면 sql이 각 비즈니스 모듈에 위치해야 한다.
    • 보통 sql과 dto는 persistence 모듈로 분리하는게 재사용성, 유지보수 측면에서 이점이 많으므로, DTO를 만들게 되는데, sql->Domain Entity로 바로 가지 못하고 sql->DTO->Domain Entity로 거쳐가는게 불필요한 매핑 작업처럼 느껴질 가능성이 있다.
    • 반면 ORM을 사용하면 ORM @Entity 하나만 만들고 persistence 모듈에 위치시키면 되니, 부담이 덜하다. (물론 인덱스 등 디테일한 컨트롤은 부족하다)
  • 쿼리를 직접 작성 할 수 있는 점은 강력하지만, 제대로 관리되지 않을 가능성도 높다.
    • 아무래도 새로운 기능을 개발 할 때, 개발자들이 쿼리를 잘 살펴보고 진행하지 않는다. => 비슷한 역할을 하는 쿼리가 여러개. 중복 심화
    • Mybatis의 include refid로 어느 정도 중복을 막을 수는 있지만 한계가 있다.
    • 단일 테이블에 대한 쿼리는 QueryByExampleExecutor나 Criteria 쓰듯 단일 인터페이스로 처리하는게 좋아보이는데, 프로젝트 초기 부터 이런 컨벤션으로 진행하지 못한 경우 나중에 변경하기가 쉽지는 않은 것 같다.

sql -> Domain Entity

1
2
3
4
비즈니스 모듈 1
ㄴ Domain Entity, Dao, sql
비즈니스 모듈 2
ㄴ Domain Entity, Dao, sql

sql -> DTO -> Domain Entity

1
2
3
4
5
6
persistence-module
ㄴ DTO, sql, Repository
비즈니스 모듈 1
ㄴ Domain Entity, Dao
비즈니스 모듈 2
ㄴ Domain Entity, Dao

ORM @Entity -> Domain Entity

1
2
3
4
5
6
persistence-module
ㄴ ORM @Entity, Repository
비즈니스 모듈 1
ㄴ Domain Entity, Dao
비즈니스 모듈 2
ㄴ Domain Entity, Dao

참고

ORM @Entity 네이밍 규칙

  • XxxEntity는 도메인 모델의 Entity와 겹칠 수 있음.
  • XxxRow는 presentation의 표나 엑셀과 겹칠 수 있음.
  • XxxRecord가 가장 시인성이 좋아보임.



  1. @Embedded는 객체의 구조화를 도와주는데, 어느정도 비즈니스적인 의미를 가지는 객체에서 사용되는 것을 의도한 것 아닌가? 생각 할 수 있지만, MyBatis에도 있는 resultMap에 대응되는 기능이라고 볼 수 있다. 

This post is licensed under CC BY 4.0 by the author.