본 글은 인프런 김영한님의 JPA 로드맵을 기반으로 정리했습니다.
엔티티는 DB의 테이블과 매핑되는 클래스다.
엔티티에 클래스에 붙이는 @Entity
애노테이션을 알아보자.
@Entity
애노테이션을 클래스에 필수적으로 붙여야 JPA가 해당 클래스를 엔티티로 인식하고 관리할 수 있게된다. 엔티티 클래스를 작성할 때 주의사항이 몇 가지 있다.
기본 생성자 필수 (접근 제한은 public 또는 protected)
final 클래스, enum, interface, inner 클래스 사용 불가
테이블의 컬럼과 매핑되는 필드에 final 사용 불가
@Entity
에는 속성으로 name
값을 줄 수 있다. JPA에서 사용하는 엔티티 이름을 지정하는 속성이다. 기본값은 클래스 이름을 그대로 사용한다. 특별한 이유가 없다면 기본값을 사용하면 된다.
엔티티 클래스에 붙이는 @Table
애노테이션을 알아보자.
@Table
애노테이션을 통해 엔티티와 매핑되는 테이블을 지정할 수 있다. 기본값으론 엔티티와 이름이 같은 테이블에 매핑된다. 엔티티와 이름이 다른 테이블에 매핑해야한다면 name
속성을 지정해주면된다.
@Table
애노테이션에서 uniqueConstraints
속성을 통해 DDL 생성 시에 유니크 제약 조건을 생성할 수 있다. 밑에서 알아보겠지만 DDL 생성 기능은 DDL만 생성할 뿐 JPA의 동작과정에는 아무런 영향을 주지 않는다. 그럼에도 uniqueConstraints
속성을 통해 테이블에 걸려있는 유니크 제약 조건을 명시해주는 것이 유지보수하기에 좋다.
@Table(
uniqueConstraints = {@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
이 외에도 catalog
, schema
속성이 있다. 각각 데이터베이스의 catalog, schema 와 매핑된다.
JPA는 애플리케이션 실행 시점에 DDL을 자동 생성해주는 기능을 지원한다. 현재 사용중인 데이터베이스 방언에 따라 적절한 DDL을 생성한다. DDL 생성 기능은 DDL 생성만 해줄 뿐이고 JPA의 동작에는 영향을 주지 않는다.
자동 생성된 DD은 한계가 있다. 컬럼의 데이터 타입이 적절하지 않고, 유니크 제약 조건의 인덱스 이름과 외래키 제약 조건의 인덱스 이름이 직관적이지 못하다. JPA가 자동 생성한 DDL을 운영서버에서 그대로 사용하면 안 된다. 적절히 다듬은 후 사용해야 한다.
DDL 자동 생성 기능은 hibernate.hbm2ddl.auto
속성에 의해 여러 형태로 동작한다.
create: 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
create-drop: 종료 시점에 테이블 삭제 (CREATE + DROP)
update: 변경분만 반영
validate: 엔티티와 테이블이 정상 매핑되었는지만 확인
none: 사용하지 않음
create, create-drop, update 속성은 실제 DB의 스키마가 변경되므로 운영 환경에서 절대 사용하면 안 된다. create, create-drop. update는 개발 초기 단계에만 사용하고 이후에는 validate, none을 사용하는 것을 권장한다.
객체의 필드는 DB 테이블의 컬럼과 매핑된다. 필드는 다양한 타입을 가질 수 있기 때문에 예제를 통해 하나씩 알아보자.
@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;
}
@Id
애노테이션은 필드와 테이블의 기본키를 매핑하는 애노테이션이다. 기본키 매핑은 밑에서 따로 알아본다.
@Column
@Column(name= "name")
을 통해 문자열 타입의 username 필드를 name 컬럼으로 매핑했다. @Column
은 필드와 컬럼을 매핑하는 애노테이션이다.
기본적으로 필드는 이름이 동일한 컬럼과 매핑이 된다. 이름이 다른 컬럼과 매핑하고 싶다면 name
속성을 통해 컬럼의 이름을 지정해주면 된다. DDL 자동 생성과 관련된 속성들은 DDL 생성만 해주고 JPA의 동작 과정에는 영향을 주지 않는다. 그럼에도 최대한 속성들을 활용하여 엔티티의 특징을 명시적으로 드러내는 것이 유지보수하기에 좋다.
참고로 관례상 자바는 주로 camel-case를 사용하고 DB는 주로 snake-case를 사용하기 때문에 스프링 부트는 JPA를 사용하는 경우 camel-case 필드를 snake-case 컬럼과 매핑해준다.
@Enumerated
RoleType은 자바의 enum타입이다. enum타입은 @Enumerated
애노테이션을 통해 컬럼과 매핑한다.
@Enumerated
는 속성이 하나밖에 없는 간단한 애노테이션이다. 그러나 주의할 것이 한 가지 있다. 속성 기본값인 EnumType.ORDINAL
을 사용하면 안 된다. EnumType.ORDINAL
은 enum의 순서 즉, 숫자값을 데이터베이스 저장한다. 숫자값은 enum의 어떤 객체와 매핑되는지 명시적이지 않을 뿐더러 enum 중간에 객체가 추가되면 순서가 바뀌어 버린다. enum타입은 항상 EnumType.STRING
속성을 통해 이름을 저장하자.
@Temporal
@Temporal
은 자바8의 이전에 사용되던 java.util.Date
, java.util.Calendar
타입의 필드를 매핑한다.
최신 하이버네이트를 쓴다면 자바8의 LocalDate, LocalDateTime 을 매핑할 때 생략 가능하다.
@Lob
필드를 데이터베이스의 BLOB, CLOB 타입과 매핑한다. 지정할 수 있는 속성이 따로 없다. 필드의 속성에 따라 BLOB이나 CLOB에 매핑된다.
CLOB: String, char[], jajva.sql.CLOB
BLOB: byte[], java.sql.BLOLB
@Transient
엔티티의 필드를 컬럼과 매핑하고 싶지 않을 때 사용한다. 데이터베이스에 저장되지도 않고 조회할 수도 없다. 주로 메모리상에서만 어떤 값을 보관하고 싶을 때 사용한다.
테이블은 기본키를 필수적으로 가진다. 엔티티에서 기본키는 @Id
애노테이션을 통해 매핑한다. 기본키를 직접 생성하는 경우에는 @Id
애노테이션만 사용하지만 데이터베이스가 자동 생성해주는 기본키를 사용하는 경우가 대부분이므로 보통 @GenerateValue
애노테이션과 함께 사용하게 된다.
@GeneratedValue
애노테이션의 속성을 통해 기본키 생성 전략을 정할 수 있다.
IDENTITY: 기본키 생성을 데이터베이스에 위임, MySQL 방식
SEQUENCE: 데이터베이스의 시퀀스 오브젝트 사용, ORACLE 방식, @SequenceGenerator
필요
TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용 가능, @TableGenerator
필요
AUTO: 데이터베이스 방언에 따라 자동 지정, 기본값
기본키 생성 전략을 데이터베이스에 위임하는 전략이다. MySQL, PostGresSQL, SQL Server, DB2 에서 주로 사용하는 전략이다. MySQL의 AUTO_INCREMENT 를 생각하면 된다.
영속성 컨텍스트의 1차 캐시는 엔티티를 기본키로 매핑하고 관리한다. 그리고 JPA 트랜잭션 커밋 시점까지 INSERT 쿼리를 버퍼링한다. 그러나, IDENTITY 전략을 쓰면 일단 DB에 INSERT해야 ID값을 얻어올 수 있기 때문에 엔티티를 영속화하는 시점에 즉시 INSERT 쿼리를 실행하고 DB에서 식별자 값을 얻어온다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 데이터베이스 오브젝트다. Oracle, PostGresSQL, DB2, H2 에서 주로 사용하는 전략이다. @SequenceGenerator
를 통해 사용할 시퀀스를 지정해줘야 한다. @SequenceGenerator
는 다음과 같은 속성들을 가진다.
@Entity
@SequenceGenerator(
name = “MEMBER_SEQ_GENERATOR",
sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
시퀀스 오브젝트가 없는 데이터베이스에서 테이블을 통해 시퀀스를 흉내내는 전략이다. 모든 데이터베이스 적용 가능하긴 하지만 성능때문에 거의 사용되지 않는다. 예제는 생략한다.
기본키 제약조건은 not null, unique 이다. 즉, 중복되면 안 되고 null값도 허용하지 않는다. 자연키(비즈니스에 의미있는 키)로는 이 조건을 충족하기 어렵다. 설령 지금 조건을 충족했다 하더라도 미래에 기본키 제약조건이 깨질 수 있다. 권장하는 방법은 기본키로 비즈니스에 의미 없는 대리키를 사용하는 것이다. 결론적으로, Long 타입의 필드를 데이터베이스에 따라 IDENTITY, SEQUENCE 전략으로 기본키에 매핑하는 것을 권장한다.
조금 더 복잡한 예제를 통해 엔티티 매핑에 익숙해져보자. 요구사항은 다음과 같다.
회원은 상품을 여러번 주문할 수 있다.
주문 한 번당 여러 상품을 선택할 수 있다.
요구사항에 따라 설계한 도메인 모델은 다음과 같다. 주문이 여러 상품을 포함하고 상품도 여러 주분에 포함되기 때문에 N:M 관계를 1:N, N:1 관계로 풀어냈다.
테이블 설계는 다음과 같다.
테이블에 매핑되는 엔티티 설계는 다음과 같다.
엔티티 설계를 코드로 옮겨보자.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
}
@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;
}
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
}
@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;
private int orderPrice;
private int count;
}
이 엔티티 설계는 테이블 설계에 종속적이다. 즉, 객체지향적이지 않은 설계다. 객체지향 세계의 컴포지션과 어울리지 않는 DB FK를 필드로 가지고 있다. FK로는 객체 그래프 탐색이 불가능하다. 다음 글에서 엔티티간 연관관계를 맺는 올바른 방법을 알아보겠다.