본문 바로가기

Programming!

AI 에게 글쓰기 시켜보았다. 응?! 오오오오..

🎯 JPA 환경에서 DDD를 실용적으로 적용하기

— Entity, Domain Model, VO, DTO를 어디까지 어떻게 나눌 것인가?

DDD(Domain-Driven Design)를 공부하거나 실무에 적용해보면 누구나 한 번쯤 마주치는 의문이 있다.

“Entity, Value Object, DTO… 말은 알겠는데, 현실에서는 이걸 어떻게 나눠야 하지?”
“엔티티에 도메인 로직을 넣고 싶지만 JPA가 자꾸 발목을 잡네…”
“VO를 쓰는 게 맞나, DTO를 쓰는 게 맞나?”

 

실제 코드에서는 이런 갈등이 더 크게 다가온다. JPA 엔티티는 영속성 컨텍스트와 묶여 있고, 프록시 때문에 예기치 못한 문제가 생기고,

DTO와 VO는 이름은 비슷한데 목적은 다르고…

그러다 보니 엔티티, 도메인 모델, DTO를 어디에 어떻게 두어야 하는지 헷갈리기 쉽다.

 

이번 글에서는 실제 현업 개발 흐름에 맞춘, 현실적인 DDD 적용 방식을 정리해본다.

 


1. DDD에서의 Entity vs VO vs DTO

DDD 문맥에서는 개념 자체가 명확히 다르다.

✔ Entity

  • 고유한 ID로 식별되는 도메인 모델
  • “정체성(identity)”이 중요
  • 비즈니스 로직 포함 가능

✔ VO(Value Object)

  • 값 그 자체로 의미가 있는 객체
  • 불변성
  • 도메인 규칙 포함

📌 중요:

VO는 DTO처럼 단순 값 묶음이 아니라 도메인 규칙과 의미를 표현하는 타입이다.

✔ DTO(Data Transfer Object)

  • 레이어 간 전달 전용 모델
  • API Request/Response 용도
  • 어떤 도메인 규칙도 포함하면 안 됨

DDD 문맥에서는 VO와 DTO를 혼용하면 안 된다. 둘은 역할이 완전히 다르다.

 


2. 그렇다면 실무에서는 왜 Entity = Domain Model로 쓸까?

이론적으로는 도메인 엔티티와 JPA 엔티티를 분리하는 게 맞지만,

실무에서는 개발 속도와 단순성 때문에 하나의 클래스로 합치는 경우가 대부분이다.

@Entity
public class Feed {
    @Id @GeneratedValue
    private Long id;
    @Embedded
    private FeedContent content; // VO
    public void changeContent(FeedContent newContent) {
        this.content = newContent;
    }
}

여기서 Feed는:

  • JPA 엔티티
  • 도메인 엔티티
  • 둘을 동시에 수행한다.

개념적으로는 분리되어야 하지만, 현실적으로는 한 클래스가 두 역할을 동시에 맡아도 문제없을 때가 많다.

 


3. 그런데… 엔티티에 도메인 로직을 넣다 보면 문제가 생긴다

실제로 JPA 엔티티에 도메인 로직을 넣기 시작하면 다음 이슈를 만나기 쉽다.

  • 영속성 컨텍스트 때문에 테스트가 어려움
  • Lazy Loading 프록시 때문에 의도치 않은 타이밍에 쿼리 발생
  • 엔티티 로딩 방식 때문에 로직 실행 순서가 꼬임
  • 특정 로직 실행 시 트랜잭션 범위를 신경 써야 함

그래서 도메인 로직을 엔티티에 넣는 대신:

“JPA 엔티티는 DB용, 도메인 모델은 순수 객체로 분리하자”

라는 접근으로 가는 경우도 꽤 많다.

 

즉,

FeedJpaEntity (@Entity) → Feed(도메인 모델)

이렇게 두 층으로 나누고, 도메인 로직은 순수 객체에만 넣는다.

 

이 방식은 유지보수성과 테스트 측면에서 매우 강력하지만, 초기 비용이 조금 있다.

 


4. VO는 쓸모가 적어 보이지만… 실제론 핵심이다

“Entity가 도메인도 하고 DB도 하니까 VO는 별 역할 없어 보인다”

이렇게 느끼기 쉽다. 하지만 VO는 도메인 모델에서 가장 중요한 표현력 강화 장치다.

 

예:

@Embeddable
public record FeedContent(String text) {
    public FeedContent {
        if (text.isBlank()) throw new IllegalArgumentException();
        if (text.length() > 1000) throw new IllegalArgumentException();
    }
}
  • 내용이 비어 있으면 안 된다
  • 길이가 1000자를 넘으면 안 된다
  • 불변이어야 한다

이런 비즈니스 규칙이 VO 하나에 응집된다.

도메인 엔티티가 훨씬 깔끔해지고, 버그도 줄어든다.

 


5. DTO(Response)로 내려보낼 때 매퍼가 필요한가?

도메인 객체를 API 응답으로 바로 내보내면 문제가 많다.

  • JPA 엔티티가 직렬화되며 내부 구조 노출
  • Lazy 필드 때문에 응답 직전에 예외 터짐
  • 보안 이슈 발생
  • 응답 모양을 바꾸기 어렵고 버전 관리 힘듦

그래서 반드시 Response DTO로 변환해야 한다.

 

그런데 이때,

DTO 생성자에 도메인 객체를 그대로 넘기는 것도 뭔가 찝찝하다…

 

맞다. 왜냐하면 DTO가 도메인 타입에 의존하는 순간 역할이 모호해지기 때문이다.

 


6. 그럼 DTO는 어떻게 만드는 게 최선일까?

정답은 “상황에 따라 선택”이다. 다만 명확한 기준은 있다.

 


선택지 A — DTO 안에  from() 정적 메서드를 두기

public record FeedResponse(Long id, String content) {
    public static FeedResponse from(Feed feed) {
        return new FeedResponse(feed.getId(), feed.getContent().text());
    }
}
  • 작은 서비스
  • 엔티티 = 도메인 모델로 쓰는 상황
  • 매핑 복잡도 낮음

이 경우 매우 실용적이고 흔히 쓰인다.

 


선택지 B — Mapper/Assembler 분리

@Component
public class FeedMapper {
    public FeedResponse toResponse(Feed feed) {
        return new FeedResponse(...);
    }
}
  • API 응답 모델이 많아질 때
  • 도메인 객체와 응답 모델을 명확히 분리하고 싶을 때
  • 계층 경계를 깨끗하게 유지하고 싶을 때
  • 나중에 도메인 엔티티와 JPA 엔티티를 분리할 예정일 때

이 패턴이 가장 안전하다.

 


선택지 C — MapStruct 등 매핑 자동화 도구 사용

  • 대규모 서비스
  • 매핑 코드가 너무 많아져서 관리가 어려움
  • 컨벤션이 잘 정리된 팀

 


7. 결론 — “실용적인 DDD”는 분리가 아니라 균형잡기다

이번 대화를 정리하면 아래로 귀결된다.

  • Entity = Domain Entity로 합쳐 쓰는 건 실전에서 흔하고 괜찮다.
  • 다만 Entity를 API로 직접 내보내지는 않는다.
  • → 반드시 DTO로 변환
  • DTO가 도메인을 “읽기 전용 입력”으로 사용하는 것은 허용
  • → 하지만 도메인 객체를 저장하거나 변경하면 안 됨
  • 도메인 로직이 JPA로 인해 꼬이기 시작한다면
  • FeedJpaEntityFeed(도메인)로 분리하는 방식으로 발전 가능
  • VO는 절대 사라지지 않는 개념이고,
  • 도메인을 “풍부하게 표현하는 핵심 기법”이다.
  • 최종 매핑 계층(Mapper)은 상황에 따라 단순하게, 혹은 엄격하게 가져가면 된다.

 


마무리

이 글은 “이론적 DDD”가 아니라

실제로 스프링/JPA 환경에서 개발할 때 자연스럽게 고민하게 되는 지점들을 기반으로 한 정리다.

 

  • 엔티티는 어디까지 책임져야 하는가?
  • VO는 왜 존재하는가?
  • DTO는 어디까지 알고 있어야 하는가?
  • 매핑은 언제 분리해야 하는가?
  • 도메인 로직은 어디까지 넣어야 하는가?

이 질문들에 대한 현실적인 해답을 찾는 과정이 곧

“실용적 DDD”라고 할 수 있다.

 


삼실을 지나가는 길에 개발자 분들이 모여서 이야기하는 주제를 대충 듣고, AI 에게 글쓰기를 시켜보았다.

응?! 오오오오..

 

이렇다고 알려주신다.. 음.