저번에도 언급했던 JPA에서 가장 중요한 2가지를 다시 알아보자.
이번에는 객체와 관계형 데이터베이스 매핑을 설계적인 측면에서 자세히 알아보자.
엔티티 매핑은 크게 4가지가 있다.
하나씩 알아보자.
@Entity
가 붙은 클래스는 JPA가 관리하며 이를 엔티티라 한다. 즉, JPA를 사용해서 테이블과 매핑할 클래스는 반드시 @Entity
를 붙여줘야한다.
final
클래스, enum
, interface
, inner
클래스 사용 X속성은 name이 있다. 이는 JPA에서 사용할 엔티티 이름을 지정한다.
엔티티와 매핑할 테이블을 지정한다.
속성들은 다음과 같다.
create문으로 DB를 생성하고 시작할 수 있지만 그러기 위해선 DB로 가서 create table을 일일히 치고... 해야하는데 매우 귀찮다. JPA는 이러한 귀찮음을 해결해준다.
DDL을 애플리케이션 실행 시점에 자동 생성해준다. 이렇게 생성된 DDL은 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL이다. 이를 통해 테이블 중심 설계를 객체 중심 설계로 바꿀 수 있다.
참고
DDL(Data Definition Language)은 SQL의 서브세트이다. 데이터베이스에서 데이터 및 데이터의 관계를 설명하기 위한 언어이다.
참고
이렇게 생성된 DDL은 개발 장비에서만 사용하자.
생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용하자.
hibernate.hbm2ddl.auto
계속 언급하지만 상황에 따라 다른 것을 사용하되 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
개발 단계별 사용 옵션은 다음과 같다.
회원 이름은 필수, 10자 초과X
@Column(nullable = false, length = 10)
@Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})
중요한 것은 DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
객체와 테이블이 매핑관계 였다면 객체안의 필드들은 테이블의 컬럼으로 매핑이 된다.
객체와 테이블 매핑은 @Entity
, @Table
어노테이션으로 매핑할 수 있었다. 이제, 필드와 컬럼의 매핑을 알아보자.
필드와 컬럼을 매핑하는 어노테이션 중 제일 많이 사용하고 그만큼 중요하다.
false
로 하면 DB에서 강제로 변경하면 변경되긴하나 JPA 사용시에는 절대로 반영되지 않음false
라 하면 not null이 붙는다. 자주 사용한다.자바 enum 타입을 매핑할 때 사용한다.
enum
순서를 데이터베이스에 저장(DB의 column에 문자가 저장되는게 아니라 열거체 순서가 저장된다)하기 때문이다.날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.
참고
LocalDate, LocalDateTime을 사용할 때는 생략 가능
(최신 하이버네이트 가 알아서 해준다.)
데이터베이스 BLOB, CLOB 타입과 매핑이다. 별도의 지정할 수 있는 속성이 없다.
매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑이 된다.
DB와 매핑할 필요가 없는 필드에 사용한다. 당연히 DB에 저장도 안되고 조회도 안된다.
주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용한다.
.
.
.
이정도면 필드랑 컬럼 매핑이 끝난다. 생각보다 매핑이 어렵지 않다.
물론 정말 어려운 것은 뒤에 있는 연관관계를 매핑하는 것이다. 기본적인 것은 지금처럼 간단하다.
기본키를 매핑하는 어노테이션은 다음 2가지이다.
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
기본 키는 직접 생성하거나 자동으로 생성하는 방법 2가지가 있다. 위 어노테이션 2개를 통해 직접 생성하거나 DB에서 자동으로 생성하게 할 수 있다.
간단히 요약하면 다음과 같다.
@Id
만 사용@Id
+ @GeneratedValue
직접 할당은 @Id
만 사용하여 직접 코드로 pk값을 설정하면 된다.
그러나, 자동 생성은 3가지의 전략(IDENTITY, SEQUENCE, TABLE)으로 구분된다. 지금부터 이 3가지를 자세히 알아보자.
(AUTO는 디폴트값이고 요약한 것으로도 설명이 충분하기에 생략하겠다.)
참고
DB에 의존하지 않고 직접 pk값을 세팅하면 commit하는 시점에서 INSERT QUERY가 날아가므로 버퍼링이 불가능하다.
기본 키 생성을 데이터베이스에 위임한다. 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. (예: MySQL의 AUTO_ INCREMENT)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
영속성 컨텍스트를 생각해보자. 영속성 컨텍스트에서 관리를 하려면 반드시 pk값(1차 캐시의 key값)을 알아야 하는데 지금은 DB에 값이 들어가야 pk값을 알 수 있는 형태다. 뭔가 모순이지 않나? 그러면 pk값을 언제 갖고오는 것일까?
IDENTITY 전략은 DB에서 commit하는 시점에 INSERT QUERY가 날라가는게 아니라, persist()
를 하는 시점에서 바로 INSERT QUERY가 날아간다.
(영속성 컨텍스트에 등록을 하지 못하므로 persist()
를 하는 순간 INSERT QUERY를 날려서 DB에서 식별자를 조회하여 가져와서 영속성 컨텍스트에 저장한다.)
정리하자면, IDENTITY 전략은 em.persist()
시점에 즉시 INSERT SQL 실행 하고 DB에서 식별자를 조회하여 가져온 후 그것을 영속성 컨텍스트에 저장한다.
참고
@GeneratedValue(strategy = GenerationType.IDENTITY)
객체의 타입은String
이라 두면 안된다.Integer
타입도 오버플로가 발생할 수 있어 애매하다. 될 수 있음Long
을 사용하자.
유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)이다. 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.
@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;
...
@GeneratedValue
의 geneartor
속성으로 사용할 시퀀스 이름을 명시해야 한다.엔티티 객체를 persist()
할 때 pk값이 있어야 하므로 먼저 DB에서 sequence를 가져와서 내 pk값을 가져와야 한다.
(따라서, Hibernate: call next value for MEMBER_SEQ이 출력된다.)
그 후, 영속성 컨텍스트에 저장한다.
(이는 commit할 때 저장되므로 버퍼링이 가능하다.)
근데, 매번 persist()
할 때 마다 DB에서 sequence를 가져와서 내 pk값을... 이러면 너무 성능이 안좋아지지 않을까? 이를 위해 allocationSize
속성이 있다.
allocationSize
속성 : 해당 값만큼 미리 sequence에서 값을 땡겨오는 것이다.키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉 내내는 전략이다. 모든 데이터베이스에 적용 가능하다는 장점이 있지만 성능이 안좋아지는 단점이 있다. (엔티티 객체 저장시 마다 새로운 테이블을 생성하면... 어우 성능이 급격히 저하될 것이다.)
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = “MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
@GeneratedValue
의 geneartor
속성으로 사용할 테이블 이름을 명시해야 한다.테이블은 다음과 같이 생성된다.
create table MY_SEQUENCES (
sequence_name varchar(255) not null,
next_val bigint,
primary key ( sequence_name )
)
allocationSize
속성 : Sequence 전략의 allocationSize
와 동일근데 서버가 여러 개면 문제가 없을까? 문제가 없다. 미리 값을 올려두는 방식이므로 여러 서버에서 동시에 호출을 해도 자신이 미리 숫자(pk값)를 확보하므로(?)
(동시성문제는 없다는 것만 알아두자)
null
이면 안된다.물론, 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 예를 들어 주민등록번호도 기본 키로 적절하기 않다. 대리키(대체키)를 사용하자.
참고) 자연키
주민등록번호같은 비즈니스 모델에서 자연스레 나오는 속성
권장하는 방법은 Long형 + 대체키 + 키 생성전략 사용이다.
지금까지 배웠던 매핑 지식들로 간단한 요구사항을 분석하고 매핑해보자.
다음과 같은 요구사항이 있다.
@Table
어노테이션을 사용하여 매핑되는 테이블의 이름을 별도로 ORDERS로 설정했다. (구현 코드에서 확인하자.)간단히 그림으로 알아보고, 구현 코드를 살펴보자.
구현 코드를 살펴보기 전
가급적이면 Setter를 사용하여 값을 입력하지말고 생성자를 사용하여 값을 입력하는게 유지보수면에서 유리하다. 간단한 예제이므로 Setter를 사용했다.
구현 코드를 살펴보기 전 2
이렇게 Getter, Setter를 반복적으로 사용하는 상황에선 롬복 라이브러리를 사용하자...
package jpabook.domain;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipCode;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
}
참고) 자바와 DB의 관례상 표기법과 대처법
자바 :memberId
DB :MEMBER_ID
이런식으로 관례를 따르므로 애매하면 테이블의 컬럼값이 하면
@Column
어노테이션을 사용하여 직접 매핑하자.
(참고로, 스프링 부트를 통해 jpa, 하이버네이트를 사용하면 DB의 관례에 맞게 컬럼 이름을 바꿔준다.)
Order
package jpabook.domain;
import javax.persistence.*;
import java.time.LocalDateTime;
@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 status;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
}
@Column(length = 10)
등으로 추가하자. 그래야 객체를 보고 JPA QUERY를 작성할 때 유리하다.OrderItem
package jpabook.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@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;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getItemId() {
return itemId;
}
public void setItemId(Long itemId) {
this.itemId = itemId;
}
public int getOrderPrice() {
return orderPrice;
}
public void setOrderPrice(int orderPrice) {
this.orderPrice = orderPrice;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
Item
package jpabook.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(int stockQuantity) {
this.stockQuantity = stockQuantity;
}
}
OrderStatus 열거체
package jpabook.domain;
public enum OrderStatus {
ORDER, CANCEL
}
이렇게 설계했을 때, DB에서 Order
객체를 통해 Member
객체를 가져오는 경우를 생각해보자. 아마 try 구문에서 다음과 같이 코드를 작성해야 할 것이다.
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);
뭔가 이상하다. Order
객체를 찾고, 거기서 MemberId
필드를 꺼내고 그걸 통해서 또 DB에 접근해서 찾고....
이게 과연 객체 지향적으로 작성된 코드라 할 수 있을까? 아니다. 이는 객체 설계를 테이블 설계에 억지로 맞춘 방식이다. 테이블의 외래키를 객체에 그대로 가져온다.
따라서, 객체 그래프 탐색이 불가능하고 참조가 없으므로 UML도 잘못됬다.
이러한 문제점을 해결하기 위해선 연관관계 매핑을 해야한다. 뒤에 알아보자.