[JPA 기본편] 3. 엔티티 매핑

HJ·2024년 2월 22일
0

JPA 기본편

목록 보기
3/10
post-thumbnail

김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.


1. 엔티티 매핑

엔티티 매핑에 필요한 어노테이션들은 아래와 같습니다.

  1. 객체와 테이블 매핑 : @Entity, @Table

  2. 필드와 컬럼 매핑 : @Column

  3. 기본 키 매핑 : @Id

  4. 연관관계 매핑 : @ManyToOne, @JoinColumn




2. 객체와 테이블 매핑

2-1. @Entity

@Entity 가 붙은 클래스는 JPA 가 관리하게 되며, 엔티티라고 합니다. JPA 를 사용해서 테이블과 매핑할 클래스는 @Entity 가 필수적으로 붙어야 합니다.

JPA 는 객체를 프록싱하는 등의 기능이 있기 때문에 파라미터가 없는 public 이나 protected 기본 생성자가 필수로 필요합니다.

final 클래스, inner 클래스, interface, enum 에는 사용할 수 없고, DB 에 저장하고 싶은 필드에 final 키워드를 사용하면 안됩니다.

속성기능기본값
nameJPA 에서 사용할 엔티티 이름을 지정합니다. JPA 가 내부적으로 사용하는 이름이며 같은 클래스 이름이 없으면 가급적 기본값을 사용합니다. 만약 다른 패키지에 같은 클래스 이름이 있다면 name 속성을 지정해주어야 합니다.클래스 이름을 그대로 사용

2-2. @Table

@Table 은 엔티티와 매핑할 테이블 지정합니다.

속성기능기본값
name매핑할 테이블 이름엔티티 이름을 사용
catalogDB catalog 매핑
schemaDB schema 매핑
uniqueConstraints( DDL )DDL 생성 시에 유니크 제약 조건 생성

[ 사용 예시 ]

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




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

JPA 는 매핑 정보만 보면 어떤 테이블인지, 어떤 쿼리를 만들어야 하는지 다 알 수 있습니다. 그래서 JPA 는 객체를 만들고 매핑을 하면 애플리케이션 로딩 시점에 DB 테이블을 생성하는 기능을 지원해줍니다.

이떄 JPA 는 데이터베이스 방언을 활용해서 DB 에 맞는 적절한 DDL 을 생성합니다. 예를 들어, hibernate.dialect 를 Oracle 로 설정하면 String 을 varchar2 로 생성하고, MySQL 로 지정하면 varchar 로 생성합니다.

이렇게 생성된 DDL 은 꼭 개발에서만 사용해야 하는데, 운영에서 create, create-drop, update 를 사용하면 안됩니다.

[ hibernate.hbm2ddl.auto ]

옵션설명
create기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
create-dropcreate와 같지만 종료시점에 테이블 DROP (DROP + CREATE + DROP)
updateDB 테이블과 엔티티를 비교해서 변경분만 반영(DROP 하지 않는다)
validate엔티티와 테이블이 정상 매핑되었는지만 확인해서 차이가 존재하면 오류 메세지가 출력되며 애플리케이션이 실행되지 않습니다
none사용하지 않음. none 이라는 옵션은 없지만 관례 상 none 이라고 적고, 매칭되는 옵션이 없기 때문에 실행되지 않습니다




4. 필드와 컬럼 매핑

컬럼 매핑에 사용되는 어노테이션들은 아래와 같습니다.

  1. 컬럼 매핑 : @Column

  2. 날짜 타입 매핑 : @Temporal ( 스프링에서 LocalDateTime 타입을 사용하면 자동으로 날짜 타입이 됩니다 )

  3. enum 타입 매핑 : @Enumrated

  4. BLOB, CLOB 매핑 : @Lob

  5. 특정 필드를 컬럼에 매핑하지 않음( 매핑 무시 ) : @Transient


4-1. 컬럼 매핑

  • name : 필드와 매핑할 테이블의 컬럼 이름 ( 기본값 : 객체의 필드 이름 )

  • length : 문자 길이 제약 조건으로 String 타입에만 사용합니다.

  • nullable : null 값의 허용 여부를 설정합니다. false 로 설정하면 DDL 생성 시에 not null 제약 조건이 붙습니다. ( 기본값 : true )

  • insertable / updatable : 컬럼 값을 수정했을 때, DB 에 저장, 혹은 수정할 것인지에 대한 여부를 지정합니다. ( 기본값 : true )

  • unique

    • @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용합니다.

    • 복잡적으로 사용할 수 없고 하나의 컬럼에만 사용할 수 있습니다.

    • 제약조건의 이름이 알아볼 수 없게 저장되기 때문에 잘 사용하지 않습니다. ( @Table 에서 지정하면 이름 지정과 복잡 지정이 가능 )

  • columnDefinition

    • 데이터베이스 컬럼 정보를 직접 줄 수 있습니다.

    • ex> columnDefinition = "varchar(100) default 'EMPTY'"

  • percision / scale

    • BigDecimal, BigInteger 타입에서 사용합니다. (double, float 에는 적용되지 않습니다)

    • percision 은 소수점을 포함한 전체 자리수를 나타냅니다

    • scale 은 소수의 자리수를 나타냅니다.


4-2. 타입에 따른 매핑

[ 날짜 타입 ]

@Temporal 은 Calendar 와 같은 날짜 타입을 매핑할 때 사용합니다.
만약 LocalDate, LocalDateTime 을 사용하면 어노테이션 자체를 생략할 수 있습니다.
속성은 value 가 있습니다.

속성설명
TemporalType.DATE날짜, 데이터베이스 date 타입과 매핑
TemporalType.TIME시간, 데이터베이스 time 타입과 매핑
TemporalType.TIMESTAMP날짜와 시간, 데이터베이스 timestamp 타입과 매핑

[ Enum 타입 ]

자바 Enum 타입을 매핑할 때 @Enumrated 를 사용합니다. 속성은 value 가 있습니다.

속성설명
EnumType.ORDINALenum 순서를 0부터 시작해서 데이터베이스에 저장( 기본값 )
EnumType.STRINGenum 이름을 데이터베이스에 저장( 사용 권장 )

[ 큰 타입 ]

varchar 를 넘어가는 큰 컨텐츠를 넣고 싶은 경우에 @Lob를 사용합니다. 속성은 존재하지 않습니다. 매핑하는 필드 타입이 String, char[] 와 같은 문자이면 CLOB 매핑, 나머지는 BLOB 를 매핑합니다.


[ 임시 타입 ]

DB 랑 관계없이 메모리에서 계산하고 싶은 필드에 @Transient 를 사용합니다. 해당 어노테이션을 붙이면 필드가 매핑되지 않아 DB 에 저장되지 않습니다.




5. 기본 키 매핑

5-1. 기본 키 매핑 어노테이션

JPA가 제공하는 데이터베이스 기본 키 매핑 어노테이션은 아래와 같습니다.

  • 직접 할당 : 기본 키를 애플리케이션에서 직접 할당할 때 @Id 를 사용합니다.

  • 자동 생성 : 데이터베이스가 값을 자동으로 할당해줄 때는 @GeneratedValue 를 사용합니다.

    • IDENTITY : 데이터베이스에 위임 ( MySQL )

    • SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, @SequenceGenerator 지정 가능 ( Oracle )

    • TABLE : 키 생성용 테이블 사용, 모든 DB 에서 사용하며 @TableGenerator 지정 가능

    • AUTO : 데이터베이스 방언에 따라 자동으로 지정하며, 기본값입니다.


5-2. IDENTITY 전략

기본키 생성을 데이터베이스에 위임하는 전략입니다. 주로 MySQL, PostgreSQL, MS SQL Server 에서 사용합니다. MySQL 에서는 AUTO_INCREMENT 가 있는데 null 로 집어넣으면 DB가 알아서 순차적으로 올라가도록 값을 세팅해줍니다.

JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행하는데 IDENTITY 는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있습니다.

엔티티가 영속 상태가 되려면( 1차 캐시에 저장되려면 ) 식별자가 필요하기 때문에 IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회합니다.


[ SEQUENCE 사용 ]

========= persist member1 이전 호출
Hibernate: 
    select 
        next value for MEMBER_SEQ
========= persist member1 이후 호출
========= transaction commit
Hibernate: 
    insert into
        Member (name, id) 
    values
        (?, ?)

SEQUENCE 를 사용하면 persist() 를 호출해도 Insert 쿼리가 수행되지 않고, 트랜잭션이 커밋될 때 INSERT 쿼리가 수행되는 것을 확인할 수 있습니다.

[ IDENTITY 사용 ]

========= persist member1 이전 호출
Hibernate: 
    insert into
        Member (name, id) 
    values
        (?, default)
========= persist member1 이후 호출
========= transaction commit

하지만 IDENTITY 는 트랜잭션이 커밋되지 않아도 persist() 를 호출하면 바로 INSERT 쿼리가 수행되는 것을 확인할 수 있습니다.


5-3. SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트입니다. 주로 Oracle, PostgreSQL, H2 에서 사용합니다.

create sequence Member_SEQ start with 1 increment by 50

웹 애플리케이션을 실행시키면 위와 같은 로그가 출력되는데 create sequence 를 통해 시퀀스를 만들어내는 것을 확인할 수 있습니다.


[ @SequenceGenerator ]

@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;
    ...
}

테이블마다 다른 시퀀스를 사용하고 싶은 경우, @SequenceGenerator 를 통해서 시퀀스를 생성하고, @GeneratedValue 에 generator 속성에 매핑해서 사용할 수 있습니다.

속성기능기본값
name식별자 생성기 이름으로써 필수값입니다
sequenceNameDB 에 등록되어 있는 시퀀스 이름
initValueDDL 생성 시에만 사용되며, 시퀀스 DDL 을 처음 생성할때 처음 1 시작하는 수를 지정합니다1
allocationSize시퀀스 한 번 호출에 증가하는 수50

참고로 위처럼 지정하고 실행하면 allocationSize 에 의해 로그가 아래처럼 변하게 됩니다.

create sequence MEMBER_SEQ start with 1 increment by 1


[ 동작 확인 ]

Member member = new Member();
member.setName("HelloA");
System.out.println("========= persist member1");
em.persist(member);
System.out.println("========= member1 id = " + member.getId());

Member member2 = new Member();
member2.setName("HelloB");
System.out.println("========= persist member2");
em.persist(member2);
System.out.println("========= member2 id = " + member2.getId());

System.out.println("========= transaction commit");
tx.commit();
========= persist member1
Hibernate: 
    select
        next value for MEMBER_SEQ
========= member1 id = 1

========= persist member2
Hibernate: 
    select
        next value for MEMBER_SEQ
========= member1 id = 2

========= transaction commit
Hibernate: 
    insert into Member (name, id) values (?, ?)
Hibernate: 
    insert into Member (name, id) values (?, ?)

위의 로그를 보면 persist() 를 호출하면 데이터베이스 시퀀스를 사용해서 식별자를 먼저 조회하는 것을 확인할 수 있습니다.

그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장하기 때문에 persist() 호출 후에 id 값을 확인해보면 정상적으로 출력되는 것을 확인할 수 있습니다.

Insert 쿼리는 트랜잭션 커밋이 호출되었을 때 실행되게 됩니다.


5-4. TABLE 전략

TABLE 전략은 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략입니다. 모든 데이터베이스에 적용 가능하다는 장점이 있지만 락이 걸릴 수도 있는 등 성능이 좋지 않습니다.

@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;
    ...
}

시퀀스와 동일하게 어노테이션으로 generator 를 지정하고, @GeneratedValue 에 매핑하는 방식을 사용합니다.

Hibernate: 
    create table MY_SEQUENCES (
        next_val bigint,
        sequence_name varchar(255) not null,
        primary key (sequence_name)
    )
Hibernate: 
    insert into MY_SEQUENCES(sequence_name, next_val) values ('MEMBER_SEQ', 0)

애플리케이션을 실행하면 JPA 가 테이블을 생성해주면서 0 값을 넣어주게 됩니다.
그 뒤에 데이터를 INSERT 하게 되면 1부터 시작되는 PK 를 갖게 됩니다.

속성기능기본값
name식별자 생성기 이름으로써 필수값입니다
table키 생성 테이블명
pkColumnName시퀀스 컬럼명sequence_name
valueColumnName시퀀스 값 컬럼명next_val
pkColumnValue키로 사용할 값 이름엔티티 이름
initValue초기값, 마지막으로 생성된 값이 기준입니다.0
allocationSize시퀀스 한 번 호출에 증가하는 수50

5-5. AUTO 전략

AUTO 전략이 기본값입니다. 데이터베이스 방언에 맞춰서 자동으로 생성되는 전략입니다. 위의 3개 중에 선택되어 사용됩니다.

Oracle 의 경우에는 SEQUENCE 전략을 사용하는데 DB 가 자동으로 숫자값을 생성해줍니다.


5-6. allocationSize

========= persist member1
Hibernate: 
    select
        next value for MEMBER_SEQ

========= persist member2
Hibernate: 
    select
        next value for MEMBER_SEQ

========= transaction commit
Hibernate: 
    insert into Member (name, id) values (?, ?)
Hibernate: 
    insert into Member (name, id) values (?, ?)

SEQUENCE 를 보면 위와 같은 로그가 있습니다. SEQUENCE 전략은 persist() 를 호출하면 데이터베이스 시퀀스를 조회해서 영속성 컨텍스트에 저장합니다.

여기서 데이터베이스 시퀀스를 조회하기 위해서 데이터베이스와 통신을 해야하고, 트랜잭션이 커밋될 때도 데이터베이스와 통신을 해야 하기 때문에 2번의 통신이 이루어져야 합니다.
이러한 것을 방지하고 성능을 높이기 위해 JPA 는 allocationSize 라는 속성을 제공합니다.

현재 시퀀스가 1이고, allocationSize 가 50이면 시퀀스를 조회할 때 DB의 시퀀스를 51로 높이게 됩니다. 그리고 메모리에서 1씩 사용하게 되면서 데이터베이스에 시퀀스 조회 쿼리가 날라가지 않게 됩니다.

시퀀스를 모두 소진하기 전에 어플리케이션이 종료될 경우, 나머지 시퀀스는 사라지며 다시 사용할 수 없는 상태가 됩니다. 만약 1-50번까지의 시퀀스 중 10번까지 사용하고 11-50번은 사용하지 않은 상태로 어플리케이션을 종료하고 다시 시작하면, 51번부터 시작하게 됩니다.

profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글