R2DBC Enum매핑

R2DBC ENUM 타입 #

R2DBC에서는 enum타입을 자동으로 처리해주지 않는다는 사실을 알고있는가?
JPA(JDBC)에 익숙해져 상상도 못하는 에러가 발생했다. 이런 부분까지 JDBC가 자동으로 처리주는거였는지 R2를 사용하면서 갓JPA였구나 라는 생각이 계속 들었다.

JDBC 드라이버 vs R2DBC 드라이버 #

JDBC와 R2DBC의 큰 차이는 데이터처리 패러다임에 있다.
JDBC가 동기적 블록킹 모델을 따라는반면 R2DBC는 비동기 리액티브 프로그래밍 모델을 따르다보니 데이터가 준비되는 즉시 리액트비 스트림을 통해 실시간으로 전파된다.
즉 데이터가 준비되면 바로 스트림으로 전달하는 방식때문이다.

이게 enum 매핑과 무슨상관인가?

DB에 enum 타입을 저장할때에는 String으로 변환하여 DB에 저장하고 -> 읽어올때에는 String을 Enum타입으로 변환 후 사용한다

7278fcf9-4267-498d-9c51-09a25392680b

JDBC 드라이버가 자동으로 처리해주고 있었지만, R2DBC 같은경우 타입변환기를 따로 만들어줘야한다.
JDBC는 모든 쿼리가 준비될때까지 모든데이터를 드라이버에서 변환하고 데이터를 응답해준다.

하지만 R2DBC의 경우 데이터가 준비되는데로 스트림을통해 전달해줘야하기 때문에 복잡한 변환로직 자체가 없다.

결론적으로 비동기의 효율을 극대화 시키기위해 기본적인 데이터타입 변환만 제공하고, 벼노환 로직은 사용자가 필요에따라 구현할수있게 만든 구조라고 보면 될 거 같다.

그럼 R2DBC에서 오히려 커스텀 변환기를 두는게 오버헤드가 아닐까라는 생각을 해본다. 데이터가 준비될때마다 커스텀 변환작업이 수행되기때문에 데이터가 많을 수록 안 좋은게 아닐까? 하지만 데이터의 일관성을 위해 필요한 작업이라고 생각해, 커스텀 변환기는 무조건 필요한 작업이라생각한다.

(쿼리 결과 데이터 많을시에 기준) 빠르지만 오버헤드냐 vs 느리지만 오버헤드가 아니냐 라고 생각한다면 빠른게 우선인거 같다. enum 타입 변환이야 복잡하지 않으니 크게 문제 될게 없지만 예를들어 복잡한 객체그래프 같은 경우 연관관계 데이터를 한꺼번에 로드할때 커스텀 변환기를 통해 작업 시 오버헤드가 크게 증가 할 수 있다.

드라이버에 enum 자동처리 등록 #

https://github.com/pgjdbc/r2dbc-postgresql#postgres-enum-types

 .codecRegistrar(
                EnumCodec.builder()
                    .withEnum("my_enum", MyEnum::class.java)
                    .withEnum("chain_type", ChainType::class.java)
                    .withEnum("account_type", AccountType::class.java)
                    .withEnum("transfer_type", TransferType::class.java)
                    .build()
            )
            .build()

enum타입을 등록하고, enum 타입이 잘 처리되길 기대했지만 에러가 발생했다 읽기쿼리를 실행순간!

d1368ed1-410e-4b36-8dd6-99141606febc

enum 타입과 varchar를 비교하려고 해서 생긴문제이다.
그럼 드라이버에서 제대로 처리가 안된건데 왜지?
관련 이슈를 찾아보던 와중 스택오버플로우에 좋은 정보를 얻을 수 있었다

c2b2eb2b-6aff-4222-ab2e-e663ef352453

스프링 데이터 R2dbc에서는 드라이버에 내장된 메커니즘을 사용하여 열거형을 처리하는 경우 EnumWriteSupport를 등록해야 한다고 합니다. 하지만 제 경험에 따르면 Spring Data R2dbc를 사용할 때 쓰기는 자동으로 처리할 수 있지만 Postgres에서 열거형을 읽으려면 읽기 변환기가 필요합니다.

Spring Data R2dbc를 사용하지 않는경우 EnumCodec에 등록하는것만으로도 충분하지만 Spring Data R2dbc를 사용하는경우 어플리케이션 레벨에서도 따로 커스텀 변환기를 생성해줘야한단다

    @Bean
    override fun r2dbcCustomConversions(): R2dbcCustomConversions {
        val converters: MutableList<Converter<*, *>?> = ArrayList<Converter<*, *>?>()
        converters.add(ContractTypeConverter(ContractType::class.java))
        converters.add(StringToEnumConverter(ContractType::class.java))
        converters.add(ChainTypeConvert(ChainType::class.java))
        converters.add(StringToEnumConverter(ChainType::class.java))
        converters.add(TokenTypeConvert(TokenType::class.java))
        converters.add(StringToEnumConverter(TokenType::class.java))
        return R2dbcCustomConversions(storeConversions, converters)
    }

그럼 각 enum타입마다 쓰기와,읽기를 생성해줘야하는것인가..? 데이터 클래스를 만들지 않고 제네릭을 만들수없나라는 생각이였지만, 제네릭타입은 런타임에 제네릭의 타입정보를 소거하는 사실을 처음깨달았다 제네릭의 타입을 알지못해 Object로 변환되어 해당 enum타입을 알지 못해 에러가 발생했다 코틀린에서는 런타임시에 제네릭타입을 기억할수있는 inline이라는 함수가있지만 안타깝게도 EnumWriteSupport 는 추상클래스라 inline 키워드를 사용하지못했다

결국 명식적으로 enumType을 매개변수로 전달하는 방법으로했다..

갓갓갓갓 JPA