Spring Data JPA - 엔티티 설계와 매핑 with Kotlin

tkppp·2022년 1월 27일
0

SpringBoot with Kotlin

목록 보기
6/12

엔티티란?

JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 객체지향에서 클래스와 인스턴스의 관계와 같이 엔티티는 테이블, 엔티티의 인스턴스는 테이블의 row와 같다.

엔티티 설계

Spring Data JPA는 엔티티 클래스의 내용에 따라 테이블을 자동생성해주는 기능이 존재한다. .properties/yml 파일에 spring.jpa.hibernate.ddl-auto 옵션을 설정하면 사용할 수 있다.

# application.yml
spring:
  jpa:
    hibernate:
      ddl-auto: create
    # show-sql: true # DDL 콘솔 출력

spring.jpa.hibernate.ddl-auto 속성

  • create: 기존 테이블을 삭제하고 새로 생성 (DROP - CREATE)
  • create-drop: create와 동일하나 어플리케이션 종료시 테이블 삭제 (DROP - CREATE - DROP)
  • update: 데이터베이스 테이블 - 엔터티 매핑정보를 비교해서 변경사항만 수정
  • validate: 데이터베이스 테이블 - 엔터티 매핑정보를 비교해서 차이가있으면 경고 메세지를 남기고 어플리케이션을 실행하지 않음
  • none: 자동 생성 기능을 사용하지 않음

개발환경에서는 필요에 따라 create, create-drop, update를 사용하고 프로덕션 환경에서는 validate, none을 사용하면 될 것 같다.

@Entity
class Post(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(length = 500, nullable = false)
    var title: String,

    @Column(columnDefinition = "TEXT", nullable = false)
    var content: String,

    val author: String? = null,
) : BaseTimeEntity()

@Entity

@Entity 어노테이션을 통해 클래스가 엔티티임을 명시한다.

@Id

엔티티의 식별자(테이블의 Primary Key)를 정의한다. @GeneratedValue 어노테이션을 통해 식별자의 생성 전략을 지정할 수 있다.

strategy

  • AUTO: 특정 데이터베이스에 맞게 자동으로 생성되는 방식
  • TABLE: 별도의 키를 생성해주는 테이블을 이용하는 방식
  • SEQUENCE: 데이터베이스의 시퀀스를 이용해서 식별키 생성(오라클에서 사용)
  • IDENTITY: 기본키 생성 방식 자체를 데이터베이스에 위임하는 방식

@Column

테이블의 컬럼 속성과 대응되는 어노테이션이다. 설정할 수 있는 속성은 아래와 같다.

속성명설명기본값
name대응하는 컬럼 이름프로퍼티 이름
nullable컬럼의 널 허용 여부true
unique컬럼 값으로 중복을 허용하는 지 여부false
columnDefinition컬럼 정보를 직접 설정
length문자열의 길이 제한, String에만 사용할 수 있다255
insertable, updatable등록, 변경 가능 여부true
@Column(length = 500, nullable = false)
var title: String,

@Column(columnDefinition = "TEXT", nullable = false)
var content: String,

자동 생성 옵션이 none, valid일 경우, JPA는 column 속성의 내용과 대응되는 컬럼 설정과의 동일성 검사를 수행하지 않는다. 만약 데이터베이스의 테이블 컬럼 설정과 엔티티의 컬럼 속성이 다르더라도 오류가 나지 않는다는 것이다.

@Column 속성 설정 정보와 엔티티 프로퍼티는 서로 관련이 없다. 만약 @Column(nullable = false)이더라도 var title: String? 과 같이 nullable한 프로퍼티여도 상관이 없다는 것이다. 물론 CRUD 수행 중 충돌하여 에러가 날 수 있기 때문에 가급적 맞춰주는 것이 좋다고 생각된다.

@ColumnDefault()

컬럼의 기본값을 설정해주는 어노테이션이다.

@Column(nullable = false)
@ColumnDefault("CURRENT_TIMESTAMP")
val createdAt: LocalDateTime?

@Column 과 마찬가지로 @ColumnDefault() 로 설정한 기본값과 프로퍼티의 기본값은 전혀 연관이 없다.
val createdAt: LocalDateTime = LocalDateTime.now() 와 같이 프로퍼티의 기본값을 지정하더라도 @ColumnDefault() 로 컬럼 기본값을 설정하지 않으면 테이블의 기본값은 설정되지 않는다.

@DynamicInsert, @DynamicUpdate

위의 코드가 속한 엔티티를 등록(Insert)하려 할때 createdAt이 null로 설정된 엔티티를 삽인한다고 해보자. 문제 없이 기본값으로 설정될 것 같지만 에러가 발생한다.

그 이유는 JPA는 엔티티 컬럼값이 null일 경우 등록 쿼리에서 제외시키는 것이 아니라 그대로 null을 삽입하는 쿼리를 생성하기 때문에 널체킹에 의해 에러가 발생한다.

만약 엔티티를 등록, 수정할 때 null인 컬럼 값을 쿼리에서 제외하고 싶을 경우 @DynamicInsert, @DynamicUpdate 어노테이션을 엔티티 클래스에 명시해야 한다.

@Enumerated

Enum 타입을 컬럼에 매핑할 때 사용한다. 설정 값은 아래와 같다.

  • EnumType.ORDINAL
  • EnumType.STRING
enum class Direction{
	WEST, EAST, NORTH, SOUTH
}

위의 열거형을 컬럼에 매핑하려 했을때 ORDINAL은 열거형의 순서값, 즉 WEST는 0, EAST는 1 이 테이블에 저장된다. 테이블 상에서는 이 순서값이 어떤것을 의미하는지 알기 어려우므로 가급적 열거형 속성이름을 저장하는 STRING으로 설정하자

@Temporal

날짜 타입을 매핑할때 사용한다. JAVA8에서 추가된 java.time 모듈의 LocalDate, LocalDateTime을 사용할 경우 생략이 가능하다. 설정 값은 아래와 같다.

  • TemporalType.DATE : 날짜 타입(DATE)과 매핑
  • TemporalType.TIME : 시간 타입(TIME)과 매핑
  • TemporalType.DATETIME : 날짜 + 시간 타입(DATETIME)과 매핑

엔티티와 세터

일반적으로 자바에서는 엔티티의 세터를 만들지 않는다. 세터의 존재가 엔티티의 일관성을 깨트릴 수 있어 유지보수에 있어 불안정성을 가져오기 때문에 엔티티의 변경이 필요할 경우 변경을 위한 메소드를 따로 만들어 private 필드에 접근했다.

문제는 코틀린에서 var로 선언된 프로퍼티는 세터를 무조건 생성한다는 것이다. JPA에서 변경이 가능한 엔티티의 프로퍼티는 final이어서는 안되기 때문에 var의 사용을 막을 수 없다.

자바와 마찬가지로 변경이 필요한 경우 프로퍼티의 세터에 접근하는 것이 아닌 메소드를 정의해 변경이 필요시 사용할 수 있으나 세터가 아예 없는 것과 존재하여 접근할 수 있는 것은 다르기 때문에 이에 대한 해결책을 알아보자

private set

코틀린은 커스텀 게터와 세터를 정의할 수 있다. 커스텀 세터에 private 접근자를 붙이면 외부에서 세터에 접근할 수 없으므로 문제가 해결된 것처럼 보인다. 문제는 JPA 엔티티는 open class 이고 open class 에서는 private 접근자를 사용할 수 없다는 것이다. 따라서 이 방법은 사용할 수 없다.

protected set

protected 접근자를 붙인 커스텀 세터는 open class 에서도 사용할 수 있고 클래스 외부에서는 접근할 수 없다. 세터를 없앨수는 없으니 외부에서 접근하지 못하도록 하는 것이다.


@Entity
class Post(
    title : String,
    content: String
) : BaseTimeEntity() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @Column(length = 500, nullable = false)
    var title: String = title
        protected set

    @Column(columnDefinition = "TEXT", nullable = false)
    var content: String = content
        protected set

    val author: String? = null

    fun update(updateDto: PostUpdateRequestDto){
        title = updateDto.title
        content = updateDto.content
    }
}

이 방법또한 세터가 존재하기 때문에 근본적으로 문제가 해결된 것은 아니다. 개인적으로 세터를 JPA를 다룰때 사용하지 않는 습관을 들인다면 세터를 만드느냐 아니냐의 문제는 너무 이론적인 문제란 생각이 든다. 패턴과 이론을 지키는 것이 중요하긴 하지만 이렇게 디테일에 목맬 필요성은 느끼지 못하겠다,

0개의 댓글