엔티티 매핑

김민우·2022년 8월 27일
0

JPA

목록 보기
3/10

저번에도 언급했던 JPA에서 가장 중요한 2가지를 다시 알아보자.

  • 객체와 관계형 데이터베이스 매핑하기
    (Object Relational Mapping)
  • 영속성 컨텍스트

이번에는 객체와 관계형 데이터베이스 매핑을 설계적인 측면에서 자세히 알아보자.

엔티티 매핑은 크게 4가지가 있다.

  • 객체와 테이블 매핑: @Entity, @Table
  • 필드와 컬럼 매핑: @Column
  • 기본 키 매핑: @Id
  • 연관관계 매핑: @ManyToOne,@JoinColumn

하나씩 알아보자.


객체와 테이블 매핑

@Entity

@Entity 가 붙은 클래스는 JPA가 관리하며 이를 엔티티라 한다. 즉, JPA를 사용해서 테이블과 매핑할 클래스는 반드시 @Entity를 붙여줘야한다.

주의점

  • 디폴트 생성자 필수(앞에서 설명함)
  • final 클래스, enum, interface, inner 클래스 사용 X
  • 저장할 필드에 final 사용 X

속성

속성은 name이 있다. 이는 JPA에서 사용할 엔티티 이름을 지정한다.

  • 기본값: 클래스 이름을 그대로 사용(예: Member)
  • 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.

@Table

엔티티와 매핑할 테이블을 지정한다.

속성

속성들은 다음과 같다.


데이터베이스 스키마 자동 생성

create문으로 DB를 생성하고 시작할 수 있지만 그러기 위해선 DB로 가서 create table을 일일히 치고... 해야하는데 매우 귀찮다. JPA는 이러한 귀찮음을 해결해준다.

DDL을 애플리케이션 실행 시점에 자동 생성해준다. 이렇게 생성된 DDL은 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL이다. 이를 통해 테이블 중심 설계를 객체 중심 설계로 바꿀 수 있다.

참고
DDL(Data Definition Language)은 SQL의 서브세트이다. 데이터베이스에서 데이터 및 데이터의 관계를 설명하기 위한 언어이다.

참고
이렇게 생성된 DDL은 개발 장비에서만 사용하자.
생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용하자.

데이터베이스 스키마 자동 생성 속성

hibernate.hbm2ddl.auto

  • none은 별도로 지정된 값은 아니다. none 외에 옵션에 없는 아무 값이나 입력하면 none과 동일한 역할을 한다. 그러나 관례상 none이라 적는 것이 좋다.

주의점

계속 언급하지만 상황에 따라 다른 것을 사용하되 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.

개발 단계별 사용 옵션은 다음과 같다.

  • 개발 초기 단계 : create 또는 update
  • 테스트 서버 : update 또는 validate
  • 스테이징과 운영 서버 : validate 또는 none

DDL 생성 기능

제약조건 추가

회원 이름은 필수, 10자 초과X

  • @Column(nullable = false, length = 10)

유니크 제약조건 추가

  • @Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})

중요한 것은 DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.


필드와 컬럼 매핑

객체와 테이블이 매핑관계 였다면 객체안의 필드들은 테이블의 컬럼으로 매핑이 된다.

객체와 테이블 매핑은 @Entity, @Table 어노테이션으로 매핑할 수 있었다. 이제, 필드와 컬럼의 매핑을 알아보자.

@Column

필드와 컬럼을 매핑하는 어노테이션 중 제일 많이 사용하고 그만큼 중요하다.

  • insertable, updatable : false로 하면 DB에서 강제로 변경하면 변경되긴하나 JPA 사용시에는 절대로 반영되지 않음
  • nullable(DDL) : false라 하면 not null이 붙는다. 자주 사용한다.
  • unique : 잘 사용하진 않는다. (이름이 매우 복잡해진다.)

@Enumerated

자바 enum 타입을 매핑할 때 사용한다.

  • EnumType.ORINAL은 절대로 사용하지 말자. enum 순서를 데이터베이스에 저장(DB의 column에 문자가 저장되는게 아니라 열거체 순서가 저장된다)하기 때문이다.
  • 다시 언급하지만 그냥 ORDINAL 사용X

@Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.

참고
LocalDate, LocalDateTime을 사용할 때는 생략 가능
(최신 하이버네이트 가 알아서 해준다.)

@Lob

데이터베이스 BLOB, CLOB 타입과 매핑이다. 별도의 지정할 수 있는 속성이 없다.

매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑이 된다.

  • CLOB: String, char[], java.sql.CLOB
  • BLOB: byte[], java.sql. BLOB

@Transient

DB와 매핑할 필요가 없는 필드에 사용한다. 당연히 DB에 저장도 안되고 조회도 안된다.
주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용한다.

.
.
.

이정도면 필드랑 컬럼 매핑이 끝난다. 생각보다 매핑이 어렵지 않다.
물론 정말 어려운 것은 뒤에 있는 연관관계를 매핑하는 것이다. 기본적인 것은 지금처럼 간단하다.


기본 키 매핑

기본키 매핑 어노테이션

기본키를 매핑하는 어노테이션은 다음 2가지이다.

  • @Id
  • @GeneratedValue
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

기본 키 매핑 방법

기본 키는 직접 생성하거나 자동으로 생성하는 방법 2가지가 있다. 위 어노테이션 2개를 통해 직접 생성하거나 DB에서 자동으로 생성하게 할 수 있다.

간단히 요약하면 다음과 같다.

  • 직접 할당: @Id만 사용
  • 자동 생성 : @Id + @GeneratedValue
    • IDENTITY: 데이터베이스에 위임, MYSQL
    • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
      • @SequenceGenerator 필요
    • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
      • @TableGenerator 필요
    • AUTO: 방언에 따라 자동 지정, 기본값

직접 할당은 @Id만 사용하여 직접 코드로 pk값을 설정하면 된다.
그러나, 자동 생성은 3가지의 전략(IDENTITY, SEQUENCE, TABLE)으로 구분된다. 지금부터 이 3가지를 자세히 알아보자.
(AUTO는 디폴트값이고 요약한 것으로도 설명이 충분하기에 생략하겠다.)

참고
DB에 의존하지 않고 직접 pk값을 세팅하면 commit하는 시점에서 INSERT QUERY가 날아가므로 버퍼링이 불가능하다.

IDENTITY 전략

기본 키 생성을 데이터베이스에 위임한다. 주로 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을 사용하자.

Sequence 전략

유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)이다. 오라클, 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;
    
    ...
  • @GeneratedValuegeneartor 속성으로 사용할 시퀀스 이름을 명시해야 한다.

엔티티 객체를 persist()할 때 pk값이 있어야 하므로 먼저 DB에서 sequence를 가져와서 내 pk값을 가져와야 한다.
(따라서, Hibernate: call next value for MEMBER_SEQ이 출력된다.)
그 후, 영속성 컨텍스트에 저장한다.
(이는 commit할 때 저장되므로 버퍼링이 가능하다.)

근데, 매번 persist() 할 때 마다 DB에서 sequence를 가져와서 내 pk값을... 이러면 너무 성능이 안좋아지지 않을까? 이를 위해 allocationSize 속성이 있다.

속성

  • allocationSize 속성 : 해당 값만큼 미리 sequence에서 값을 땡겨오는 것이다.
    • 동시성 문제없이 다양한 문제를 해결가능하다.
    • 이론적으로는 이 값을 충분히 늘리는 것이 좋긴한데, 이러면 별도의 문제가 생길 수 있으므로 50정도가 적당하다.

TABLE 전략

키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉 내내는 전략이다. 모든 데이터베이스에 적용 가능하다는 장점이 있지만 성능이 안좋아지는 단점이 있다. (엔티티 객체 저장시 마다 새로운 테이블을 생성하면... 어우 성능이 급격히 저하될 것이다.)

@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;
    
    ...
  • @GeneratedValuegeneartor 속성으로 사용할 테이블 이름을 명시해야 한다.

테이블은 다음과 같이 생성된다.

create table MY_SEQUENCES (
	sequence_name varchar(255) not null,
	next_val bigint,
	primary key ( sequence_name )
)

속성

  • allocationSize 속성 : Sequence 전략의 allocationSize와 동일

근데 서버가 여러 개면 문제가 없을까? 문제가 없다. 미리 값을 올려두는 방식이므로 여러 서버에서 동시에 호출을 해도 자신이 미리 숫자(pk값)를 확보하므로(?)
(동시성문제는 없다는 것만 알아두자)

권장하는 식별자 전략

기본키 제약 조건

  • null이면 안된다.
  • 10년이고 100년이고 지나도 변하면 안된다.
  • 유일해야 한다.

물론, 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 예를 들어 주민등록번호도 기본 키로 적절하기 않다. 대리키(대체키)를 사용하자.

참고) 자연키
주민등록번호같은 비즈니스 모델에서 자연스레 나오는 속성

권장하는 방법은 Long형 + 대체키 + 키 생성전략 사용이다.


실전 예제 - 요구사항 분석과 기본 매핑

지금까지 배웠던 매핑 지식들로 간단한 요구사항을 분석하고 매핑해보자.

요구사항 분석

다음과 같은 요구사항이 있다.

  • 회원은 상품을 주문할 수 있다.
  • 주문 시 여러 종류의 상품을 선택할 수 있다.

기능 목록

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소

도메인 모델 분석

  • 회원과 주문의 관계 : 회원은 여러 번 주문할 수 있다. (일대다)
  • 주문과 상품의 관계 : 주문할 때 여러 상품을 선택할 수 있다. 반대로 같은 상품도 여러 번 주문될 수 있다. 주문상품 이라는 모델 을 만들어서 다대다 관계를 일다대, 다대일 관계로 풀어냄

테이블 설계

  • DB에서 테이블명으로 ORDER가 사용안되는 경우도 있다. (내부에서 별도로 사용할 수 있어서) 그래서, 이 에제에서는 @Table 어노테이션을 사용하여 매핑되는 테이블의 이름을 별도로 ORDERS로 설정했다. (구현 코드에서 확인하자.)
    이렇게 상품 주문 관련 예제에서 Order라는 이름의 클래스를 사용할 때에는 대부분 이렇게 해준다.

엔티티 설계와 매핑

간단히 그림으로 알아보고, 구현 코드를 살펴보자.

구현 코드를 살펴보기 전
가급적이면 Setter를 사용하여 값을 입력하지말고 생성자를 사용하여 값을 입력하는게 유지보수면에서 유리하다. 간단한 예제이므로 Setter를 사용했다.

구현 코드를 살펴보기 전 2
이렇게 Getter, Setter를 반복적으로 사용하는 상황에선 롬복 라이브러리를 사용하자...

Member

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;
    }
}
  • 매핑될 테이블 이름을 ORDERS로 바꿔주었다. (이유는 위에서 확인)
  • 가급적이면 개발자들이 필드값 길이 제한이 있음을 알 수 있도록 즉, 제약을 명시할 수 있도록 DB가 아닌 객체 코드에서 @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도 잘못됬다.

이러한 문제점을 해결하기 위해선 연관관계 매핑을 해야한다. 뒤에 알아보자.

0개의 댓글