객체와 테이블 매핑
@Entity
- @Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
- JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
주의사항
- 기본 생성자 필수(파라미터가 없는 public or protected)
- 내부적으로 동적으로 객체를 관리할 일이 많기 때문에 이때 기본 생성자가 사용된다.(ex. 리플렉션)
- final 클래스, enum, interface, inner 클래스 사용 x
- 저장할 필드에 final 사용 X
속성
- name
- JPA에서 사용할 엔티티 이름 지정.
- 기본값 클래스 이름을 그대로 사용(예: Member)
- 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
final class는 JPA Entity Class가 될 수 없다. 왜 그럴까?
JPA는 DB에서 데이터를 조회한 후 엔티티를 생성할 때 지연 로딩이라는 방식을 사용한다.
지연 로딩이란?
해당 엔티티(테이블)와 관계(join)를 맺고 있는 엔티티(테이블)들에 대한 정보는 그 즉시 로딩되지 않고 getter 메소드가 호출되는 등 실제 사용될 때 로딩된다. 이러한 방식을 지연 로딩이라 한다.
지연 로딩 방식을 이용해 데이터를 조회하기 위해서, JPA는 프록시 객체라는 것을 생성한다. 프록시 객체는 간단히 말해서 엔티티를 상속해서 확장한 클래스이다. 하지만 final class는 상속될 수 없기 때문에 JPA는 final class를 확장해서 프록시 객체로 사용할 수가 없다. 따라서 JPA Entity로 사용하고 싶은 클래스는 final class가 아니어야 한다.
final field가 포함된 class 역시 JPA의 Entity가 될 수 없다. 왜?
Entity class는 JPA에 의해 프록시 객체로 확장된다. 그리고 JPA는 이 프록시 객체를 생성할 때 Reflection API를 이용한다. Reflection으로 객체를 생성하기 위해서는 그 객체가 기본 생성자를 가지고 있어야 한다.
그리고 이렇게 생성된 프록시 객체의 필드들을 초기화 하기 위해 setter를 사용한다. 하지만 final 필드는 setter를 이용해서 초기화할 수 없다. final 필드는 클래스 로딩 시점에 초기화 되거나 생성자를 이용해서만 초기화될 수 있다. 따라서 JPA의 Entity로 사용할 class는 final 필드를 가질 수 없다.
@Table
@Table은 엔티티와 매핑할 테이블 지정
속성 | 기능 | 기본값 |
name | 매핑할 테이블 이름 | 엔티티 이름 사용 |
catalog | 데이터베이스 catalog 매핑 | |
schema | 데이터베이스 schema 매핑 | |
uniqueConstraint(DDL) | DDL 생성 시 유니크 제약 조건 생성 |
데이터베이스 스키마 자동 생성
- DDL을 애플리케이션 실행 시점에 자동 생성
- 테이블 중심 → 객체 중심
- 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
- 이렇게 생성된 DDL은 개발 장비에서만 사용
- 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용
속성
- hibernate.hbm2ddl.auto
- create: 기존 테이블 삭제 후 다시 생성(DROP + CREATE)
- create-drop: create와 같으나 종료시점에 테이블 DROP
- update: 변경분만 반영(운영 DB에는 사용하면 안됨)
- validate: 엔티티와 테이블이 정상 매핑되었는지만 확인
- none: 사용하지 않음
주의 사항
- 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
- 개발초기 : create or update
- 테스트 서버: update or validate
- 스테이징과 운영서버: validate or none
테스트 및 스테이징, 운영서버에서도 가급적이면 validate와 none 만 사용하는 것을 권장.
update를 통한 alter 구문이 실행되면 그 순간 DB에 Lock이 걸리기 때문에 서비스 장애가 발생할 수도 있다!!
(5분 멈추는 것도 엄청난 장애라...)
필드와 컬럼 매핑
요구사항 추가
- 회원은 일반 회원과 관리자로 구분해야 한다.
- 회원 가입일과 수정일이 있어야 한다.
- 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다
package hellojpa;
import java.util.Date;
import javax.persistence.*;
@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;
public Member(){}
}
어노테이션 | 설명 |
@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) | 데이터베이스 컬럼 정보를 직접 줄 수 있다 . ex) varchar(100) default ‘EMPTY' |
필드의 자바 타입과 방언 정보를 사용해 서 적절한 컬럼 타입 |
length(DDL) | 문자 길이 제약조건, String 타입에만 사용한다. | 255 |
precision, scale(DDL) | BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다). precision은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수 다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정 밀한 소수를 다루어야 할 때만 사용한다. |
precision=19, scale=2 |
※ 참고
unique(DDL)
1. 사용 빈도가 낮다(제약조건의 이름이 너무 난수값이라 알아보기 힘듬)
또한 하나의 column에만 적용되면 복합일 경우 불가능하다.
ex ) alter table Member
add constraint UK_ektea7vp6e3low620iewuxhlq unique (name)
2. @Table(uniqueConstraint())을 사용하여 이름을 지정하는 것을 권장.
@Enumerated
속성 | 설명 | 기본값 |
value | • EnumType.ORDINAL: enum 순서를 데이터베이스에 저장 • EnumType.STRING: enum 이름을 데이터베이스에 저장 |
EnumType.ORDINAL |
- 자바 Enum 타입을 매핑할 때 사용
- ORDINAL 타입을 사용하지 말자.
→ enum타입이 추가, 변경, 삭제 되어 순서가 달라질 경우 side-effect가 발생할 우려가 있다.
@Temporal
속성 | 설명 | 기본값 |
value | • TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑 (예: 2013–10–11) • TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑 (예: 11:11:11) • TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예: 2013–10–11 11:11:11) |
- 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
- LocalDate(년월), LocalDateTime(년월일)을 사용할 때는 생략 가능(최신 하이버네이트 지원)
@Lob
데이터베이스 BLOB, CLOB 타입과 매핑
- @Lob에는 지정할 수 있는 속성이 없다.
- 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
- CLOB: String, char[], java.sql.CLOB
- BLOB: byte[], java.sql. BLOB
@Transient
- 필드 매핑X
- 데이터베이스에 저장X, 조회X
- 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
@Transient private Integer temp;
기본 키 매핑
직접 할당
- @Id만 사용
자동 생성(@GeneratedValue)
IDENTITY
- 데이터베이스에 위임
- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용
※ 주의 사항
JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 수행한다. 그래서 한 트랜잭션 안에서 영속성 컨텍스트는 이미 @Id(PK) 값을 알고 있기 때문에 엔티티 관리가 가능하다.
그러나 AUTO_INCREMENT는 DB에 INSERT SQL을 실행 한 이후에 ID값을 알 수 있다. 즉, IDENTITY 전략은 예외적으로 커밋 시점이 아닌 em.persist() 시점에 즉시 INSERT SQL 실행 하고 DB에서 식별자를 조회하여 1차 캐시에 등록한다.
그렇기에 IDENTITY 에서는 지연쓰기가 제한된다 .
Member member = new Member();
member.setUsername("C");
System.out.println("===============");
em.persist(member);
System.out.println("===============");
tx.commit();
물론 IDENTITY 전략 외에 직접 할당 혹은 SEQUENCE 와 같은 다른 전략일 경우 이미 @Id 값을 알고 있기 때문에 실제 commit 하는 시점에 INSERT 쿼리가 날라간다.
※ 참고
IDENTITY 전략의 경우 persist() 시점에 INSERT 쿼리가 실행되고 DB에서 식별자를 조회하여 1차 캐시에 등록한다
고 하였다.
그렇다면 DB에서 식별자를 조회하는 select 쿼리는 왜 실행이 안되는 것일까??
이런 경우 JDBC 드라이버 내에 insert 쿼리를 했을 경우 바로 식별자를 반환받는 로직이 이미 짜여있기 때문에 SELECT 쿼리가 실행되지 않는 것이다.
※ 참고
IDENTITY 전략일 경우 쓰기 지연이 제한되는데 성능에 큰 문제가 있을까?
사실 버퍼링에서 쓰기를 하는 것이 그렇게 큰 이점이 있지는 않다. 트랜잭션 자체를 여러 트랜잭션으로 계속 나누는 것은 성능에 큰 문제가 있지만 한 트랜잭션 안에서만 INSERT 쿼리가 여러 번 실행돼서 네트워크를 탄다고해서 그렇게 비약적으로 성능에 영향을 주지 않는다.
SEQUENCE
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트 사용(예: 오라클 시퀀스)
- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
- @SequenceGenerator 필요
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Member2 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
@Column(name = "MEMBER_ID")
private Long id;
...
}
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 1 시작하는 수를 지정한다. | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값 을 반드시 1로 설정해야 한다 |
50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
Member member = new Member();
member.setUsername("C");
System.out.println("===============");
em.persist(member);
System.out.println("member.id = " + member.getId());
System.out.println("===============");
tx.commit();
위 코드를 실행하면 처음에 다음과 같은 쿼리가 실행된다.
MEMBER_SEQ 라는 시작 1부터 1씩 증가시키는 시퀀스 오브젝트를 만들라는 쿼리이다. 보통 시퀀스 오브젝트는 1부터 1씩 증가시키는 전략을 Default로 하기 때문에 이런 옵션을 간과했을 가능성이 있다.
이때 JPA의 경우 Member 객체를 생성하고 Member 객체를 영속성 컨텍스트에서 관리하도록 하기 위해 em.persist()를 실행하기 위해서는 Member의 Id(PK) 값을 할당해야 한다. 그러면 이때 SEQUENCE 전략에 의해 DB에서 관리하고 있는 시퀀스 오브젝트로부터 PK 값을 가져온다.
그래서 이후에 우리가 정의한 MEMBER_SEQ에 아래 쿼리가 실행되는 것이다.
DB한테 MEMBER_SEQ 의 다음 PK 값을 내놓으라는 뜻이다. 이렇게 얻은 PK 값을 영속성 컨텍스트에 엔티티를 넣을려는 시점인 em.persist() 할 때 member에 Id 값을 할당하고 그 다음에 영속성 컨텍스트에 저장한다.
이러한 과정에 의해 SEQUENCE 전략은 영속성 컨텍스트가 엔티티를 관리하도록 할 수 있고 커밋 시점에 INSERT 쿼리가 실행되는 쓰기 지연이 가능한 것이다.
고급 성능 개선 : 기본키 생성 전략이 SEQUENCE인 경우
그런데 여기서 의문점이 들 수 있다. 이런 과정도 결국 시퀀스를 얻느라 한 번, 엔티티를 저장한다고 한 번... 잦은 네트워크 통신이 발생하는데 성능에 문제는 없을까??
아래 그림을 보면 allocationSize의 기본값이 50인 것을 확인할 수 있을 것이다. 굳이 왜 50을 기본값으로 해뒀을까?
우선 결론은 (최적화 방안(allocationSize) : 미리 인자값 갯수만큼 가져와서 사용한다
이는 allocationSize를 50으로 해두면 DB에는 미리 50개 size만큼 시퀀스를 올려두고 애플리케이션 단에서 next call을 할때마다 그 다음 50개의 size 만큼 시퀀스를 미리 올려둔다는 것이다.
즉, initialValue가 1이면 DB에는 미리 51번까지 시퀀스 오브젝트를 세팅해두고 애플리케이션 메모리에서는 1씩 사용하는 것이다. 그러고 애플리케이션 시퀀스가 50이되면 next call을 호출하여 DB의 시퀀스는 101번으로 올려두고 애플리케이션에는 그 다음 51부터 메모리에서 사용한다.
다음 코드를 살펴보자.
Member member1 = new Member();
member1.setUsername("A");
Member member2 = new Member();
member2.setUsername("B");
Member member3 = new Member();
member3.setUsername("C");
System.out.println("===============");
// MEM DB
em.persist(member1); // 1 51
// em.persist(member2); // 2 51
// em.persist(member3); // 3 51
System.out.println("member1 = " + member1.getId());
System.out.println("member2 = " + member2.getId());
System.out.println("member3 = " + member3.getId());
System.out.println("===============");
tx.commit();
실행 경과를 살펴보면 em.persist() 를 한번 실행했는데 call next value는 2번 실행되는 것을 볼 수 있다. 왜 그럴까?
우선 처음 call next value를 실행하면 DB의 시퀀스는 1이고 두 번째 call next value를 실행하면 DB의 시퀀스를 51일 것이다. 이는 처음 call next를 실행할 때 1을 받아왔는데 allocationSize가 50인 것을 확인하고 JPA가 문제가 있다는 판단하에 한번 더 call next를 호출하여 정상적인 범위 값을 가져오는 것이다.
이때 애플리케이션은 시퀀스를 1, 2, 3...정상적으로 메모리에서 사용한다.
※ 참고
이렇게 범위 값을 미리 가져와서 사용할 때 WAS 서버가 여러 대 일 경우에도 동시성 이슈 없이 사용이 가능하다.
Q. 그렇다면 서버1에서 100개의 next value를 가져오고 서버2에서 다음 100개를 가져온 상황에서 서버1의 트랜잭션이 롤백된다면 시퀀스는 어떻게 되는가?
sequence에 구멍생긴다. 그렇다고 이 부분이 문제가 되지는 않는다. 그리고 서버를 다시 시작하는 경우에만 이런 부분들이 발생한다.
그리고 100까지 구멍으로 두는게 더 낫다. 왜냐하면 시퀀스를 100까지 받았다고 하면 100번이라 로깅도 남기고 비즈니스 로직을 수행하게 되는데, 문제가 생겨 롤백이 되었을때 시퀀스도 롤백되면 로그에는 다시 100이 남을 것이고 다음 비즈니스 로직에서 시퀀스를 받을 경우 다시 100번이 된다. 이럴 경우 어디가 정상이고 어디가 비정상 로깅인지 확인이 어려워진다.
추가로 allocationsize 1로 두고 사용도 트래픽이 아주 큰 서비스가 아닌 이상 큰 영향은 없다. 시퀀스를 받아오는 것은 매우 매우 빠르기 때문이다.
※ 참고
아래의 Table 전략에서의 allocationSize도 마찬가지로 위와 같은 성능 개선이 가능하다.
Table
- 키 생성 용 테이블 사용, 모든 DB에서 사용 가능
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
- 모든 데이터 베이스에서 사용할 수 있지만, 성능이 떨어진다.
- @TableGenerator 필요
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnName = "MEMBER_SEQ", allocationSize = 1)
public class Member2 {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
@Column(name = "MEMBER_ID")
private Long id;
...
}
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준이다. | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraint s(DDL) | 유니크 제약 조건을 지정할 수 있다. |
AUTO
- 방언에 따라 자동 지정, 기본값
권장하는 식별자 전략
- 기본 키 제약 조건: not null, unique, not update
- 이 조건을 계속 만족하는 자연키는 찾기 힘들기 때문에 대리키(대체키)를 사용하자.
- 자연키 : 비지니스적으로 의미 있는 키 ex) 주민등록번호, 전화번호 등...
- 예를 들면 주민등록번호도 기본 키로 적절하지 않다. -> 기본 키가 주민등록번호가 되면 연관 매핑을 맺은 다른 테이블에서도 외래키로 주민번호를 사용하기에 여기저기에 개인정보가 퍼지게 된다.
- 그러므로 GenerateValue 혹은 랜덤값과 같은 비지니스랑 전혀 상관없는 키를 사용하는 것을 권장
- 권장: Long형 + 대체키 + 키 생성전략 사용.
(AUTO나 SequenceObject 혹은 회사 내의 룰대로 사용하자)
실전 예제 1 - 요구사항 분석과 기본 매핑
요구사항 분석
- 회원은 상품을 주문할 수 있다.
- 주문 시 여러 종류의 상품을 선택할 수 있다.
기능 목록
- 회원 기능
- 회원등록
- 회원조회
- 상품 기능
- 상품등록
- 상품수정
- 상품조회
- 주문 기능
- 상품 주문
- 주문내역조회
- 주문취소
도메인 모델 분석
- 회원과 주문의 관계: 회원은 여러 번 주문할 수 있다 -> 일대다(1:N)
- 주문과 상품의 관계: 주문 시 여러 상품을 선택할 수 있다. 반대로 상품도 여러 번 주문될 수 있다. → 다대다 관계(N:M)
→ 주문 상품(OrderItem)이라는 모델을 만들어서 일대다(1:N), 다대일(N:1) 관계로 풀어낸다.
테이블 설계
엔티티 설계와 매핑
코드
Member
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipdode;
}
Order
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@Column(name = "MEMBER_ID")
private Long memberId;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
}
Item
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long Id;
private String name;
private int price;
private int stockQuantity;
}
OrderItem
@Entity
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@Column(name="ORDER_ID")
private Long orderId;
@Column(name = "ITEM_ID")
private Long itemId;
}
데이터 중심 설계의 문제점
- 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
- 테이블의 외래키(식별자)를 객체에 그대로 가져옴
- 객체 그래프 탐색이 불가능
- 주문한 사람의 정보를 바로 가져오고 싶지만 식별자를 가져오기 때문에 Member 엔티티를 다시 조회.
- 참조가 없으므로 UML도 잘못됨
- UML 상으로 연관관계가 작성되어 있지만 객체 설계에서 식별자만 갖고 있기 때문에 사실상 연관관계가 끊어짐.