본문 바로가기

3.구현/Java or Kotlin

[kotlin] JPA 3: Spring Boot에서 Entity 매핑 확장

들어가기

이번에는 엔티티 정의위한 확장된 어노테이션에 대해서 다룰려고 한다. 더 다양한 타입과 알아두면 좀더 도움이 되는 어노테이션에 대해서 다룰 예정이다.

작성자: http://ospace.tistory.com/ (ospace114@empal.com)

엔티티 정의

엔티티 정의를 위한 추가적인 어노테이션이다.

  • @Enumerated: enum 타입 매핑
  • @Temporal: 날짜 타입 매핑
  • @Transient: 매핑 무시
  • @Lob: BLOB CLOB 타입 매핑
  • @CreationTimestamp: insert 시간 자동 저장
  • @UpdateTimestamp: update 시간 자동 저장
  • @CreateDate: 엔티티 생성시간
  • @LastModifiedDate: 엔티티 변경시간
  • @CreatedBy: 엔티티 생성 사용자
  • @LastModifiedBy: 엔티티 수정 사용자
  • @Access: 엔티티 필드 접근 방식

@Enumerated 어노테이션

열거형인 enum 타입 매핑하기 위한 어노테이션이다. 설정할 수 있는 값은 EnumType 열거형으로 ORDINAL과 STRING을 지정할 수 있다. 기본은 ORDINAL이다. 즉, 열거형인 필드는 자동으로 @Enumerated을 사용하며 ORDINAL로 설정된다. 아래와 같이 MemberType라는 열거형이 있다.

enum class MemberType {
  ADMIN, GENERAL, GUEST,
}

이를 type이라는 필드에 설정해보자.

@Enumerated
private var type: MemberType = MemberType.GUEST

DDL에서 type필드는 다음과 같게 된다. 가장 기본적인 매핑 형태이다.

type tinyint check (type between 0 and 2)

EnumType.STRING으로 매핑정보를 설정해보자.

@Enumerated(EnumType.STRING)
private var type: MemberType = MemberType.GUEST

DDL에서 type필드는 다음처럼 정의된다.

type varchar(255) check (type in ('ADMIN','GENERAL','GUEST')),

만약 EnumType.ORDINAL로 이미 사용할 경우 열거형 추가는 가능하지만 변경은 불가하다. 물론 변경은 가능하지만 문제가 발생할 가능성이 높다. STRING인 경우는 변경에도 안전하다. 단지 데이터 크기가 늘어날 뿐이다.

@Temporal 어노테이션

java.util.Date, java.util.Calendar, 그리고 java.time.LocalDateTime인 날짜 타입을 매핑하는 어노테이션이다. 열거형인 TemporalType으로 값을 지정할 수 있다. 지정할 수 있는 값은 DATE, TIME, TIMESTAMP가 있다. 기본값은 없기에 반드시 값을 설정해줘야 한다. H2기준으로 DATE는 date 타입으로, TIME, time 타입으로 TIMESTAMP는 timestamp 타입으로 매핑된다. MySQL 기준으로는 datetime으로 매핑된다.

java.util.Date와 java.util.Calendar은 모두 가능하지만, java.time.LocalDateTime을 사용할 경우 TIMESTAMP만 가능하다.

@Temporal(TemporalType.TIMESTAMP)
private var signedAt: LocalDateTime = LocalDateTime.now()

@Transient 어노테이션

특정 객체 필드를 데이터베이스 매핑하지 않도록 하는 어노테이션이다. 즉, 내부적으로만 사용 객체 필드이다. DDL에도 포함되지 않는다. 추가로 설정하는 값은 없다.

@Transient
private val cache: Int = 0

@Lob 어노테이션

객체 필드를 BLOB CLOB 타입으로 매핑하는 어노테이션이다. 이 어노테이션도 추가 설정할 값은 없다. 필트 타입이 문자형이면 CLOB이고 나머지는 BLOB형태로 매핑된다.

@Lob
private val extra: String? = null

@Lob
private val extra: Array<Byte>? = null

@CreationTimestamp 어노테이션

insert 쿼리 실행할때 시간이 자동 저장된다. 즉, 데이터베이스에 데이터 추가시간으로 갱신된다. 열거형 SourceType으로 DB 또는 VM으로 설정한다. 기본은 VM으로 Java VM에서 값 설정할 때에 시간을 가지고 설정한다. DB는 데이터베이스에 추가할때 데이터베이스 시간을 사용한다.

@CreationTimestamp(source = SourceType.DB)
private var createdAt: LocalDateTime = LocalDateTime.now()

SourceType.DB는 데이터 추가시 데이터베이스 시간 정보가 추가해서 실행된다. 주의할 부분은 SourceType.VM인 경우 Java VM에서 시간정보가 갱신되기 때문에 update 쿼리 실행에서도 createdAt이 변경될 수 있다. 그래서 변경시에는 반영 안되도록 @Column을 이용해서 updatable을 false로 설정한다.

@CreationTimestamp
@Column(updatable = false)
private var createdAt: LocalDateTime? = null

그러면 update 쿼리 실행에서 createdAt이 업데이트에서 제외된다.

@UpdateTimestamp 어노테이션

update 쿼리 실행할때 시간이 자동 저장된다. 즉, 데이터베이스에 데이터 수정시간으로 갱신된다. 열거형 SourceType으로 설정하며 값들도 @CreationTimestamp에 내용과 동일하다.

@UpdateTimestamp(source = SourceType.DB)
private var updatedAt: LocalDateTime = LocalDateTime.now()

getter와 setter은 반드시 별도 정의해야 한다. setter가 없다면 제대로 값이 저장되지 않는다. SourceType.VM은 데이터 수정 후에 별도로 update 쿼리가 실행된다.

@CreateDate 어노테이션

엔티티 생성시간이 자동 저장된다. 추가 설정하는 값은 없다. 이 어노테이션이 Spring의 Auditing으로 이를 활성화하기 위해서 아래 “Spring Auditing 활성화”를 참고하시기 바란다. @CreateDate을 사용하기 위해 몇가지 작업이 필요하다.

@CreatedDate
@Column(updatable = false)
private var createdDate: LocalDateTime? = null

@CreateDate에 의해서 Java VM의 시간이 설정된다. @Column에서 updatable이 false을 설정함으로써 엔티티 수정할 때에 같이 변경되지 않도록 해야 한다. insert 쿼리에서 해당 값이 같이 저장된다.

@LastModifiedDate 어노테이션

엔티티 변경시간이 자동 저장된다. 추가 설정하는 값은 없다. @CreateDate 처럼 Spring Auditing을 활성화해야 한다. 별다른 추가 작업은 없다.

@LastModifiedDate
private var modifiedDate: LocalDateTime? = null

@LastModifiedDate도 Java VM 시간이 설정된다.

@CreatedBy 어노테이션

엔티티가 생성될 경우 자동으로 생성한 사용자 정보를 저장한다. 수정에 의해 변경될 수 있으므로 @Column(updatable=false)와 같이 사용한다. 이를 사용하기 위해서는 Spring Auditing 활성화가 필요하다.

@CreatedBy
private var creator: Long? = null

@LastmodifiedBy 어노테이션

엔티티가 수정될 경우 자동으로 수정한 사용자 정보를 저장한다. 이를 사용하기 위해서는 Spring Auditing 활성화가 필요하다.

@CreatedBy
private var modifier: Long? = null

@Access 어노테이션

JPA가 엔티티의 매핑정보 엑세스 방식을 설정한다. JPA가 엔티티의 매핑 정보을 어디서 가져올지를 정한다. 열거형인 AccessType으로 설정할 수 있고 설정 가능한 값으로 FIELD와 PROPERTY가 있다.

  • FIELD: 필드로 접근하는 방식으로 필드에 있는 매핑 정보를 접근
  • PROPERTY: 속성으로 접근하는 방식으로 getter 접근자에 매핑 정보를 접근

기본 값은 없기 때문에 한가지 방식을 설정해야 한다. 만약 @Access를 사용하지 않는다 @Id가 설정된 위치에 따라서 결정된다. 즉 필드에 @Id가 되어 있다면 앞으로 모든 액세스 방식이 FIELD가 되고 @Id가 속성인 getter에 있다면 AccessType.PROPERTY가 된다.

AccessType.FIELD로 적용하면 어노테이션이 필드에 위치한다.

@Entity
@Access(AccessType.FIELD)
open class Member {
  @Id
  @GeneratedValue
  private var id:Long? = null
//...
}

AccessType.PROPERTY를 적용하면 어노테이션 위치가 getter에 있다.

@Entity
@Access(AccessType.FIELD)
open class Member {
  @Id
  @GeneratedValue
  private var id:Long? = null
  //...

  @Id
  @GeneratedValue
  open fun getId(): Long? = id
  //...
}

그렇기 때문에 다른 위치에 있다면 제대로 매핑이 되지 않는다. 그렇기에 하나로 통일해서 정의해야한다. 아니면 FIELD나 PROPERTY를 미리 지정해서 사용한다. 만약 객체 필드별로 다르게 지정하고 싶다면 개별 필드 또는 getter의 어노테이션에서 직접 @Access를 지정하면 된다.

@Entity
@Access(AccessType.FIELD)
open class Member {
  @Id
  @GeneratedValue
  private var id:Long?,

  private val extra: String?,
  //...

  @Lob
  @Access(AccessType.PROPERTY)
  open fun getExtra(): String? = extra
  //...
}

AccesType.FIELD에 의해서 id는 제대로 필드에 적용하고 있다. extra인 경우는 @Acces에 의해서 getExtra()라는 getter 접근자에 적용하고 있다.

Spring Auditing 활성화

Spring의 Auditing을 사용하기 위해서 추가적인 환경구성이 필요하다. 먼저, @EnableJpaAuditing을 @SpringBootApplication 선언에 추가해야 한다.

@SpringBootApplication
@EnableJpaAuditing
open class ProjectApplication
//...

다음으로 엔티티에 해당 기능이 활성화하기 위해서 @EntityListeners을 추가해야 한다.

@Entity
@EntityListeners(AuditingEntityListener::class)
open class Member {
//...
}

추가로 Auditing에서 사용자 정보를 얻기 위해서는 AuditorAware를 구현해야 한다.

@Component
class UserAuditorAware(
  private val memberCtrl: MemberController
) : AuditorAware<Long> {
    override fun getCurrentAuditor(): Optional<Long> {
        val obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
        return Optional.ofNullable(if (obj is Member) obj.getId() else null)
    }
}

사용자 정보 타입이 Long으로 사용되고 있다. 물론 Member 엔티티를 사용해서 표현할 수 도 있다. 혹은 스프링 시큐리에 User 객체를 사용할 수도 있다. 이는 현재 사용 목적에 맞게 반영하면 된다. 앞에서 CreatedBy와 LastModifiedBy에서 Long을 사용하고 있기에 Long형으로 리턴하고 있다.

이후 부터는 Member 엔티티에서 @CreatedDate와 @CreatedBy 그리고 @LastModifiedDate 와 @LastModifiedBy가 활성화된다.

마무리

엔티티를 정의하는 다양한 어노테이션을 살펴보았다. 기본적으로 매핑이 되지만, 좀더 세부적으로 매핑 설정할려고 할 때 사용한다. 그리고 JPA가 자동으로 데이터베이스 매핑해준다고 하지만, 실제 세부적인 부분에서 설정을 하고 매핑할 경우 원하는데로 잘 되지 않을 경우가 있다. @CreateDate나 @CreationTimestamp인 경우 업데이트할 때에도 같이 변경될 수 있기 때문에 확인이 필요하다. 이런 부분에서 주의가 필요하다.

부족한 글이지만 여러분에게 참고가 되었으면 하네요. 모두 즐거운 코딩생활되세요. ospace.

참고

[1] 최범균, JPA 프로그래밍 입문, 가메출판사, 2017

[2] 김영한, 자바 ORM 표준 JPA 프로그래밍, 에이콘, 2015

[3] Spring Data - Auditing, https://docs.spring.io/spring-data/jpa/reference/auditing.html

[4] Package org.hibernate.annotations, https://docs.jboss.org/hibernate/orm/5.5/javadocs/org/hibernate/annotations/package-summary.html

반응형