[JPA] Entity 작성
JPA에서 Entity라 함은 테이블에 맵핑되는 비즈니스 도메인 객체이다.
Entity를 통해 DB와 상호 작용을 하게 되며, Entity의 인스턴스는 각 테이블의 Row에 대응되는 개념이다.
이전 포스트부터 따라왔다면 익숙한 Users Entity를 다시 보자.
@Entity
@Data
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long userId;
private String userName;
}
간단한 예시지만 이해를 위해 하나씩 뜯어보자.
@Entity: JPA가 관리하는 Entity라는 것을 알려준다.@Data: lombok 의 기능이며 getter/setter/constructor 등 객체 핸들링에 필요한 설정을 자동으로 해준다.(필수 X)class: Entity 자체는 POJO로 작성하기 때문에 class로 선언된다.@Id: Entity는 DB의 테이블에 대응되는 만큼 기본적으로 기본키를 가져야 하기에 필수라 할 수 있다.(RDB 기준)@GeneratedValue(strategy = GenerationType.IDENTITY): ID 값을 자동으로 생성해준다.
이처럼 기본적으로 위와 같은 구조를 가지게 되는데 순서대로 짚어보면서 자세히 알아보자.
Entity Class 정의
고객센터에 고객과, 고객이 작성하는 문의 사항이 있다 가정했을 때
와 같은 ERD 구조를 가질 수 있을 것이다.
관계 설정은 일단 넘어가고 Member Entity를 먼저 작성해보자.
1. 클래스를 정의한다.
public class Member{
}
2. @Entity를 정의한다.
@Entity
public class Member{
}
Entity의 이름은 기본적으로 class의 이름을 기반으로 자동 설정되지만 @Entity(name="student")와 같이 Entity 이름을 정할 수 있다.
3. @Id를 포함한 필드를 정의한다.
JPA의 Entity는 ID를 필수로 가지고 있어야 한다.
ID에 해당하는 필드에 @Id를 부여하고 추가로 기본키 값을 자동으로 생성해주는 @GeneratedValue 를 정의하자.
@GeneratedValue는 strategy 옵션으로 다양한 자동 생성값을 제공한다.
이에 대해 설명하자면
Auto
- JPA 구현체가 DB에 적합한 기본 키 생성방식을 IDENTITY, SEQUENCE, TABLE 중에서 자동으로 선택.
- 가장 유연한 방식.
@Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id;Hibernate의 버전별로 전략이 달라질 수 있으니 너무 맹신하지는 말자. 기존에는 IDENTITY의 전략을 가져갔지만, Hibernate 5부터는 TABLE(또는 SEQUENCE) 전략을 사용한다고 한다.
IDENTITY
- DB에게 키 생성 권한을 위임한다.
- AUTO_INCREMENT 등과 같은 기능을 사용한다.
- 주로 MySQL, PostgreSQL에서 사용.
- 먼저 DB가 기본 키 값을 자동으로 생성하고 이를 JPA가 가져와서 엔티티에 설정.
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;JPA는 보통 Persistence context에서 객체를 관리하다가 commit 되는 시점에, 쓰기 지연 저장소에 쌓인 쿼리문을 한 번에 DB로 전송하여 실행한다.
하지만 IDENTITY는 EntityManager.persist() 를 호출하여 객체를 영속화하는 시점에 곧바로 INSERT SQL을 실행하여 DB 식별자를 조회하고, 반환받은 ID를 1차 캐시 엔티티에 등록하여 관리한다.
이런 과정을 거쳐야 하는 이유는 키 생성을 DB에 위임하기에, JPA는 INSERT를 수행하기 전까지 DB에서 생성될 기본키를 모르기 때문이다.
SEQUENCE
- DB의 시퀀스를 사용하여 기본키 생성.
- 주로 Oracle, PostgreSQL에서 사용.
@SequenceGenerator를 함께 사용하여 시퀀스 생성기를 정의.@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq") @SequenceGenerator(name = "product_seq", sequenceName = "product_sequence", allocationSize = 1) private Long id;이 또한 IDENTITY와 같이 DB에서 기본키를 생성하기 때문에, EntityManager.persist()가 호출될 때
hibernate: call next value for [table]_PK_SEQ등과 같은 명령어를 실행하여 기본키를 가져온다.
하지만 계속해서 기본키를 조회하는 동작을 수행하면 성능 저하 이슈가 생길 수 있기에, allocationSize를 적절히 조절하여 성능을 신경써주자.
참고로 allocationSize는 시퀀스를 한 번 호출할 때 size만큼의 시퀀스를 미리 할당한다는 의미이며, 기본값으로는 50이 할당되어 있다.
TABLE
- 키 값을 별도의 키 생성 테이블에서 관리할 때 사용.
- DB에 종속되지 않는 독립적인 방식으로 어떤 DB에서도 동일하게 작동한다.
@TableGenerator를 함께 사용하여 키 생성 테이블을 정의.@Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "product_table") @TableGenerator(name = "product_table", table = "key_gen", pkColumnName = "gen_name", valueColumnName = "gen_value", pkColumnValue = "product_id", allocationSize = 1) private Long id;하지만 최적화되어 있지 않은 별도의 테이블을 사용하기에 성능을 보장할 수 없다.
이제 ERD로 작성하였던 Member의 필드들을 작성해보자.
@Entity
@Data
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
private Integer age;
private Date birthDate;
private Gender gender;
}
이 정도만 작성해도 이용할 수 있지만 더욱 상세한 설정을 제공하니 계속 알아보자.
4. @Table
실질적으로 Mapping 되는 Table 명을 지정할 수 있다.
기본적으로는 Entity class와 같은 이름의 테이블을 자동으로 지정하는데, Entity의 클래스와 테이블명이 다를 수 있는 상황을 위해 직접 지정한다.
@Entity
@Data
@Table(name="MEMBER", schema="고객")
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
private Integer age;
private Date birthDate;
private Gender gender;
}
@Table(name="MEMBER", schema="고객") 부분을 클래스 단위로 설정하면 된다.
name 옵션은 Mapping할 Table의 이름을 정의하고, schema는 도메인과 같은 그룹으로 나눴을 때 이를 구분하기 위해 사용한다.
5. @Column
테이블에 있는 컬럼을 직접적으로 표현할 수 있다.
@Entity
@Data
@Table(name="MEMBER", schema="고객")
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "MEMBER_NAME", length = 20, nullable = false, unique = false)
private String name;
private Integer age;
private Date birthDate;
private Gender gender;
}
@Column(name = "MEMBER_NAME", length = 20, nullable = false, unique = false)로 직접 Table Column에 Mapping할 수 있으며 length, nullable, unique, updateable, insertable 등을 설정할 수 있다.
설정하지 않을 시 Entity class 에 설정된 필드의 이름과 같은 column을 맵핑하게 된다.
참고로 DB에서 member_name 과 같이 snake case로 설정되었다면, JPA에서는 memberName 처럼 camel case를 사용하여 표현해야 한다.
6. @Transient
필드를 영속화하고 싶지 않을 때 사용하는 옵션이다.
설정된 필드는 Table의 column에 Mapping 되지 않으며, DB와의 상호작용에서 이 필드를 무시하게 된다.
즉, DB에 저장하고 싶지 않은 경우에 사용하며, 테이블에 정의되지 않은 필드라도 사용 가능하게 해준다.
우리가 정의한 필드 중 age 필드는 birth_date를 기반으로 계산하여 값을 넣는다고 가정했을 때
@Entity
@Data
@Table(name="MEMBER", schema="고객")
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "MEMBER_NAME", length = 20, nullable = false, unique = false,)
private String name;
@Transient
private Integer age;
private Date birthDate;
private Gender gender;
}
와 같이 정의할 수 있겠다.
7. @Temporal
시간 데이터 타입을 정의할 때 사용한다.
@Entity
@Data
@Table(name="MEMBER", schema="고객")
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "MEMBER_NAME", length = 20, nullable = false, unique = false,)
private String name;
@Transient
private Integer age;
@Temporal(TemporalType.DATE)
private Date birthDate;
private Gender gender;
}
TemporalType에는 DATE, TIME, TIMESTAMP의 타입을 지원한다.
Java 8 이상부터는 java.time 패키지의 LocalDate, LocalTime, LocalDateTime의 클래스를 사용하여 날짜와 시간을 처리하는 것이 권장된다.
@Temporal Annotation이 없어도 JPA가 자동으로 적절한 DB 타입과 Mapping 시켜준다.
8. @Enumerated
Java의 enum type을 영속화하고 싶을 때 설정할 수 있는 옵션이다.
아까부터 계속 따라다니는
enum Gender{
Male,
FEMALE
}
를 필드에 Mapping 할 것이다.
@Entity
@Data
@Table(name="MEMBER", schema="고객")
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "MEMBER_NAME", length = 20, nullable = false, unique = false,)
private String name;
@Transient
private Integer age;
@Temporal(TemporalType.DATE)
private Date birthDate;
@Enumerated(EnumType.STRING)
private Gender gender;
}
@Enumerated(EnumType.STRING)로 설정해줬는데 EnumType에는 ORDINAL, STRING이 있다.
ORDINAL이 기본값이며, 이때는 enum의 순서 값이 저장된다.
MALE == 0, FEMALE == 1
즉, FEMALE로 값을 주었다면 1이 DB에 저장된다.
반면 STRING은 MALE, FEMALE의 String 값이 그대로 들어간다.
ORDINAL은 순서가 변경됐을 때 문제가 생길 수 있지만 저장 공간을 절약할 수 있고, STRING은 데이터 변경 시에 일관성을 유지할 수 있게 된다.
이제 ERD에서 정의한 대로 Entity를 다시 작성해보자.
@Entity
@Data
@Table(name="MEMBER", schema="고객")
public class Member {
enum Gender{
Male,
FEMALE
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "MEMBER_NAME", length = 20, nullable = false, unique = false,)
private String name;
@Transient
private Integer age;
@Temporal(TemporalType.DATE)
private Date birthDate;
@Column(length = 10)
@Enumerated(EnumType.STRING)
private Gender gender;
}
------------------------------------
@Entity
@Data
@Table(name = "POSTS", schema="문의 사항")
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(length = 40)
private String title;
@Column(length = 200)
private String content;
@Temporal(TemporalType.DATE)
private Date reg_date;
private Long memberId;
}
여기까지 Entity를 작성하는 방법에 대해 알아보았다.
하지만 ERD에서 정의된 관계 형성이 되지 않았다.
이를 위해서 다음은 연관 관계를 설정하는 방법에 대해 알아보겠다.
Ref.
https://velog.io/@gudnr1451/GeneratedValue-%EC%A0%95%EB%A6%AC