Peony의 기록 창고 🌼
Published 2022. 6. 26. 20:30
[JPA] 엔티티 매핑 SpringBoot/JPA 기본
반응형

객체와 테이블 매핑

엔티티 매핑 소개

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

 

@Entity

  1. @Entity가 붙은 클래스는 JPA가 관리하는 클래스로 엔티티라고 부른다.
  2. JPA를 사용해서 테이블과 매핑할 클래스는 반드시 @Entity 필수!
  3. 주의사항
    • 기본 생성자 필수 (파라미터가 없는 public 또는 protected)
    • final 클래스, enum, interface, inner 클래스 사용할 수 없다.
    • 저장할 필드에 final 키워드를 사용할 수 없다.
  4. 속성
    • name
      • JPA에서 사용할 엔티티 이름 지정.
      • 기본값 : 클래스 이름을 그대로 사용(예: Member)
      • 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.

 

@Table

  1. 엔티티와 매핑할 테이블 지정
  2. 속성
    • name : 매핑할 테이블 이름
    • catalog: 데이터베이스 catalog 매핑
    • schema: 데이터베이스 schema 매핑
    • uniqueConstraint(DDL): DDL 생성 시 유니크 제약 조건 생성

 

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

  • DDL을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심 -> 객체 중심
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용
  • 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬 은 후 사용
  • 속성
    • hibernate.hbm2ddl.auto
      1. create: 기존 테이블 삭제 후 다시 생성(DROP + CREATE)
      2. create-drop: create와 같으나 종료 시점에 테이블 DROP
      3. update: 변경분만 반영(운영 DB에는 사용하면 안 됨)
      4. validate: 엔티티와 테이블이 정상 매핑되었는지만 확인
      5. none: 사용하지 않음
  • 주의 사항
    1. 운영 장비에는 절대 create, create-drop, update 사용하면 안 된다.(데이터 변경 여지)
    2. 개발 초기 : create or update
    3. 테스트 서버: update or validate
    4. 스테이징과 운영서버: 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케이스에서는 지연 쓰기가 제한된다. (하지만 크게 성능 하락이 있거나 하지는 않다.)
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를 맞추기 위해 한번 더 호출하고 이후는 메모리에서 시퀀스 값을 가져온다.

 

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) 관계로 풀어낸다.

 

테이블 설계

image

 

엔티티 설계와 매핑

image

 

프로젝트 생성

image

 

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 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.

 

 

반응형
profile

Peony의 기록 창고 🌼

@myeongju