Search

JPA의 사실과 오해 (2) 기본 매핑, 참조 객체와 값 객체

Tags
Study
JPA
Database
Last edited time
2025/06/29 13:13
2 more properties

1. 기본 매핑

1.1. 기본 매핑

JPA를 썼다고 해서 객체 지향은 아님
JPA는 데이터 관점
객체 지향 설계를 했을 때 도메인 레이어가 복잡해지는데 맵핑을 해주는 것 (임피던스 불일치 해결)
데이터와 객체 사이에 어떻게 왔다갔다 해야할지를 알려주는 도구
기본 매핑 예시
@Entity // JPA 엔티티 선언 @Table(name = "movie") // 테이블(생략 가능) @NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자 필수 @AllArgsConstructor @Getter @Setter public class Screening { @Id // 식별자 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "movie_id", nullable = false) // 컬럼 정의 private Long movieId; private Integer sequence; private LocalDateTime screeningTime; }
Java
복사

1.2. 영속성 컨텍스트 (Persistence Context)

데이터베이스에서 조회했거나 저장할 객체들을 보관하는 곳
나와 DB 사이에는 항상 영속성 컨텍스트가 존재한다고 생각
작업단위와 식별자 맵의 합
작업단위 (Unit Of Work)
작업을 쌓아놓고 안 보냄. 한방에 보냄
식별자 맵
식별자 키를 기준으로 객체 관리
식별자 키를 기준으로 영속성 컨텍스트에서 관리되는 엔티티들을 보관
DB에서 읽어 오면 바로 나한테 주는게 아니라 일단 식별자 맵에 넣고 줌
반대로 저장하고 싶을 때도 일단 식별자 맵에 넣고 나중에 넣음
하나의 작업단위로 처리하여 나중에 한방에 넣음

1.3. 식별자

@Id annotation로 지정된 식별자 필드에 저장

1.3.1. 식별자 선택시 고려 사항

자연 키 vs 대리 키
자연 키 (Natural Key)
비즈니스 의미를 가지는 식별자
대리 키 (Surrogate Key)
비즈니스 의미가 없는 식별자
식별자는 변경되지 말아야 하기 때문에 대리키 선호
erDiagram

%% 자연키
CUSTOMER_NATURAL {
    string SSN PK "주민번호(PK)"
    string NAME "이름"
}

%% 대리키
CUSTOMER_SURROGATE {
    int ID PK "고유 ID(PK)"
    string SSN "주민번호"
    string NAME "이름"
}
Mermaid
복사
단순 키 vs 복합 키
단순키
하나의 컬럼으로 식별자 구성
복합키
여러 컬럼을 조합해서 식별자 구성
일관성과 불변성 측면에서 하나의 컬럼을 사용하는 단순키 선호
erDiagram

%% 단순키
SCREENING_SIMPLE {
    int ID PK "상영 ID(PK)"
    int MOVIE_ID FK "영화 ID(FK)"
    int SEQUENCE "회차"
    datetime SCREENING_TIME "상영시간"
}

%% 복합키
SCREENING_COMPOSITE {
    int SCREENING_ID PK "상영 ID(PK)"
    int MOVIE_ID PK, FK "영화 ID(PK, FK)"
    int SEQUENCE "회차"
    datetime SCREENING_TIME "상영시간"
}

%% 관계 정의 (예시)
%% CUSTOMER_NATURAL ||--o{ SCREENING_SIMPLE : "관람"
%% CUSTOMER_SURROGATE ||--o{ SCREENING_COMPOSITE : "관람"
Mermaid
복사
생성 키 vs 할당 키
생성 키
엔티티 저장 시 JPA 또는 DB가 생성하는 키
할당 키
사용자가 직접 키를 객체에 할당
단순성과 관리 관점에서 생성키 선호

1.3.2. 생성키 전략

AUTO
데이터베이스에 적합한 최선의 전략을 선택
GeneratedValue의 기본값
SEQUENCE
데이터베이스 시퀸스를 이용해서 기본키를 생성
IDENTITY
기본키 생성을 데이터베이스에 위임 (MySQL Auto Increment)
데이터 INSERT 되는 시점에 순차적인 숫자 값 생성
TABLE
키 생성을 위한 테이블을 이용해서 데이터벵스 시퀸스 시뮬레이션

1.3.3. GenerationType.IDENTITY

기본적으로는 트랜잭션과 영속성 컨텍스트 라이프 사이클을 맞춤
트랜잭션이 열리면 영속성 컨텍스트를 열고, 커밋할때 영속성 컨텍스트를 닫음
IDENTITY 전략의 특징
IDENTITY 엔티티의 INSERT SQL은 쓰기 지연 저장소를 사용하지 않고 즉시 실행됨 (채번 필요)
하지만 해당 엔티티 자체는 ID를 부여받아 영속성 컨텍스트(1차 캐시)에서 관리됨
커밋 시점에는 쓰기 지연 저장소의 SQL들이 실행되고, DB 트랜잭션이 커밋됨
이 DB 트랜잭션 커밋 과정에서, 이전에 즉시 실행되었던 IDENTITY 엔티티의 INSERT 결과를 포함한 트랜잭션 내 모든 작업이 영구적으로 반영됨
별도의 저장소가 필요한 것이 아니라, DB의 트랜잭션 원자성(Atomicity)에 의해 함께 처리
@Entity public class Movie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; }
Java
복사
create table Item ( id bigint not null auto_increment, primary key (id) );
SQL
복사

1.3.4. GenerationType.SEQUENCE

SEQUENCE 전략은 데이터베이스의 시퀀스 객체를 이용해 기본키를 생성.
allocationSize는 미리 DB에서 시퀀스 값을 여러 개(예시: 50개) 할당받아 성능을 최적화.
주로 Oracle, PostgreSQL 등 시퀀스 지원 DB에서 사용.
@Entity @SequenceGenerator( name = "movie_sequence", sequenceName = "movie_seq", initialValue = 1, allocationSize = 50 ) public class Movie { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "movie_sequence") private Long id; }
Java
복사
create sequence movie_seq start with 1 increment by 50; select next value for movie_seq; insert into ITEM_SEQUENCE values (1);
SQL
복사

1.3.5. GenerationType.TABLE

TABLE 전략은 별도의 테이블(sequence_table)을 만들어서 시퀀스처럼 사용.
DB 종류와 무관하게 일관적으로 사용 가능(시퀀스 없는 MySQL에서도 사용).
동시성 이슈로 인해 성능이 떨어질 수 있음.
@Entity @TableGenerator( name = "movie_table", table = "sequence_table", pkColumnName = "seq_name", pkColumnValue = "movie_seq", initialValue = 1, allocationSize = 20 ) public class Movie { @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "movie_table") private Long id; }
Java
복사
create table sequence_table ( next_val bigint, seq_name varchar(255) not null, primary key (sequence_name) ); insert into sequence_table(seq_name, next_val) values ('movie_seq', 1); select tbl.next_val from sequence_table tbl where tbl.seq_name='movie_seq' for update; update sequence_table set next_val=21 where next_val=1 and seq_name='movie_seq';
SQL
복사

1.3.6. GenerationType.AUTO

AUTO 전략은 데이터베이스에 맞는 생성 전략을 자동으로 선택.
Oracle은 SEQUENCE, MySQL은 IDENTITY(AUTO_INCREMENT) 방식 등을 자동 선택.
개발자가 직접 지정하지 않으면 기본값으로 가장 많이 사용됨.
@Entity public class Movie { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; }
Java
복사
create table movie_seq ( next_val bigint ); insert into movie_seq values (1);
SQL
복사

1.3.7. 전역 범위 명명 식별자 (named identifier generator)

정의
하나의 식별자 생성기(예: ITEM_TABLE_GENERATOR)를 여러 엔티티 클래스에서 generator 이름으로 지정해 재사용할 수 있음
장점
중복 정의 없이 하나의 식별자 생성 정책을 여러 엔티티에서 사용할 수 있음
정책/설정 변경 시 여러 엔티티에 일괄 적용 가능
예시
여러 테이블의 PK 값을 하나의 시퀀스에서 발급받아 일관된 ID 정책 유지
회사 공통 정책으로 ID 범위, 증가 단위 등을 통합 관리할 때 유용
@Entity @SequenceGenerator( name = "ITEM_TABLE_GENERATOR", // 전역 이름 지정 sequenceName = "ITEM_SEQUENCE", initialValue = 1, allocationSize = 1 ) public class Item { @Id @GeneratedValue( strategy = GenerationType.TABLE, generator = "ITEM_TABLE_GENERATOR") private Long id; }
Java
복사
@Entity public class Product { @Id @GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "ITEM_TABLE_GENERATOR") private Long id; }
Java
복사

2. 참조 객체와 값 객체

DDD 관점에서의 참조 객체와 값 객체와 관련된 내용들은 도메인 주도 설계의 사실과 오해 (2) Preface, Entity & VO 에서 확인 가능합니다.

2.1. 참조 객체 (Reference Object)

2.1.1. 정의

객체 식별자를 기반으로 동등성 체크
협력하는 객체들 사이에 상태 변경을 공유하고 생명주기를 함께 관리
일반적으로 가변 객체로 구현
상태가 바뀐다는 것을 의미
객체의 상태를 계속 바꿀거고 다른 객체에게 동일한 상태를 공유하게 할거라는 것을 의미

2.1.2. 특징

동일한 상태 공유로 인한 부수 효과
하나의 참조에 의한 상태 변경이 다른 참조에도 전파
classDiagram
    class client1
    class client2

    class Sales {
        +quantity = 10
        +totalAmount = "87,000원"
    }
    class Product {
        +name = "오브젝트"
        +price = "10,000원"
    }

    client1 --> Sales : addSale(1, 0.1)
    client2 <-- Sales

    Sales --> Product
Mermaid
복사
동등성 확인
참조 대상 객체의 식별자(메모리 주소)를 사용해서 확인
모든 객체 지향 언어는 동등성 확인을 위한 연산자가 있음
별칭
하나의 객체를 가리키는 여러개의 참조 변수
위 그림에서 client1과 client2는 같은 객체를 가리키는 다른 이름을 의미

2.1.3. 별칭 버그

참조 객체의 별칭은 양날의 검
동일한 상태 공유 가능
원하지 않는 부수효과 발생 (별칭 버그)
sequenceDiagram
    participant client1
    participant Sales
    participant client2

    client1->>Sales: addSale(1, 0.1)
    client1->>Sales: addSale(3, 0.2)
    client1->>Sales: addSale(1, 0.1)
    client2->>Sales: addSale(5, 0.1)

    Note over Sales: quantity = 10, totalAmount = 87,000원, profit() = 2,610원
Mermaid
복사

2.2. 값 객체 (Value Object)

2.2.1. 정의

객체를 추적할 필요 없이, 값만 같으면 동일한 객체
불변 객체로 구현
equal(hashCode) 메서드 오버라이딩
예시
public class Money { public static final Money ZERO = Money.wons(0); // 불변 객체. 생성자에서 대입한 후 변경 불가능 private final BigDecimal fee; public static Money wons(long fee) { return new Money(BigDecimal.valueOf(fee)); } Money(BigDecimal amount) { this.fee = fee; } // 불변 객체. 상태 변경이 필요하면, 새로운 객체 생성 후 반환 public Money plus(Money amount) { return new Money(this.fee.add(amount.fee)); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Money money = (Money) o; // 속성 기반 동등성. 필드 값이 같으면 동일한 객체 return Objects.equals(fee, money.fee); } @Override public int hashCode() { return Objects.hashCode(amount); } }
Java
복사

2.2.3. 참조객체의 복잡성 낮추기

참조 객체의 복잡한 로직은 값 객체로 이동하여 위임할 수 있음
참조 객체(엔티티)를 값 객체로 잘 표현하면 복잡성을 많이 낮츨 수 있음
classDiagram
    class Sales {
        quantity
        addSale(quantity, discountRate)
        profit()
    }

    class Product {
        name
        salePrice(discountRate)
    }

    class Money {
        <<value>>
        amount
        plus(amount)
        minus(amount)
        times(times)
        ceil(percent)
    }

    Sales "1" *-- "1" Money : totalAmount
    Product "1" *-- "1" Money : price

    Sales "1" --> "1" Product : product
Mermaid
복사

2.2.4. 합성

값 객체는 참조 객체의 생명주기에 종속됨
값 객체를 만들었다는 것은 쓰고 버린다는 것을 의미
합성의 다른 표현
값 객체를 클래스의 필드로 표현

2.2.5. 데이터베이스 포함 값 매핑

DB 관점에서는 컬럼으로 매핑

2.2.6. JPA와 포함 값 (Embedded Value)

포함 값 매핑
변경 전
public class Money { public static final Money ZERO = Money.wons(0); private final BigDecimal fee; public static Money wons(long amount) { return new Money(BigDecimal.valueOf(amount)); } public Money plus(Money amount) { return new Money(this.fee.add(amount.fee)); } }
Java
복사
변경 후
@Embeddable : JPA 임베디드 값 타입으로 사용
VO를 테이블에 매핑할 때 사용
final 제거
생성자 추가(@NoArgsConstructor, @AllArgsConstructor)
@Embeddable // 생성자 추가 @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Money { public static final Money ZERO = Money.wons(0); private BigDecimal fee; // final 제거 public static Money wons(long amount) { return new Money(BigDecimal.valueOf(amount)); } public Money plus(Money amount) { return new Money(this.fee.add(amount.fee)); } }
Java
복사
포함 값 재정의 (@AttributeOverride)
변경 전
@Entity public class Movie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private Integer runningTime; private Long fee; // 금액을 단순 Long으로 표현 }
Java
복사
변경 후
금액을 Long에서 Money 타입으로 변경
@Embedded로 Money 타입 필드 사용
@AttributeOverride로 Money의 fee 필드를 DB의 "MOVIE_FEE" 컬럼에 매핑
@Entity public class Movie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private Integer runningTime; @Embedded @AttributeOverride(name = "fee", column = @Column(name = "MOVIE_FEE")) private Money fee; // Money 타입의 fee를 MOVIE_FEE 컬럼에 매핑 }
Java
복사
AttributeConverter 등록
DB 테이블과 엔티티 간의 매핑을 할때 VO를 이러이러하게 변환해줘 를 정의
@Converter(autoApply = true) : Money 타입을 Long 타입으로 자동 변환
JPA가 Money 타입을 DB에 저장/조회할 때 자동으로 변환
@Converter(autoApply = true) public class MoneyConverter implements AttributeConverter<Money, Long> { @Override public Long convertToDatabaseColumn(Money attribute) { return attribute.longValue(); } @Override public Money convertToEntityAttribute(Long dbData) { return Money.wons(dbData); } }
Java
복사