1. 객체와 테이블 매핑
@Entity
@Entity 애노테이션이 붙은 class는 JPA가 관리하며, 엔티티라고 한다. 따라서 JPA를 사용해서 테이블과 매핑할 class는 해당 애노테이션을 필수로 붙여야 한다. 아래는 JPA 스펙상 꼭 지켜야 할 주의사항이다.
- 기본 생성자(파라미터가 없는 public 또는 protected 생성자)가 필수로 있어야 한다.
- JPA 리플렉션 등의 기술을 써서 객체를 프록시하는 경우에 필요하기 때문이다.
- final class, enum, interface, inner class엔 사용할 수 없다.
- 내가 DB에 저장할 필드에 final을 사용하면 안 된다.
@Entity(name = "Member") // 기본값 Member
public class Member {
...
}
속성 정리
속성 | 기능 | 기본값 |
name | JPA에서 사용할 엔티티의 이름을 지정한다. | class 이름을 그대로 사용한다. (ex. Member) - 같은 class 이름이 없으면 가급적 기본값을 사용한다. |
@Table
@Table 애노테이션으로 엔티티와 매핑할 테이블을 지정할 수 있다.
@Table(name = "MBR") // Member 객체를 MBR 테이블과 매핑
public class Member {
...
}
속성 정리
속성 | 기능 | 기본값 |
name | 매핑할 테이블 이름을 지정한다. | 엔티티 이름을 그대로 사용한다. |
catalog | DB catalog 매핑 시 적용된다. | |
schema | DB schema 매핑 시 적용된다. | |
uniqueConstraints(DDL) | DDL 생성 시 유니크 제약 조건을 생성한다. |
2. 데이터베이스 스키마 자동 생성
JPA에선 애플리케이션 실행 시점에 DDL(CREATE, ALTER, DROP 등)을 자동으로 생성한다.
- 여기서 DDL은 Data Definition Language의 약자로, 테이블을 생성하고 삭제하는 등의 SQL을 말한다.
- 객체를 만들고 매핑을 해두면, 애플리케이션을 쓸 때 필요한 테이블을 알아서 만들어 준다.
- DDL을 생성할 때, 미리 설정해 둔 데이터베이스 방언(Dialect)을 활용해서 해당 데이터베이스에 맞는 적절한 DDL을 만들어 준다.
- 이렇게 생성된 DDL은 개발 장비에서만 사용해야 한다. 운영 서버에서는 사용하지 않거나, 적절히 다듬어서 사용하는 경우가 많다.
hibernate.hbm2ddl.auto
속성 정리
옵션 | 설명 |
create | 기존 테이블이 있다면 삭제하고 다시 생성한다. (DROP + CREATE) |
create-drop | create와 같으나 종료 시점에 테이블을 DROP한다. 보통 테스트할 때 사용한다. |
update | 변경(추가)된 내용만 반영한다. (운영 DB에선 사용하면 안 된다.) |
validate | 엔티티와 테이블이 정상 매핑됐는지만 확인한다. |
none | DDL 자동 생성 기능을 사용하지 않는다. |
운영 장비에서는 절대 create, create-drop, update를 사용하면 안 된다. 이렇게 설정하면 데이터가 날아가거나 심한 경우 데이터에 락(Lock)이 걸려 시스템이 중단될 수도 있다.
- 개발 초기 단계에선 create 또는 update를 주로 사용한다.
- 테스트 서버에선 update 또는 validate를 주로 사용한다.
- 스테이징과 운영 서버는 validate 또는 none을 주로 사용한다.
DDL 생성 기능에서 제약 조건을 추가할 수 있다. 이때, 애플리케이션엔 영향을 주지 않고, DB에만 영향을 준다. DDL을 자동으로 생성할 때만 사용되고, JPA의 실행 로직에는 영향을 주지 않는다는 뜻이다.
- ex. 회원 이름은 필수이고, 10자를 넘으면 안 된다.
- @Column(nullable = false, length = 10)
- ex. 유니크 제약 조건 추가
- @Table(uniqueConstraints = {name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
3. 필드와 컬럼 매핑
엔티티와 테이블은 매핑하기 어렵지 않지만, 필드와 컬럼은 요구사항에 따라 다양한 필드를 생성하고 컬럼과 매핑해야 한다. 아래 예시를 보면서 하나씩 알아보자.
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name")
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
...
}
매핑 애노테이션 정리 → hibernate.hbm2ddl.auto
애노테이션 | 설명 |
@Column | 컬럼 매핑 |
@Temporal | 날짜 타입 매핑 |
@Enumerated | enum 타입 매핑 |
@Lob | BLOB, CLOB 매핑 |
@Transient | 특정 필드를 컬럼에 매핑하지 않음 (매핑 무시) |
@Column
컬럼을 매핑할 때 사용한다.
속성 | 설명 | 기본값 |
name | 필드와 매핑할 테이블의 컬럼 이름 | 객체의 필드 이름 |
insertable, updatable | 등록 및 변경 가능 여부 | TRUE |
nullable (DDL) | null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시 not null 제약 조건이 붙는다. | |
unique (DDL) | @Table의 uniqueConstraints와 같지만, 한 컬럼에 간단히 유니크 제약 조건을 걸 때 사용한다. 제약 조건 이름이 랜덤으로 나와 활용하기가 어렵기 때문에 잘 쓰지 않는다. |
|
columnDefinition (DDL) | DB 컬럼 정보를 직접 줄 수 있다. ex. varchar(100) default 'EMPTY' |
필드의 자바 타입과 방언 정보 사용 |
length (DDL) | 문자 길이 제약 조건으로, String 타입에만 사용한다. | 255 |
precision, scale (DDL) | BigDecimal과 BigInteger 타입에서 사용한다. precision은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수를 의미한다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다뤄야 할 때만 사용한다. |
precision=19. scale=2 |
@Temporal
날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.
속성 | 설명 | 기본값 |
value | TemporalType.DATE: 날짜, DB date 타입과 매핑 (ex. 2014-10-22) TemporalType.TIME: 시간, DB time 타입과 매핑 (ex. 11:34:00) TemporalType.TIMESTAMP: 날짜와 시간, DB timestamp 타입과 매핑 (ex. 2023-10-22 22:44:10) |
참고
LocalDate, LocalDateTime을 사용할 때는 생략할 수 있다. (최신 하이버네이트에서 지원하는 기능이다.)
@Enumerated
자바 enum 타입을 매핑할 때 사용한다. DB에는 enum 타입이 따로 존재하지 않는다.
- 기본값인 ORDINAL 대신 STRING을 꼭! 사용하자.
- ORDINAL을 사용하면, 2과 3 사이에 값이 추가될 경우 뒤에 있는 순서도 하나씩 밀리게 된다. 이때 DB에 이미 저장된 값들엔 반영되지 않기 때문에 값이 꼬여 장애가 발생하게 된다.
속성 | 설명 | 기본값 |
value | EnumType.ORDINAL: enum 순서(0, 1, 2, ...)를 DB에 저장 EnumType.STRING: enum 이름을 DB에 저장 |
EnumType.ORDINAL |
@Lob
DB의 BLOB, CLOB 타입과 매핑할 때 사용한다.
- @Lob에는 지정할 수 있는 속성이 없다.
- 매핑하는 필드 타입이 문자면 CLOB으로, 나머지는 BLOB으로 매핑한다.
- CLOB: String, char[], java.sql.CLOB
- BLOB: byte[], java.sql.BLOB
@Transient
특정 필드를 컬럼에 매핑하지 않을 때 사용한다. 따라서 DB에 저장되지 않으므로 조회도 불가능하다.
- 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용한다.
4. 기본 키 매핑
권장하는 식별자 전략
- 기본 키 제약 조건: not null, unique, 변화 X
- 미래까지 이 모든 조건을 만족하는 자연 키(비즈니스와 관련 있는 키)는 찾기 어렵다. 따라서 대리키(대체키)를 주로 사용한다. ex. 주민등록번호 사용 X
- 권장: Long 형(10억 넘어도 동작) + 대체키(UUID 등) + 키 생성 전략(IDENTITY, SEQUENCE) 사용
@Id - 직접 할당
아래처럼 @Id 애노테이션만 사용하면 PK를 직접 할당해야 한다.
@Id
private Long id;
@GeneratedValue - 자동 생성
@Id와 @GeneratedValue 애노테이션을 같이 사용하면 PK를 자동으로 생성할 수 있다. strategy 속성으로 들어갈 수 있는 값은 아래와 같다. 하나씩 알아보자.
- IDENTITY: 데이터베이스에 위임 → MySQL
- SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용 → ORACLE
- TABLE: 키 생성용 테이블 사용 →모든 DB에서 사용
- AUTO: 설정한 데이터베이스 방언에 따라 자동 지정 (기본값)
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
a. IDENTITY 전략
PK 생성을 DB에 위임한다.
- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. ex. MySQL의 AUTO_INCREMENT
- JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행한다. AUTO_INCREMENT는 DB에 INSERT SQL을 실행한 이후에 ID 값을 확인할 수 있다.
- 이 전략을 사용하면 em.persist() 시점에 즉시 INSERT SQL을 실행(id를 null로 전달)하고 DB에서 식별자를 바로 조회(PK 자동 생성)할 수 있다. 따라서 쓰기 지연 SQL 저장소를 사용하지 않는다는 단점이 있다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
b. SEQUENCE 전략
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 DB 오브젝트다.
- Oracle, PostgreSQL, DB2, H2 데이터베이스에서 사용한다. ex. 오라클 시퀀스
- @SequenceGenerator 애노테이션을 적용하면, 테이블마다 다른 시퀀스를 만들어 지정할 수 있다.
- em.persist()를 하기 전, next call을 통해 DB 시퀀스에서 다음 PK 값을 가져온다. 따라서 쓰기 지연 SQL 저장소를 사용할 수 있다.
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", // 매핑할 DB 시퀀스 이름
initialValue = 1,
allocationSize = 1
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
@SequenceGenerator에 들어갈 수 있는 속성은 아래와 같다. allocationSize 속성의 기본값은 50이다. 주의하자.
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | DB에 등록된 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용되며, 시퀀스 DDL을 생성할 때 처음 시작할 수를 지정한다. | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수로, 성능 최적화에 사용된다. DB 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다. |
50 |
catalog, schema | DB catalog, schema 이름 |
allocationSize 속성으로 성능을 최적화하는 방법에 대해 알아보자. @SequenceGenerator에서 allocationSize를 50으로 설정해두면, DB 시퀀스에서 미리 1부터 50까지의 값을 가져와 메모리에 올려놓고 em.persist()를 호출할 때마다 메모리에서 1개씩 사용할 수 있다. 따라서 50번까진 DB를 한 번만 호출해도 되기 때문에 성능이 향상되는 것이다.
- 아래 @TableGenerator에서도 동일하다.
try {
Member m1 = new Member();
m1.setUsername("A");
Member m2 = new Member();
m2.setUsername("B");
Member m3 = new Member();
m3. setUsername("C");
em.persist(m1); // DB SEQ = 1 | application 1 -> DB 50개 호출
em.persist(m2); // DB SEQ = 51 | application 2 -> 메모리에서 다음 번호 가져오기
em.persist(m3); // DB SEQ = 51 | application 3 -> 메모리에서 다음 번호 가져오기
tx.commit();
} catch {...}
c. TABLE 전략
키 전용 테이블을 하나 만들어서 DB 시퀀스를 흉내 낸 전략이다. 모든 데이터베이스에 적용할 수 있지만, 테이블을 직접 사용하기 때문에 성능이 떨어진다는 단점이 있다.
- @TableGenerator 애노테이션을 적용하면, 테이블마다 다른 시퀀스를 만들어 지정할 수 있다.
- 실무에서 잘 사용하지 않는다.
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCE",
pkColumnValue = "MEMBER_SEQ",
allocationSize = 1
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
@TableGenerator에 들어갈 수 있는 속성은 아래와 같다.
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
table | 키를 생성할 테이블 이름 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값을 기준으로 한다. | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수로 성능 최적화에 사용된다. | 50 |
catalog, schema | DB catalog, schema 이름 | |
uniqueConstraints (DDL) | 유니크 제약 조건을 설정할 수 있다. |