https://github.com/MoochiPark/jpa의 깃을 참고하였습니다.
Chapter 04. 엔티티 매핑
JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다. 따라서 매핑 애노테이션을 숙지하고 사용해야 한다. JPA는 다양한 매핑 애노테이션을 지원하는데 크게 4가지로 분류할 수 있다.
- 객체와 테이블 매핑: @Entity, @Table
- 기본 키 매핑: @Id
- 필드와 컬럼 매핑: @Column
- 연관관계 매핑: @ManyToOne, @JoinColumn
먼저 객체와 테이블 매핑 애노테이션부터 알아보자.
4.1 @Entity
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 애노테이션을 필수로 붙여야 한다. @Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔티티라 부른다.
표 4.1 @Entity 속성 정리
속성기능기본값
name | JPA에서 사용할 엔티티 이름을 지정한다. 보통 기본값인 클래스 이름을 사용한다. 만약 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다. | 설정하지 않으면 클래스 이름을 그대로 사용한다 (예: Member). |
@Entity 적용 시 주의사항은 다음과 같다.
- 기본 생성자는 필수다파라미터가 없는 public 또는 protected 생성자.
- final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
- 저장할 필드에 final을 사용하면 안 된다.
JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다. 자바는 생성자가 하나도 없으면 자동으로 기본 생성자를 만든다. 따라서 생성자를 하나 이상 만들면 기본 생성자를 자동으로 만들지 않기 때문에 직접 만들어야 한다.
4.2 @Table
@Table은 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
표 4.2 @Table 속성 정리
속성기능기본값
name | 매핑할 테이블 이름 | 엔티티 이름을 사용한다. |
catalog | catalog 기능이 있는 데이터베이스에서 catalog를 사용한다. | |
schema | schema 기능이 있는 데이터베이스에서 schema를 사용한다. | |
uniqueConstraints (DDL) | DDL 생성 시에 유니크 제약조건을 만든다. 2개 이상의 복합 유니크 제약조건도 만들 수 있다. 참고로 이 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다. |
DDL 생성 기능은 조금 뒤에 알아보자.
4.3 다양한 매핑 사용
JPA 시작하기 장에서 개발하던 회원 관리 프로그램에 다음 요구사항이 추가되었다고 해보자.
- 회원은 일반 회원과 관리자로 구분해야 한다.
- 회원 가입일과 수정일이 있어야 한다.
- 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.
예제 4.1 회원 엔티티
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String username;
private Integer age;
// 추가
@Enumerated(EnumType.STRING)
private RoleType roleType; --- 1
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate; --- 2
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate; --- 2
@Lob
private String description; --- 3
...
- roleType: enum을 사용해서 회원의 타입을 구분했다. 이처럼 enum을 사용하려면 Enumrated 애노테이션으로 매핑해야 한다.
- createdDate, lastModifiedDate: 자바의 날짜 타입은 @Temporal을 사용해서 매핑한다.
- description: 회원을 설명하는 필드는 길이 제한이 없다. 따라서 데이터베이스의 VARCHAR가 아닌 CLOB 타입으로 저장해야 한다. @Lob을 사용하면 CLOB, BLOBpostgresql의 TEXT타입을 매핑할 수 있다.
지금까지는 테이블을 먼저 생성하고 그 다음에 엔티티를 만들었지만 이번에는 데이터베이스 스키마 자동 생성을 사용해서 엔티티만 만들고 테이블은 자동으로 생성되도록 해보자.
4.4 데이터베이스 스키마 자동 생성
JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다. 클래스의 매핑정보를 보면 어떤 테이블에 어떤 칼럼을 사용하는지 알 수 있다. JPA는 이 매핑정보와 데이터베이스 방언을 사용해서 데이터베이스 스키마를 생성한다.
스키마 자동 생성 기능을 사용해보자. 먼저 persistence.xml에 다음 속성을 추가하자.
<property name="hibernate.hbm2ddl.auto" value="create"/>
이 속성을 추가하면 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.
<property name="hibernate.show_sql" value="true"/>를 추가하면 콘솔에 실행되는 DDL을 출력할 수 있다.
Hibernate:
drop table if exists MEMBER cascade
Hibernate:
create table MEMBER (
ID varchar(255) not null,
age int4,
createdDate timestamp,
description text,
lastModifiedDate timestamp,
roleType varchar(255),
NAME varchar(255),
primary key (ID)
)
실행된 결과를 보면 기존 테이블을 삭제하고 다시 생성한 것을 알 수 있다.
자동 생성되는 DDL은 데이터베이스 방언마다 달라진다.
스키마 자동 생성 기능이 만든 DDL은 운영 환경에서 사용할 만큼 완벽하지는 않으므로 개발 환경에서 사용하거나 매핑을 어떻게 해야하는지 참고하는 정도로만 사용하는 것이 좋다.
그래도 이 기능을 사용하여 생성된 DDL을 보면 엔티티와 테이블이 어떻게 매핑되는지 쉽게 이해할 수 있어 훌륭한 학습 도구이다.
표 4.3 hibernate.hbm2ddl.auto 속성
옵션설명
create | 기존 테이블을 삭제하고 새로 생성한다. DROP + CREATE |
create-drop | create 속성에 추가로 애플리케이션이 종료할 때 생성한 DDL을 제거한다. DROP + CREATE + DROP |
update | 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정한다. |
validate | 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 수행하지 않는다. 이 설정은 DDL을 수정하지 않는다. |
none | 자동 생성 기능을 사용하지 않으려면 none과 같은 유효하지 않은 옵션 값을 주면 된다. |
HBM2DDL 주의사항
운영 서버에서 create, create-drop, update처럼 DDL을 수정하는 옵션은 절대 사용하면 안 된다. 이 옵션들은 운영 중인 데이터베이스의 테이블이나 컬럼을 삭제할 수 있다.
개발 환경에 따른 추천 전략은
- 개발 초기 단계는 create 또는 update
- 초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
- 테스트 서버는 update 또는 validate
- 스테이징과 운영 서버는 validate 또는 none
참고
JPA 2.1부터 스키마 자동 생성 기능을 표준으로 지원한다. 하지만 하이버네이트의 update, validate 속성은 지원하지 않는다. <property name="javax.persistence.schema-generation.database.action" value="create"/>
지원 옵션 : none, create, drop-and-create, drop
*참고 이름 매핑 전략 변경하기
자바 언어는 관례상 roleType과 같이 카멜 표기법을 주로 사용하고 데이터베이스는 관례상 role_type과 같은 스네이크 표기법을 주로 사용한다. 따라서 이렇게 사용하려면 @Column의 name 속성을 사용해서 매핑해주어야 한다.
@Column(name="role_type")
String roleType
hibernate.ejb.naming_strategy 속성을 사용하면 이름 매핑 전략을 변경할 수 있다. 이를 직접 구현해서 변경해도 되지만, 하이버네이트는 org.hibernate.cfg.improveNamingStrategy 클래스를 제공한다. 이 클래스는 테이블 명이나 컬럼 명이 생략되면 자바의 카멜 표기법을 테이블의 스네이크 표기법으로 매핑한다.
<property name="hibernate.ejb.naming_strategy"
value="org.hibernate.cfg.ImprovedNamingStrategy" />
이 속성을 사용해서 엔티티를 생성해보자.
Hibernate:
create table member (
id varchar(255) not null,
age int4,
created_date timestamp,
description text,
last_modified_date timestamp,
role_type varchar(255),
name varchar(255),
primary key (id)
)
하지만 하이버네이트 5 부터는 위 방식을 지원하지 않는다.
따라서 다음과 같이 변경했다.
build.gradle
implementation 'org.apache.commons:commons-lang3:3.9' 추가
persistence.xml
<property name="hibernate.physical_naming_strategy" value="io.wisoft.daewon.namingstrategy.PhysicalNamingStrategyImpl" /> 추가
CustomPhysicalNamingStrategy.java
import org.apache.commons.lang3.StringUtils;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
public class PhysicalNamingStrategyImpl implements PhysicalNamingStrategy {
@Override
public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return name;
}
@Override
public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return name;
}
@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
final LinkedList<String> parts = splitAndReplace(name.getText());
return jdbcEnvironment.getIdentifierHelper().toIdentifier(
join(parts),
name.isQuoted()
);
}
@Override
public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return name;
}
@Override
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
final LinkedList<String> parts = splitAndReplace(name.getText());
return jdbcEnvironment.getIdentifierHelper().toIdentifier(
join(parts),
name.isQuoted()
);
}
private LinkedList<String> splitAndReplace(String name) {
LinkedList<String> result = new LinkedList<>();
for (String part : StringUtils.splitByCharacterTypeCamelCase(name)) {
if (part == null || part.trim().isEmpty()) {
continue;
}
result.add(part.toLowerCase(Locale.ROOT));
}
return result;
}
private String join(List<String> parts) {
boolean firstPass = true;
String separator = "";
StringBuilder joined = new StringBuilder();
for (String part : parts) {
joined.append(separator).append(part);
if (firstPass) {
firstPass = false;
separator = "_";
}
}
return joined.toString();
}
}
4.5 DDL 생성 기능
회원 이름은 필수로 입력되어야 하고, 10자를 초과하면 안 된다는 제약 조건이 추가되었다. 스키마 자동 생성하기를 통해 만들어지는 DDL에 이 제약조건을 추가해보자.
예제 4.4 추가 코드
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10) // 추가
private String username;
...
- @Culumn 매핑정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약 조건을 추가할 수 있다.
- 그리고 length 속성 값으로 문자의 크기를 지정할 수 있다.
- 자동 생성되는 DLL에 문자의 크기를 지정할 수 있다. nullable = false, length = 10으로 지정
...
ID varchar (255) not null,
name varchar(10) not null,
...
- DDL의 NAME 컬럼을 보면 not null 제약 조건이 추가되었고 varchar(10)으로 문자의 기가 10자리로 제한된 것을 확인 할 수 있다.
- 이번엔 유니크 제약조건을 만들어주는 @Table의 uniqueConstraints 속성을 알아보자.
예제 4.6 유니크 제약조건
// 이번엔 유니크 제약조건을 만들어주는 @Table의 uniqueConstraints 속성을 알아보자.
@Entity(name = "Member")
@Table(name = "MEMBER", uniqueConstraints = {@UniqueConstraint(
name = "NAME_AGE_UNIQUE",
columnNames = {"NAME", "AGE"} )})
public class Member {
...
생성된 DDL
Hibernate:
alter table member
add constraint NAME_AGE_UNIQUE unique (name, age)
- DDL을 보면 유니크 제약조건이 추가되었다.
- 앞의 기능들을 포함해서 이런 기능들은 단지 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다. 따라서 스키마 자동 생성 기능을 사용하지 않는다면 사용할 이유가 없다.
그래도 이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 쉽게 다양한 조건을 파악할 수 있다는 장점이 있다.
다음은 데이터베이스의 기본 키를 어떻게 매핑하는지 알아보자.
4.6 기본 키 매핑
기본 키primary key 매핑을 알아보자.
기본 키 매핑 시작
public class Member {
@Id
@Column(name = "ID")
private String id;
...
- 지금까지 @Id 애노테이션만 사용해서 회원의 기본 키를 애플리케이션에서 직접 할당했다.
- 기본 키를 애플리케이션에서 직접 할당하는 것이 아니라 데이터베이스가 생성해주는 값, 예를 들어 postgresql의 SERIAL을 사용하려면 어떻게 해야할까?
- 예를 들어 MySQL의 AUTO_INCREMENT 같은 기능을 사용해서 생성된 값을 기본키로 어떻게 할까
데이터베이스마다 기본 키를 생성하는 방식이 다른데, JPA는 이 문제들을 어떻게 해결하는지 보자. JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같다.
- 직접 할당: 기본 키를 애플리케이션에서 직접 할당한다.
- 자동 생성: 대리 키 사용 방식
- IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
- SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
- TABLE: 키 생성 테이블을 사용한다.
자동 생성 전략이 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다.
기본 키를 직접 할당하려면 @Id만 사용하면 되고, 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.
* 주의 <기존 하이버 네이스 시스템을 유지보수하는 것이 아니라면 반드시 true로 설정하자>
- 키 생성 전략을 사용하려면 persistence.xml에 `<property name="hibernate.id.new_generator_mappings" value="true"/>` 속성을 반드시 추가해주어야 한다. 하이버네이트는 더 효과적이고 JPA 규격에 맞는 새로운 키 생성 전략을 개발했는데 과거 버전과의 호환성을 유지하려고 기본값을 false로 두었다.
- 기존 하이버 네이스 시스템을 유지보수하는 것이 아니라면 반드시 true로 설정하자
- 이 옵션을 true로 설정하면 키 생성 성능을 최적화 하는 allocationSize 속성을 사용하는 방식이 달라지낟.
allocationSize 속성은 뒤에서 설명한다.
`<property name="hibernate.id.new_generator_mappings" value="true"/>`
4.6.1 기본 키 직접 할당 전략 <직접 할당, Id, pk>
@Id
@Column(name = "ID")
private String id;
@Id 적용 가능 자바 타입은 다음과 같다.
- 자바 기본형
- 자바 래퍼형
- String
- java.util.Date
- java.sql.Date
- BigDecimal
- BigInteger
기본 키 직접 할당 전략은 em.persist()로 엔티티를 저장하기 전에 애플리케이션에서 기본 키를 직접 할당하는 방법이다.
Board board = new Board();
board.setId("id1"); // 기본 키 직접 할당
em.persist(board);
*참고 식별자 값 없이 저장하면 예외 발생 <hibernate.id.IdentifierGenerationException>
기본 키 직접 할당 전략에서 식별자 값 없이 저장하면 예외가 발생하는데, 어떤 예외가 발생하는지 JPA 표준에는 정의되어 있지 않다.
하이버네이트를 구현체로 사용하면 JPA 최상위 예외인 javax.persistence.PersistenceException 예외가 발생하는데.
내부에 하이버에이스 예외인 org.hibernate.id.IdentifierGenerationException 예외를 포함하고 있다.
4.6.2 IDENTITY 전략
- IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다.
- 주로 MySQL, PostgreSQL, SQL Server 등에서 사용한다.
- 예를 들어 PostgreSQL의 SERIAL 기능은 데이터베이스가 기본 키를 자동으로 생성해준다.
CREATE TABLE board (
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
DATA VARCHAR(255)
);
INSERT INTO BOARD(DATA) VALUES('A');
INSERT INTO BOARD(DATA) VALUES('B');
- 테이블을 생성할 때 기본 키 컬럼인 ID에 AUTO_INCREMENT를 추가 했다.
- 이제 데이터베이스에 값을 저장할 때 ID 컬럼을 비워두면 데이터베이스가 순서대로 값을 채워준다.
- 지금처럼 식별자가 생성되는 경우에는 @GeneratedValue 어노테이션을 사용하고 식별자 생성 전략을 선택해야 한다.
- IDENTITY 전략을 사용하려면 @GeneratedValuedㅢ strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다.
- 이 전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가를 조회한다.
@Id
@Column(name = "ID")
// strategy 속성 값을 GenerationType.IDENTITY
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
public static void main(String... args) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
logic(em);
tx.commit();
}
private static void logic(final EntityManager em) {
Member member = new Member();
member.setUsername("pdw");
em.persist(member);
System.out.println("member.id = " + member.getId());
}
실행 결과
meber.id = 1
- em.persist()을 호출해서 엔티티를 저장한 직후에 할당된 식별자 값을 출력했다.
- 출력된 값 1은 저장 시점에 데이터베이스가 생성한 값을 JPA가 조회한 것이다.
참고 * IDENTITY 전략과 최적화
IDENTITY 전략은 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있다.
따라서 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 한다.
JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성된 기본 키 값도 얻어 올 수 있다.
하이버네이트는 이 메소드를 사용해서 데이터베이스와 한 번만 통신한다.
* 주의 <IDENTITY전략은 쓰기 지연을 지원을 하지 않는다>
엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다.
그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다.
따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.
4.6.3 SEQUENCE 전략
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 이 전략은 시퀀스를 지원하는 Oracle, PostgreSQL 등에서 사용할 수 있다.
CREATE SEQUENCE member_seq START WITH 1 INCREMENT BY 1;
시퀀스 매핑 코드
@Entity(name = "Member")
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
initialValue = 1, allocationSize = 1) // allocationsSize의 기본은 50
public class Member {
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
- @SequenceGenerator를 사용해서 MEMBER_SEQ_GENERATOR라는 시퀀스 생성기를 등록했다.
- sequenceName 속성의 이름으로 MEMBER_SEQ를 지정했는데 JPA는 이 시퀀스 생성기를 실제 데이터베이스의 MEMBER_SEQ 시퀀스와 매핑한다.
- 키 생성 전략을 GenerationType.SEQUENCE로 설정하고 generator = "MEMBER_SEQ_GENERATOR"로 방금 등록한 시퀀스 생성기를 선택했다.
- 시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다르다.
- SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다.
- 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다.
- 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다.
- 반대로 이전에 설명한 IDENTITY는 먼저 엔티티를 데이터베이스에 저장한 후에 식별자를 조회해서 엔티티의 식별자에 할당한다.
@SequenceGenerator
표 4.5 @SequenceGenerator
속성기능기본값
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용됨. 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) | 50(주의) |
catalog, schema | 데이터베이스 catalog, schema 이름 |
매핑할 DDL은 다음과 같다.
CREATE SEQUENCE [sequenceName]
START WITH [initialValue] INCREMENT BY [allocationSize];
- JPA 표준 명세에서는 sequenceName의 기본값을 JPA 구현체가 정의하도록 했다.
- 위에서 설명한 기본값은 하이버네이트 기준이다.
* 주의 <SequenceGenerator.allocationSize의 기본값은 50이다.>
SequenceGenerator.allocationSize의 기본값이 50이다.
JPA가 기본으로 생성하는 데이터베이스 시퀀스는
create sequence [sequenceName] start with 1 increment by 50 이므로 시쿼스를 호출할 때마다 값이 50씩 증가한다.
기본값이 50인 이유는 최적화 때문인데 다음 참고에서 설명한다.
데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이값을 반드시 1로 설정해야한다.
* 참고 SEQUENCE 전략과 최적화 <데이터베이스와 2번 통신한다.>
SEQUENCE 전략과 최적화
SEQUENCE 전략은 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요하다.
따라서 데이터베이스와 2번 통신한다.
- 식별자를 구하려고 데이터베이스 시퀀스를 조회한다.
- SELECT nextval('member_seq');
- 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장한다.
- INSERT INTO MEMBER ...
JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequenceGenerator.allocationSize를 사용한다. 간단히 설명하면 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당한다. 예를 들어 allocationSize 값이 50이면 시퀀스를 한 번에 50 증가시킨 다음에 1~50까지는 메모리에서 식별자를 할당한다. 그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음에 51~100까지 메모리에서 식별자를 할당한다.
이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.
여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.
반면에 데이터베이스에 직접 접근해서 데이터를 등록할 때 시쿼스 값이 한번에 많이 증가한다는 점을 염두해두어야 한다.
이런 상황이 두담스럽고 INSERT 성능이 중요하지 않으면 allocationSize의 값을 1로 설정하면 된다.
참고로 앞서 설명한 hibernate.id.new_generator_mappings 속성을 truefㅗ 설정해야 지금까지 설명한 최적화 방법이 적용된다.
이 속성을 적용하지 않으면 하이버 네이트가 과거에 사용하던 방법으로 키 생성을 최적화 한다.
과거에는 시쿼스 값을 하나씩 할당받고 애플리케이션에서 a llocationSize만큼 사용했다.
예를 들어 allocationSize을 50으로 설정했다고 가정하면, 반환된 시퀀스 값이 1이면 애플리 케이션에서 1~50까지 사용하고 시쿼스 값이 2이면 애플리 케이션에서 51~100까지 기본 키를 사용하는 방식이였다.
*참고 @WequenceGenerator는 다음과 같이 @GeneratedValue 옆에 사용해도 된다.
@Entity
public class Board{
@Id
@GeneratedValue(...)
@SequenceGenerator(...)
private Long id;
}
4.6.4 테이블 전략
- TABLE 전략은 키 생성 전용 테이블을 만들고
- 여기에 이름과 값으로 사용할 컬럼을 만들어 시퀀스를 흉내내는 전략이다.
- 이 전략은 테이블을 사용하기 때문에 모든 데이터베이스에 적용할 수 있다.
- TABLE 전략을 사용할려면 먼저 키 생성 용도로 사용할 테이블을 만들어야한다.
TABLE 전략 키 생성 DDL
create table MY_SEQUENCES (
sequence_name varchar(255) primary key, --- 시퀀스 이름
next_val higint, --- 시퀀스 값
primary key( sequence_name )
)
- Sequence_name 컬럼을 시쿼스 이름으로 사용하고
- Next_val 컬럼을 시쿼스 값으로 한다.
- 컬럼의 이름은 변경할 수 있는데 여기서 사용하는 것이 기본 값이다.
TABLE 전략 매핑 코드 예제 4.15
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "MEMBER_SEQ", allocationSize = 1)
...
@Id
@GeneratorValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
- 예제 4.15의 코드를 보면 @TableGenerator를 사용해서 테이블 키 생성기를 등록한다.
- 여기서는 BOARD_SEQ_GENERATOR라는 이름의 테이블 키 생성기를 등록하고
- 방금 생성한 MY_SEQUENCES 테이블을 키 생성용 테이블로 매핑했다.
- 다음으로 TABLE 전략을 사하기 위해 GenerationType.TABLE을 선택했다.
- 그리고 @GeneratedValue.generator에 방금 만든 테이블 키 생성기를 지정했다.
- 이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키를 생성기가 할당한다.
- TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작방식이 같다.
- 이제 키 생성기를 사용할 때마다 next_val 컬럼 값이 증가한다. 참고로 테이블에 값이 없어도 JPA가 INSERT 하면서 초기화하므로 값을 미리 넣어둘 필요는 없다.
@TableGenerator
표 4.5 @TableGenerator
속성기능기본값
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnsName | 시퀀스 값 컬럼 명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | DDL 생성 시에만 사용됨. 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraint(DDL) | 유니크 제약 조건을 지정할 수 있다. |
- 위에서 설명한 기본값은 하이버네이트 기준이다.
- 매핑할 테이블은 표 4.8을 참고하자.
매핑할 DDL, 테이블 명 {table}
{pkColumnName}{valueColumnName}
{pkColumnValue} | {initialValue} |
* 참고 TABLE 전략과 최적화
TABLE 전략과 최적화
TABLE 전략은 값을 조회하면서 SELECT, 다음값으로 증가시키기 위해 UPDATE를 사용한다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점이 있다. 마찬가지로 TABLE 전략을 최적화하려면 @TableGenerator.allocationSize를 사용하면 된다.
4.6.5 AUTO 전략 <IDENTITY.SEQUENCE, TABLE>
- 데이터베이스의 종류도 많고 기본 키를 만드는 방법도 다양하다.
- GenerationType.AUTO는 선택한 데이터베이스 방언에 따라
- IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다. (PostgreSQL은 SEQUENCE를 선택한다.)
- 오라클은 SEQUENCE를
- MySQL을 선택하면 IDENTITY를 사용한다.
AUTO 전략 매핑 코드
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@GeneratedValue.strategy의 기본값은 AUTO다. 따라서 다음과 같이 사용해도 결과는 같다.
@Id @GeneratedValue
private Long id;
- AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다.
- 특히 키 생성 전략이 아직 확정되지 않은 개발 초기나 프로토타입 개발 시 편리하게 사용할 수 있다.
- AUTO를 사용할 때 SEQUENCE나 TABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다.
- 만약 스키마 자동 생성을 이용한다면 하이버네이트가 기본값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 줄 것이다.
4.6.6 기본 키 매핑 정리
영속성 컨텍스트는 엔티티를 식별자 값으로 구분하므로 엔티티를 영속 상태로 만들려면 식별자 값이 반드시 있어야 했다. em.persist()를 호출한 직후에 발생하는 일을 식별자 할당 전략별로 정리해보자.
- 직접 할당: em.persist()를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외가 발생한다.
- SEQUENCE: 데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- TABLE: 데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- IDENTITY: 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- (IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.)
권장하는 식별자 선택 전략
데이터베이스 기본 키는 다음 3가지 조건을 모두 만족해야 한다.
- null값은 허용하지 않는다.
- 유일해야 한다.
- 변해선 안 된다.
테이블의 기본 키를 선택하는 전략은 크게 2가지가 있다.
- 자연 키natural key
- 비즈니스에 의미가 있는 키
- 예: 주민등록번호, 이메일, 전화번호
- 대리 키surrogate key
- 비즈니스와 관계 없는 임의로 만들어진 키, 대체 키로도 불린다.
- 예: 시퀀스, serial, 키생성 테이블 사용
자연 키 보다는 대리 키를 권장한다
예를 들어 자연 키인 전화번호를 기본 키로 선택한다면 그 번호가 유일할 순 있지만, 전화번호가 없을 수도 있고 변경될 수도 있다. 따라서 기본 키로 적절하지 않다. 주민번호도 3가지 조건을 만족해보이지만 현실과 비즈니스 규칙은 생각보다 쉽게 변한다. 주민등록번호 조차도 여러 가지 이유로 변경될 수 있다.
JPA는 모든 엔티티에 일관된 방식으로 대리키 사용을 권장한다
비즈니스 요구사항은 계속해서 변하는데 테이블은 한 번 정의하면 변경하기 어렵다. 그런 면에서 대리 키가 일반적으로 좋은 선택이라 생각된다.
* 주의 <기본키는 변하면 안된다>
기본키는 변하면 안된다는 기복 원칙으로 인해
저장된 엔티티의 기본키 값은 절대 변경하면 안 된다.
이 경우 JPA는 예외를 발생시키거나 정상 동작하지 않는다.
setId() 같이 식별자를 수정하는 메소드를 외부에 공개하지 않는 것도 문제를 예방하는 하나의 방법이 될 수 있다.
4.7 필드와 컬럼 매핑: 레퍼런스
- JPA가 제공하는 필드와 컬럼 매핑용 어노테이션들을 레퍼런스 형식으로 정리해 보았다.
- 필요한 매핑을 사용할 일이 있을 때 찾아서 자세히 읽어보는 것을 권장
표 4.9 필드와 컬럼 매핑
분류매핑 어노테이션설명
필드와 컬럼 매핑 | @Column | 컬럼을 매핑한다. |
@Enumerated | 자바의 enum 타입을 매핑한다. | |
@Temporal | 날짜 타입을 매핑한다. | |
@Lob | BLOB, CLOB 타입을 매핑한다. | |
@Transient | 특정 필드를 데이터 | |
기타 | @Access | JPA가 엔티티에 접근하는 방식을 지정한다. |
4.7.1 @Column
- @Column은 객체 필드를 테이블 컬럼에 매핑한다.
- 가장 많이 사용되고 기능도 많다.
- 속성 중에 name, nullable이 주로 사용되고 나머지는 자 사용되지 않는 편이다.
- insertable, updatable 속성은 데이터베이스에 저장되어 있는 정보를 읽기만하고 실수로 변경하는 것을 방지하고 싶을때만 사용한다.
표 4.10 @Column 속성 정리
속성기능기본값
Name | 필드와 매핑할 테이블의 컬럼 이름 | 객체의 필드 이름 |
Insertable () (거의 사용하지 않음) | 엔티티 저장 시 이 필드도 같이 저장한다. false로 설정하면 이 필드는 데이터베이스에 저장하지 않는다. false 옵션은 읽기 전용일 때 사용한다. | true |
Updatable() (거의 사용하지 않음) | 엔티티 수정 시 이 필드도 같이 수정한다. false로 설정하면 데이터베이스에 수정하지 않는다. false옵션은 읽기 전용일 때 사용한다. | true |
table (거의 사용하지 않음) | 하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용한다. 지정한 필드를 다른 테이블에 매핑할 수 있다. | 현재 클래스가 매핑된 테이블 |
Nullable(DDL) | null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 nonull 제약이 붙는다. | true |
Unique(DDL) | @Table의 uniqueConstraints와 같지만 한 컬럼에 유니크 제약조건을 걸 때 사용한다. 만약 두컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints을 사용해야 한다. | |
columnDefinition(DDL) | 데이터 베이스 컬럼 정보를 직접 줄 수 있다. | 필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입을 생성한다. |
Length(DDL) | 문자 길이 제약조건, String 타입에만 사용한다. | 255 |
Precision, scale(DDL) | BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다.) precisiondㅡㄴ 소수점을 포함한 전체 자릿수를, scaledㅡㄴ 소수의 자릿수다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다. | precision = 19, scale=2 |
DDL 생성 속성에 따라 어떤 DDL이 생성되는지 확인해보자.
- Nullable(DDL 생성 기능)
@Column (nullable = false)
private String data;
// 생성된 DDL
data varchar(255) not null
- unique(DDL 생성 기능)
@Column(unique = true)
private String username;
// 생성된 DDL
alter table Tablename
add constraint UK_Xxx unique(username)
- columnDefinition(DDL 생성 기능)
@Column(columnDefinition = "varchar(100) default 'EMPTY'")
private String data;
// 생성된 DDL
data varchar(100) default 'EMPTY'
- length(DDL 생성 기능)
@Colum(length = 400)
private String data;
// 생성된 DDL
data varchar(400)
- precision, scale(DDL 생성 기능)
@Column(precision = 10, scale = 2)
private BigDecimal cal;
// 생성된 DDL
cal numberic(10, 2) // H2, PostgreSQL
cal number(10, 2) // 오라클
cal decimal(10, 2) // MySQL
* 참고 @Colum 생략
@Colum을 생략하게 되면 어떻게 될까? 대부분 @Column 속성의 기본값이 적용되는데. 자바 기본 타입일 때는 nullable 속성에 예외가 있다.
int data1; // @Column 생략, 자바 기본 타입 => 기본적 not null data1 integer not null // 생성된 DDL Integer data2; // @Column 생략, 객체 타입 data2 integer // 생성된 DDL @Column int data3; // @Column 사용, 자바 기본 타입 data3 integer // 생성된 DDL
int data1 같은 자바 기본 타입에는 Null 값을 입력할 수 없다. Integer data2처럼 객체 타입일 때만 null 값이 허용된다.
따라서 자바 기본 타입인 int data1을 data1을 DDL로 생성할 때는 not null 제약 조건을 추가하는 것이 안전하다.
JPA는 이런 상황을 고려해서 DDL 생성 기능을 사용할 때 int data1 같은 기본 타입에는 notnull 제약조건을 추가한다.
반면에 Integer data2처럼 객체타입이면 nul 이 입력 될수 있으므로 not null 제약 조건을 설정하지 않는 다 그런데 in data3cj럼
@Colum은 nullable = true가 기본값이므로 not null 제약 조건을 설정하지 않는다.
따라서 자바 기본 타입에 @Column을 사용하면 nullable = false로 지정하는 것이 안전하다.
4.7.2 @Enumerated <enum 사용>
- 자바의 enum 타입을 매핑할 때 사용한다.
표 4.11 @Enumberated 속성 정리
속성기능기능 설명기본값
value | EnumType.ORDINAL | enum 순서를 데이터베이스에 저장 | EnumType.ORDINAL |
EnumType.STRING | Enum 이름을 데이터베이스에 저장 |
@Enumerated 사용 예
Enum 클래스는 다음과 같다.
enum RoleType{
ADMIN, USER
}
- 다음은 enum 이름으로 매핑한다.
// enum 이름으로 매핑
@Enumerated(EnumType.STRING)
private RoleType roleType;
- enum은 다음처럼 사용한다.
member.setRoleType(RoleType.ADMIN); // DB에 문자 ADMIN으로 저장됨
@Enumerated를 사용하면 편리하게 enum 타입을 데이터베이스에 저장할 수 있다.
- EnumType.ORDINAL은 enum에 정의된 순서대로 ADMIN은 0, USER는 1 값이 데이터베이스에 저장된다.
- 장점: 데이터베이스에 저장되는 데이터 크기가 작다.
- 단점: 이미 저장된 enum의 순서를 변경할 수 없다.
- EnumType.STRING은 enum 이름 그대로 ADMIN은 'ADMIN', USER는 'USER'라는 문자로 데이터베이스에 저장된다.
- 장점: 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다.
- 단점: 데이터베이스에 저장되는 데이터 크기가 ORDINAL에 비해서 크다.
* 주의
기본값인 ORDINAL은 주의해서 사용해야 한다.
ADMIN(0번), NEW(1번), USER(2번)로 설정되면 이제부터 USER는 2로 저장되지만 기존에 데이터베이스에 저장된 값은 여전히 1로 남아 있다. 따라서 이런 문제가 발생하지 않는 EnumType.STRING을 권장한다.
4.7.3 @Temporal
- 날짜 타입(java.utill.Date, java.util.Calendar)을 매핑할 때 사용한다.
표 4.12 @Temporal 속성 정리
속성기능기능 설명기본값
value | TemporaType.DATE | 날짜, 데이터베이스 date 타입과 매핑(예: 2013 - 10 - 11) | TemporaType은 필수로 지정해야 한다. |
TemporalType.TIME | 시간, 데이터베이스 time 타입과 매핑(예: 11:11:11) | ||
TemporalType.TIMESTAMP | 날짜와 시간, 데이터베이스 timestamp 타입과 매핑(예: 2013 - 10 - 11 11:11:11) |
다음은 @Temporal의 사용 예다.
@Temporal(TemporalType.DATE)
private Date date; // 날짜
@Temporal(TemporalType.TIME)
private Date time; // 시간
@Temporal(TemporalType.TIMESTAMP)
private Date timestamp; // 날짜와 시간
// === 생성된 DDL == //
date date,
time time,
timestamp timestamp,
- 자바의 Date 타입에는 년원일 시분초가 있지만 데이터베이스에는 date(날짜), time(시간), timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재 한다.
- @Temporal을 생략하면 자바의 Date와 가장 유사한 timestamp로 정의 된다. 하지만 timestamp 대신에 datetime을 예약어로 사용하는 데이터베이스도 있는데 데이터베이스 방언 덕분에 애플리 케이션 코드는 변경하지 않아도 된다.
Datetime: MySQL
Timestamp: H2, 오라클, PostgreSQL
4.7.4 @Lob
데이터베이스 BLOB, CLOB 타입과 매핑한다.
속성 정리
@Lob에는 지정할 수 있는 속성이 없다. 대신에 매핑하는 필드 타입이 문자면 CLOB으로 매핑하고 나머지는 BLOB으로 매핑한다.
- CLOB: String, char[], java.sql.CLOB // 큰데이터 자정 방법
- BLOB: byte[], java.sql.BLOB // 바이너리
@Lob 사용 예
@Lob 사용 예는 다음과 같다.
@Lob // CLOB
private String lobString;
@Lob // BLOB
private byte[] lobByte;
생성된 DDL은 다음과 같다.
// 오라클
lobString clob,
lobByte blob,
// MySQL
lobString longtext,
lobByte longblob,
// PostgreSQL,
lobString text,
lobByte oid,
4.7.5 @Transient <매핑 하지 않는다.> 임시로 어떤 값을 보관하고 싶을때
- 이 필드는 매핑하지 않는다. 따라서 데이터베이스에 저장하지 않고 조회하지도 않는다.
- 객체에 임시로 어떤 값을 보관하고 싶을 때 사용한다.
@Transient
private Integer temp;
4.7.6 @Access <class 전체 정의>
JPA가 엔티티 데이터에 접근하는 방식을 지정한다.
- 필드 접근: AccessType.FIELD로 지정한다. 필드에 직겁 접근한다. 필드 접근 권한이 private이여도 접근할 수 있다.
- 프로퍼티 접근: AccessType.PROPERTY로 지정한다. 접근자(Getter)를 사용한다.
@Access(AccessType.FIELD) // 필드 접근
@Entity
@Access(AccessType.FIELD) // 필드 접근
public class Member{
@Id
private String id;
private String data1;
...
}
@Id가 필드에 있으므로 @Access(AccessType.FIELD)로 설정한 것과 같다.
- @Access(AccessType.PROPERTY
// 예제 4.19 프로퍼티 접근 코드
@Entity
@Access(AccessType.PROPERTY)
public class Member{
private String id;
@Id
public String getId(){
return id;
}
}
- @Id가 프로퍼티에 있으므로 @Access(AccessType.PROPERTY)로 설정한 것과같다.
- 따라서 @Access는 생략해도 된다.
예제 4.20 필드, 프로퍼티 접근 함께 사용
@Entity
public class Member{
@Id
private String id;
@Transient // 매핑 X
private String firstName;
@Transient // 매핑 X
private String lastName;
// 막상 하면 안된다.
// get, set 헷갈려 하는 것같다.
// Property 접근
@Access(AccessType.PROPERTY)
public String getFullName(){
return firstName + lastName;
}
}
- @Id가 필드에 있으므로 기본은 필드 접근 방식을 사용하고 getFullName()만 프로퍼티 접근 방식을 사용한다.
- 따라서 회원 엔티티를 저장하면 회원 테이블의 FULLNAME 컬럼에 firstName + lastName의 결과가 저장된다.
4.8 정리
이 장을 통해 객체와 테이블 매핑, 기본 키 매핑, 필드와 컬럼 매핑에 대해 알아보았다. 그리고 데이터베이스 스키마 자동 생성하기 기능도 알아보았는데, 이 기능을 사용하면 엔티티 객체를 먼저 만들고 테이블은 자동으로 생성할 수 있었다.
JPA는 다양한 기본 키 매핑 전략을 지원한다. 기본 키를 애플리케이션에서 직접 할당하는 방법부터 데이터베이스가 제공하는 기본 키를 사용하는 SEQUENCE, IDENTITY, TABLE 전략에 대해서도 알아보았다.
회원 엔티티는 다른 엔티티와 관계가 없었는데, 회원이 특정 팀에 속해있는 연관관계가 있는 엔티티들을 어떻게 매핑하는지는 다음 장을 통해 알아볼 것이다.
'개인공부 > JPA' 카테고리의 다른 글
6장 다양한 연관관계 매핑 (1) | 2024.02.05 |
---|---|
5장 연관관계 매핑 기초 (1) | 2024.02.05 |
3장 영속성 관리 (1) | 2024.01.23 |
2장 JPA 시작 (0) | 2024.01.20 |
1장 JPA 소개 (0) | 2024.01.20 |