| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- kafka
- redis
- dockerhub
- Spring Data JPA
- mybatis
- DLQ
- 쿠버네티스
- 스프링 부트
- Spring Container
- JWT
- @ComponentScan
- Dead Letter Queue
- 페이징
- JPA
- docker compose
- docker
- MSA
- Spring
- 컨테이너
- Web
- 지연 로딩
- securitycontextholderfilter
- AWS
- JPQL
- @Transactional
- Routing Key
- DI
- 서블릿 컨테이너
- JdbcTemplate
- CORS
- Today
- Total
look-forest
JPA와 도메인 모델 패턴 (작성 중) 본문
JPA 모델과 도메인 모델은 다른 것인가?
어댑터 계층에 JPA 엔티티 등의 모델을 따로 만들고, Repository를 구현한 어댑터를 이용해서 도메인 오브젝트와 JPA 오브젝트를 매핑해야 한다는 오해가 있다.
(사진)
이는 하기 케이스에는 적합할 수 있다.
- 데이터 모델과 도메인 모델이 너무 다른 경우, 가령 레거시 DB에 도메인 모델 설계를 적용하는 경우.
- 복잡한 도메인 모델이 데이터 모델과 간단히 매핑되지 않는 경우. (JPA 모델과는 다른 도메인 모델이 존재한다면)
사실 JPA의 엔티티를 도메인으로 표현한 것은 문제가 되진 않는다. JPA 관련 애노테이션은 주석일 뿐, JPA를 안 써도 동작에 문제가 되지 않으므로 기술 의존적이라고 보기 어렵다.
JPA 기술의 정체성
JPA의 주요 목표는 도메인 모델과 DB 간 매핑이다.
ORM: 패러다임이 다른 관계형 DB와 객체지향 모델의 불일치를 해결하는 기술.
JPA의 기술적 목표는 관계형 데이터베이스를 관리하기 위해 자바 도메인 모델을 활용할 수 있는 객체/관계 매핑 기능을 제공하는 것이다.
JPA의 엔티티는 경량 영속 도메인 오브젝트.
도메인 모델 패턴
단순한 도메인 모델은 테이블과 클래스가 1:1로 매핑되는데,
복잡한 도메인 모델은 DB 매핑이 어렵다는 문제가 있다. 이를 해결해주는 것이 JPA(ORM) 기술이다.
JPA(ORM)이 매핑을 통해 해결하려는 패러다임 불일치 문제
1. 세분성(Granularity) 불일치: 1:1이 아니라 한 테이블에 여러 클래스로 나뉘는 것
2. 상속(Subtype) 불일치: DB는 상속 개념이 없는데, 클래스는 상속 관계를 가질 때, JPA가 간단히 매핑해준다.
3. 정체성(Identity) 불일치: 테이블과 다르게 객체는 정체성이 PK가 아니라 equals를 쓴다.
4. 연관(Association) 불일치: FK
5. 데이터 탐색(Navigation) 불일치: 테이블은 참고관계를 가지고 타고 타고 내려갈 수 있다.
스프링 데이터 프로젝트
우리는 JPA가 아니라 Spring Data JPA를 사용할 것이다.
다양한 데이터 저장소에 대한 데이터 접근을 단순하고 일관된 프로그래밍 모델로 제공한다.
반박
대부분 데이터 모델과 도메인 모델이 다르지 않다.
복잡한 도메인 모델의 매핑은 JPA가 충분히 지원해준다.
모델 변환 로직과 유사한 두 가지 클래스로 인해 불필요한 복잡성만 증가할 뿐이다.
JPA는 근본적으로 도메인 오브젝트의 매핑을 위해서 설계된 기술이다.
JPA는 도메인 계층을 침범하지 않는다. JPA 관련 애노테이션은 주석일 뿐, JPA를 안 써도 동작에 문제가 되지 않으므로 기술 의존적이라고 보기 어렵다.
복잡한 쿼리 로직은 커스텀 리포지토리와 어댑터 구현을 통해서 개발 가능하다.
근본적으로 도메인 계층과 데이터 계층의 결합은 불가피하다. (DB에 저장 안하면 어떻게 생명주기를 갖는가?)
결론: JPA 엔티티는 도메인 오브젝트이다!
하지만 도메인 모델에 드장하는 도메인 관심사가 아닌 각종 JPA 매핑 애노테이션은 좀 거슬린다.

어떻게 제거할 방법이 없을까?
엔티티 클래스와 JPA 매핑 정보 분리
ORM 관련 설정을 코드 밖에 둘 수 있다!
=> XML을 사용하면 된다. XML은 애노테이션을 override한다.

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://www.hibernate.org/xsd/orm/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hibernate.org/xsd/orm/mapping
https://hibernate.org/xsd/orm/mapping/mapping-3.1.0.xsd">
<access>FIELD</access>
<mapped-superclass class="tobyspring.splearn.domain.AbstractEntity">
<attributes>
<id name="id">
<column name="id"/>
<generated-value strategy="IDENTITY"/>
</id>
</attributes>
</mapped-superclass>
<entity class="tobyspring.splearn.domain.Member">
<table name="member">
<unique-constraint name="UK_MEMBER_EMAIL_ADDRESS">
<column-name>email_address</column-name>
</unique-constraint>
</table>
<attributes>
<basic name="nickname">
<column name="nickname" nullable="false" length="100"/>
</basic>
<basic name="passwordHash">
<column name="password_hash" nullable="false" length="200"/>
</basic>
<basic name="status">
<column name="status" nullable="false" length="50"/>
<enumerated>STRING</enumerated>
</basic>
<embedded name="email">
</embedded>
</attributes>
</entity>
<embeddable class="tobyspring.splearn.domain.Email">
<attributes>
<basic name="address">
<column name="email_address" nullable="false" length="150"/>
</basic>
</attributes>
</embeddable>
</entity-mappings>
이로써, 순수한 도메인 정보만 남기면서 JPA 엔티티로 사용할 수 있게된다.

엔티티의 equals()와 hashCode() 구현
JPA는 프록시를 사용하는 경우가 있는데, 이때도 Id로 같은 지 정확한 비교를 위해 equals와 hashcode를 구현하는 것이 좋다.
직접 만들기에는 너무 복잡해서, JPA Buddy라는 플러그인을 설치 후에 플러그인이 제공하는 Replace 기능으로 생성한다.

/**
* Id는 공통으로 사용하기 때문에 관리 포인트를 하나로 모은다!
*/
@MappedSuperclass
@ToString //자식 클래스에 @ToString(callSuper = true) 붙여야 함
public abstract class AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Getter(onMethod_ = { @Nullable})
private Long id;
@Override
public final boolean equals(Object o) {
if (this == o)
return true;
if (o == null)
return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ?
((HibernateProxy)o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ?
((HibernateProxy)this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass)
return false;
AbstractEntity that = (AbstractEntity)o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ?
((HibernateProxy)this).getHibernateLazyInitializer().getPersistentClass().hashCode() :
getClass().hashCode();
}
}
참고 자료 & 이미지 출처
토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처
'Architecture > 도메인 모델과 헥사고날 아키텍처(Clean Spring)' 카테고리의 다른 글
| 웹 API 어댑터 개발 (작성중) (0) | 2025.09.08 |
|---|---|
| 애그리거트를 이용한 일관성 있는 도메인 모델 설계(작성중) (0) | 2025.09.06 |
| 헥사고날 아키텍처 (0) | 2025.09.02 |
| 도메인 모델 (0) | 2025.09.02 |