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

Lee Han Sol·2021년 9월 28일
0
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편

이 글은 김영한님의 Inflearn 강의를 학습한 내용을 정리하였습니다.
글에 포함된 그림의 출처는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의와 자바 ORM 표준 JPA 프로그래밍입니다.

목표

JPA에서 꼭 알아야 할 2가지는 JPA의 동작 원리를 이해하고 객체와 관계형 데이터베이스를 정확히 매핑하는 것이다.

이번 글은 객체와 관계형 데이터베이스 매핑 방법을 알아본다.

  • @Entity
  • @Table
  • 데이터베이스 스키마 자동 생성
  • 다양한 매핑 사용
  • DDL 생성 기능
  • 기본 키 매핑
  • 필드와 컬럼 매핑: 레퍼런스

매핑 정보는 XML 또는 Annotation 을 사용해서 기술한다.
이 글은 Annotation 방식만 알아본다.


@Entity

역할

@Entity가 붙은 클래스는 테이블과 매핑되고 JPA가 관리한다.

속성

@Entity에 사용할 수 있는 속성으로 name이 있다.

name

JPA에서 사용할 엔티티 이름을 지정한다.

사용 코드

@Entity(name = "myUser")
public User { ...}

기본 값은 클래스 이름을 그대로 사용한다.

활용 예
다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌을 피할수 있다.

주의사항

@Entity 적용 시 주의사항은 아래와 같다.

  • 기본 생성자가 꼭 존재해야한다. (파라미터가 없는 public 또는 protected 생성자)
  • final 클래스, enum, interface, inner 클래스에 사용할 수 없다.
  • 저장할 필드에 final을 사용하면 안 된다.

@Table

역할

엔티티와 매핑할 테이블을 지정한다.
생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.

속성

@Table에서 사용할 수 있는 속성 4가지에 대해서 알아보자.

  • name
  • catalog
  • schema
  • uniqueConstraints

name

매핑할 테이블 이름을 지정한다.

catalog

catalog 기능이 있는 데이터베이스에서 catalog를 매핑한다.

schema

schema 기능이 있는 데이터베이스에서 schema를 매핑한다.

uniqueConstraints

DDL 생성 시 유니크 제약조건을 만들고 2개 이상의 복합 유니크 제약조건도 만들 수 있다.
이 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다. (이미 만들어진 테이블에 유니크 제약조건을 추가해주진 않는다.)

사용 코드

@Entity
@Table(
    name = "tn_user",
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"name"})
    })
public user {...}

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

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.
스키마 자동 생성 기능을 사용하면 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.

스키마 자동 생성 기능을 적용하려면 아래 코드를 persistence.xml의 property 부분에 추가하자.

<property name="hibernate.hbm2ddl.auto" value="create"/>

위 코드의 value 값으로 사용할 수 있는 속성은 아래와 같다.

  • create
    기존 테이블을 삭제하고 새로 생성한다. DROP + CREATE

  • create-drop
    create 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거한다. DROP + CREATE + DROP

  • update
    데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정한다.
    (column 삭제는 반영하지 않는다.)

  • validate
    데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다.

  • none
    자동 생성 기능을 사용하지 않는다. (자동 생성 기능을 사용하지 않는 또 다른 방법은 <property name="hibernate.hbm2ddl.auto" value="..."/> 속성 자체를 삭제하면 된다.

주의사항!
운영 서버에서 create, create-drop, update 처럼 DDL을 수정하는 옵션은 절대 사용하면 안 된다. (애시당초 DB 계정을 분리시켜야한다.)

개발 환경에 따른 추천 전략은 다음과 같다.

개발 초기 단계는 create 또는 update
초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
테스트 서버는 update 또는 validate
스테이징과 운영 서버는 validate 또는 none


DDL 생성 기능

DDL 생성 기능은 DDL을 자동 생성할 때만 사용된다.
(hibernate.hbm2ddl.auto가 create, create-drop, update에서만 동작한다.)
그리고 JPA의 실행 로직에는 영향을 주지 않는다.

아래 2가지 예를 통해 DDL 생성을 확인해보자.

#1. nullable, length 설정

빨간 박스는 DDL에 not null 제약조건과 문자의 크기를 지정하는 속성을 설정한다.

실제 실행한 결과로 아래의 DDL을 확인할 수 있다.

#2. 유니크 제약조건 설정

빨간 박스는 DDL에 유니크 제약조건을 설정한다.

생성된 DDL은 아래와 같다.

위에서 말했듯 이런 기능들은 애플리케이션 로직에는 영향을 주지 않는다.
따라서 DDL을 직접 만든다면 굳이 작성할 필요 없는 코드다.
하지만 이 기능을 사용하면 Entity만 보고도 다양한 제약조건을 파악할 수 있는 장점이 있다.


필드와 컬럼 매핑

@Entity@Table을 이용해 클래스를 매핑해봤다.
이번에는 필드와 컬럼 매핑을 알아보자.

아래 그림은 다양한 자료형을 사용한 코드와 그 결과이다.

우선 뚜비를 보면 시간 자료형들이 DB의 어떤 자료형으로 매핑되는지 알 수 있다.

  • LocalDate : date

  • LocalTime : time

  • LocalDateTime : timestamp

  • @Temporal(TemporalType.TIMESTAMP) : timestamp

@Temporal은 속성으로 지정한 타입으로 매핑시킨다. (JDK 8 아래에서 사용)

TemporalType은 DATE, TIME, TIMESTAMP 이렇게 3가지가 있다.

나나는 Column의 name 속성을 통해 db의 column 이름을 지정할 수 있다는 것을 보이고 싶었다.
(사진 찍고 그림 다 그리고 나서 name이 특별하지 않다는 것을 알았다...ㅜㅜ)

는 @Enumerated를 활용해 열거 타입의 이름을 DB에 저장한다.

@EnumeratedSTRING, ORDINAL 이렇게 2가지가 있다.

STRING은 enum 타입의 이름을 사용한다.
ORDINAL은 enum 타입에서 순서를 사용한다.

enum type 활용의 참고하면 좋은 글

추가적으로 @Trasient 도 있다.
이 어노테이션이 적힌 필드는 db 컬럼에 매핑하지 않는다.

@Column

@Column은 필드와 컬럼에 더 섬세한 설정을 할 때 사용할 수 있는 속성들이 있다.

@Column 속성은 2가지로 나눌 수 있다.

  1. 애플리케이션 로직에 영향을 주는 속성
    name, insertable, updatable, table

  2. DDL 생성 기능에 영향을 주는 속성
    nullable, unique, columnDefinition, length, precision, scale

각 항목의 기능은 아래 정리에 작성하였다.

정리

나중에 보기 쉽게 깔끔하게 정리해보자.

어노테이션설명
@Column컬럼 매핑
@Temporal날짜 타입 매핑
@Enumeratedenum 타입 매핑
@LobBLOB, CLOB 매핑
@Transient특정 필드를 컬럼에 매핑하지 않음(매핑 무시)
@AccessJPA가 엔티티에 접근하는 방식을 지정

주의!
자바 열거 타입을 매핑할 때는 반드시 STRING을 사용하자.
STRING은 ORDINAL과 달리 순서가 바껴도 문제되지 않는다.

속성기능기본값
name필드와 매핑할 테이블의 컬럼 이름 객체의 필드 이름
insertable, updatable등록, 변경 가능 여부TRUE
nullable(DDL)null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다.
unique(DDL)@Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다.
columnDefinition(DDL)데이터베이스 컬럼 정보를 직접 줄 수 있다.
ex) varchar(100) default ‘EMPTY'
필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입을 매핑
length(DDL)문자 길이 제약조건, String 타입에만 사용한다.255
precision, scale(DDL)BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다).
precision은 소수점을 포함한 전체 자릿수를 설정한다.
scale은 소수의 자릿수를 지정한다.
참고로 double, float 타입에는 적용되지 않는다.
아주 큰 숫자나 정밀한 소수를 다루어야 할 경우만 사용한다.
precision=19, scale=2

기본 키 매핑

JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같다.

  • 직접 할당
    기본 키를 애플리케이션에서 직접 할당한다.

  • 자동 생성
    대리 키 사용 방식으로 3가지 전략이 있다.

    • IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
    • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
    • TABLE: 키 생성 테이블을 사용한다.

자동 생성 전략이 다양한 이유!
데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다.

오라클 데이터베이스 : 시퀀스 제공
MySQL : AUTO_INCREMENT 제공

각각의 전략을 어떻게 사용하는지 알아보면서 @GeneratedValue 어노테이션도 살펴보자.

기본 키 적용 가능 타입

기본 키 적용 가능한 타입은 아래와 같다.

  • 자바 기본형
  • 자바 래퍼(Wrapper)형
  • String
  • java.util.Date
  • java.sql.Date
  • java.math.BigDecimal
  • java.math.BigInteger

기본 키 적용 방법

@Id 어노테이션을 통해서 기본 키 필드를 지정한다.

사용 예

@Id
private Long id;

기본 키 할당 전략

JPA가 지원하는 기본 키 할당 전략은 크게 2가지로 나뉜다.

  1. 직접 할당
  2. 자동 생성

그리고 자동 생성 전략은 3가지 방식으로 또 나뉜다.

  1. IDENTITY
  2. SEQUENCE
  3. TABLE

각 전략에 대해 자세히 알아보자.

직접 할당 전략

기본 키를 직접 할당하기 위해선 @Id를 적용하면 된다.
적용 방법은 기본 키 적용 방법과 같다.

키 값을 지정하지 않으면 벌어지는 일!
키(식별자) 값 없이 저장하면 예외가 발생한다.
이때 발생하는 예외는 JPA 구현체의 예외이다. (JPA 표준에는 정의되어 있지 않다.)
하이버네이트를 사용할 경우 JPA 최상위 예외인 javax.persistence.PersistenceException이 발생한다.
이 예외는 하이버네이트의 org.hibernate.id.IdentifierGenerationException 예외를 포함한다.

IDENTITY 전략

자동 생성 전략에는 3가지 방식이 있다.
우선 IDENTITY 전략을 알아보자.

IDENTITY 전략은 기본 키 생성을 데이터베이스에 위임하는 전략이다.

주로 MySQL, PostgreSQL, SQL Server, DB2에 사용한다.

사용 예는 아래와 같다.

@Entity
public class Board {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  ...
}
private static void logic(EntityManager em) {
  Board board = new Board();
  em.persist(board);
  System.out.println("board.id = " + board.getId());
}
// 출력 : board.id = 1

참고

IDENTITY 전략과 최적화
IDENTITY 전략은 데이터를 데이터베이스 INSERT한 후에 기본 키 값을 조회할 수 있다.
따라서 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 한다.
JDBC3에 추가된 Statement.getGenratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성된 키 값도 얻어 올 수 있다.
하이버네이트는 이 메소드를 사용해서 데이터베이스와 한 번만 통신한다.

주의

IDENETITY 전략은 쓰기 지연 불가
엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다.
그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다.
따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다.

주로 오라클, PostgreSQL, DB2, H2 데이터에빙스에서 사용할 수 있다.

사용 예는 아래와 같다.

@Entity
@SequenceGenerator (
  name = "BOARD_SEQ_GENERATOR",
  sequenceName = "BOARD_SEQ", //매핑할 데이터베이스 시퀀스 이름 
  initialValue = 1, allocationSize = 1)
public class Board {
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE,
                  generator = "BOARD_SEQ_GENERATOR")
  private Long id;
  ...
}

@SequenceGenerator(...)를 통해서 시퀀스 생성기를 등록한다.
그리고 @GeneratedValue()에 키 생성 전략을 GeneratedType.SEQUENCE로 설정하고 generator = SEQUENCE_NAME을 통해 시퀀스 생성기를 할당한다.

private static void logic(EntityManager em) {
  Board board = new Board();
  em.persist(board);
  System.out.println("board.id = " + board.getId());
}
// 출력 : board.id = 1

@SequenceGenerator 속성 정리

속성기능기본값
name식별자 생성기 이름필수
sequenceName데이터베이스에 등록되어 있는 시퀀스 이름hibernate_sequence
initialValueDDL 생성 시에만 사용되고, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정1
allocationSize시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)50
catalog, schema데이터베이스 catalog, schema 이름

참고

SEQUNCE 전략과 최적화
SEQUENCE 전략은 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요하다.
따라서 데이터베이스와 2번 통신한다.

  1. 식별자를 구하려고 데이터베이스 시퀀스를 조회한다.
  2. 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장한다.

JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequenceGenerator.allocationSize를 사용한다.
allocationSize 값만큼 시퀀스를 증가시키고 애플리케이션 메모리에서 식별자를 할당한다.

예를들면 allocationSize 값이 10이면 시퀀스를 한 번에 10 증가시킨 다음에 1~10까지는 메모리에서 식별자를 할당한다.

IDENTITY 방식과 SEQUENCE 방식의 차이
SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다.
그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다.
이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다.

IDENTITY 방식은 엔티티를 데이터베이스에 저장한 후 식별자를 조회해서 엔티티의 식별자에 할당한다.

TABLE 전략

TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스와 비슷한 전략이다.
이 방식은 테이블을 사용하므로 모든 데이터베이스에 적용할 수 있다.

사용 예

@Entity
@TableGenerator(
  name = "BOARD_SEQ_GENERATOR",
  table = "MY_SEQUENCES",
  pkColumnValue = "BOARD_SEQ", allocationSize = 1)
public class Board {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE,
                  generator = "BOARD_SEQ_GENERATOR")
  private Long id;
  ...
}

@TableGenerator를 사용해서 테이블 키 생성기를 등록한다.
그리고 @GeneratedValue에서 키 생성 전략을 TABLE 방식으로 지정하고 생성기를 등록해준다.

private static void logic(EntityManager em) {
  Board board = new Board();
  em.persist(board);
  System.out.println("board.id = " + board.getId());
}
// 출력 : board.id = 1
속성기능기본값
name식별자 생성기 이름필수
table키생성 테이블명hibernate_sequences
pkColumnName시퀀스 컬럼명sequence_name
valueColumnName시퀀스 값 컬럼명next_val
initialValue초기 값, 마지막으로 생성된 값이 기준이다.0
allocationSize시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)50
catalog, schema데이터베이스 catalog, schema 이름
uniqueConstraints(DDL)유니크 제약 조건을 지정할 수 있다.

참고

Table 전략과 최적화
TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용한다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점이 있다.
TABLE 전략을 최적화하려면 SEQUENCE 방식처럼 allocationSize를 지정하면 된다.

권장하는 식별자 전략

식별자는 어떤 값으로 사용해야될까

데이터베이스 키 조건

일단 데이터베이스의 기본 키 조건을 확인해보자.

  • null 값은 허용하지 않는다.
  • 유일해야 한다.
  • 변해선 안 된다.

키 선택 전략

테이블의 기본 키를 선택하는 전략은 크게 2가지가 있다.

  • 자연 키(natural key)
    비즈니스에 의미가 있는 키
    예 : 주민등록번호, 이메일, 전화번호

  • 대리 키(surrogate key)
    비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불림
    예 : 시퀀스, auto_increment, 키 생성 테이블

그래서 식별자는 뭐로 해?

결론은 자연 키보다는 대리 키를 권장한다.
비즈니스 환경은 언젠가 변할 수 있기 때문에 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다.

궁금한 점

  • @Entity 적용 시 주의사항 항목들의 이유가 궁금해요.
profile
주 7일, 배움엔 끝이 없다

0개의 댓글