본문 바로가기
Server/[JAVA] JPA - Hibernate

01. 그 Enum 사용 방법은 틀렸다. (Attribute Converter에 대해 알아보자)

by YangsDev 2020. 5. 23.

시작

우리는 개발을 하다 보면 열거형 타입의 데이터를 많이 다루게 된다.

가장 흔한 데이터로는 '남성', '여성'이 있기도 있을 것이고 계절이라는 데이터에서는 '봄', '여름', '가을', '겨울' 이 있을 것이다.

 

이제 DB로 다시 이야기를 해보자. 우리는 DB에 데이터를 효율적이게 넣고 싶을 것이다.

어느 누가 int로 저장 가능한 정보를 '남성' 이런 식으로 값을 낭비하면서 적고 싶겠는가! 

 

오늘은 JPA에서 제공하는 Enum의 처리법에 대해 알아보고, 그중 'Attribute Converter'에 대해 이야기해보려고 한다.

 

 

@Enumerated 있는데 그거 아님?

 

"이거 이거 정말 멍청한 놈이군..  우리에겐 @Enumerated이 존재하다고~"

맞다. 우리에겐 @Enumerated가 존재한다.

하지만 과연 @Enumerated이 효율적이고, 유지보수를 편하게 할 수 있게 Enum을 저장한다고 생각하는가? 난 아니라고 생각한다.

 

@Enumerated이 이야기하는 2가지 저장방법

<죽창.. 죽창이 필요하다>

@Enumerated이 이야기 하는 2가지 저장방법에 대해 이야기해보자.

이름 방식
EnumType.ORDINAL ENUM이 정의된 순서의 인덱스로 DB에 값을 저장.
EnumType.STRING ENUM의 이름 자체를 DB에 값으로 저장.

이렇게 사용 중인 Entity를 본다면 분노할 것 같지만.. 일단 하나씩 정리해보도록 하자 

EnumType.ORDINAL

이 방식 자체가 Enum에 정의된 순서를 기반으로 인덱스가 만들어지고, DB에 저장되는 방식이다.

데이터의 무결성을 나의 코드 한 번에 깰 수 있는 굉장히 위험한 처리 방식이라고 생각한다.

위에 있는 CouponErrorStatus라는 Enum이 있고 @Enumerated(EnumType.ORDINAL)로  데이터를 DB에 저장하고 있다는 가정을 해보겠다.

실제 디비에는 아래와 같이 저장된다.

ENUM 실제 저장 값 
EXPIRED 0
NOT_ASSIGN 1
NOT_FOUND 2

여기까지만 보면 별로 문제가 없어 보인다 (사실문제가 있어 보여야 정상이긴 한데 없다고 치자)

근데 CouponErrorStatus에 UNKNOWN이라는 스펙이 추가되었다.

이렇게 되면 실제 DB에 저장되는 값이 아래와 같이 달라지게 된다.

ENUM 실제 저장 값 
EXPIRED 0
UNKNOWN 1
NOT_ASSIGN 2
NOT_FOUND 3

기존에는 NOT_ASSIGN이 1이었으나, 중간에 UNKNOWN이 치고 들어오면서 UNKNOWN이 1이 되어버린 것이다.

 

대략 정신이 멍해진다.

그럼 그냥 순서대로 뒤로 추가하면 되는 거 아님?이라고 이야기할 수 있지만, 굳이 그런 리스크를 가지고 가야 하는가 싶기도 하고,

우리에겐 야생에서 온 신입이 항상 기다리고 있다는 것을 잊지 말자.

 

EnumType.STRING

Enum에 정의된 값의 이름(EXPIRED, NOT_ASSIGN)을 그대로 DB에 넣겠다는 뜻이다.

이렇게 개발을 한다면 'ORDINAL'로 사용하는 것만큼 큰 문제는 없지만, DB에 데이터를 낭비하면서 넣게 된다.

 

소개합니다 @Converter 그중 AttributeConverter

그래서 어쩌라는거지...

위에서 내가 잔뜩 겁을 주었다. 거의 이 정도면 Enum 쓰지 말라는 수준 아닌가 라는 생각이 들 정도다. (아님 말고)

하지만, 우리보다 똑똑한 오픈소스 개발자님들도 이러한 문제를 인지하였고, JPA의 철학을 최소로 건드리면서 DB에 값을 효율적이게 저장할 수 있는 방안에 대해 이야기한다. 그것이 바로 '@Converter'이다.

@Convertor?

어떤식으로 동작 하는지 궁금해서 디버그를 찍어 확인을 해보니, 아래와 같은 구조로 사용 되었다.

영속성 컨텍스트 -> Convetor -> DB

영속성 컨텍스트에 데이터가 들어가고, 실제 디비로 들어가거나 나오기 직전에 Convetor로직이 있다면 돌고 난 후에 DB로 접근하게 되어있다.

 

자 그럼 어떻게 사용 하는데?

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.stream.Stream;
import javax.persistence.*;

import com.yangs.event.coupon.domain.enums.CouponStatus;
import lombok.*;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Table(name = "coupon")
@Entity
@Getter
@Setter
public class Coupon implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "coupon_id", insertable = false, nullable = false)
    private Long couponId;

    @Column(name = "coupon", nullable = false, unique = true)
    private String coupon;

    @Column(name = "coupon_status", nullable = false)
    private CouponStatus status;

    @Column(name = "expired_timestamp", nullable = false)
    private LocalDateTime expiredTimestamp;

    @Column(name = "update_timestamp")
    private LocalDateTime updateTimestamp;

    @Column(name = "reg_timestamp", nullable = false)
    private LocalDateTime regTimestamp;
}

 

이렇게 된 Entity가 있고, CouponStatus라는 Enum이 있다.

public enum CouponStatus {
    CREATE(1), ASSIGN(2), USE(3);
    public int value;

    CouponStatus(int val) {
        this.value = val;
    }
}

말 그대로 쿠폰 정보를 저장하고, 상태를 Flag로 관리하고 있는 Entity라고 생각하면 될 것 같다.

나는 CouponStatus에 있는 VALUE가 디비에 저장되었으면 좋겠다. 

그렇게 하기 위해서는 아래와 같이 Converter Class를 작성해야 한다.

 

import com.yangs.event.coupon.domain.enums.CouponStatus;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.stream.Stream;

@Converter(autoApply = true)
public class CouponStatusConverter implements AttributeConverter<CouponStatus, Integer> {

    @Override
    public Integer convertToDatabaseColumn(CouponStatus couponStatus) {
        return couponStatus.value;
    }

    @Override
    public CouponStatus convertToEntityAttribute(Integer integer) {
        return Stream.of(CouponStatus.values())
                .filter(c -> c.value == integer)
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }
}

JPA AttributeConverter 인터페이스 스펙상 2개의 method를 제공하는데, 상세한 내용은 아래와 같다.

Method 명 역할
convertToDatabaseColumn DB에 들어갈때, 어떤 값을 저장 할 것인가? 
convertToEntityAttribute DB에서 Entity로 값을 넣어줄때 어떤 값을 리턴 해줄것이냐?

아 그리고 나 같은 경우는 귀찮다는 이유로 Global 하게 지정을 했는데 @Converter(autoApply=true)로 지정하였는데, 실제로는 Entity 객체에 @Converter(converter=CouponStatusConverter.class) 이런 형태로 해주는 것이 좋다.

활용

이 글을 작성하면서  ENUM 대신 다른 케이스가 있는 것 같아 한번 소개를 해보려고 한다.

 

1) 개인정보 암호화/복호화

ISMS나 여러 심사를 받아본 사람이라면, DB에 값을 암호화해서 저장해야 하는 경우가 있을 것이다.

그래서 매번 암호화, 복호화하는 로직이 돌아가는 것이 코드 여기저기에  있는 것이 보일 것이다. (으.. 상상도 하기 싫다)

AttributeConverter를 사용한다면, 이런 로직을 Convertor에 위임하고, 실제 로직상에서는 복호화된 값으로 코드를 작성할 수 있다고 생각은 했다.

 

하지만, 아래와 같은 문제가 있어서 사용에 조금은 고민을 해보고 싶다.

 

- 암/복호화가 필요 없는 필드 혹은 호출에서도 매번 암/복호화를 통한 비효율적인 리소스 사용

아무래도 암호화/복호화가 필요 없는 작업을 할 때도, 암/복호화를 위해 리소스를 사용하기 때문에 비효율적인 호출이라고 생각한다.

 

2) JSON 데이터 부르기

 

Attribute Converter를 이용한 커스텀 컬럼 사용하기

Attribute Converter를 이용한 커스텀 컬럼 사용하기 데이터를 저장할때 사용하는 방식으로 JSON 방식으로 사용하는 경우가 많다. DB에서 데이터를 읽을 때에는 객체로 변환하고 저장할때에는 스트링 �

helloino.tistory.com

이 내용은 아웃링크로 대신하겠다.

솔직히 난 별로 일 것 같다는 생각은 좀 든다.

 

그 외에 사실 몇 개 생각나긴 했는데 시간이 없어서 일단 하나만 적으려고 한다

 

마치며

오늘은 "Spring 이 정도는 해줘야지"라는 대분류에서 중분류 "JPA" 항목의 첫 번째 글을 작성해보았다.

이 글은  내가 회사에서 업무를 보던 중, 열거형 데이터를 더 효율적이게 저장 하려면 어떻게 해야 할까 하고 찾아보던중 

'AttributeConverter'에 대해 알게 되었고, 현재까지도 이렇게 사용중에 있다.

 

하지만 지금 업무에 사용중인 Entity가 많다보니, Converter가 남발되기 시작하였고, 지금은 어느정도 정리 하긴 하였는데

이 내용도 조만간 한번 이야기 해보면 좋을 것 같다.

 

댓글0