[Java] Entity 필드를 단순 자료형으로 표현하지 못할 경우 그 책임을 Value Object로 정의하는 방안(책임분리 및 어노테이션 동작 원리까지 연결하여)

Hyo Kyun Lee·2025년 9월 29일
0

Java

목록 보기
102/106

1. 개요

개인 프로젝트를 진행하면서 테이블의 Entity를 단순 자료형으로 표현하기엔, 내장기능 등의 항목이 많아 다른 참조형 변수로 매핑해야 하는 상황이 발생하였다.

일전에 Article Entity에서 Article Update의 정책적 책임을 Entity 내부에(넓게 보면 Domain 내부) 부여하였듯이, 이와 유사한 원리로 단순 기본형 필드가 아닌 참조형 변수(값을 담아주는 Value Object)에 담아주는 방안이 있음을 알게 되었다.

이는 단순한 Value Object의 활용을 넘어 DDD로까지 이어질 수 있는 개념이고, 깊이 보았을때 어노테이션과 프레임 워크의 관계(동작원리)까지 알 수 있었던 과정이기도 하여 이에 대해 공부한 내용을 기록하고자 한다.

2. Entity 필드를 단순 자료형으로 표현하기엔 내장 정보가 많을 경우

Entity 필드에 Value Object를 활용한다면, 단순 기본형 자료형이 아닌 참조형 객체를 활용할 수 있어 단순히 표현할 수 있는 정보의 양을 늘릴 수 있을 뿐만 아니라, 특정 기능적 책임을 객체에 전가해줄 수 있는 구조적 유리함을 확보할 수 있겠다.

2-1. @Embedded

그 중 대표적으로 Embedded 어노테이션을 활용하여 참조형 객체 변수를 활용한 필드 정의를 알아보겠다.

public class CommentV2 {
    @Id
    private Long commentId;
    private String content;
    private Long articleId; // shard key
    private Long writerId;

    /*
    * parent Id 불필요
    * 그대신 comment Path 정보만 필요
    * 내부적으로 기본 자료형이 아닌 값을 표현하는 객체 (참조형) Value Object가 필요할 경우 사용하는 어노테이션
    * -> Embedded 어노테이션 사용
    * */
    @Embedded
    private CommentPath commentPath;
}

위의 commentPath의 경우 특정 알고리즘을 활용하여 root node에서 leaf node까지의 경로열거(Path Enumeration)을 표현해야하는 책임을 가지고 있어, 단순 기본형 자료형으로는 해당 필드를 표현하기 어렵다.

따라서, 그 필드와 관련한 모든 기능적/구조적 정보를 표현하기 위해 @Embedded 어노테이션을 사용하여 필드 정보를 객체에 나타내었다.

객체를 통해 테이블 정보와 매핑해주어 직접적인 row data를 표기하지 않고도 바로 영속적인 데이터로 매핑해줄 수 있다. JPA를 사용하는 강력한 이유이기도 하다.

2-2. @Embedable

사실 순서가 @Embedable 어노테이션이 "정의" 부분에 해당하기에, 이 항목이 먼저일 수도 있겠다.

이 @Embedable 어노테이션을 사용한 "Class"가 독립적인 Entity가 아닌 특정 Entity 객체에 종속되어, 해당 Entity의 테이블 매핑에 추가적으로 합쳐 반영해주어야 한다는 의미를 지니고 있다.

/*
* 독립적인 Entity 객체가 아닌 다른 Entity에 종속되어 사용하는 객체(Value Object)임을 명시하는 어노테이션
* Embedded의 어노테이션에서 추적하여 VO에 해당하는 모든 필드정보를 테이블과 매핑한다.
* */
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentPath {
    private String path;
}

따라서 Embeddable로 Value Object를 생성해주어 특정 Entity에 연결해준다면, 방대해질 수 있는 책임범위를 명확히 응집해주고 방대해질 수 있는 표현정보들을 안정적으로 표현해줄 수 있겠다.

더불어 access level을 private로 설정하여 캡슐화와 불변성을 보장하면서, 특정 필드항목의 변경점 발생 시 중복적인 유지보수 비용을 제거하기 위해 해당 Value Object를 사용함으로써 연결해주었다.

3. 각각의 어노테이션은 Framework와 어떻게 상호작용하는가?

이때 의문이 생겼다.

이 어노테이션들은 서로, 혹은 Framework와 어떻게 연결되어 사용하는 것일까?

지금까지 정말 단순하게 어노테이션을 필드, 클래스위에 "필요한 특정 어노테이션"을 기재해주어 사용하였는데..어노테이션을 서로 연결해주는 과정을 보면서 어노테이션의 기초적인 동작 원리를 이해할 필요가 있겠다라는 생각이 들게 되었다.

3-1. Framework는 생명주기를 가지고 있다.

먼저 Framework의 정의, Framework를 왜 사용하는지 생각할 필요가 있겠다.

Framework는 생명주기를 개발자가 정하지 않고, Framework 자체적으로 정의한 생명주기에 개발자가 작성한 로직을 그 생명주기에 역으로 소속하게 해주는(=IoC) 하나의 체계이다.

3-2. 이 생명주기에 어노테이션도 소속 대상이다.

그리고 이 생명주기에는 우리가 작성한, 정확히 말하면 "정의해주었던" 어노테이션도 소속 대상이기도 하다.

즉, @Embedded, @Embedable 등의 어노테이션을 사용하면, JPA Hibernate의 어노테이션 Runtime 반영 과정인 Retention에 해당 메타데이터 정보가 들어가게 된다.

3-3. Annotation(기능 명세) - Framework(실제 기능의 동작)

정리하면 다음과 같다.

Annotation - 기능명세/규칙정의서
Framework - 기능동작

JPA Hibernate, MyBatis, Boot 등 모든 Framework들은 대체적으로 자바 표준 라이브러리(java.lang.Reflect) 패키지에 어노테이션에 따라 동작할 기능들을 명세해둔다.

이후, 여기에 명세된 어노테이션을 사용하면, Retention(RETENTION(RUNTIME)) 시점에 근거하여 메타데이터를 해당 시점에, 프레임워크가 리플렉션 패키지 혹은 RETENTION에 정의한 기능 명세대로 그 기능을 수행하게 된다.

이때, Framework가 어노테이션 동작을 수행하는 것을 Reflection이라 한다.

4. Retention의 예시

Framework가 어노테이션을 인식하고 이를 동작하는 시점인 Retention에는 Runtime/Class/Source 등이 있다.

각각의 시점에 대해 따로 정의하지는 않겠으나, 다만 Framework 별로 사용하는 어노테이션과 동작 시점을 살펴보면 이해가 편할 것이다(자주 보던 어노테이션이 많이 보이므로).

  • Hibernate/JPA - @Entity/@Embedded (RUNTIME) - 실행
  • Spring - @Component / @Service / @Autowired (Class) - 스캔(컴파일 후 .class 생성시점까지 해석)
  • Spring Batch - Job/Step/Listenere (RUNTIME) - 리플렉션으로 어노테이션 메타데이터 해석
  • MyBatis - @Mapper/@Select (RUNTIME) 리플렉션으로 메타데이터 해석 및 Proxy 객체 생성

0개의 댓글