[jpa] 엔티티 매핑

@Entity

JPA가 관리하는 엔티티가 되기 위해서 필수로 붙여야 하는 어노테이션이다.

속성 기능 기본값
name JPA에서 사용할 엔티티 이름 지정. 중복되는 이름이 있어선 안된다. 클래스 이름 그대로 사용(e.g. Member)

(name은 나중에 JPQL 작성할 때 사용된다.)

엔티티가 될 클래스에는 몇가지 룰이 존재한다.

  • 기본 생성자 필수(public or protected)
  • final class, enum, interface, inner class 에는 사용 불가
  • 저장할 필드에 final 사용 불가

이 제약조건은 proxy 패턴, reflection에서 자주 등장하는 용어이다.
proxy 패턴을 사용하려면 대상 클래스를 상속 받아야 하는데, final class, enum, interface, inner class는 상속이 불가능하다.
reflection으로 클래스를 생성할 때, constructor는 대부분 사용하지 않는다.
외부에서는 생성자의 매개변수들이 무엇을 의미하는지 알수가 없기 때문이다.
그러므로 대부분 기본 생성자 + setter를 사용하는데, final은 setter 호출이 불가능하다.

아마 이런 이유일거라고 생각하고, JPA는 결국 proxy 패턴과 reflection을 쓴다는 것을 알 수 있다.
(틀렸을수도 있음)

@Table

엔티티와 매핑할 테이블을 지정할 때 사용한다. 생략하면 엔티티 이름을 테이블 이름으로 사용한다.

속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름
uniqueConstraints(DDL) DDL 생성 시에 unique constraints 만듬. 2개 이상의 복합 unique constrains도 가능

기본 키 매핑

위에서도 언급했듯이 기본키는 필수값이다.
@Id 어노테이션을 사용해서 지정할 수 있다.

1
2
3
4
5
class Member{
@Id
@Column(name="id")
private String id;
}

@Id 적용 가능한 타입은 아래와 같다.

  • 자바 기본형
  • 자바 wrapper형

nullable한 컬럼 때문에 primitive 대신 wrapper를 사용하는 것을 권장(?)한다.
상황에 따라 쓰면되긴 하지만, 알관성을 위해서라도 하나만 쓰는 것이…

  • String
  • java.util.Date, java.sql.Date
  • java.math.BigDecimal, java.math.BigInteger

기본키 생성 전략

엔티티의 기본키를 설정하는 방법에는 크게 직접할당자동생성이 있다.
직접할당은 말 그대로 기본키 값을 애플리케이션에서 직접 할당하는 방법이다.
자동생성의 경우 직접할당과 반대로 데이터베이스에 의존하는 방식이다.

자동생성을 사용히려면 기본키 컬럼에 @GeneratedValue 어노테이션 + 전략(strategy)을 지정해줘야 하며, 전략들은 아래와 같다.

  1. IDENTITY 전략
    MYSQL의 AUTO_INCREMENT와 같다고 보면 된다.
    아래와 같이 선언해주면 된다.
1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

row insert시에 데이터베이스가 자동으로 생성해주는 기본 키 값을 사용하는 방식이다.
이 말인 즉 데이터베이스에 직접 insert 하는 작업이 선행되어야 한다는 뜻이므로,
JPA의 쓰기지연이 동작하지 않는다는 의미이다.

원래라면 insert 1회 + select 1회(저장된 로우의 기본키 값을 얻어오기 위해)로 2번 튱산해야 하는데,
하이버네이트는 JDBC3 부터 추가된 Statement,getGeneratedKeys()(저장과 동시에 생성된 기본 키 값을 얻어오는 메서드)를 사용함으로써 데이터베이스와 한번만 통신한다(최적화)

  1. SEQUENCE 전략
    ORACLE의 SEQUENCE와 같다고 보면 된다.
    기본적으로 SEQUENCE가 생성되어 있어야한다. 그리고 아래와 같이 선언해주면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@SequenceGenerator(
name = "BOARD_SEQ_GENERATOR", // 사용할 sequence 이름
sequenceName = "BOARD_SEQ", // 실제 데이터베이스 sequence 이름
initialValue = 1, allocationSize = 1
)
public class Board{
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "BOARD_SEQ_GENERATOR" // 위의 sequence 이름
)
private Long id;
}

이제 id 식별자 값을 얻어올 때 마다 BOARD_SEQ 시퀀스에서 식별자를 조회해오게 된다.
조회한 식별자를 엔티티에 할당한 후, 영속성 컨텍스트에 저장한다.
이후 flush가 발생하면 엔티티를 데이터베이스에 저장한다.
(identity와 달리 insert를 선행할 필요 없어므르 쓰기지연을 사용할 수 있다)
하지만 결과적으로 보면 데이터베이스와 2번 통신하는 셈이다(select 1회 + insert 1회)
@SequenceGenerator를 통해 생성기를 등록해야 한다.

속성 기능 기본값
name 식별자 생성기 이름 필수
sequenceName 데이터베이스에 등록되어 있는 시퀀스 이름 hibernate_sequence
initialValue DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. 1
allocationSize 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) 50
  • initialValue는 sequence 초기값을 설정할 떄 사용하는 옵션인데, 이말인 즉 sequence도 미리 생성해놓지 않으면 자동 생성 가능하다는 것이다(DDL 자동생성을 on 하면 됨)

  • allocationSize는 시퀀스 한번 호출에 증가하는 수이다.
    default 값이 50인데, 이는 최적화를 위해서이다. 1-50까지의 sequence 값을 한번에 받고 메모리에 저장해서 할당해주다가, 51번째 sequence가 필요할 떄 데이터베이스 sequence에서 51-100의 sequence를 조회해오는 식으로 동작한다.

  1. TABLE 전략
    sequence를 흉내내는 전략이다. table을 하나 만들어 name과 sequence 값을 저장해둔다.
    table을 사용하므로 모든 데이터베이스에서 사용 가능하다. 사용법은 sequence와 거의 동일하다.
    @TableGenerator를 통해 생성기를 등록해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@TableGenerator(
name = "MY_BOARD_SEQ_GENERATOR", // 사용할 table sequence 이름
table = "MY_BOARD_SEQ", // 실제 데이터베이스 table 이름
pkColumnValue = "BOARD_SEQ", allocationSize = 1
)
public class Board{
@Id
@GeneratedValue(
strategy = GenerationType.TABLE,
generator = "MY_BOARD_SEQ_GENERATOR" // 위의 sequence 이름
)
private Long id;
}

기본적으로 테이블은 sequence_name, next_val 의 컬럼을 가진 형태로 생성되고, 로우의 내용은 아래와 같다.
(이름이 맘에 안들면 pkColumnName=XXX, valueColumnName=XXX 의 형태로 지정해주면 된다.)

sequence_name next_val
BOARD_SEQ 2
MEMBER_SEQ 7
PRODUCT_SEQ 50

보다시피 하나의 테이블로 관리하므로, pkColumnValue로 어떤 sequence_name을 사용할지 지정해줘야 한다.

참고로 table key는 양이 많아질수록 성능이 급격히 안좋아지므로, 사용하지 않는 것이 좋다.
https://vladmihalcea.com/why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate/

  1. AUTO 전략
    선택한 데이터베이스 dialect에 따라 전략(IDENTITY, SEQUENCE, TABLE)을 자동으로 선택해주는 방식이다.
    예를 들면 오라클은 SEQUENCE, MYSQL은 IDENTITY가 선택된다.
    @GeneratedValue의 기본값은 AUTO이다.

auto_increment랑 sequence를 양쪽 다 지원하는 데이터베이스가 있을 경우(PostgreSQL)
sequence를 우선적으로 선택한다고는 하는데, db마다 다를 수 있을 듯 하다.

참고로 @GeneratedValue를 사용한 Id 컬럼에 대해서는 wrapper형을 써주는 것이 좋다.
primitive 타입의 경우 초기화 하지 않을 경우 값이 0인데, 이는 명시적이지 않기 때문이다.
(0으로 세팅한건지, 값이 세팅하지 않은건지 모호함)
물론 기본 auto_increment가 1부터 시작하기 때문에 0일 경우 ORM이 id를 generate 해줘야겠다고 판단하겠지만, 그래도 명시적인게 좋다고 생각한다.
엔티티 모델링에서 boxing/unboxing 비용은 큰 관심사가 아니다.

식별자 권장 전략

기본키의 형태는 크게 자연키(비즈니스 의미가 있는 키. e.g. 주민등록번호)
와 대리키(임의로 만들어진 키)가 있는데, 외부풍파에 쉽게 흔들리지 않는 대리키를 사용하는 것아 좋다.
비즈니스라는 것은 내 생각보다 훨씬 쉽게 변하기 때문이다.

필드 매핑

@Column

객체를 필드 테이블에 매핑할 때 사용한다. 가장 많이 사용된다.

속성 기능 기본값
name 필드와 테이블 이름 매핑 객체 필드 이름
nullable(DDL) null 값 허용 여부. false 설정하면 DDL 생성 시 not null이 붙는다 true
unique(DDL) 컬럼 하나에 unique constraints 지정할 때 사용. 여러개 지정하려면 @Table의 uniqueConstraints를 사용해야 함
length(DDL) 문자 길이 제약조건. 명시적으로 길이를 볼 수 있는 장점도 있다 255

@Column을 생략해도 엔티티의 필드는 전부 자동으로 테이블과 매핑된다.
이 때 몇가지 특징이 있다.

  1. 이름은 어떻게 매핑되는가?
    @Column의 기본값과 동일하게 컬럼명으로 사용된다. 근데 여기서 딜레마가 하나 있다.
    java는 naming을 관례적으로 camel case를 사용하고, database는 naming을 관례적으로 under score를 사용한다는 것이다.
    이떄 persistence.xml에
1
<property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy"></property>

전략을 주게 되면 위의 딜레마를 해결 가능하다(서로간에 자동 변환)

  1. nullable 속성
1
2
3
4
5
6
int id; // not null로 생성됨. primitive에는 null이 들어갈 수 없기 때문.

Integer id; // nullable true로 생성됨

@Column
int id; // @Column의 기본값인 nullable=true가 적용되서 nullable=true로 생성됨. 주의해야함

보다시피 3번쨰 방법은 nullable=true 임에도 불구하고 null을 넣을 수 없다.
이런 상황을 위해 그냥 primitive 대신 wrapper형을 써주는 것이 좋다.
(not null에는 primitive, nullable에는 wrapper형을 쓸수도 있지만 통일시키지 않아서 오는 불편함이 더 클것이다)

@Enumerated

java의 enum 타입을 매핑할 때 사용된다. 유용하게 사용 가능하다.

1
2
@Enumerated(EnumType.STRING)
private RoleType roleType;

이렇게 주면 enum의 값 그대로(문자열) 데이터베이스에 저장된다.
ORDINAL을 속성을 사용하면 enum의 순서대로 index가 데이터베이스에 저장되는데,
유연하지 못하므로 STRING 속성을 사용하는 것이 낫다.

@Temporal

날짜 타입을 매핑할 때 사용된다.

1
2
3
4
5
6
7
8
@Temporal(TemporalType.DATE)
private Date date; // date date 생성

@Temporal(TemporalType.TIME)
private Date time; // time time 생성

@Temporal(TemporalType.TIMESTAMP)
private Date timestamp; // timestamp timestamp 생성

자바의 Date 타입에는 년월일 시분초가 있지만, 데이터베이스에서는 date, time, datetime 3가지 타입이 존재한다.
그러므로 @Temporal을 생략하였을 시, 가장 비슷한 timestamp가 지정된다.
@TemporalDate, Calendar에만 붙이는 속성이고,
java8 부터 등장한 LocalDate, LocalTime, LocalDateTime에는 @Temporal 속성을 붙일 수 없다.

java8 날짜 타입들은 jpa가 바로 인식하지 못하므로 추가적인 조치가 필요하다
https://homoefficio.github.io/2016/11/19/Spring-Data-JPA-에서-Java8-Date-Time-JSR-310-사용하기/

@Lob

BLOB, CLOB 타입에 매핑된다.

1
2
3
4
5
@Lob
String lob; // CLOB으로 매핑. mysql에선 longtext로 생성됨

@Lob
byte[] lob; // BLOB으로 매핑. mysql에선 longblob으로 생성됨

@Transient

매핑하지 않을 필드에 설정한다. 임의로 값을 보관하고 싶을때 등에 사용한다.

@Access

JPA가 엔티티 데이터에 접근하는 방식을 지정한다.
필드 접근: AccessType.FIELD로 지정한다. 필드에 직접 접근한다. 접근 권한이 private이어도 접근할 수 있다.
프로퍼티 접근: AccessType.PROPERTY로 지정한다. Getter를 사용한다.
설정하지 않으면 @id의 위치를 기준으로 접근 방식이 설정된다.

1
2
3
4
5
6
7
8
9
@Entity
@Access(AccessType.FIELD)
public class Member{
@Id
private String id;

private String data1;
private String data2;
}

@id가 필드에 있으므로 @access(AccessType.FIELD)로 설정한 것과 같다. @access 생략가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Access(AccessType.PROPERTY)
public class Member{
private String id;

private String data1;
private String data2;

@Id
public String getId(){
return id;
}

@Column
public String getData1(){
return data1;
}

public String getData2(){
return data2;
}
}

@id가 프로퍼티에 있으므로, @access 생략가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Member{
@Id
private String id;

@Transient
private String firstName;

@Transient
private String lastName;

private String fullName;

@Access(AccessType.PROPERTY)
public String getFullName(){
return firstName + lastName;
}

@id가 필드에 있으므로 기본은 필드 접근 방식 사용, @getFullName()만 프로퍼티 접근방식을 사용한다.
결과적으로 회원 엔티티를 저장하면 회원 테이블의 FULLNAME 컬럼에 firstName + lastName 결과가 저장된다.

@Access를 사용하는 이유
https://stackoverflow.com/questions/13874528/what-is-the-purpose-of-accesstype-field-accesstype-property-and-access