4장_ 엔티티 매핑
Updated:
JPA는 다양한 매핑 어노테이션을 지원한다.
크게 4가지로 분류할 수 있다.
- 객체와 테이블 매핑: @Entity, @Table
- 기본 키 매핑: @Id
- 필드와 컬럼 매핑: @Column
- 연관관계 매핑: @ManyToOne, @JoinColumn
1. @Entity
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.
@Entity 속성 정리
- name
- JPA에서 사용할 엔티티 이름을 지정한다
- 보통 기본값인 클래스 이름을 사용한다
- 만약 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다
- 기본값은 설정하지 않으면 클래스 이름을 그대로 사용한다
@Entity 적용 시 주의사항
- 기본 생성자는 필수다 (파라미터가 없는 public 또는 protected 생성자)
- final 클래스, enum, interface, inner 클래스에는 사용할 수 없다
- 저장할 필드에 final을 사용하면 안 된다
참고) 자바에서 파라미터가 있는 생성자를 만들면 자바는 기본 생성자를 자동으로 만들지 않으므로, 이때는 기본 생성자를 직접 만들어야 한다.
2. @Table
@Table은 엔티티와 매핑할 테이블을 지정한다.
생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
@Table 속성 정리
- name
- 매핑할 테이블 이름
- 기본값은 엔티티 이름을 사용한다
- catalog
- catalog 기능이 있는 데이터베이스에서 catalog를 매핑한다
- schema
- schema 기능이 있는 데이터베이스에서 schema를 매핑한다
- uniqueConstraints(DDL)
- DDL 생성 시에 유니크 제약조건을 만든다
- 2개 이상의 복합 유니크 제약조건도 만들 수 있다
- 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다
3. 데이터베이스 스키마 자동 생성
JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.
이 속성을 추가하면 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.
spring:
jpa:
hibernate:
ddl-auto: create
hibernate.hbm2ddl.auto 속성 (application.yml 에서는 spring.jpa.hibernate.ddl-auto 로 표기한다)
- create
- 기존 테이블을 삭제하고 새로 생성한다
- DROP + CREATE
- create-drop
- create 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거한다
- DROP + CREATE + DROP
- update
- 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정한다
- validate
- 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다
- 이 설정은 DDL을 수정하지 않는다
- none
- 자동 생성 기능을 사용하지 않으려면 hibernate.hbm2ddl.auto 속성 자체를 삭제하거나 유효하지 않은 옵션 값을 주면 된다
- none은 유효하지 않은 옵션 값이다
참고) HBM2DDL 주의사항
개발 환경에 따른 추천 전략은 다음과 같다.
- 개발 초기 단계는 create 또는 update
- 초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
- 테스트 서버는 update 또는 validate
- 스테이징과 운영 서버는 validate 또는 none
4. DDL 생성 기능
@Column 매핑정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약조건을 추가할 수 있다.
그리고 length 속성 값을 사용하면 자동 생성되는 DDL에 문자의 크기를 지정할 수 있다.
@Column(name = "NAME", nullable = false, length = 10)
private String username;
이번에는 유니크 제약조건을 만들어 주는 @Table의 uniqueConstraints 속성이다.
@Entity(name = "Member")
@Table(name = "MEMBER", uniqueConstraints = {@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10)
private String username;
private Integer age;
이런 기능들은 단지 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
따라서 스키마 자동 생성 기능을 사용하지 않고 직접 DDL을 만든다면 사용할 이유가 없다.
그래도 이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 손쉽게 다양한 제약 조건을 파악할 수 있는 장점이 있다.
5. 기본 키 매핑
JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같다.
- 직접 할당: 기본 키를 애플리케이션에서 직접 할당한다
- 자동 생성: 대리 키 사용 방식
- IDENTITY: 기본 키 생성을 데이터베이스에 위임한다
- SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다
- TABLE: 키 생성 테이블을 사용한다
5-1. 기본 키 직접 할당 전략
기본 키를 직접 할당하려면 @Id만 사용하면 되고, 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.
키 생성 전략을 사용하려면 아래 속성을 반드시 추가해야 한다.
과거 버전과의 호환성을 유지하려고 기본값을 false로 두었기 때문이다.
jpa:
properties:
hibernate:
id:
new_generator_mappings: true
먼저 기본 키 직접 할당 전략에 대해 알아보자
기본 키 직접 할당 전략은 em.persist() 로 엔티티를 저장하기 전에 애플리케이션에서 기본 키를 직접 할당하는 방법이다.
기본 키를 직접 할당하려면 @Id로 매핑만 하면 된다.
@Id 적용 가능 자바 타입은 다음과 같다.
- 자바 기본형
- 자바 래퍼(Wrapper) 형
- String
- java.util.Date
- java.sql.Date
- java.math.BigDecimal
- java.math.BigInteger
5-2. IDENTITY 전략
IDENDITY 전략은 기본 키 생성을 데이터베이스에 위임하는 전략이다.
주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
예를 들어서 AUTO_INCREMENT 처럼 데이터베이스가 순서대로 값을 채워주는 기능을 말한다.
이 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있다.
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
참고) IDENTITY 전략과 최적화
JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성된 기본 키 값도 얻어올 수 있다고 한다.
하이버네이트는 이 메소드를 사용해서 데이터베이스와 한 번만 통신 하면 된다는데, 추후 테스트를 한번 해봐야겠다.
참고) 주의할 점
해당 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.
엔티티가 영속 상태가 되려면 식별자가 반드시 필요하므로, em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다.
5-3. SEQUENCE 전략
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 테이터베이스 오브젝트다.
SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있다.
먼저 시퀀스를 아래처럼 생성해야 한다.
CREATE TABLE BOARD (
ID BIGINT NOT NULL PRIMARY KEY,
DATA VARCHAR(255)
)
// 시퀀스 생성
CREATE SEQUENCE BOARD BOARD_SEQ START WITH 1 INCREMENT BY 1;
그리고 사용할 데이터베이스 시퀀스를 매핑한다.
@SequenceGenerator를 사용해서 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했다.
그리고 sequenceName 속성의 이름으로 BOARD_SEQ를 지정했는데 JPA는 이 시퀀스 생성기를 실제 데이터베이스의 BOARD_SEQ 시퀀스와 매핑한다.
다음으로 키 생성 전략을 GenerationType.SEQUENCE로 설정하고 generator = “BOARD_SEQ_GENERATOR” 로 방금 등록한 시퀀스 생성기를 선택했다.
이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.
@Entity
@SequenceGenerator(name = "BOARD_SEQ_GENERATOR"
, sequenceName = "BOARD_SEQ" // 매핑할 데이터베이스 시퀀스 이름
, initialValue = 1
, allocationSize = 1)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "BOARD_SEQ_GENERATOR")
private Long id;
}
SEQUENCE 전략은 IDENTITY 전략과 같지만 내부 동작 방식이 다르다.
em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다.
그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다.
이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다.
@SequenceGenerator 속성 정리
- name
- 식별자 생성기 이름
- 기본값은 필수
- SequenceName
- 데이터베이스에 등록되어 있는 시퀀스 이름
- 기본값은 hibernate_sequence
- initialValue
- DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다
- 기본값은 1
- allocationSize
- 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용됨)
- 기본값은 50
- catalog, schema
- 데이터베이스 catalog, schema 이름
참고) SEQUENCE 전략과 최적화
JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequenceGenerator.allocationSize를 사용한다.
간단히 설명하자면 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당한다.
예를 들어 allocationSize 값이 50이면 시퀀스를 한 번에 50 증가시킨 다음에 1~50까지는 메모리에서 식별자를 할당한다.
그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음 51~100까지 메모리에서 식별자를 할당한다.
이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.
반면에 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한번에 많이 증가한다는 점을 염두해두어야 한다.
이런 상황이 부담스럽고 INSERT 성능이 중요하지 않으면 allocationSize의 값을 1로 설정하면 된다.
참고) @SequenceGenerator 위치 @SequenceGenerator는 다음과 같이 @GeneratedValue 옆에 사용해도 된다.
@Entity
public class Board {
@Id
@GeneratedValue(...)
@SequenceGenerator(...)
private Long id;
}
5-4. TABLE 전략
TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다.
이 전략은 테이블을 사용하므로 모든 데이터베이스에 적용할 수 있다.
TABLE 전략을 사용하려면 먼저 키 생성 용도로 사용할 테이블을 만들어야 한다.
create table MY_SEQUENCES (
sequence_name varchar(255) not null,
next_val bigint,
primary key (sequence_name)
)
TABLE 전략 매핑 코드는 다음과 같다.
@Entity
@TableGenerator(name = "BOARD_SEQ_GENERATOR"
, table = "MY_SEQUENCES"
, pkColumnValue = "BOARD_SEQ"
, allocationSize = 1)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "BOARD_SEQ_GENERATOR")
private Long id;
}
@TableGenerator를 사용해서 테이블 키 생성기를 등록한다.
BOARD_SEQ_GENERATOR라는 이름의 테이블 키 생성기를 등록하고 방금 생성한 MY_SEQUENCES 테이블을 키 생성용 테이블로 매핑했다.
다음으로 TABLE 전략을 사용하기 위해 GenerationType.TABLE을 선택했다.
그리고 @GeneratedValue.generator에 방금 만든 테이블 키 생성기를 지정했다.
이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당한다.
TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작 방식이 같다.
아래 그림을 보면 @TableGenerator.pkColumnValue에서 지정한 “BOARD_SEQ” 가 sequence_name 컬럼 값으로 추가된 것을 확인할 수 있다.
@TableGenerator 속성 정리
- name
- 식별자 생성기 이름
- 기본값은 필수
- table
- 키생성 테이블명
- 기본값은 hibernate_sequences
- pkColumnName
- 시퀀스 컬럼명
- 기본값은 sequence_name
- valueColumnName
- 시퀀스 값 컬럼명
- 기본값은 next_val
- pkColumnValue
- 키로 사용할 값 이름
- 기본값은 엔티티 이름
- initialValue
- 초기 값, 마지막으로 생성된 값이 기준이다
- 기본값은 0
- allocationSize
- 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용됨)
- 기본값은 50
- catalog, schema
- 데이터베이스 catalog, schema 이름
- uniqueConstratins(DDL)
- 유니크 제약 조건을 지정할 수 있다
참고) TABLE 전략과 최적화 TABLE도 SEQUENCE와 마찬가지로 @TableGenerator.allocationSize를 사용할 수 있다.
5-5. AUTO 전략
GenerationType.AUTO 는 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다.
예를 들어 오라클을 선택하면 SEQUENCE를, MySQL을 선택하면 IDENTITY를 사용한다.
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다.
AUTO를 사용할 때 SEQUENCE나 TABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다.
만약 스키마 자동 생성 기능을 사용한다면 하이버네이트가 기본값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 준다.
참고) 권장하는 식별자 선택 전략
테이블의 기본 키를 선택하는 전략
- 자연 키(natural key)
- 비즈니스에 의미가 있는 키
- 예: 주민등록번호, 이메일, 전화번호
- 대리 키(surrogate key)
- 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다
- 예: 오라클 시퀀스, auto_increment, 키생성 테이블 사용
비즈니스 환경은 언젠가 변하기 마련이므로, 자연 키보다는 대리 키를 권장한다.
JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다.
6. 필드와 컬럼 매핑: 레퍼런스
JPA가 제공하는 필드와 컬럼 매핑용 어노테이션들을 하나씩 알아보자
6-1. @Column
@Column은 객체 필드를 테이블 컬럼에 매핑한다.
@Column 속성 정리
- name
- 필드와 매핑할 테이블의 컬럼 이름
- 기본값은 객체의 필드 이름
- insertable (거의 사용하지 않음)
- 엔티티 저장 시 이 필드도 같이 저장한다
- false로 설정하면 이 필드는 데이터베이스에 저장하지 않는다
- false 옵션은 읽기 전용일 때 사용한다
- 기본값은 true
- updatable (거의 사용하지 않음)
- 엔티티 수정 시 이 필드도 같이 수정한다
- false로 설정하면 데이터베이스에 수정하지 않는다
- false 옵션은 읽기 전용일 때 사용한다
- 기본값은 true
- table (거의 사용하지 않음)
- 하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용한다
- 지정한 필드를 다른 테이블에 매핑할 수 있다
- 기본값은 현재 클래스가 매핑된 테이블이다
- nullable(DDL)
- null 값의 허용 여부를 설정한다
- false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다
- 기본값은 true
- unique(DDL)
- @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다
- 만약 두 컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints를 사용해야 한다
- columnDefinition(DDL)
- 데이터베이스 컬럼 정보를 직접 줄 수 있다
- 기본값은 필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입을 생성한다
- length(DDL)
- 문자 길이 제약조건, String 타입에서만 사용한다
- 기본값은 255
- precision, scale(DDL)
- BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다)
- precision은 소수점을 포함한 전체 자릿수를, sclae은 소수의 자릿수다
- 참고로 double, float 타입에는 적용되지 않는다
- 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다
- 기본값은 precision=19, scale=2
참고) @Column을 생략할 수 있는데, 자바 타입에 따라서 nullable 속성에 예외가 있다.
int data1 같은 자바 기본 타입에는 null 값을 입력할 수 없다.
Integer data2처럼 객체 타입일 때만 null 값이 허용된다.
따라서 자바 기본 타입인 int data1을 DDL로 생성할 때는 not null 제약 조건을 추가하는 것이 안전하다.
6-2. @Enumerated
자바의 enum 타입을 매핑할 때 사용한다.
enum RoleType {
ADMIN, USER
}
...
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Enumerated 속성 정리
- value
- EnumType.ORDINAL: enum 순서를 데이터베이스에 저장
- 정의된 순서대로 0, 1 저장된다
- 장점: 데이터베이스에 저장되는 데이터 크기가 작다
- 단점: 이미 저장된 enum의 순서를 변경할 수 없다
- EnumType.STRING: enum 이름을 데이터베이스에 저장
- 장점: 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다
- 단점: 데이터베이스에 저장되는 데이터 크기가 ORDINAL에 비해서 크다
- 기본값은 EnumType.ORDINAL
- EnumType.ORDINAL: enum 순서를 데이터베이스에 저장
6-3. @Temporal
날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.
@Temporal 속성 정리
- value
- TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑(예: 2013-10-11)
- TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑(예: 11:11:11)
- TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑(예: 2013-10-11 11:11:11)
- 기본값은 없고, 필수 지정이다
6-4. @Lob
데이터베이스 BLOB, CLOB 타입과 매핑한다.
@Lob에는 지정할 수 있는 속성이 없다. 대신에 매핑하는 필드 타입이 문자면 CLOB으로 매핑하고 나머지는 BLOB으로 매핑한다.
- CLOB: String, char[], java.sql.CLOB
- BLOB: byte[], java.sql.BLOB
6-5. @Transient
이 필드는 매핑하지 않는다.
따라서 데이터베이스에 저장하지 않고 조회하지도 않는다.
객체에 임시로 어떤 값을 보관하고 싶을 때 사용한다.
6-6. @Access
JPA가 엔티티 데이터에 접근하는 방식을 지정한다.
두 접근 방식 모두 생략이 가능하며, @Id 위치를 기준으로 접근 방식이 설정된다.
- 필드 접근
- AccessType.FIELD로 지정한다
- 필드에 직접 접근한다
- 필드 접근 권한이 private이어도 접근할 수 있다
- 프로퍼티 접근
- AccessType.PROPERTY로 지정한다
- 접근자(Getter)를 사용한다
// 필드 접근
@Entity
@Access(AccessType.Field)
public class Member {
@Id
private String id;
}
...
// 프로퍼티 접근
@Entity
@Access(AccessType.PROPERTY)
public class Member {
private String id;
@Id
public String getId() {
return id;
}
}
그리고 필드 접근 방식과 프로퍼티 접근 방식을 함께 사용할 수도 있다.
@Id가 필드에 있으므로 기본은 필드 접근 방식을 사용하고 getFullName()만 프로퍼티 접근 방식을 사용한다.
따라서 회원 엔티티를 저장하면 회원 테이블의 FULLNAME 컬럼에 firstName + lastName의 결과가 저장된다.
@Entity
public class Member {
@Id
private String id;
@Transient
private String firstName;
@Transient
private String lastName;
private String fullName;
@Access(AccessType.PROPERTY)
public String getFullName() {
return firstName + lastName;
}
}
아래와 같이 이름, 성을 지정하여 저장하면 DB에 full_name 필드에는 다음과 같이 확인할 수 있다.
Board board = new Board();
testEntityManager.persist(board);
String id = "id1";
String userName = "지한";
int age = 2;
Member member = new Member();
member.setId(id);
member.setUsername(userName);
member.setAge(age);
member.setFirstName("효성");
member.setLastName("안");
// 등록
testEntityManager.persist(member);
Leave a comment