[JPA] 자바 ORM 표준 JPA 프로그래밍 - 기본편 #4. 엔티티 매핑

bien·2024년 1월 18일
0

jpa-basic

목록 보기
2/6

0. 목차

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

1. 객체와 테이블 매핑

@Entity

  • @Entity가 붙은 클래스는 JPA가 관리(테이블과 매핑), 엔티티라 한다.
  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
  • 주의
    • 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
    • final 클래스, enum, interface, innter 클래스 사용 X
    • 저장할 필드에 final 사용 X

속성

  • name
    • JPA에서 사용할 엔티티 이름을 지정한다. (JPA가 내부적으로 구분하는 이름)
      • 기본 값을 사용하는 편이 좋다. 아니면 엄청 헷갈림.
    • 기본값: 클래스 이름을 그대로 사용 (예: Member)
    • 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
@Entity(name="MBR")
public class Member() {...}

@Table

  • 엔티티와 매핑할 테이블 지정 (DB에서 @Table의 name속성으로 넘겨받은 테이블을 찾고, 그 테이블과 엔티티를 연결한다.)


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

JPA는 애플리케이션 로딩 시점에 데이터베이스 생성 기능도 지원하고 있다!

  • DDL을 애플리케이션 실행 시점에 자동 완성
  • 테이블 중심 -> 객체 중심
    • 일반적으로 테이블을 다 생성해두고 객체를 생성하는 기존의 방식과 달리, 객체를 만들고 매핑해두면 프로그램 실행 시 알아서 만들어준다.
  • 데이터베이스 방언을 활용해 데이터 베이스에 맞는 적절한 DDL을 생성해준다.
  • 이렇게 생성된 DDL은 반드시 개발 장비에서만 사용해야 한다.
  • 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용해야 한다.

사용법

application.properties

# Hibernate 설정
spring.jpa.hibernate.ddl-auto=update

application.properties파일에 속성을 추가하고, 적절한 속성 값을 부여한다.

속성값

  • create
    • 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
  • create-drop
    • create와 같으나 종료 시점에 테이블 DROP
  • update
    • 변경분만 반영 (운영 DB에는 사용하면 안됨)
  • validate
    • 엔티티와 테이블이 정상 매핑되었는지만 확인
      • 엔티티와 테이블 컬럼 구조가 일치하지 않는 경우 오류를 발생시킨다.
  • none
    • DDL 기능 사용하지 않음.

주의사항

  • 운영 장비에는 절대 create, create-drop, update를 사용하면 안 된다.
    • 개발 초기 단계는 create 또는 update 사용도 ok
    • 테스트 서버(개발 초기 함께사용하는 경우)는 update 또는 validate 사용 권장.
    • 스테이징과 운영 서버는 validate 또는 none.

특징

  • DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
    • 직접 DDL을 만든다면 사용할 필요가 없는 기능이지만, 이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 계약 조건을 파악할 수 있는 장점이 있다.
  • 제약 조건 추가: 회원 이름은 필수, 10자 초과 X
    • @Column(nullable = false, length = 10)
  • 유니크 제약 조건 추가:
    • @Table(uniqueConstraints = (@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames={"NAME", "AGE"} )});

3. 필드와 컬럼 매핑

요구사항 추가

  1. 회원은 일반 회원과 관리자로 구분해야 한다.
  2. 회원 가입일과 수정일이 있어야 한다.
  3. 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.
@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 String description;
    
    @Transient
    private int temp;
    
    public Member() {
    }
    
    // Getter, Setter ...
}

매핑 애노테이션 정리

  • @Column : 컬럼 매핑
  • @Temporal : 날짜 타입 매핑
  • @Enumerated : enum 타입 매핑
  • @Lob : BLOB, CLOB 매핑
  • @Transient: 특정 필드를 컬럼에 매핑하지 않음 (매핑 무시)

@Column

@Enumerated

  • 자바 enum 타입을 매핑할 때 사용
  • 주의! ORDINAL 사용 X
    • 기본값이 EnumType.ORDINAL 인데, 이것은 Enum의 작성 순서를 기준으로 해 오류의 발생 가능성이 크다. 사용하지 않는 것을 추천한다.
    • EnumType.ORDINAL : enum 순서를 데이터베이스에 저장
    • EnumType.STRING : enum 이름을 데이터베이스에 저장.

@Temporal

  • 날자 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
  • LocalDate, LocalDateTime을 사용할 때는 생략 가능하다. (최신 하이버네이트 지원)
    • TemporalType.DATE : 날짜, 데이터베이스 date 타입과 매핑
      • 예: 2013-10-11
    • TemporalType.TIME : 시간, 데이터베이스 time 타입과 매핑
      • 예: 11:11:11
    • TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑
      • 예: 2013-10-11 11:11:11

@Lob

  • 데이터베이스 BLOB, CLOB 타입과 매핑
    • @Lob 에는 지정할 수 있는 속성이 없다.
    • 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
      • CLOB: String, char[], java.sql.CLOB
      • BLOB: byte[], java.sql.BLOB

@Transient

  • 필드에 매핑하지 않는 변수에 사용
  • 데이터베이스에 저장 및 조회하지 않는다.
  • 주로 메모리 상에서만 임시로 어떤 값을 보관하고 싶을 때 사용한다.
@Transient
private Intege temp;

4. 기본 키 매핑

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

기본키를 직접 할당하는 경우 @Id를 사용할 수 있다. 만약 자동으로 생성되는 seq같은 기본 키를 사용하고 싶은 경우를 위해 JPA에서 자동 생성 기능을 제공하고 있는데, @GeneratedValue의 설정을 통해 사용할 수 있다.

  • 직접 할당: @Id만 사용
  • 자동 생성 (@GeneratedValue)
    • IDENTITY: 데이터베이스에 위임, MYSQL
      • 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다. (IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.)
    • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
      • 데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
      • @SequenceGenerator 필요
    • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
      • 데이터베이스에 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍트스에 저장한다.
      • @TableGenerator 필요
    • AUTO: 방언에 따라 자동 지정, 기본값

1) IDNETITY 전략

  • 기본 키 생성을 데이터베이스에 위임.
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용
    • 예: MySQL의 AUTO_INCREMENT
@Entity
public class Member {

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

}

특이사항

  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행.
  • AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음.
  • IDENTITY 전략은 em.persist()시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회.
    • DB에 id값이 null로 입력되어야만 DB에서 id값을 셋팅해준다. 따라서 DB에 값이 들어가야만 id 값을 알 수 있다. 그러나 영속성 컨텍스트에서 관리되려면 무조건 PK값이 있어야 한다. 결과적으로, IDENTITY 전략에서만 PK값을 조회하기 위해 em.persist() 시점에 바로 INSERT query를 전송한다.
    • (단점) 따라서, SQL을 모아서 인서트하는 버퍼링 라이트 기능을 제공하지 않는다. 그러나 성능상 그렇게 비약적인 차이가 있지는 않음.
public static void main(String[] args) {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory();
    
    EntityManager em = emf.createEntityManager()
    
    EntityTransaction tx = em.getTransaction();
    tx.begin();
    
    try {
    	Member member = new Member();
        member.setUsername("C");
        
        System.out.println("===============");
        em.persist(member); // DB에 바로 삽입 & PK 구하기
        System.out.println("member.id = " + member.getId()); // 바로 직후 PK조회가 가능함.
        System.out.println("===============");
        
        tx.commit();
    } catch (Exception e) {
    
    }

결과

===============
Hibernamte:
	/* insert hellojpa.Member
    	*/ insert
        into Member
        	(id, name)
        values
        	(null, ?)
member.id = 1
===============

2) SEQUENCE 전략

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트 (예: 오라클 시퀀스)
  • 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
  • 따로 seq 이름을 부여하지 않으면 자동으로 hibernate_sequence값으로 seq가 들어간다.
    • 원하는 경우 name 속성을 이용해 seq 컬럼의 이름을 부여할 수 있다.
@Entity
@SequenceGeneratory(
	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;
}

특이사항

  • IDNETITY 전략과 달리 SEQUENCY 전력은 시퀀스
public static void main(String[] args) {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory();
    
    EntityManager em = emf.createEntityManager()
    
    EntityTransaction tx = em.getTransaction();
    tx.begin();
    
    try {
    	Member member = new Member();
        member.setUsername("C");
        
        System.out.println("===============");
        em.persist(member); // DB에 바로 삽입 & PK 구하기
        System.out.println("member.id = " + member.getId()); // 바로 직후 PK조회가 가능함.
        System.out.println("===============");
        
        tx.commit();
    } catch (Exception e) {
    
    }

결과

Hibernate:
	call next value for MEMBER_SEQ
member.id =1
===============
Hibernamte:
	/* insert hellojpa.Member
    	*/ insert
        into Member
        	(id, name)
        values
        	(null, ?)
member.id = 1
===============

@SequenceGenerator 속성

3) TABLE 전략

  • 키 생성 전용 테이블을 하나 만들어 데이터베이스 시퀀스를 흉내내는 전략
    • 장점: 모든 데이터베이스에 적용 가능
      • auto_increment와 seq를 사용하는 등 디비마다 다른 시퀀스 형식을 가지고 있는데, TABLE을 사용하면 따로 TABLE을 생성하므로 모든 디비에서 적용 가능하다.
    • 단점: 성능
      • 테이블을 따로 생성해 사용하므로 최적화가 안되어 있어 성능이 좀 떨어진다.(락도 발생 가능)

@TableGenerator 속성

4) 권장하는 식별자 전략

  • 기본 키 제약조건은 null이 아니고, 유일하며, 변하면 안된다.
    • 미래까지 이 기본키 제약 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자. (비즈니스 정보를 기본키로 끌어오지 말자.)
      • 예를 들어 주민등록번호도 기본 키로 적절하지 않다.
  • 권장: Long 형 + 대체키 + 키 생성전략 사용
  1. 자연 키 (natural key)
    • 비즈니스에 의미가 있는 키
    • 예: 주민등록번호, 이메일, 전화번호
  2. 대리 키 (surrogate key)
    • 비즈니스와 관련 없는 임의로 만들어진 키. 대체 키로도 불린다.
    • 예: 오라클 시퀀스, auto_increment, 키생성 테이블 사용
  3. JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다.

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

요구사항 분석

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

도메인 모델 분석

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

테이블 설계

엔티티 설계와 매핑

  • 지금은 예제이기 때문에 setter를 사용했다. 실제에서는 Setter사용은 최소화하고 생성자에서 값을 setting하는 것이 좋다.
  • 엔티티보고 바로 확인 가능하도록, 가급적이면 매핑(메타데이터)을 적어주는 편이 좋다.
    • @Column(length = 10)
    • @Table(indexes = @Index()) 인덱스 제약조건 표기 가능
    • jpql의 while문 작성 등의 경우에, 일일이 DB를 확인하는 것 보다 엔티티만 보고 바로 작성 가능해 좋다.

Member

package com.study.jpashop.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

}

Item

package com.study.jpashop.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

}

Order

package com.study.jpashop.domain;

import jakarta.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;

}

OrderItem

package com.study.jpashop.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.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;

}

OrderStatus.enum

package com.study.jpashop.domain;

public enum OrderStatus {

    ORDER, CANCEL;

}

데이터 중심 설계의 문제점

  • 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
  • 테이블의 외래키를 객체에 그대로 가져옴
  • 객체 그래프 탐색이 불가능
  • 참조가 없으므로 UML도 잘못됨

외래키만 가지고 있는 테이블의 경우, 데이터를 조회하는 경우 id를 통해 테이블을 다시 조회해야 한다. 위의 예시에서 주문을 조회한 다음 주문과 연관된 회원을 조회하려면 외래키를 사용해 다시 조회해야 한다.

Order order = em.find(Order.class, orderId);

// 왜리키로 다시 조회
Member member = em.find(Member.class, order.getMemberId());

그러나 이와 달리, 참조를 사용해 연관된 객체를 조회하는 것이 객체지향적인 방법이다.

Order order = em.find(Order.class, orderId);
Member member = order.getMember(); // 참조사용

JPA는 객체의 참조와 테이블의 외래 키를 매핑해 객체에서는 참조를 사용하고 테이블에서는 외래 키를 사용할 수 있도록 한다. 다음 장을 통해 참조외 외래키를 어떻게 매핑하는지 알아보자.


Reference

profile
Good Luck!

0개의 댓글