개발/Spring

JPA 엔티티 매핑 마스터하기: 기본부터 고급 전략까지

난중후니 2024. 1. 12. 16:13
728x90
반응형

Entity Mapping

  • JPA를 사용하는데 가장 중요한 일은 Entity와 Table을 정확하게 매핑하는 것이다.
  • JPA의 매핑 어노테이션은 크게 4가지로 나뉜다.
    • 객체(Entity) <-> 테이블(Table): @Entity, @Table
    • 기본 키(Primary Key): @Id
    • 필드(Field) <-> 컬럼(Column): @Column
    • 연관관계(FK, Foreign Key): @ManyToOne, @OneToMany, @ManyToMany, @OneToOne, @JoinColumn

Naming Strategy

  • Spring Data JPA 사용시 hibernate가 기본 구현체로 채택되어있고, hibernate 매핑시 ImprovedNamingStrategy를 기본 클래스로 사용하고 있다.
  • 소문자 -> 소문자
  • 대문자 -> 대문자
  • 카멜 케이스 -> 스네이크 케이스 (ex: DB의 book_name이라는 컬럼명을 Entity 클래스에서는 bookName으로 선언하여 사용)

@Entity

  • 이 어노테이션이 붙은 클래스는 JPA에서 관리하며 해당 클래스를 엔티티라고 한다.
  • JPA를 사용해서 테이블과 매핑할 클래스는 반드시 @Entity를 붙여야 한다.
  • 주의
    • 기본 생성자 필수!
      • 파라미터가 없는 public 또는 protected 생성자가 필요하다.
      • final, enum, interface, inner 클래스에서는 사용할 수 없다.
      • DB에 저장할 필드에 final을 사용할 수 없다.
      • 엔티티 객체는 내부적으로 Class.getDeclaredConstructor().newInstance()라는 자바 리플렉션을 사용하여 동작하는데 이 API는 생성자의 인수를 읽을 수 없기 때문에 인수가 존재하지 않는 기본 생성자가 반드시 필요하다.
      • 실무에서는 특별한 이유가 존재하지 않고서 기본 생성자를 열어두지 않는 방향, 즉, 제약적으로 개발하는게 일반적이므로 일반적으로 protected를 많이 사용한다.
@Entity
public class Member{
    @Id
    private Long id;
    private String name;

    protected Member(){}
}

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member{
    @Id
    private Long id;
    private String name;
}
  • Entity 속성
    • name
      • @Entity(name = "Member")
      • 기본값: 클래스 이름을 그대로 사용
      • 같은 클래스 이름이 없으면 가급적 기본값을 사용하도록 한다.

@Table

  • name
    • @Table(name ="Member")
    • 매핑할 테이블 이름을 지정한다.
    • 기본값: 엔티티 이름을 사용
/*
 * Copyright (c) 2008, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Linda DeMichiel - 2.1
//     Linda DeMichiel - 2.0

package javax.persistence;

import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Specifies the primary table for the annotated entity. Additional
 * tables may be specified using {@link SecondaryTable} or {@link
 * SecondaryTables} annotation.
 *
 * <p> If no <code>Table</code> annotation is specified for an entity 
 * class, the default values apply.
 *
 * <pre>
 *    Example:
 *
 *    &#064;Entity
 *    &#064;Table(name="CUST", schema="RECORDS")
 *    public class Customer { ... }
 * </pre>
 *
 * @since 1.0
 */
@Target(TYPE) 
@Retention(RUNTIME)
public @interface Table {

    /**
     * (Optional) The name of the table.
     * <p> Defaults to the entity name.
     */
    String name() default "";

    /** (Optional) The catalog of the table.
     * <p> Defaults to the default catalog.
     */
    String catalog() default "";

    /** (Optional) The schema of the table.
     * <p> Defaults to the default schema for user.
     */
    String schema() default "";

    /**
     * (Optional) Unique constraints that are to be placed on 
     * the table. These are only used if table generation is in 
     * effect. These constraints apply in addition to any constraints 
     * specified by the <code>Column</code> and <code>JoinColumn</code> 
     * annotations and constraints entailed by primary key mappings.
     * <p> Defaults to no additional constraints.
     */
    UniqueConstraint[] uniqueConstraints() default {};

    /**
     * (Optional) Indexes for the table.  These are only used if
     * table generation is in effect.  Note that it is not necessary
     * to specify an index for a primary key, as the primary key
     * index will be created automatically.
     *
     * @since 2.1
     */
    Index[] indexes() default {};
}
속성 기능 기본값
name JPA에서 사용할 엔티티 이름을 지정한다. 이후 JPQL등에서 이 이름을 사용한다. 프로젝트 전체에서 고유한 이름을 설정해야만 충돌로 인한 문제가 발생하지 않는다. 클래스 이름을 그대로 사용
catalog catalog 기능이 있는 DB에서 catalog를 매핑한다. -
schema schema 기능이 있는 DB에서 scheme를 매핑한다 -
uniqueConstraints DDL 생성 시 복합 유니크 인덱스를 생성한다. 설정파일의 DDL 옵션에 따라 동작한다.

DDL 옵션

  • create: Application 시작 시 모든 스키마를 삭제 후 새로 생성
  • create-drop: Application 시작 시 모든 스키마 삭제 후 생성, Application 종료 시 모든 스키마 삭제
  • update: Application 시작 시 기존 스키마에서 변경된 내역을 적용
  • validate: Application 시작 시 스키마가 제대로 매핑되는지 유효성 검사만 진행, 실패하면 Application을 종료
#application.yml
spring:
  jpa:
    hibernate:
      ddl-auto: crate-drop
  • DDL 옵션은 항상 신중하게 확인해야 한다!
  • 실무에서 이 옵션으로 인해 중요한 테이블이 통째로 드랍되거나, 중요 데이터가 삭제되는 등의 많은 사고가 발생한다. 실제 업무에서 어떤분이 Local에서 테스트를 위한 설정을 개발서버에 그대로 적용해 기존 Table에 있던 데이터가 모두 삭제 되었고, 기존 Table과 불일치 되어 난리가 난 경험이 있다.

@Id

  • 일반적으로 @Id를 붙인 필드는 테이블의 기본키(PK)와 매핑된다.
  • 다만, 물리적인 기본키(DB상의 PK)와 반드시 매핑될 필요는 없다. Hibernate 공식 문서에서는 @Id가 무조건 PK와 매핑될 필요가 없으며, 다만 식별할 수 있는 값이기만 하면 된다고 한다.
  • 영속성 컨텍스트(Persistence Context)에 저장되는 엔티티들이 @Id를 Key로 하여 HashMap으로 저장되는 특징 때문인 것으로 보인다.
  • Hibernate 공식 문서에는 @Id에 다음과 같은 타입을 지원한다고 한다.
    • 자바 기본타입(privmitive type): int, long, float, double 등..
    • 자바 Wrapper타입: Integer, Long 등..
    • String
    • javaUtil.Date
    • java.sqlDate
    • javamath.BigDecimal
    • java.math.BigInteger
  • 기본키를 사용하는 많은 방법이 존재하지만 실무에서는 일반적으로 primitive type은 사용하지 않는다.

왜냐하면 기본타입인 int를 id로 사용한다면 int에는 아무 값을 입력하지 않았을 때 자동으로 0이라는 값이

들어가게되는데 0이라는 값이 의도가 있어서 넣은 0인지, 자동으로 들어간 0인지 확인이 불가하기 때문이다.

하지만, 오브젝트 타입의 경우 nullable하기 때문에, 값을 초기화하지 않는다면 null로 초기화가 되어

0인 데이터가 들어있다면 의도적으로 넣은게 명확하다.

We recommend that you declare consistently-named identifier attributes

on persistent classes and that you use a nullable (ie, non-primitive) type

Hibernate 공식 문서

@GeneratedValue

  1. IDENTITY
  • 기본키 생성을 데이터베이스에게 위임하는 방식으로 id값을 따로 할당하지 않아도 데이터베이스가 자동으로 AUTO_INCREMENT를 하여 기본키를 생성해준다.
@Entity
public class Member{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}
  • JPA는 보통 영속성 컨텍스트에서 객체를 관리하다가 Commit이 호출되는 시점에 쿼리문을 실행하게 된다.
  • 하지만 IDENTITY 전략에서는 EntityManager.persist()를 하는 시점에 Insert SQL을 실행하여 데이터베이스에서 식별자를 조회해온다.
  • 그 이유는 영속성 컨텍스트는 1차 캐시에 PK와 객체를 가지고 관리를 하는데 기본키를 데이터베이스에게 위임했기 때문에 EntityManager.persist()를 호출 하더라도 데이터베이스에 값을 넣기 전까지 기본키를 모르고 있기 때문에 관리가 되지 않기 때문이다.
  • 따라서, IDENTITY 전략에서는 EntityManager.persist()를 하는 시점에 Insert SQL을 실행하여 데이터베이스에서 식별자를 조회하여 영속성 컨텍스트 1차 캐시에 값을 넣어주기 때문에 관리가 가능해진다.

2. SEQUENCE

  • 데이터베이스의 Sequence Object를 사용하여 데이터베이스가 자동으로 기본키를 생성해준다.
  • @SequenceGenerator 어노테이션이 필요하다.
@Entity
@SequenceGenerator(
    name = "MEMBER_PK_GENERATOR",
    sequenceName = "MEMBER_PK_SEQ",
    initailValue = 1,
    allocationSize = 50
)
public class Member{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="MEMBER_PK_GENERATOR")
    private Long id;

    private String name;
}
  • SEQUENCE 전략도 IDENTITY 전략과 동일한 문제가 있다. 데이터베이스가 직접 기본키를 생성해 주기 때문이다.
  • EntityManager.persist() 가 호출되기 전에 기본키를 가져와야 하므로 하이버네이트에서 hibernate: call next value for MEMBER_PK_SEQ을 실행하여 기본키를 가져온다.
  • 그 후에 EntityManager.persist()를 호출하기 때문에 IDENTITY 전략과 다르게 쿼리문을 실행하지 않는다.
  • 하지만 SEQUENCE 값을 계속 DB에서 가져와서 사용해야 하기 때문에 성능에 저하를 일으킬 수 있다.
  • 따라서 allocationSize의 크기를 적당히 설정하여 성능 저하를 개선시킬 수 있다.
  1. TABLE
  • 키를 생성하는 테이블을 사용하는 방법으로 Sequence와 유사하다.
  • @TableGenerator 어노테이션이 필요하다.
@Entity
@TableGenerator(
    name = "MEMBER_PK_GENERATOR",
    table = "MEMBER_PK_SEQ",
    pkColumnValue = "MEMBER_SEQ",
    allocationSize = 1
)
public class Member{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_PK_GENERATOR")
    private Long id;

    private String name;
}
  • TABLE 전략은 모든 데이터베이스에서 사용이 가능하지만 최적화 되어있지 않은 테이블을 사용하기 때문에 성능문제에 이슈가 있다.
  • 실무에서 거의 사용 X
  1. AUTO
  • 기본 설정 값으로 각 데이터베이스에 따라 기본키를 자동으로 생성한다.
  • Oracle - Sequence, MySQL - AUTO_INCREMENT
  • 기본키의 제약조건
    • NULL이면 안된다.
    • 유일하게 식별할 수 있어야 한다.
    • 변하지 않는 고유값이어야 한다.

@Column

/*
 * Copyright (c) 2008, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Linda DeMichiel - 2.1
//     Linda DeMichiel - 2.0


package javax.persistence;

import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Specifies the mapped column for a persistent property or field.
 * If no <code>Column</code> annotation is specified, the default values apply.
 *
 * <blockquote><pre>
 *    Example 1:
 *
 *    &#064;Column(name="DESC", nullable=false, length=512)
 *    public String getDescription() { return description; }
 *
 *    Example 2:
 *
 *    &#064;Column(name="DESC",
 *            columnDefinition="CLOB NOT NULL",
 *            table="EMP_DETAIL")
 *    &#064;Lob
 *    public String getDescription() { return description; }
 *
 *    Example 3:
 *
 *    &#064;Column(name="ORDER_COST", updatable=false, precision=12, scale=2)
 *    public BigDecimal getCost() { return cost; }
 *
 * </pre></blockquote>
 *
 *
 * @since 1.0
 */ 
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface Column {

    /**
     * (Optional) The name of the column. Defaults to 
     * the property or field name.
     */
    String name() default "";

    /**
     * (Optional) Whether the column is a unique key.  This is a 
     * shortcut for the <code>UniqueConstraint</code> annotation at the table 
     * level and is useful for when the unique key constraint 
     * corresponds to only a single column. This constraint applies 
     * in addition to any constraint entailed by primary key mapping and 
     * to constraints specified at the table level.
     */
    boolean unique() default false;

    /**
     * (Optional) Whether the database column is nullable.
     */
    boolean nullable() default true;

    /**
     * (Optional) Whether the column is included in SQL INSERT 
     * statements generated by the persistence provider.
     */
    boolean insertable() default true;

    /**
     * (Optional) Whether the column is included in SQL UPDATE 
     * statements generated by the persistence provider.
     */
    boolean updatable() default true;

    /**
     * (Optional) The SQL fragment that is used when 
     * generating the DDL for the column.
     * <p> Defaults to the generated SQL to create a
     * column of the inferred type.
     */
    String columnDefinition() default "";

    /**
     * (Optional) The name of the table that contains the column. 
     * If absent the column is assumed to be in the primary table.
     */
    String table() default "";

    /**
     * (Optional) The column length. (Applies only if a
     * string-valued column is used.)
     */
    int length() default 255;

    /**
     * (Optional) The precision for a decimal (exact numeric) 
     * column. (Applies only if a decimal column is used.)
     * Value must be set by developer if used when generating 
     * the DDL for the column.
     */
    int precision() default 0;

    /**
     * (Optional) The scale for a decimal (exact numeric) column. 
     * (Applies only if a decimal column is used.)
     */
    int scale() default 0;
}
  • 엔티티 필드와 테이블 컬럼을 매핑하는데 사용한다.
  • 기본값은 위와 같으며 주로 사용되는 옵션은 name 정도이다.
속성 기능 기본값
name 필드와 매핑할 테이블의 컬럼명 객체의 필드명
insertable 엔티티 저장 시 이 필드도 같이 저장, false로 설정하면 이 필드는 데이터베이스에 저장하지 않음 true
updatable 엔티티 수정 시 이 필드도 같이 수정, false로 설정하면 이 필드는 데이터베이스에 수정하지 않음 true
nullable null 허용 여부로, false로 설정하면 DDL 옵션으로 인한 DDL 생성 시 컬럼에 not null 제약조건이 붙는다. true
unique @Table의 uniqueConstraints는 복합 유니크 인덱스를 생성할 때 사용하며, 이 기능은 단일 유니크 인덱스를 생성할 때 사용 -
columnDefinition 데이터 베이스 컬럼 정보를 직접 입력한다.(Native) -
length 문자 길이 제약 조건, String에만 사용 255
precision BigDecimal, BigInteger 타입 사용시 사용한다. 정밀도를 의미하는데 전체 자리수를 의미한다. 0
scale BigDecimal, BigInteger 타입 사용시 사용한다. 소수점 이하 자리수를 의미한다 0

@Enumerated

/*
 * Copyright (c) 2008, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Linda DeMichiel - 2.1
//     Linda DeMichiel - 2.0


package javax.persistence;

import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static javax.persistence.EnumType.ORDINAL;

/**
 * Specifies that a persistent property or field should be persisted
 * as a enumerated type.  The <code>Enumerated</code> annotation may
 * be used in conjunction with the <code>Basic</code> annotation, or in
 * conjunction with the <code>ElementCollection</code> annotation when the
 * element collection value is of basic type.  If the enumerated type
 * is not specified or the <code>Enumerated</code> annotation is not
 * used, the <code>EnumType</code> value is assumed to be <code>ORDINAL</code>.
 *
 * <pre>
 *   Example:
 *
 *   public enum EmployeeStatus {FULL_TIME, PART_TIME, CONTRACT}
 *
 *   public enum SalaryRate {JUNIOR, SENIOR, MANAGER, EXECUTIVE}
 *
 *   &#064;Entity public class Employee {
 *       public EmployeeStatus getStatus() {...}
 *       ...
 *       &#064;Enumerated(STRING)
 *       public SalaryRate getPayScale() {...}
 *       ...
 *   }
 * </pre>
 *
 * @see Basic
 * @see ElementCollection
 *
 * @since 1.0
 */
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface Enumerated {

    /** (Optional) The type used in mapping an enum type. */
    EnumType value() default ORDINAL;
}
  • 상태값을 표현하는데 많이 사용한다.
  • 엔티티에서 보통 어떤 상태값에 대해 enum 클래스를 작성하고, 이에 부수적인 기능들을 추가하여 엔티티 필드에 매핑해 사용하는데 이때, Ordinal, String을 선택한다.
  • Ordinal은 Enum의 순서(0부터 시작), String은 Enum의 이름을 명칭한다. Ordinal은 Enum이 추가, 변경, 삭제된 경우 순서가 바뀔 여지가 크므로 String으로 적용한다고 생각하자.

@Transient

package javax.persistence;

import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Specifies that the property or field is not persistent. It is used
 * to annotate a property or field of an entity class, mapped
 * superclass, or embeddable class.
 *
 * <pre>
 *    Example:
 *
 *    &#064;Entity
 *    public class Employee {
 *        &#064;Id int id;
 *        &#064;Transient User currentUser;
 *        ...
 *    }
 * </pre>
 *
 * @since 1.0
 */
@Target({METHOD, FIELD})
@Retention(RUNTIME)

public @interface Transient {}
  • 데이터베이스와 무관한 필드임을 명시하는 어노테이션이다.
  • 주로 엔티티에서 데이터베이스 테이블과 관련되지 않는 데이터를 작업할 때 사용한다.
728x90
반응형