유랑하는 나그네의 갱생 기록

だけど素敵な明日を願っている -HANABI, Mr.children-

Devlog/SpringBoot

도메인 순수성을 포기하고 얻은 것들 (순수 POJO vs Rich Domain Model)

Madirony 2026. 2. 27. 14:53
반응형

Intro

tl;dr 순수 POJO와 JPA Entity를 완벽히 분리하려다 도메인 클래스에 @Entity를 허용하도록 전면 롤백했고, 프레임워크 종속성은 타협했지만, 비즈니스 로직을 내부에 응집시킨 'Rich Domain Model'의 본질은 완벽하게 지켜냈다-?!

 

좋은 아키텍처는 결정을 미루는 것이다.

 

클린 아키텍처나 DDD를 공부하다 보면 귀에 못이 박히도록 듣는 말입니다. 프레임워크나 DB 기술에 종속되지 않는 '순수 도메인'을 만들어야 한다는 뜻이죠.

 

2026.02.06 - [Devlog/SpringBoot] - 테스트 코드가 알려주는 객체의 책임과 구조의 미학

2026.02.13 - [Devlog/SpringBoot] - HOW적 사고에서 벗어나기 - Why? (의도가 있는 설계법)

 

이전 포스트에서 Member라는 단일 도메인에 이 원칙을 적용해 보았습니다. 나름대로 계층도 나누고 VO도 도입해 보니 테스트에도 용이하고 꽤 그럴싸해 보였습니다.

 

하지만 도메인이 하나가 아니라 수십-수백개인 규모에서도 이 '순수성'을 극한으로 유지하는 게 맞을까요? 이번에 5개 정도 도메인을 새롭게 추가하면서 의문이 들었습니다.

 

 

첫 주간 한도 Limit reached..

그래서 리팩토링한 코드를 다시 롤백하고 리팩토링 작업을 반복하다 보니까 처음으로 주간 한도를 넘어버렸습니다. 다행히 프로모션 토큰을 가지고 있어서 초과분을 쓸 수 있었지만, 상당히 무거운 작업이라 Sonnet 4.6으로 모델 사용량을 아꼈습니다.

 

 

생각보다 많은 사람들이 Max vs Pro+추가 사용량 사이에서 고민하고 있다.

 

 


본론

객체의 순수성이 불러온 비효율

이번에 기존 Member 도메인에 더해 Brand, Product, Option, Like, Order, Cart 등 5개의 도메인이 새롭게 추가되는 요구사항이었습니다. 앞선 단일 도메인에서의 경험을 바탕으로, 전체 아키텍처에 엄격한 기준을 적용했습니다.

 

도메인 객체에는 @Entity 같은 JPA 어노테이션을 절대 사용하지 않고 순수 POJO로 유지한다.
DB 영속성은 무조건 별도의 JpaEntity 클래스가 전담한다.

 

Brand나 Product를 하나씩 추가할 때까지만 해도 이 구조는 꽤 합리적이고 깔끔해 보였습니다. 하지만 도메인이 늘어나고 객체 간의 연관관계가 복잡해지기 시작하자 상황이 달라졌습니다. 모든 JpaEntity마다 데이터를 주고받기 위한 매핑 메서드들이 늘어나기 시작한 겁니다.

 

 

// 도메인 모델로 변환 (Read)
public Product toDomain() {
    return Product.of(
        this.id, 
        this.brandId, 
        this.name, 
        Money.of(this.basePrice)
        // ... 파라미터들
    );
}

// JPA 엔티티로 변환 (Write)
public static ProductJpaEntity fromEntity(Product product) {
    return new ProductJpaEntity(
        product.getId(), 
        product.getBrandId(),
        product.getName(),
        product.getBasePrice().getAmount()
        // ... 
    );
}

만약 기획이 바뀌어 새로운 필드가 하나 추가된다면 어떨까요? Domain, JpaEntity, toDomain(), fromEntity() 무려 네 곳의 코드를 수정해야 할 것입니다. 물론 요즘 코드들은 ai가 다 작성한다고는 하지만...

 

코드를 짜는 시간보다 객체를 매핑하는 데 쓰는 시간이 더 길어지고, 점차 비즈니스 로직을 설계하고 있는 건지, 그저 데이터를 옆 객체로 옮겨 담는 단순 반복 작업을 하고 있는 건지 헷갈리기 시작합니다.

 


rollback-!

"내가 지키려는 이 '프레임워크 독립성'이 지금 이 프로젝트에 어떤 가치를 주고 있지?"

 

  • 나중에 JPA를 걷어내고 다른 구조로 교체할 가능성이 있는가? → 사실상 없습니다.
  • 도메인 클래스에 @Entity가 붙으면 순수 자바 단위 테스트가 불가능한가? → 아닙니다. 어노테이션은 런타임 환경을 강제하지 않으므로, 스프링 컨테이너 없이 new로 객체를 생성해 단위 테스트를 하면 될 것입니다.

 

결국 제가 고집했던 순수 POJO 구조는 '이론적으로 완벽한 아키텍처'라는 이상향이었을 뿐, 실제로는 막대한 유지보수 비용과 리소스를 의미 없이 소모하게 만드는 오버엔지니어링이었습니다.

 

 

Claude 압수

설계 일관성을 맞추기 위해 수많은 매핑 클래스들을 동시에 수정하고 리팩토링하는 과정에서 코딩 어시스턴트의 주간 API 토큰 한도를 초과해 버리는 상황까지 발생했습니다. (Intro에서 언급했던..)

 


 

코드를 덜어내며 찾은 타협점

11개의 JpaEntity 클래스를 모두 삭제하고, 도메인 클래스에 @Entity, @Column을 선언하는 Rich Domain Model로 롤백했습니다.

 

단, 타협은 하되 도메인 객체의 역할은 지켰습니다. 비즈니스 로직(Option.decreaseStock(), Order.pay() 등)은 철저히 엔티티 내부에 응집시켰고, Money와 같은 객체는 @Embeddable을 활용해 값 객체(VO)의 형태를 유지했습니다. 프레임워크 어노테이션이 추가되었을 뿐, 핵심 로직은 여전히 객체지향적이었습니다.

 

Rich Domain Model 패턴 적용 및 JpaEntity 계층 제거

무의미한 매핑 코드가 사라지면서 총 735줄의 코드가 삭제되었고 결과적으로 354줄의 텍스트가 줄어들었습니다. 실외에서는 github를 통해 코드를 보는데, 클래스 수가 줄어 가독성 측면에서 확실히 이점이 있다고 생각합니다.

 


빈약한 모델?

https://www.linkedin.com/pulse/what-anemic-domain-model-why-can-harmful-daniel-rusnok?trk=public_profile_article_view

추가로 소프트웨어 개발에서 흔히 쓰이지만 장기적으로 위험한 안티 패턴인 '빈약한 도메인 모델(Anemic Domain Model)'을 알면 좋을 것 같습니다.

 

빈약한 도메인 모델이란 데이터(Entity)와 연산(Service)이 완전히 분리된 구조를 말합니다. 엔티티는 Getter와 Setter만 가지고 있고, 핵심 비즈니스 로직은 상태가 없는(Stateless) 서비스 클래스가 모두 가져가서 처리하는 형태입니다.

 

여기서 이 모델이 치명적인 가장 큰 이유로 '캡슐화의 부재(Lack of encapsulation)'를 꼽습니다. 엔티티 스스로 데이터의 무결성을 방어하지 못하고 외부(Service)에서 주는 대로 상태를 바꾸기 때문에, 프로젝트 규모가 커질수록 개발자가 실수할 여지가 늘어납니다. 첨부한 그래프에서 볼 수 있듯, 빈약한 도메인 모델은 초기 개발 속도는 빠를지 몰라도 시간이 지날수록 유지보수 비용이 수직으로 상승합니다.

 

그래서 이번에는 이를 피하고, 객체 스스로 자신의 상태를 검증하고 행동하는 '풍부한 도메인 모델(Rich Domain Model)'을 만들고자 했습니다.

 

처음 설계를 잡을 때는 이 '풍부한 도메인 모델'을 프레임워크(JPA)로부터 완벽히 분리된 순수 POJO로 구현하려고 했습니다. 도메인 로직을 가진 순수 객체와 DB 영속성을 담당하는 JpaEntity를 철저하게 분리한 것이죠. 하지만 앞서 언급했듯, 이 구조는 도메인이 늘어날수록 객체 간 매핑을 위한 유지보수 비용을 증가시켰습니다.

 

롤백을 진행하면서 짚고 넘어간 점은, '순수 POJO'라는 원칙과 '풍부한 도메인 모델'이라는 객체지향의 가치는 분리해서 타협할 수 있다는 것입니다.

 

현재 도메인 클래스에는 @Entity와 같은 JPA 어노테이션이 추가되어 프레임워크에 대한 의존성이 생겼습니다. 하지만 이번에 설계한 엔티티들은 decreaseStock(), pay() 처럼 핵심 비즈니스 로직을 여전히 내부에 응집하고 있습니다. 외부에서 무분별하게 상태를 변경할 수 없도록 캡슐화를 유지하고 있으니, 프레임워크 독립성은 일부 타협했더라도 충분히 Rich Domain Model의 형태를 유지하고 있다고 볼 수 있습니다.

 


P.S.

도메인 모델링을 위한 이벤트스토밍! 한번 읽어보고 적용하면 좋을 것 같습니다.

https://www.msaschool.io/operation/design/design-three/

 

msaschool - msaschool

*MSA School의 모든 콘텐츠에 대한 권리는 MSA School에 있으며, 무단 복제 및 배포를 금합니다. 영리 목적의 사용은 허용되지 않으며, 개인적 용도로 복제할 경우 반드시 출처를 표기해야 합니다. © uE

www.msaschool.io

 

 

References

https://www.linkedin.com/pulse/what-anemic-domain-model-why-can-harmful-daniel-rusnok?trk=public_profile_article_view

 

What is Anemic Domain Model and why it can be harmful?

Anemic Domain Model is a common approach in software development that many folks don’t even know they are using it including myself. If you are using Entity Framework, there is a chance you know this approach.

www.linkedin.com

https://www.reddit.com/r/Anthropic/comments/1rbr645/when_does_max_plan_become_worth_it_over_pro/

 

Reddit의 Anthropic 커뮤니티

Anthropic 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요

www.reddit.com

 

반응형
TOP