JPA를 사용하는 데 중요한 것은 엔티티와 테이블을 정확히 매핑하는 것이다.
따라서 매핑 어노테이션을 숙지하고 사용해야 한다.
JPA는 다양한 매핑 어노테이션을 지원하는데 크게 4가지로 분류할 수 있다.
객체와 테이블 매핑 | @Entity, @Table |
기본 키 매핑 | @Id |
필드와 컬럼 매핑 | @Column |
연관관계 매핑 | @ManyToOne, @JoinColumn |
@Entity
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션은 필수이다.
이 어노테이션이 붙은 클래스는 JPA가 관리한다.
속성 정리
속성 | 기능 | 기본값 |
name | - JPA에서 사용할 엔티티 이름을 지정 - 보통 기본값인 클래스 이름을 사용 - 만약 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 충돌하 지 않도록 해야함 |
설정하지 않으면 클래스 이름을 그대로 사용한다. |
주의사항
- 기본 생성자는 필수다(파라미터가 없는 public 또는 protected 생성자)
- final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
- 지정한 필드에 final을 사용하면 안 된다.
JPA가 엔티티 객체를 생성할 떄 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다. 자바는 생성자가 하나도 없으면 다음과 같은 기본 생성자를 자동으로 만든다.
public Member() {} // 기본 생성자
문제는 다음과 같이 생성자를 하나 이상 만들면 자바는 기본 생성자를 자동으로 만들지 않는다. 이 때는 기본 생성자를 직접 만들어야 한다.
public Member() {} // 직접 만든 기본 생성자
//임의의 생성자
public Member(String name){
this.name = name
}
@Table
@Table은 엔티티와 매핑할 테이블을 지정한다. 생략하면 엔티티 이름을 테이블 이름으로 사용한다.
속성정리
속성 | 기능 | 기본값 |
name | 매핑할 테이블 이름 | 엔티티 이름을 사용한다. |
catalog | catalog 기능이 있는 데이터베이스에서 catalog를 매핑한다. | |
schema | schema 기능이 있는 데이터베이스에서 schema를 매핑한다. | |
uniqueConstraints(DDL) | DDL 생성 시에 유니크 제약조건을 만든다. 2개 이상의 복합 유니크 제약조건도 만들 수 있다. 참고로 이 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다. |
uniqueConstraints 예시
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.UniqueConstraint;
@Entity
@Table(
name = "example_table",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"column1", "column2"})
}
)
public class ExampleEntity {
@Id
private Long id;
@Column(name = "column1")
private String column1;
@Column(name = "column2")
private String column2;
@Column(name = "column3")
private String column3;
// getters and setters
}
column1, column2에 대한 고유 제약 조건을 설정한다.
CREATE TABLE example_table (
id BIGINT NOT NULL,
column1 VARCHAR(255),
column2 VARCHAR(255),
column3 VARCHAR(255),
PRIMARY KEY (id),
UNIQUE (column1, column2)
);
CREATE DDL문이 생성될 때 위와 같이 삽입된다.
col1 | col2 |
value1 | valueA |
value1 | valueB |
한마디로 UniqueConstraint 속성은 col1과 col2끼리는 중복이 되어도 되지만
col1와 col2의 조합이 겹치면 안된다.
다양한 매핑 사용
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name="MEMBER") // 테이블 이름 매핑, 생략시 클래스 이름으로 생성됨
public class Member {
@Id //기본키 매핑
@Column(name = "ID") // 컬럼 이름 매핑, 갱략시 변수명으로 생성
private String id;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType; // 1
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate; // 2
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate; // 3
@Lob
private String description; // 4
public enum RoleType {
ADMIN, USER
}
1. roleType | 자바의 enum을 사용해서 회원타입을 구분 일반회원은 USER, 관리자는 ADMIN enum을 사용하려면 @Enumerated 어노테이션을 매핑해야 함 |
2. createdDate, lastModifiedDate |
자바의 날짜 타입은 Temporal을 사용해서 매핑 |
3. description | 회원을 설명하는 필드는 길이 제한이 없음 따라서 데이터베이스의 VARCHAR 타입 대신 CLOB타입으로 저장해야 함 @Lob을 사용하면 CLOB, BLOB 타입을 매핑할 수 있음 |
데이터베이스 스키마 자동 생성
JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.
클래스의 매핑 정보를 보면 어떤 테이블에 어떤 컬럼을 사용하는지 알 수 있다.
JPA는 이 매핑정보와 데이터베이스 방언을 사용해서 데이터베이스 스키마를 생성한다.
참고로 이전의 "JPA 시작" 게시글에서 설명했다.
DDL 생성 기능
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint ( // 유니크 제약조건 지정
name = "NAME_AGE_UNIQUE", // 제약조건 이름
columnNames = {"NAME", "AGE"} )}) // NAME과 AGE의 조합이 유니크해야함.
@Entity
public class Member {
@Id
@Column(name = "ID")
prvate Long id;
@Column(name = "NAME", nullable = false, length = 10)
private String username;
...
}
속성 | 기능 |
name | 컬럼 이름을 지정 |
nullable | not null 제약조건 추가 |
length | 문자 길이를 제한(10으로 제한함) |
uniqueConstraints는 이전에 설명했다.
유니크 제약조건이 추가되었다. 앞서 @Column의 length와 nullable 속성을 포함해서 이런 기능들은 단지 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
기본 키 매핑
@Entity
public class Member {
@Id
@Column(name = "ID")
private String id;
...
}
@Id 어노테이션만 사용해서 회원의 기본 키를 애플리케이션에서 직접 할당했다.
직접 할당하는 대신 오라클의 시퀀스 오브젝트, mysql의 AUTO_INCREMENT 같은 기능은 JPA가 제공하는 기본 키 생성 전략을 사용해야 한다.
직접할당: 기본키를 애플리케이션에서 직접 할당
자동생성 : 대리 키 사용 방식
- IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.
- SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
- TABLE: 키 생성 테이블을 사용한다.
기본 키 직접 할당 전략
기본 키를 직접 할당하려면 @Id로 매핑하면 된다.
@Id
@Column(name = "id")
private String id;
@Id 적용 가능 자바 타입은 다음과 같다.
- 자바 기본형
- 자바 래퍼(Wrapper)형
- String
- java.util.Date
- java.sql.Date
- java.math.BigDecimal
- java.math.BigInteger
기본 키 직접 할당 전략은 em.persist()로 엔티티를 저장하기 전에 애플리케이션에서 기본 키를 직접 할당하는 방법이다.
Board board = new Board();
board.setId("id1") // 기본 키 직접 할당
em.persist(board);
**참고
기본 키 직접 할당 전략에서 식별자 값 없이 저장하면 예외가 발생하는데 어떤 예외가 발생하는지 JPA 표준에는 정의되어 있지 않다. 하이버네이트 구현체로 사용하면 JPA 최상위 예외인 javax.persistence.PersistenceException 예외가 발생하는데, 내부에 하이버네이트 예외인 org.hibernate.id.identifierGenerationException 예외를 포함하고 있다.
IDENTITY 전략
IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다.
주로 mysql, postgreSQL, sql server, db2에서 사용한다.
예를 들어 mysql의 AUTO_INCREMENT 기능은 데이터베이스가 기본 키를 생성해준다.
AUTO_INCREMENT 예제
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 컬럼을 비워두면 데이터베이스가 순서대로 값을 채워준다.
BOARD 테이블 결과
ID | DATA |
1 | A |
2 | B |
개발자가 엔티티에 직접 할당하면 @Id 어노테이션만 있으면 되지만 식별자가 생성되는 경우에는 @GeneratedValue 어노테이션을 사용하고 식별자 생성 전략을 선택해야한다.
IDENTITY 전략을 사용하려면 @GeneratedValue의 strategy 속성값을 GeneRationType.IDENTITY로 지정하면 된다.
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
}
IDENTiTY 로직
private static void logic(Entity Manager em) {
Board board = new Board();
em.persist(board);
};
em.persist()를 호출해서 엔티티를 저장한 직후에 할당된다.
IDENTITY 전략과 최적화
IDENTITY 전략은 데이터를 데이터베이스를 INSERT한 후에 기본 키 값을 조회할 수 있다. 따라서 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야한다. JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성된 기본키 값도 얻어 올 수 있다. 하이버네이트는 이 메소드를 사용해서 데이터베이스와 한 번만 통신한다.
주의점
엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다. 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.
SEQUENCE 전략
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있다.
시퀀스 DDL
CREATE TABLE BOARD {
ID BIGINT NOT NULL PRIMARY KEY,
DATA VARCHAR(255)
)
// 시퀀스 생성
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;`
시퀀스 매핑 코드
@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;
...
}
우선 사용할 데이터베이스 시퀀스를 매핑해야 한다.
@SequenceGenerator를 사용해서 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했다.
그리고 sequenceName 속성의 이름으로 BOARD_SEQ를 지정했는데 JPA는 이 시퀀스 생성기를 실제 데이터베이스의 BOARD_SEQ 시퀀스와 매핑한다.
키 생성 전략을 GenerationType.SEQUENCE로 설정하고 generator = "BOARD_SEQ_GENERATOR"로 등록한 시퀀스 생성기를 선택 했다.
식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기를 할당한다.
시퀀스 전략 로직
private static void logic(EntityManager em) {
Board board = new Board();
em.persist(board);
}
시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다르다.
SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다.
그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다.
이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다.
이전에 설명했던 IDENTITY 전략은 엔티티를 데이터베이스에 저장한 후에 식별자를 조회해서 엔티티의 식별자에 할당한다.
속성 정리
속성 | 기능 | 기본 값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
*주의*
allocationSize의 기본값이 50인 것을 주의해야 한다. JPA가 기본으로 생성하는 데이터베이스 시퀀스는 호출할 때마다 값이 50씩 증가한다. 기본값이 50인 이유는 최적화 때문이다.
데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다.
*참고*
SEQUENCE 전략과 최적화
SEQUENCE 전략은 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요하다 따라서 데이터베이스와 2번 통신한다.
1. 식별자를 구하려고 데이터베이스 시퀀스를 조회
2. 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장
JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequnceGenerator.allocationSize를 사용한다.
여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당한다.
예를 들어 allocationSize 값이 50이면 시퀀스를 한 번에 50증가 시킨 다음에 1~50까지는 메모리에서 식별자를 할당한다.
그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음 51~100까지 메모리에서 식별자를 할당한다.
이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.
반면에 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한 번에 많이 증가한다는 점을 염두해두어야 한다.
이런 상황이 부담스럽고 INSERT 성능이 중요하지 않으면 allocationSize의 값을 1로 설정하면 된다.
참고로 앞서 설명한 hivernate.id.new_generator_mappings 속성을 true로 설정해야 설명한 최적화 방법이 적용된다.
이 속성을 적용하지 않으면 하이버네이트가 과거에 사용하던 방법으로 키 생성을 최적화한다.
과거에는 시퀀스 값을 하나씩 할당받고 애플리케이션에서 allocationSize만큼 사용했다.
예를 들어 allocationSize를 50으로 설정했다고 가정하면, 반환된 시퀀스 값이 1이면 애플리케이션에서 1~50까지 사용하고 시퀀스 값이 2이면 애플리케이션에서 51~100까지 기본 키를 사용하는 방식이었다.
*참고*
@SequenceGenerator는 다음과 같이 @GeneratedValue 옆에 사용해도 된다.
@Entity
public class Board {
@Id
@GeneratedValue(...)
@SequenceGenerator(...)
private Long id;
...
}
TABLE 전략
TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다.
이 전략은 테이블을 사용하므로 모든 데이터베이스에 적용할 수 있다.
TABLE 전략을 사용하려면 키 생성 용도로 사용할 테이블을 만들어야 한다.
create talbe MY_SEQUENCES (
sequence_name varchar(255) not null,
next_val bigint,
primary key ( sequnce_name )
)
sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용한다.
참고로 컬럼의 이름은 변경 가능한데 여기서 사용한 것이 기본값이다.
@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라는 이름의 테이블 키 생성기를 등록하고
MYSEQUENCES 테이블을 키 생성용 테이블로 매핑했다.
TABLE 전략을 사용하기 위해 GenerationType.TABLE을 선택했다.
@GeneratedValue.generator에 방금 만든 테이블 키 생성기를 지정했다.
id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당한다.
TABLE 전략 매핑 사용코드
private static void logic(EntityManager em) {
Board board = new Board();
em.persist(board);
System.out.println("board.id = " + board.getId());
}
TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작방식이 같다.
MY_SEQUENCES 테이블
sequence_name | next_val |
BOARD_SEQ | 2 |
MEMBEER_SEQ | 10 |
PRODUCT_SEQ | 50 |
테이블을 보면 @TableGenerator.pkColumnValue에서 지정한 "BOARD_SEQ" 컬럼명으로 추가된 것을 확인 할 수 있다.
키 생성기를 사용할 때마다 next_val 컬럼이 증가한다.
참고로 MY_SEQUENCES 테이블에 값이 없으면 JPA가 값을 INSERT하면서 초기화하므로 값을 미리 넣어둘 필요는 없다.
속성 정리
속성 | 기능 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준 | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용 | 50 |
catalog,schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraints(DDL) | 유니크 제약 조건을 지정 |
JPA 표준 명세에는 table, pkColumnName, valueColumnName의 기본값을 JPA 구현체가 정의하도록 했다.
위 기본값은 하이버네이트 기준이다.
*참고*
TABLE 전략과 최적화
TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용한다.
SEQUENCE 전략과 비교해서 데이터베이스와 한번 더 통신하는 단점이 있다.
최적화 하려면 @TableGenerator.allocationSize를 사용하면 된다. 최적화 방법은 SEQUENCE 전략과 같다.
AUTO 전략
데이터베이스 종류는 많고 기본 키를 만드는 방법도 다양하다. GenerationType.AUTO는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략중 하나를 자동으로 선택한다.
오라클을 선택하면 SEQUENCE를, MySQL을 선택하면 IDENTITY를 사용하는 등...
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
...
}
AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다.
특히 키 전략이 아직 확정되지 않은 개발 초기 단계나 프로토타입 개발 시 편리하게 사용할 수 있다.
기본 키 매핑 정리
영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다.
엔티티를 영속 상태로 만드려면 식별자 값이 반드시 있어야 한다.
em.persist()를 호출한 직후에 발생하는 일을 식별자 할당 전략별로 정리해보자면
- 직접 할당: em.persist()를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외가 발생한다.
- SEQUENCE: 데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- TABLE : 데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- IDENTITY : 데이터베이스에 엔티티를 저장해서 식별자 값을 흭득한 후 영속성 컨텍스트에 저장한다.(테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.)
'JPA' 카테고리의 다른 글
JPA 연관관계 매핑 기초 (0) | 2024.06.25 |
---|---|
JPA 영속성 관리 (0) | 2024.06.20 |
JPA 시작 (2) | 2024.06.08 |
JPA(Java Persistent API) 소개 (0) | 2024.05.11 |