자바 ORM 표준 JPA 프로그래밍 - 엔티티 매핑

Song Chae Won·2023년 6월 4일
0
post-thumbnail

📝 엔티티 매핑

JPA를 사용하는데 가장 중요한 것은 엔티티와 테이블을 정확하게 매핑하는 것이다. 실제로 이펍 과제를 하면서 다양한 엔티티를 사용해보았고, 적절하지 않은 엔티티를 매핑하면서 오류가 발생하여, 빨간줄이 뜨지 않아도 스프링부트가 종료되는 경험을 하였다. 이번 스터디로 제대로 알아보자!

크게 4가지로 분류를 하였을 때,

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

이번 기술 블로그에는 객체와 테이블 매핑, 기본 키 매핑, 필드와 컬럼 매핑에 대해 알아보고 연관관계 매핑은 5,6,7장에 걸쳐서 설명할 것이다.

4.1 @Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다. @Entity에는 name 속성이 있는데 설정하지 않을 경우에는 기본적으로 클래스 이름을 그대로 사용한다(예: Member). 하지만 만약 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다.

  • Entity는 기본 생성자가 필수이다.
  • final 클래스, enum, interface, inner 클래스에는 사용할 수 없다. - 저장할 필드에 final을 사용하면 안된다.

JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하기 때문에, 이 생성자는 반드시 있어야 하는데 만약 생성자가 하나도 없는 클래스는 자바가 기본 생성자를 자동으로 만든다.

public void Member() {} // 기본 생성자

임의의 생성자를 만들게 되면 자바는 기본 생성자를 자동으로 만들지 않기 때문에, 임의의 생성자를 만들 경우에는 기본 생성자를 직접 더 만들어야한다!

public Member() {} //직접 만든 기본 생성자

//임의의 생성자
public Member(String name){
this.name = name;
}

4.2 @Table

@Table은 엔티티와 매핑할 테이블을 지정한다. 만약에 어노테이션을 생략한다면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.

🔻 @Table 속성 정리

4.3 다양한 매핑 사용

JPA 시작하기 장에서 개발하던 회원 관리 프로그램에 다음 요구사항이 추가 되었다.

1) 회원은 일반 회원과 관리자로 구분됩니다.
2) 회원 가입일과 수정일이 있어야 합니다.
3) 회원을 설명할 수 있는 필드가 있어야 합니다. 이 필드는 길이 제한이 없습니다.

회원 엔티티에 기능을 추가하면,

@Entity
@Table(name = "MEMBER") // Member 엔티티를 MEMBER로 매핑
public class Member

@Id 
@Column(name = "ID")
private String id;

@Column(name = "NAME")
private String username;

private Integer age;

// 기능 추가
@Enumerated(EnumType.STRING)
private RoleType roleType;

@Temporal(TemporalType.TIMESTAMP)
private Date createDate;

@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;

@Lob
private String description;

// Getter, Setter ...
}

public enum RoleType{
    ADMIN, USER
}

application.yml 파일에

spring:
  jpa:
    hibernate:
      ddl-auto: create

속성을 추가한다. 이 속성을 추가하면 애플리케이션 실행 시점에 DB 테이블을 자동으로 생성한다.

객체와 테이블을 매핑하는데 아직 익숙치 않기에 '데이터베이스 스키마 자동 생성'을 적극 활용해보자. 이 기능을 사용해서 생성된 DDL을 보면 엔티티와 테이블이 어떻게 매핑되는지 쉽게 이해할 수 있다.

🔻 hibernate.hbm2ddl.auto 속성

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

데이터베이스 스키마 자동 생성을 사용해서 엔티티만 만들고 테이블은 자동 생성 되도록 해보자. JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다. 클래스의 매칭정보를 보면 어떤 테이블에 어떤 칼럼을 사용하는지 알 수 있다. JPA는 이 매핑 정보와 DB 방언을 사용하여 스키마를 생성한다.

4.5 DDL 생성 기능

회원 이름은 필수로 입력되어야 하고, 10자를 초과하면 안 되는 제약조건이 추가되었다. 스키마 자동 생성하기를 통해 만들어지는 DDL에 이 제약조건을 추가해보자.

@Entity
@Table(name = "MEMBER")
public class Member{

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10) // 추가
    private String username;
}

위 코드에서 @Column 매핑정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약조건을 추가할 수 있다. 그리고 length 속성 값을 사용하면 자동 생성되는 DDL에 문자의 크기를 지정할 수 도 있다.

이 기능을 사용했을 때 장점은 애플리케이션 개발자가 엔티티만 보고도 손쉽게 다양한 제약조건을 파악할 수 있다. JPA에는 애플리케이션의 실행 동작에는 영향을 주지 않지만, 자동 생성되는 DDL을 위한 기능이 있다.

4.6 기본 키 매핑

🔻 JPA가 제공하는 데이터베이스 기본 키 생성 전략

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

자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문! 오라클은 시퀀스를 제공하지만, MySQL은 시퀀스를 제공하지 않고 대신에 기본 키 값을 자동으로 채워주는 AUTO_INCREMENT 기능을 제공한다. 따라서 SEQUENCE나 IDENTITY 전략은 사용하는 데이터베이스에 의존한다.

4.6.1 기본 키 직접 할당 전략

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

@Id
@Column(name = "id")
private String id;

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

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

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

Board board = new Board();
board.setId("id1");   // 기본 키 직접 할당
em.persist(board);

기본 키 직접 할당 전략에서 식별자 값 없이 저장하면 예외가 발생하는데, 어떤 예외가 발생하는지 JPA 표준에는 정의되어 있지 않다. 하이버네이트를 구현체로 사용하면 JPA 최상위 예외인 javax.persistence.PersistenceException 예외가 발생하는데, 내부에 하이버네이트 예외인 org.hibernate.id.IdentifierGenerationException 예외를 포함하고 있다.

4.6.2. IDENTITY 전략

IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다. 예를 들어 MySQL의 AUTO_INCREMENT 기능은 데이터베이스가 기본 키를 자동으로 생성해준다. MySQL의 AUTO_INCREMENT 기능을 수행하는 다음 예제를 보자.

CREATE TABLE BOARD (
  ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  DATA VARCHAR(255)
);

INSERT INTO BOARD(DATA) VALUES('A');
INSERT INTO BOARD(DATA) VALUES('B');

테이블을 생성할 때 기본 키 컬럼인 ID에 AUTO_INCREMENT를 추가했다. 이제 데이터베이스에 값을 저장할 때 ID 컬럼을 비워두면 데이터베이스가 순서대로 값을 채워준다.

IDENTITY 전략은 지금 설명한 AUTO_INCREMENT를 사용한 예제처럼 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다.

개발자가 엔티티에 직접 식별자를 할당하면 @Id 어노테이션만 있으면 되지만 지금처럼 식별자가 생성되는 경우에는 @GeneratedValue 어노테이션을 사용하고 식별자 생성 전략을 선택해야 한다. IDENTITY 전략을 사용하려면 @GenereatedValue의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다. 이 전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회한다.

@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

위의 예제의 사용 코드를 보면 em.persist()를 호출해서 엔티티를 저장한 직후에 할당된 식별자 값을 출력했다. 출력된 값 1은 저장 시점에 데이터베이스가 생성된 값을 JPA가 조회한 것이다.

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

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

4.6.3. 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_GENERATOR",
    sequenceName = "BOARD_SEQ",  // 매핑할 데이터베이스 시퀀스 이름
    initialValue = 1, allocationSize = 1)
private class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = "BOARD_SEQ_GENERATOR")
    private Long id;
    ...
}

우선 사용할 데이터베이스 시퀀스를 매핑하고, 키 생성 전략을 Generation.SEQUENCE로 설정하고 generator = "BOARD_SEQ_GENERATOR"로 방금 등록한 시퀀스 생성기를 선택했다. 이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.

@SequenceGenerator

🚨 주의할 점
SequenceGenerator.allocationSzie의 기본값이 50인 것에 주의해야 한다. JPA가 기본으로 생성하는 데이터베이스 시퀀스는 create sequence [sequenceName] start with 1 increment by 50 이므로 시퀀스를 호출할 때마다 값이 50씩 증가한다. 기본값이 50인 이유는 최적화 때문이다. 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다.

4.6.4 TABLE 전략

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

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

sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용한다.

@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를 사용해서 테이블 키 생성기를 등록한다. 여기서는 BOARD_SEQ_GENERATOR라는 이름의 테이블 키 생성기를 등록하고 방금 생성한 MY_SEQUENCES 테이블을 키 생성용 테이블로 매핑했다. 다음으로 TABLE 전략을 사용하기 위해 GenerationType.TABLE을 선택했다. 그리고 @GeneratedValue.generator에 방금 만든 테이블 키 생성기를 지정했다. 이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당

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

위의 예제의 TABLE 전략을 매핑한 사용 코드를 보자. TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작방식이 같다.

@TableGenerator

4.6.5 AUTO 전략

데이터베이스의 종류도 많고 기본 키를 만드는 방법도 다양하다. GenerationType.AUTO는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다. 예를 들어 오라클을 선택하면 SEQUENCE를, MySQL을 선택하면 IDENTITY를 사용한다.

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

@GeneratedValue.strategy의 기본값은 AUTO다.

AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다. 특히 키 생성 전략이 아직 확정되지 않은 개발 초기 단계나 프로토타입 개발 시 편리하게 사용할 수 있다.

AUTO를 사용할 때 SEQUENCE나 TABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다. 만약 스키마 자동 생성 기능을 사용한다면 하이버네이트가 기본값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 줄 것이다.

4.6.6 기본 키 매핑 정리

  • 직접 할당 : em.persist()를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외가 발생한다.

  • SEQUENCE : 데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.

  • TABLE : 데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.

  • IDENTITY : 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다(IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다).

➕ 권장하는 식별자 선택 전략
1. null 값은 허용하지 않는다.
2. 유일해야 한다.
3. 변해선 안 된다.

🔻 테이블의 기본 키를 선택하는 전략
자연 키(natural key)

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

대리 키(surroage key)

  • 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다.

  • 예 : 오라클 시퀀스, auto_increment, 키생성 테이블 사용

  • 자연 키보다는 대리 키를 권장한다

  • 비즈니스 환경은 언젠가 변한다

  • JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다

🚨 주의할 점
기본 키는 변하면 안된다는 기본 원칙으로 인해, 저장된 엔티티의 기본 키 값은 절대 변경하면 안 된다. 이 경우 JPA는 예외를 발생시키거나 정상 동작하지 않는다. setId() 같이 식별자를 수정하는 메소드를 외부에 공개하지 않는 것도 문제를 예방하는 하나의 방법이 될 수 있다.

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

JPA가 제공하는 필드와 컬럼 매핑용 어노테이션들을 레퍼런스 형식으로 정리

4.7.1 @Column

@Column은 객체 필드를 테이블 컬럼에 매핑한다. 가장 많이 사용되고 기능도 많다. 속성 중에 name, nullable이 주로 사용되고 나머지는 잘 사용되지 않는 편이다.

insertable, updatable 속성은 데이터베이스에 저장되어 있는 정보를 읽기만 하고 실수로 변경하는 것을 방지하고 싶을 때 사용한다.


DDL 생성 속성에 따라 어떤 DDL이 생성되는지 확인해보자.

🔻 nullable(DDL 생성 기능)

@Column(nullable = false)
private String data;

// 생성된 DDL
data varchar(255) not null

🔻 unique(DDL 생성 기능)

@Column(unique = true)
private String username;

// 생성된 DDL
alter table Tablename
    add constraint UX_Xxx unique (username)

🔻 columnDefinition(DDL 생성 기능)

@Column(columnDefinition = "varchar(100) default 'EMPTY'")
private String data;

// 생성된 DDL
data varchar(100) default 'EMPTY'

🔻 length(DDL 생성 기능)

@Column(length = 400)
private String data;

// 생성된 DDL
data varchar(400)

🔻precision, scale(DDL 생성 기능)

@Column(precision = 10, scale = 2)
private BigDecimal cal;

// 생성된 DDL
cal numeric(10,2)  // H2, PostgreSQL
cal number(10,2)   // 오라클
cal decimal(10,2)  // MySQL

4.7.2 @Enumerated

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

EnumType.ORIGINAL은 enum에 정의된 순서대로 ADMIN은 0, USER는 1 값이 데이터베이스에 저장된다.

  • 장점 : 데이터베이스에 저장되는 데이터 크기가 작다.
  • 단점 : 이미 저장된 enum의 순서를 변경할 수 없다.

EnumType.STRING은 enum 이름 그대로 ADMIN은 'ADMIN' USER는 'USER'라는 문자로 데이터베이스에 저장된다.

  • 장점 : 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다.
  • 단점 : 데이터베이스에 저장되는 데이터 크기가 ORIGINAL에 비해서 크다.

4.7.3 @Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다. 아래 표에 속성을 정리했다.

자바의 Date 타입에는 년월일 시분초가 있지만 데이터베이스에는 date(날짜), time(시간), timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재한다.

@Temporal을 생략하면 자바의 Date와 가장 유사한 timestamp로 정의된다. 하지만 timestamp 대신에 datetime을 예약어로 사용하는 데이터베이스도 있는데 데이터베이스 방언 덕분에 애플리케이션 코드는 변경하지 않아도 된다.

4.7.4 @Lob

@Lob에는 지정할 수 있는 속성이 없다. 대신에 매핑하는 필드 타입이 문자면 CLOB으로 매핑하고 나머지는 BLOB으로 매핑한다.

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

4.7.5 @Transient

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

4.7.6 @Access

JPA가 엔티티 데이터에 접근하는 방식을 지정한다.

  • 필드 접근 : AccessType.FIELD로 지정한다. 필드에 직접 접근한다. 필드 접근 권한이 private이어도 접근할 수 있다.
  • 프로퍼티 접근 : AccessType.PROPERTY로 지정한다. 접근자(Getter)를 사용한다.

@Access를 설정하지 않으면 @Id의 위치를 기준으로 접근 방식이 설정된다.

profile
@chhaewxn

0개의 댓글