이번 글에서는 <기본키 매핑, 필드와 컬럼 매핑>에 대해 알아보겠습니다.
이 시리즈 글은 김영한 님의 강의, 책을 보고 적은 것임을 알려드립니다. (강추)
JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같습니다.
이제 자동 생성하는 방법들에 대해 자세히 알아보자
IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략입니다.
주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용됩니다. IDENTITY 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용합니다.
// IDENTITY 매핑 코드
@Entity
public class Member{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
}
private static void logic(EntityManager em) {
Member member = new Member();
em.persist(member);
System.out.println("member.id = " + member.getId());
}
// 출력: member.id = 1
위 코드를 처음 보고, "트랜잭션 커밋을 하지 않았는데 실행되나?" 라는 의문을 가졌습니다.
하지만 아래와 같은 이유를 보고 가능하다는 것을 알았습니다.
❗️주의
엔티티가 영속 상태가 되려면 식별자가 반드시 필요합니다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달됩니다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않습니다.
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트입니다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성합니다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에 사용됩니다.
// 시퀀스 DDL
CREATE TABLE MEMBER (
ID BIGINT NOT NULL PRIMARY KEY,
DATA VARCHAR(255)
)
// 시퀀스 생성
CREATE SEQUENCE MEMBER_SEQ START WITH 1 INCREMENT BY 1;
// 시퀀스 매핑 코드
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Member{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
}
// 시퀀스 사용 코드
private static void logic(EntityManager em) {
Member member = new Member();
em.persist(member);
System.out.println("member.id = " + member.getId());
}
// 출력: member.id = 1
시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다릅니다.
SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회합니다.
그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장합니다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장합니다.
반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후에 식별자를 조회해서 엔티티의 식별자에 할당합니다.
@SequenceGenerator 속성
속성 | 기능 | 기본값 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
ㅤㅤㅤsequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용됨. 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용) | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
❗️주의: allocationSize의 기본값이 50인 것
TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략입니다.
// TABLE 전략 키 생성 DDL
create table MY_SEQUENCE(
sequence_name varchar(255) not null ,
next_val bigint,
primary key ( sequence_name )
)
sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용합니다.
// TABLE 전략 매핑 코드
@Entity
@TableGenerator(
name = "BOARD_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "BOARD_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
...
}
// TABLE 전략 매핑 사용 코드
private static void logic(EntityManager em) {
Member member = new Member();
em.persist(member);
System.out.println("member.id = " + member.getId());
}
// 출력: member.id = 1
@TableGenerator 속성
속성 | 설명 | 기본값 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준 | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraints(DDL) | 유니크 제약 조건을 지정할 수 있다 |
결론은 데이터베이스에 여러번 접근하는 것을 방지해서 성능적으로 장점이 있기 때문입니다.
SEQUENCE 전략에서는...
SEQUENCE 전략은 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요합니다. 따라서 다음과 같이 데이터베이스와 2번의 통신을 합니다.
위와 같이 시퀀스에 접근하는 것을 줄이기 위해 JPA는 @SequenceGenerator.allocationSize를 사용합니다. 간단히 설명하면, 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당합니다.
예를들어, allocation 값이 50이면 시퀀스를 한 번에 50을 증가시킨 다음에 1 ~ 50까지는 메모리에 시퀀스 값을 할당합니다. 그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음 51 ~ 100까지 메모리에서 식별자를 할당합니다.
이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있습니다. 반면에 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한 번에 많이 증가한다는 점을 염두해두어야 합니다. 이런 상황이 부담스럽고 INSERT 성능이 중요하지 않으면 allocationSize의 값을 1로 설정하면 됩니다.
TABLE 전략에서는...
TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용합니다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점이 있습니다. 이 전략도 최적화하기 위해 allocationSize를 사용하면 되고, 방법은 SEQUENCE 전략과 같습니다.
JPA가 제공하는 필드와 컬럼 매핑용 어노테이션들을 레퍼런스 형식으로 보여드리겠습니다.
필드와 컬럼 매핑 분류
분류 | 매핑 어노테이션 | 설명 |
---|---|---|
필드와 컬럼 매핑 | @Column | 컬럼을 매핑 |
@Enumerated | 자바의 enum 타입을 매핑 | |
@Temporal | 날짜 타입을 매핑 | |
@Lob | BLOB, CLOB 타입을 매핑 | |
@Transient | 특정 필드를 데이터베이스에 매핑하지 않음 | |
기타 | @Access | JPA가 엔티티에 접근하는 방식을 지정 |
@Column은 객체 필드를 테이블 컬럼에 매핑합니다. 가장 많이 사용되고 기능도 많습니다. 속성 중에 name, nullabe이 주로 사용되고 나머지는 잘 사용되지 않는 편입니다.
insert, updatable 속성은 데이터베이스에 저장되어 있는 정보를 읽기만 하고 실수로 변경하는 것을 방지하고 싶을 때 사용합니다.
속성 | 기능 | 기본값 |
---|---|---|
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은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다. | precision=19, scale=2 |
@Column을 생략한다면?
컬럼 어노테이션을 생략하면 대부분 @Column 속성의 기본값이 적용되는데, 자바 기본 타입일 때는 nullable 속성에 예외가 있습니다.
@Column 생략 시: 객체 타입일 때는 nullable이 기본값인 true가 되지만, 자바 기본 타입일 때는 JPA가 not null 제약조건을 추가합니다.
@Column 사용 시: 자바 기본 타입에 대해 not null 제약조건을 추가하지 않습니다. 따라서 자바 기본 타입에 @Column을 사용하면 nullable = false로 지정하는 것이 안전합니다.
자바의 enum 타입을 매핑할 때 사용합니다.
속성 | 기능 | 기본값 |
---|---|---|
value | EnumType.ORDINAL: enum 순서를 데이터베이스에 저장, EnumType.STRING: enum 이름을 데이터베이스에 저장 | EnumType.ORDINAL |
❗️ 기본값인 ORDINAL은 enum의 순서가 바뀌면 안되므로 EnumType.STRING을 권장합니다.
데이터베이스 BLOB, CLOB 타입과 매핑합니다.
@LOB에는 지정할 수 있는 속성이 없습니다. 대신에 매핑하는 필드 타입이 문자면 CLOB으로 매핑하고 나머지는 BLOB으로 매핑합니다.
CLOB: String, char[], java.sql.CLOB
BLOB: byte[], java.sql.BLOB
이 필드는 매핑하지 않습니다. 따라서 데이터베이스에 저장하지 않고 조회하지도 않습니다. 객체에 임시로 어떤 값을 보관하고 싶을 때 사용합니다.
JPA가 엔티티 데이터에 접근하는 방식을 지정합니다.
필드 접근: AccessType.FIELD는 필드에 직접 접근합니다. 필드 접근 권한이 private이어도 접근할 수 있습니다.
프로퍼티 접근: AccessType.PROPERTY로 지정합니다. 접근자(getter)를 사용합니다.