개발 일상

JPA Entity 와 Domain 을 분리하면서 느낀점

GustavEiffels 2025. 5. 15. 22:05
반응형
Garbage Collection Guide

Entity 와 도메인 나누기

여태 SpringBoot 로 개발을 해오면서 Entity 와 Domain 을 나눈 경험이 없었는데,
이번 사이드 프로젝트를 하면서 나누게 되었다.


Q1. 왜 엔티티 클래스 랑 도메인 모델 이랑 나누는 걸까?

가장 큰 이유는 관심사 분리라고 한다.
내가 말하고 있는 Entity 는 JPA 에서 DB 테이블과 매핑되는 객체를 말하며
DB 에 데이터를 저장,수정, 조회 하는 기능에 초점이 맞춰져 있다.

반면, 도메인 모델 은 객체 관점에서 시스템이 해결하려는
비지니스 문제를 정의하는데에 초점이 맞춰져있다.

즉, 둘은 엄연히 다른 존재이다.

하지만 현실 적으로, 실무에서 코드 변환 과정이 추가되며
Entity 는 @Entity 가 붙은 자바 객체일 뿐 내부에 도메인 로직을 두어도 어색하지 않다.
쉽게 얘기하면 Entity도 결국 자바 객체이기 때문에 도메인 로직을 함께 담는 것이 완전히 잘못된 건 아니다


✅ 엔티티 클래스 와 도메인 모델을 나누는 이유

관심사 분리 할 수 있다.

Entity : 데이터베이스 조작에 집중 (저장, 조회 등)
Domain : 비즈니스 로직에 집중 (검증, 계산 등)

엔티티 클래스 는 DB 조작에만 집중할 수 있고, 도메인 모델 은 비지니스 로직에만 집중 할 수 있다.

프로젝트 적용

현재 사이드 프로젝트에서 처음 도메인과 Entity를 나누어 개발을 진행하고 있는데
적용된 내용은 다음과 같다.


Entity

@Entity
@Table(name = "event")
public class EventEntity extends BaseTimeEntity {
    /** 스터디 id */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "event_id")
    private Long eventId;

...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EventEntity event = (EventEntity) o;
        return Objects.equals(eventId, event.eventId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(eventId);
    }

    public static EventEntity fromDomain(Event event){
        EventEntity eventEntity = new EventEntity();

        eventEntity.eventId     = event.eventId();
        eventEntity.categoryId  = event.categoryId();
        eventEntity.locationId  = event.locationId();
...
        return eventEntity;
    }

    public Event toDomain() {
        return new Event(
                eventId,
                categoryId,
                locationId,
                name,
...
                capacity,
                approveType,
                isOnline,
                locationDetail,
                recurringRules.toDomain()
        );
    }
}

Domain 와 Entity 를 나누면서
Entity 의 이름을 DomainEntity 이런식으로 작성하였다.
여기서는 따로 값에 대한 검증을 하지 않으며 ( Domain 으로 부터 값에 대한 검증을 함 )
Domain 으로 변환하는 로직을 작성하였다.


Domain

public record Event(
        Long eventId,
        Long categoryId,
        Long locationId,
        String name,
        String content,
        LocalDateTime startAt,
        LocalDateTime endAt,
        Long hostId,
        int capacity,
        EventEnums.ApproveType approveType,
        boolean isOnline,
        String locationDetail,
        RecurringRules recurringRules
) {
    private static final Pattern TITLE_REGEX = Pattern.compile(
            "^[가-힣A-Za-z0-9_\\-\\(\\)!?,\\[\\]@#\\$%\\^&\\*\\uD83C-\\uDBFF\\uDC00-\\uDFFF]{2,}$"
    );

    public Event{
        if (name == null || !TITLE_REGEX.matcher(name).matches()) {
            throw new BaseException(EventException.TITLE_REGEX);
        }
        if (startAt != null && endAt != null && ChronoUnit.HOURS.between(startAt, endAt) < 1 ) {
            throw new BaseException(EventException.WRONG_TIME_SETTING);
        }
        if ( capacity > 30 || capacity < 1 ){
            throw new BaseException(EventException.WRONG_CAPACITY);
        }
    }
} 

도메인 객체이다.
비지니스 로직에만 집중 할 수 있게 되었다.
위의 코드에서는 도메인 객체의 정규식과 값에 대한 검증이 이루어지고 있다.

Entity 에 Domain 객체로의 변환 로직이 작성된 이유는
Domain 객체는 비지니스 로직에만 집중하기 위해서이다.

Architecture

├── domain
│   └── event
│       ├── Event.java
│       ├── EventDto.java  ← DomainDto(toDomain(), from())
├── infra
│   └── event
│       ├── EventEntity.java(fromDomain(), toDomain())
├── application
│   └── facade
│       └── EventFacade.java(InterfaceDto → ApplicationDto → DomainDto )
├── interface
│   └── controller
│       └── EventController.java
│   └── dto
│       └── CreateEventRequest.java(InterfaceDto)
│       └── EventResponse.java(view용)

적용하면서 느낀점

  1. 아무래도 익숙하지 않다보니 시간이 오래 걸렸고 적응하는데 시간이 걸렸다.
  2. 명확하게 책임이 분리 되다 보니 이해하기 쉬워 졌다.
반응형