개발/JPA

[JPA] 엔티티 매핑

highright96 2021. 12. 8.

JPA 스터디를 진행하며 작성한 글입니다.

 

JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다. JPA는 다양한 매핑 어노테이션을 지원하는데 크게 4가지로 분류할 수 있다.

 

  • 객체와 테이블 매핑
  • 기본 키 맵핑
  • 필드와 컬럼 매핑
  • 연관관계 매핑

 

이 글에서는 객체와 테이블 매핑, 기본 키 매핑, 필드와 컬럼 매핑에 대해 알아보려 한다.

 

@Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다. @Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔티티라 부른다.

 

 

몇 가지 주의해야할 점이 있다.

 

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

 

@Table

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

 

 

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

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.

 

hibernate.ddl-auto 속성

 

application.yml

spring:
  jpa:
    generate-ddl: true
    hibernate:
      ddl-auto: create
    show-sql: true

 

hibernate.ddl-auto 주의사항

운영 서버에서 create, create-drop, update처럼 DDL을 수정하는 옵션은 절대 사용하면 안 된다.

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

 

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

 

기본 키 매핑

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

 

  • 직접 할당: 기본 키를 애플리케이션에 직접 할당한다.
  • 자동 생성: 대리 키 사용 방식
    • IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
    • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
    • TABLE: 키 생성 테이블을 사용한다.

 

기본 키 직접 할당 전략

기본 키를 직접 할당하려면 다음 코드와 같이 @Id로 매핑하면 된다.

기본 키 직접 할당 전략은 em.persist() 로 엔티티를 저장하기 전에 애플리케이션에서 기본 키를 직접 할당하는 방식이다.

 

@Id
private Long id;

 

@Id 적용 가능 자바 타입은 다음과 같다.

 

  • Primitive 타입
  • Wrapper 타입
  • String
  • java.util.Date
  • java.sql.Date
  • java.math.BigDecimal
  • java.math.BigInteger

 

IDENTITY 전략

IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다. 이 전략을 사용하려면 @GenerateValue의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다. 이 전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회한다.

 

@Entity
public class Board {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

 

엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다. 따라서 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달되므로, 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.

 

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오프젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다.

 

먼저 시퀀스를 생성해야 한다.

 

CREATE TABLE BOARD (
    ID BIGINT NOT NULL PRIMARY KEY,
    DATA VARCHAR (255)
)

// 시퀀스 생성
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;

 

시퀀스를 생성했으면 다음과 같이 매핑할 수 있다.

 

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

 

위와 같이 IDENTITY 전략과 SEQUENCE 전략이 같지만 내부 동작 방식은 다르다. SEQUENCE 전략은 em.persist() 를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다. 반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후에 식별자를 조회해서 엔티티의 식별자를 할당한다.

 

@SequenceGenerator 속성 정리

 

TABLE 전략

TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 모든 데이터베이스에 적용할 수 있다.

TABLE 전략을 사용하려면 먼저 키 생성 용도로 사용할 테이블을 만들어야 한다.

 

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

 

다음과 같이 테이블을 매핑할 수 있다.

 

@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 속성 정리

 

AUTO 전략

AUTO 전략은 키 생성 전략이 아직 확정되지 않은 개발 초기 단계나 프로토타입 개발 시 편리하게 사용할 수 있다.

 

@Entity
public class Board {
  
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

 

필드와 컬럼 매핑: 레퍼런스

@Column

객체 필드를 테이블 컬럼에 매핑한다.

 

@Column 속성 정리

 

@Enumerated

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

 

@Enumerated 속성 정리

EnumType.String을 사용하는 것이 좋다.

 

@Temporal

날짜 타입을 매핑할 때 사용된다.

 

@Lob

데이터베이스 CLOB, BLOB 타입과 매핑한다.

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

 

@Transient

이 필드는 매핑하지 않는다. 따라서 데이터베이스에 저장하지 않고 조회하지도 않는다. 객체에 임시로 어떤 값을 보관하고 싶을 때 사용한다.

 

기타 알게 된 사항

Spring Data JPA에서 기본 키 매핑 전략을 IDENTITY 혹은 AUTO로 설정하면, saveAll()을 수행하더라도 데이터의 개수만큼 save()를 반복한다. 즉, 한꺼번에 여러 개의 데이터를 넣는 것이 아니라 하나씩 넣게 된다.

 

만약 한꺼번에 Insert 또는 Update 해야 되는 엔티티의 경우 기본 키 매핑 전략을 SEQUENCE 또는 TABLE을 고려하는 것이 좋다.

 

하지만 이 중 Table 매핑 전략은 다음과 같이 번호 하나를 딸 때마다 쿼리를 2개씩 날리므로 비효율적이다.

 

Hibernate: 
    select
        tbl.next_val 
    from
        hibernate_sequences tbl 
    where
        tbl.sequence_name=? for update

Hibernate: 
    update
        hibernate_sequences 
    set
        next_val=?  
    where
        next_val=? 
        and sequence_name=?

 

참고로 SEQUENCE 매핑 전략은 다음과 같이 하나의 쿼리만 날린다.

 

Hibernate: 
    call next value for hibernate_sequence

 

그래서 SEQUENCE 전략을 가져갈 수 없는 데이터베이스일 때, 많은 양의 데이터를 한 번에 넣어야 하고 싶다면 Spring Date JDBC를 사용하는 것이 바람직하다.

 

@Repository
@RequiredArgsConstructor
public class ItemJdbcRepositoryImpl implements ItemJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    @Value("${batchSize}")
    private int batchSize;

    @Override
    public void saveAll(List<ItemJdbc> items) {
        int batchCount = 0;
        List<ItemJdbc> subItems = new ArrayList<>();
        for (int i = 0; i < items.size(); i++) {
            subItems.add(items.get(i));
            if ((i + 1) % batchSize == 0) {
                batchCount = batchInsert(batchSize, batchCount, subItems);
            }
        }
        if (!subItems.isEmpty()) {
            batchCount = batchInsert(batchSize, batchCount, subItems);
        }
        System.out.println("batchCount: " + batchCount);
    }

    private int batchInsert(int batchSize, int batchCount, List<ItemJdbc> subItems) {
        jdbcTemplate.batchUpdate("INSERT INTO ITEM_JDBC (`NAME`, `DESCRIPTION`) VALUES (?, ?)",
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, subItems.get(i).getName());
                        ps.setString(2, subItems.get(i).getDescription());
                    }
                    @Override
                    public int getBatchSize() {
                        return subItems.size();
                    }
                });
        subItems.clear();
        batchCount++;
        return batchCount;
    }
}

 

참고

'개발 > JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계 매핑  (0) 2021.12.14
[JPA] 연관관계 매핑 기초  (0) 2021.12.14
[JPA] 영속성 관리  (0) 2021.12.08
[JPA] JPA 소개  (0) 2021.12.08
[Querydsl] 검색 쿼리를 Querydsl로 구현하기(3)  (0) 2021.07.16

댓글