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. 참조 객체와 값 객체
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
복사