[JPA] 엔티티(Entity) 매핑

2022년 08월 23일

TOC

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 기반으로 작성하였습니다.

1. JPA의 Entity란?

Entity란 DB에서 영속적으로 저장된 데이터를 자바 객체로 매핑하여 '인스턴스의 형태'로 존재하는 데이터를 말한다. JPA는 자바의 ORM의 표준인만큼 간단한 설정만으로 DB의 데이터를 자바 객체로 매핑하는 엔티티를 만들 수 있다. 간단한 설정이란 JPA 의존성을 추가했다면 Entity로 만들고 싶은 클래스에 @Entity 어노테이션을 붙여주기만하면 된다. 그러면 해당 클래스는 JPA가 관리하는 엔티티가 된다.

이와 같이 간단히 어노테이션만 붙여주면 엔티티 설정이 끝나지만 몇가지 주의할 점이 존재한다. 먼저 JPA를 구현하여 쓰는 라이브러리들이 Reflection을 사용하는 경우가 있기에 기본 생성자를 필수로 작성해줘야 한다. 해당 생성자는 public 또는 protected 생성자로 만들어주면 된다. 두번째로 @Entity 어노테이션은 final, enum, interface, inner클래스에는 사용할 수 없다. 마지막으로 DB에 저장할 필드에는 final 키워드를 작성하면 안된다.

이제 JPA의 엔티티에 대해 알았으니 @Entity 어노테이션 및 JPA의 엔티티 설정 관련 어노테이션들에 대해 알아보겠다.

2. Entity를 DB 테이블에 매핑하기

2.1. @Entity

@Entity 어노테이션은 앞서 말했듯이 JPA에서 엔티로 관리하고 싶은 클래스 위에 붙여주면 JPA는 “이 클래스는 내가 엔티티로 관리할 클래스이구나!”라 인지하고 직접 관리를 하게 된다.

@Entity의 속성은 name 속성만 존재한다. 해당 속성은 JPA에서 사용할 엔티티 이름을 지정하는 속성이다. 기본 값으로는 클래스의 이름을 그대로 사용하게 되며 다른 패키지에 같은 이름의 클래스가 있는 것이 아니라면 가급적 기본 값을 사용한다.

2.2. @Table

엔티티를 통해 DDL을 생성 및 연결하면 엔티티 이름의 테이블을 생성 및 연결하게 된다. 하지만 상황에 따라 테이블의 이름 변경이 필요한 상황이 발생할 수도 있고 테이블에 대한 설정이 필요할 수도 있을 것이다. 이러한 상황에는 @Table 어노테이션을 통해 테이블명과 여러 제약조건들을 설정할 수 있다.

@Table의 여러 속성들

  • name: 매핑할 테이블의 이름을 설정한다. 기본값으로는 엔티티의 이름을 사용한다.
  • catalog: 데이터베이스의 catalog를 매핑한다.
  • schema: 데이터베이스의 schema를 매핑한다.
  • uniqueContraints: DDL 생성 시에 유니크 제약조건을 생성한다.

3. 기본 키(PK) 매핑하기

DB 테이블에는 항상 기본 키가 존재한다. Entity는 DB테이블에 매핑이 되는 만큼 기본키에 매핑되는 필드가 필수적으로 필요하다. JPA에서는 기본키 설정과 관련하여 @Id, @GeneratedValue 어노테이션을 제공하고 있다.

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

3.1. @Id

@Id 어노테이션은 해당 어노테이션이 붙은 필드가 “이 필드가 기본키에 매핑되는 필드이다!” 라고 알려주는 어노테이션이다. Id를 직접 할당해주고 싶으면 @Id 어노테이션만을 붙인 후에 직접 할당해주면 된다. 하지만 일반적으로 Id는 MySQL의 auto_increment와 같이 데이터베이스에서 자동으로 할당해주는 방법을 사용한다. 해당 기능을 사용하기 위해서는 @GeneratedValue을 사용하면 된다.

3.2. @GeneratedValue

@GeneratedValue@Id 어노테이션과 항상 붙어다니는 어노테이션으로 엔티티 데이터를 DB에 저장할 때 기본키를 자동으로 생성해주는 어노테이션이다. 해당 어노테니션에는 4가지 설정이 존재한다.

  • GenerationType.IDENTITY

    • 기본 키 생성을 데이터베이스에 위임하는 전략이다. 실제로 실행되는 insert쿼리를 보면 id의 값은 null인 상태로 실행된다.
    • Identity 전략을 사용하여 auto increment를 하는 경우에는 DB에 쿼리를 날려야지 PK값이 할당되어서 트랜잭션이 끝날때가 아닌 persist()를 할 때 Insert SQL을 보내어 PK값을 가져온다.
    • PK값을 DB에 들어간 이후에 알 수 있어서 Insert 쿼리를 모아서 한번에 보내는게 불가능하다는 단점이 있다.
    • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
  • GenerationType.SEQUENCE

    • 데이터베이스 시퀀스 오브젝트를 사용한다.
    • 해당 방법을 사용할 때는 매핑할 데이터베이스 시퀀스가 필요하여 @SquenceGenerator를 통한 추가 설정이 필요하다.
    • 시퀀스 방법은 IDENTITY와 다르게 DB에 PK값이 없는 채로 데이터를 보내는 것이 아니라 먼저 Sequence Generator를 DB 서버에서 찾아서 다음번에 사용할 PK값을 call해서 불러온다. 그 후 애플리케이션에서 PK를 객체에 할당하여 영속성 컨텍스트에 보관한다. 덕분에 데이터를 저장할 때 바로 Insert 쿼리를 날리지 않아도 된다.

      • 해당 방법을 사용하면 매번 sequence를 받아와야해서 성능상의 손해를 볼 수 있다. 해당 손해는 @SquenceGeneratorallocationSize 속성을 설정하면 손해를 줄일 수 있다.

        • allocationSize설정으로 얻는 이점 - 데이터베이스 시퀀스를 한번에 설정한 개수를 증가시킵니다. 데이터베이스 시퀀스를 생성할 때 한번 호출에 50씩 증가하게 만들 수 있다. 그러면 처음에 시퀀스가 1이면 이 시퀀스를 호출하면 50이 된다.그러면 애플리케이션에서는 1~50번까지는 메모리에서 값을 꺼내서 사용하고, 51이 되는 순간 DB 시퀀스를 호출합니다. 그러면 DB 시퀀스가 50 -> 100이 된다. 그러면 애플리케이션은 51~100까지 메모리에서 값을 증가시키면서 사용합니다. 결과적으로 애플리케이션은 50번에 1번만 DB를 호출해서 시퀀스를 증가시키면 됩니다. 이렇게 하면 네트워크 호출이 줄어서 성능이 향상된다.
    • 영속성 컨텍스트에 객체를 추가할 때 이미 PK값을 알 수 있어 여러 데이터에 대해 buffer에 모아서 한번에 insert query를 보낼 수 있다.
    • Oracle, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.
  • GenerationType.TABLE

    • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.
    • 각 테이블의 next sequence value의 값을 갖는 테이블을 생성하여 테이블에 값을 추가할 때는 해당 시퀀스 테이블의 값을 읽어오고 업데이트하면서 id를 정한다.
    • 테이블 설정을 위해 시퀀스 값을 저장할 테이블이 필요하여 @TableGenerator를 통한 설정이 필요하다.
    • 모든 데이터베이스에 적용 가능하다는 장점이 있다.
    • 별도의 테이블을 만들어야하고 최적화가 되지 않아서 성능이 떨어진다는 단점이 있다. → TABLE방법보다는 각 DB에 맞는 앞에 나온 방법을 쓰기를 추천한다.
  • GenerationType.AUTO

    • default값으로 데이터베이스 방언에 따라 자동으로 지정된다.

📌  기본키의 경우 절대로 비즈니스와 관련된 값을 식별자로 사용해서는 안된다! 가능하면 Auto_Incremet나 Sequence Object를 사용하는 것을 추천한다.

4. 엔티티 필드와 DB 컬럼 매핑하기

@Entity
public class Member {

	@Id
	private Long id;

	@Column(name = "name")
	private String username;private Integer age;

	@Enumerated(EnumType.STRING)
	private RoleType roleType;

	@Temporal(TemporalType.TIMESTAMP)
	private Date createdDate;

	@Temporal(TemporalType.TIMESTAMP)
	private Date lastModifiedDate;

	@Lob
	private String description;
}

4.1. @Column

필드와 컬럼을 매핑해주는 어노테이션이다.

속성

  • name: 필드와 매핑할 테이블의 컬럼 이름을 설정해준다. default값은 객체의 필드 이름이 된다.

    자바에서는 주로 roleType과 같은 camel case를 사용하지만 데이터베이스는 과거에 대소문자의 구분이 없었기에 현재까지 관례적으로 snake case를 많이 사용하고 있다. 그래서 Spring Boot를 통해 JPA를 사용할 때는 기본 설정으로 객체의 camel case로 되어있는 필드명을 snake case로 변환하여 테이블을 생성해준다. (기본 JPA만을 사용할 때는 camel case로 컬럼명이 만들어진다.)

  • insertable, updatable: 추가, 변경 가능 여부를 설정하는 속성이다. 해당 속성을 false로 설정하면 애플리케이션에서 해당 필드가 추가되거나 업데이트 되어도 DB에는 반영되지 안게 된다.
  • nullable: null값의 허용 여부를 설정한다. false로 설정하면 DDL 생성시에 not null 제약조건이 붙게 된다. default값은 true이다.
  • unique: 컬럼의 유니크 제약조건을 걸 때 사용하는 속성이며 @TableuniqueConstraints와 같다. 하지만 해당 속성을 통해 생성을 하면 UK_ekajlzxkcjiriowqa12ada 와 같이 알아볼 수 없는 이름의 제약조건이 걸리게 되어서 해당 속성을 사용하기보다 @TableuniqueConstraints 속성을 통해 제약조건을 걸어두는 것이 좋다.
  • columnDefinition: 데이터베이스 컬럼 정보를 직접 줄 수 있다. ex) @Column(columnDefinition = “varchar(100) default ‘EMPTY’"
  • length: String 타입의 문자 길이 제약조건을 설정한다. default값은 255이다.

🚨 주의할 점!! column 설정 변경을 할 때 테이블의 기본 세팅값에 변경이 있는 length, nullable, name 등의 설정들은 테이블을 초기에 DDL이 생성될 때만 적용되고 이후에는 동작하지 않는 속성들이다. 반면에 insertable, updatable의 경우는 테이블 생성 이후에도 애플리케이션 내에서 데이터 추가 및 업데이트 할 때도 동작하게 된다..

4.2. @Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다. DB의 경우 날짜 타입을 구분해서 사용하기에 value 속성을 통해 사용하고자하는 타입을 정해줘야 한다.

속성

  • value

    • TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑한다. (ex. 2022-07-31)
    • TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑한다. (ex. 17:32:13)
    • TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑한다.(ex. 2022–07–31 17:32:13)

📌  Java8이상에서는 LocalDate, LocalDateTime 타입이 지원되기에 @Temporal을 사용하지 않아도 된다.

4.3. @Enumerated

Java의 enum타입을 매핑할 때 사용하는 어노테이션이다.

속성

  • value

    • EnumType.ORDINAL: enum 순서를 데이터베이스에 저장한다. (Default)
    • EnumType.STRING: enum 이름을 데이터베이스에 저장한다.

    🚨 기본 값인 ordinal로 사용하게 된다면 운영 중간에 enum값들이 추가되게 된다면 이전에 값들의 index와 추가된 값의 index가 꼬일 수가 있다. 이런 경우 큰 문제를 유발할 수 있기에 약간의 데이터를 아끼려고 ordinal을 사용하기보다 데이터를 조금 더 쓰더라도 string을 사용하는 것이 좋다!

4.4. @Lob

데이터베이스에서 varchar의 크기를 넘는 데이터를 저장하기 위해 BLOB, CLOB타입을 매핑할때 사용한다. 해당 어노테이션은 속성 설정이 존재하지 않으며 매핑되는 필드가 문자면 CLOB타입으로 매핑되고 나머지 타입은 자동으로 BLOB 타입으로 매핑된다.

4.5. @Transient

특정 필드를 컬럼에 매핑하지 않고 매핑을 무시할 때 사용한다. 해당 어노테이션은 주로 메모리상에서만 임시로 어떠한 값을 보관하고 싶을 때 사용한다.

5. JPA의 스키마 생성 기능

JPA는 애플리케이션 실행 시점에 DDL을 자동 생성해주는 기능을 제공한다. 이는 설정파일(xml or yml파일)에 설정한 데이터베이스에 맞는 방언을 이용해 DDL을 생성해줘서 우리는 운영중에 DB가 변경되더라도 해당 파일의 설정 값만 변경해주면 된다.

하지만 해당 기능을 개발 초기부터 운영까지 사용하는 것은 아니다. 애플리케이션을 실행할 때마다 DDL이 재생성한다면 서비스의 데이터들이 항상 초기화되게 되게 된다. 그래서 해당 기능은 운영서버에서는 사용하지 않고 개발 장비에서만 사용한다.

스키마 생성 기능은 아래와 같이 ddl.auto설정을 통해 속성 변경을 할 수 있다.

spring:
  sql.init.mode:always
datasource:
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
...
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="create" />

hibernate.hbm2ddl.auto 옵션

  • create: 기존 테이블을 drop시킨 후 다시 생성한다.
  • create-drop: create와 같으나 종료 시점에 테이블을 drop한다.
  • update: 변경된 부분만 반영한다. (컬럼을 추가하는 것은 가능하지만 지우는 것은 업데이트되지 않는다.)
  • validate: 엔티티와 테이블이 정상 배핑되었는지만 확인한다.
  • none: 기능을 사용하지 않는다.

해당 속성들은 개발을 하는 환경에 따라 유동적으로 변경하여야 한다. 보통 개발 초기 단계에는 자신의 local 또는 개발 서버에서 create, update를 사용하고 어느정도 개발을 진행하고 여러 개발자가 사용하는 중간 서버(테스트 서버)의 경우 Update, validate를 사용한다. 그 후 스테이징과 운영 서버는 validate또는 none을 사용한다. (가능하면 개발 서버에서도 가능하면 직접 스크립트를 짜서 추가를 하는 것을 추천한다.)

운영 장비에서는 절대로 create, create-drop, update를 사용하면 안된다.

엔티티 객체에서 unique, length와 같은 @Column의 속성들은 실제 애플리케이션의 성능에 영향을 주지는 않는다. 해당 테이블을 사용할 때마다 애플리케이션에서 조회하는 것이 아닌 처음 테이블을 생성할 때 ALTER를 통한 제약조건을 DB에 추가할 때만 사용하게 된다. (이러한 것도 DDL 생성 기능에 속한다.)

📚 Reference

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh