JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 객체지향에서 클래스와 인스턴스의 관계와 같이 엔티티는 테이블, 엔티티의 인스턴스는 테이블의 row와 같다.
Spring Data JPA는 엔티티 클래스의 내용에 따라 테이블을 자동생성해주는 기능이 존재한다. .properties/yml 파일에 spring.jpa.hibernate.ddl-auto 옵션을 설정하면 사용할 수 있다.
# application.yml
spring:
jpa:
hibernate:
ddl-auto: create
# show-sql: true # DDL 콘솔 출력
개발환경에서는 필요에 따라 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
어노테이션을 통해 클래스가 엔티티임을 명시한다.
엔티티의 식별자(테이블의 Primary Key)를 정의한다. @GeneratedValue
어노테이션을 통해 식별자의 생성 전략을 지정할 수 있다.
테이블의 컬럼 속성과 대응되는 어노테이션이다. 설정할 수 있는 속성은 아래와 같다.
속성명 | 설명 | 기본값 |
---|---|---|
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 수행 중 충돌하여 에러가 날 수 있기 때문에 가급적 맞춰주는 것이 좋다고 생각된다.
컬럼의 기본값을 설정해주는 어노테이션이다.
@Column(nullable = false)
@ColumnDefault("CURRENT_TIMESTAMP")
val createdAt: LocalDateTime?
@Column
과 마찬가지로 @ColumnDefault()
로 설정한 기본값과 프로퍼티의 기본값은 전혀 연관이 없다.
val createdAt: LocalDateTime = LocalDateTime.now() 와 같이 프로퍼티의 기본값을 지정하더라도 @ColumnDefault()
로 컬럼 기본값을 설정하지 않으면 테이블의 기본값은 설정되지 않는다.
위의 코드가 속한 엔티티를 등록(Insert)하려 할때 createdAt이 null로 설정된 엔티티를 삽인한다고 해보자. 문제 없이 기본값으로 설정될 것 같지만 에러가 발생한다.
그 이유는 JPA는 엔티티 컬럼값이 null일 경우 등록 쿼리에서 제외시키는 것이 아니라 그대로 null을 삽입하는 쿼리를 생성하기 때문에 널체킹에 의해 에러가 발생한다.
만약 엔티티를 등록, 수정할 때 null인 컬럼 값을 쿼리에서 제외하고 싶을 경우 @DynamicInsert
, @DynamicUpdate
어노테이션을 엔티티 클래스에 명시해야 한다.
Enum 타입을 컬럼에 매핑할 때 사용한다. 설정 값은 아래와 같다.
enum class Direction{
WEST, EAST, NORTH, SOUTH
}
위의 열거형을 컬럼에 매핑하려 했을때 ORDINAL은 열거형의 순서값, 즉 WEST는 0, EAST는 1 이 테이블에 저장된다. 테이블 상에서는 이 순서값이 어떤것을 의미하는지 알기 어려우므로 가급적 열거형 속성이름을 저장하는 STRING으로 설정하자
날짜 타입을 매핑할때 사용한다. JAVA8에서 추가된 java.time 모듈의 LocalDate, LocalDateTime을 사용할 경우 생략이 가능하다. 설정 값은 아래와 같다.
일반적으로 자바에서는 엔티티의 세터를 만들지 않는다. 세터의 존재가 엔티티의 일관성을 깨트릴 수 있어 유지보수에 있어 불안정성을 가져오기 때문에 엔티티의 변경이 필요할 경우 변경을 위한 메소드를 따로 만들어 private 필드에 접근했다.
문제는 코틀린에서 var로 선언된 프로퍼티는 세터를 무조건 생성한다는 것이다. JPA에서 변경이 가능한 엔티티의 프로퍼티는 final이어서는 안되기 때문에 var의 사용을 막을 수 없다.
자바와 마찬가지로 변경이 필요한 경우 프로퍼티의 세터에 접근하는 것이 아닌 메소드를 정의해 변경이 필요시 사용할 수 있으나 세터가 아예 없는 것과 존재하여 접근할 수 있는 것은 다르기 때문에 이에 대한 해결책을 알아보자
코틀린은 커스텀 게터와 세터를 정의할 수 있다. 커스텀 세터에 private 접근자를 붙이면 외부에서 세터에 접근할 수 없으므로 문제가 해결된 것처럼 보인다. 문제는 JPA 엔티티는 open class 이고 open class 에서는 private 접근자를 사용할 수 없다는 것이다. 따라서 이 방법은 사용할 수 없다.
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를 다룰때 사용하지 않는 습관을 들인다면 세터를 만드느냐 아니냐의 문제는 너무 이론적인 문제란 생각이 든다. 패턴과 이론을 지키는 것이 중요하긴 하지만 이렇게 디테일에 목맬 필요성은 느끼지 못하겠다,