Search
moon
sun

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

Tags
Study
JPA
Database
Last edited time
2025/07/03 11:29
2 more properties
Search
JPA의 사실과 오해 (9) 동시성 제어 / 애그리거트 완성하기
Study
JPA
Database
JPA의 사실과 오해 (9) 동시성 제어 / 애그리거트 완성하기
Study
JPA
Database

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
복사