반응형
객체와 테이블 매핑
엔티티 매핑 소개
- 객체와 테이블 매핑 :
@Entity
,@Table
- 필드와 컬럼 매핑 :
@Column
- 기본 키 매핑 :
@Id
- 연관관계 매핑 :
@ManyToOne
,@JoinColumn
@Entity
@Entity
가 붙은 클래스는 JPA가 관리하는 클래스로 엔티티라고 부른다.- JPA를 사용해서 테이블과 매핑할 클래스는 반드시
@Entity
필수! - 주의사항
- 기본 생성자 필수 (파라미터가 없는 public 또는 protected)
- final 클래스, enum, interface, inner 클래스 사용할 수 없다.
- 저장할 필드에 final 키워드를 사용할 수 없다.
- 속성
- name
- JPA에서 사용할 엔티티 이름 지정.
- 기본값 : 클래스 이름을 그대로 사용(예: Member)
- 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
- name
@Table
- 엔티티와 매핑할 테이블 지정
- 속성
name
: 매핑할 테이블 이름catalog
: 데이터베이스 catalog 매핑schema
: 데이터베이스 schema 매핑uniqueConstraint(DDL)
: DDL 생성 시 유니크 제약 조건 생성
데이터베이스 스키마 자동 생성
- DDL을 애플리케이션 실행 시점에 자동 생성
- 테이블 중심 -> 객체 중심
- 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
- 이렇게 생성된 DDL은 개발 장비에서만 사용
- 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬 은 후 사용
- 속성
- hibernate.hbm2ddl.auto
create
: 기존 테이블 삭제 후 다시 생성(DROP + CREATE)create-drop
: create와 같으나 종료 시점에 테이블 DROPupdate
: 변경분만 반영(운영 DB에는 사용하면 안 됨)validate
: 엔티티와 테이블이 정상 매핑되었는지만 확인none
: 사용하지 않음
- hibernate.hbm2ddl.auto
- 주의 사항
- 운영 장비에는 절대 create, create-drop, update 사용하면 안 된다.(데이터 변경 여지)
- 개발 초기 : create or update
- 테스트 서버: update or validate
- 스테이징과 운영서버: validate or none
- DDL 생성 기능
- 제약 조건 추가 : 회원 이름은 필수, 10자 초과 X
- 유니크 제약 조건 추가
- DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고, JPA의 실행 로직에는 영향을 주지 않는다.
필드와 컬럼 매핑
- 요구 사항 추가
- 회원은 일반 회원과 관리자로 구분해야 한다.
- 회원 가입일과 수정일이 있어야 한다.
- 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name") // 객체는 username, 컬럼명은 name
private String username;
private Integer age;
@Enumerated(EnumType.STRING) // enum 타입 설정 , DB에는 기본적으로 없다고 봐야한다
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP) //날짜 타입
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob //큰 컨텐츠를 넣을 때
private String description;
public Member() {
}
}
- 요구사항 설정을 위한 Member 클래스 재구성
매핑 애노테이션
1. @Column
: 제일 자주 쓰이는 어노테이션
name
: 필드와 매핑할 테이블의 컬럼 이름. (기본 값 : 객체의 필드 이름)insertable, updatable
: 등록, 변경 가능 여부. (기본 값 : TRUE)nullable(DDL)
: null 값의 허용 여부 설정(false로 설정 : DDL 생성 시에 not null 제약조건이 붙는다.)(기본 값 : true)unique(DDL)
:@Table
의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약 조건을 걸 때 사용한다.
→ 유니크 제약조건은 잘 사용하지 않는다.(제약조건의 이름이 너무 난수 값이라 알아보기 힘듦)
→ 클래스 레벨에서@Table(unique~)
와 같은 방식을 사용하는 것을 권장columnDefinition (DDL)
: 데이터베이스 컬럼 정보를 직접 줄 수 있다.
ex) varchar(100) default ‘EMPTY'length(DDL)
: 문자 길이 제약조건, String 타입에만 사용한다. (기본 값 : 255)precision, scale(DDL)
: BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다). precision은 소수점을 포함한 전체 자릿수, scale은 소수의 자릿수다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정 밀한 소수를 다루어야 할 때만 사용한다. (기본 값 : precision=19, scale=2)
2. @Enumerated
- 자바 Enum 타입을 매핑할 때 사용
EnumType.ORDINAL
- enum 순서를 DB에 저장 → 순서대로 숫자로 들어감
- 사용 ❌
- 숫자마저도 새로운 enum이 들어가면 위치가 변경할 수 있음.
EnumType.STRING
- enum 이름을 DB에 저장
3. @Temporal
- 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
- 참고: LocalDate, LocalDateTime을 사용할 때는 생략 가능(최신 하이버네이트 지원)
private LocalDate testLocalDate;
private LocalDateTime testLocalDateTime;
- 요즘 하이버네이트 버전은 LocalDate를 사용할 수 있으므로
@Temporal
이런게 있다는 정도만 보고 넘어가자.
4. @Lob
- 데이터베이스 BLOB, CLOB 타입과 매핑
- 큰 컨텐츠를 넣을 때 사용 (게시판의 게시글)
@Lob
에는 지정할 수 있는 속성이 없다.- 매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB으로 자동 매핑해준다.
- CLOB : String, char[], java.sql.CLOB
- BLOB : byte [], java.sql.BLOB
5. @Transient
- 필드 매핑 X
- 데이터베이스에 저장 X, 조회 X
- 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
@Transient
private Integer temp;
기본 키 매핑 방법
직접 할당
@Id
만 사용
자동 생성 (@GenerateValue
)
1. IDENTITY 전략
매핑
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- 기본 키 생성을 DB에 위임 , Id에 값을 넣으면 안 된다.
- 주로 MYSQL, SQL Server, DB2에서 사용 (ex. MYSQL의 AUTO_INCREMENT)
- AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행 한 이후에 ID 값을 알 수 있다.
- 영속성 관리 시점에서 1차 캐시에
@Id
값(키값)을 알 수 없다. - 때문에 이 케이스에서는
persist()
수행 시 바로 insert 쿼리가 수행된다. - 그렇기에 IDENTITY케이스에서는 지연 쓰기가 제한된다. (하지만 크게 성능 하락이 있거나 하지는 않다.)
- 영속성 관리 시점에서 1차 캐시에
- AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행 한 이후에 ID 값을 알 수 있다.
System.out.println("===========");
em.persist(member);
System.out.println("===========");
=====
사이에서 쿼리문이 날아간다. 즉 쓰기 지연 기능을 사용하지 못한다.
IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회한다. 즉, JPA 입장에선 영속성 컨텍스트의 1차 캐시에 키와 객체를 관리하는 데 키값을 모르게 된다. 그렇기 때문에 커밋 시점이 아닌 시점에서 강제로 DB를 호출해서 키값을 가져와야 하는 문제가 있다.
2. SEQUENCE 전략
- 데이터베이스 시퀀스
- 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)
- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
@SequenceGenerator
필요======
사이에 시퀀스 값만 가지고 와서, 쓰기 지연 기능을 사용할 수 있다.- 속성
name
: 식별자 생성기 이름. (기본 값 : 필수)sequenceName
: DB에 등록되어 있는 시퀀스 이름 : (기본 값 : hibernate_sequence)initialValue
: DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. (기본 값 : 1)allocationSize
: 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)
데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다. (기본값 : 50)catalog
,schema
: 데이터베이스, catalog, schema 이름
@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;
allocationSize
설정을 통해 해당 개수만큼 미리 시퀀스 값을 갖고 와서 , 그 범위 안은 메모리로 띄우기 때문에 성능 문제는 없다고 볼 수 있다. (동시성 이슈가 없다.)- 예 ) initialSize = 1, allocationSize = 50 → (1, 1) (51, 2) (51, 3) (51, 4) : 50개를 미리 가져와서 그 후 메모리에서 갖고 온다. (DB SEQ, APP SEQ)로 보면 된다.(1,1)에서
allocationSize
를 맞추기 위해 한번 더 호출하고 이후는 메모리에서 시퀀스 값을 가져온다.
- 예 ) initialSize = 1, allocationSize = 50 → (1, 1) (51, 2) (51, 3) (51, 4) : 50개를 미리 가져와서 그 후 메모리에서 갖고 온다. (DB SEQ, APP SEQ)로 보면 된다.(1,1)에서
3. TABLE 전략
- 키 생성 용 테이블 사용, 모든 DB에서 사용
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내 내는 전략이다.
@TableGenerator
필요- 모든 데이터베이스에 적용할 수 있는 장점이 있지만, 성능 문제가 있어서 잘 사용하지 않는다.
@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;
- 속성
name
: 식별자 생성기 이름 (기본값 : 필수)table
: 키 생성 테이블명 (기본값 : hibernate_sequences)pkColumnName
: 시퀀스 컬럼명 (기본값 : sequence_name)valueColumnNa
: 시퀀스 값 컬럼명 (기본값 : next_val)pkColumnValue
: 키로 사용할 값 이름, (기본값 : 엔티티 이름)initialValue
: 초기 값, 마지막으로 생성된 값이 기준이다. (기본값 : 0)allocationSize
: 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨). (기본값 : 50)catalog
,schema
: 데이터베이스 catalog, schema 이름uniqueConstraints(DDL)
: 유니크 제약 조건을 지정할 수 있다.
4. AUTO
: 방언에 따라 자동 지정, 기본값
권장하는 식별자 전략
- 기본 키 제약 조건 : null 아니고 유일, 변하면 안 된다.
- 미래까지 조건 하는 자연 키는 찾기 어렵다. 대체키(비즈니스와 전혀 상관없는 키)를 사용하자.
- 예를 들어 국가에서 주민번호를 보관할 수 없도록 법적 규제할 가능성이 있으므로 주민등록번호도 기본 키로 적절하지 않다.
- 권장 : Long형 + 대체키(시퀀스 or UUID) + 키 생성 전략 사용
실전 예제 1 - 요구사항 분석과 기본 매핑
요구사항 분석
- 회원은 상품을 주문할 수 있다.
- 주문 시 여러 종류의 상품을 선택할 수 있다.
기능 목록
- 회원 기능
- 회원등록
- 회원 조회
- 상품 기능
- 상품등록
- 상품 수정
- 상품 조회
- 주문 기능
- 상품 주문
- 주문내역 조회
- 주문 취소
도메인 모델 분석
- 회원과 주문의 관계 : 회원은 여러 번 주문할 수 있다. → 일대다 관계(
1:N
) - 주문과 상품의 관계: 주문 시 여러 상품을 선택할 수 있다. 반대로 상품도 여러 번 주문될 수 있다. → 다대다 관계(
N:M
)
→ 주문 상품(OrderItem
)이라는 모델을 만들어서 일대다(1:N
), 다대일(N:1
) 관계로 풀어낸다.
테이블 설계
엔티티 설계와 매핑
프로젝트 생성
MAVEN과 이전의 XML 방식의 JPA와 H2 DB를 사용해서 프로젝트를 생성해보자.
pom.xml 설정
더보기
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jpabook</groupId>
<artifactId>jpashop</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- JPA 하이버네이트 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.6.9.Final</version>
</dependency>
<!-- H2 데이터베이스 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
META-INF/persistence.xml
더보기
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Member
더보기
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@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;
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;
}
}
Item
더보기
package jpabook.jpashop.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;
}
}
Order
더보기
package jpabook.jpashop.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;
}
}
- Enum 타입을 사용할 때 순서의 보장을 위해 ORDINAL 이 아닌, String 방식을 사용한다는 점을 기억하자.
OrderItem
더보기
package jpabook.jpashop.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;
}
}
OrderStatus
package jpabook.jpashop.domain;
public enum OrderStatus {
ORDER, CANCEL
}
JpaMain
package jpabook.jpashop;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
테이블 설계대로 엔티티를 매핑해서 만들어보았다. 하지만, RDB의 패러다임대로 따르다 보니 다음과 같이 객체지향적이지 못한 문제점이 발생한다.
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);
- 주문한 회원을 조회하기 위해 주문 테이블에서 객체를 만들고, 그 객체에 해당하는 아이디 값을 다시 회원 테이블에서 조회해야 한다. 즉, 객체 그래프 탐색이 불가능하므로, 객체지향 관점에 맞지 않다.
Member member = order.getMember();
- 다음과 같이 주문 테이블에서 멤버 객체를 바로 꺼낼 수 있도록 설계해야 한다. 즉, 주문 테이블에서 회원의 아이디 타입이
Long
이 아니라Member
타입을 가져야 한다.
이 문제를 객체지향 관점에서 해결하기 위한 연관관계 매핑에 대해 다음 포스팅에서 알아보자.
이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.
반응형