본문 바로가기
DBMS || SQL_MAPPER/POSTGRE

JAVA enum <-> DB enum 바인딩 실패기

by DanteMustDie 2024. 1. 19.
728x90

필요성

프로젝트 진행 중, 변할 일이 거의 없는 데이터들의 대해 DB insert를 어떻게 할 것이냐에 대한 회의가 있었다. 원랜 공통코드 들을 명세해서 테이블로 저장해두고, 프론트로 쏴서 foreach (value : list) 등의 방법으로 반복적으로 동적 그리기 용이하기 좋게 구상해서 만들었는데 성별, 연령대 같은 데이터는 왠만해선 범위 자체가 대격변이 발생하지 않는이상 불변에 가까운 데이터이기 때문에 이 데이터들은 있는 그대로 보관해도 좋지 않겠냐는 의견이 나왔다.

그렇다보니 DB 자체에 저장할때도 enum값으로 저장하면 좋겠다는 생각이 들었다. 즉, 클라이언트(프론트엔드)에서 gender : man 또는 woman 으로 보낸다면, enum을 활용하여 보여지는 텍스트는 man, woman이지만 실제 기록되는 값은 0,1이 될 것이다. 추후 추천 시스템을 만들 때 수치화 된 데이터들을 가지고 있다면 계산하기에도 더 좋지 않을까? 싶은 추측도 있었고, 아주 작은 차이겠지만 DB 자체에도 용량이 줄어들꺼라 추측이 되었다. 무엇보다 enum으로 받을  수 있는 값을 정해두면, 반입되는 데이터를 의도한 값만 받을 수 있도록 제약을 가할 수 있다. 단점이라면 하나의 enum을 1코드 or 1도메인 초과해서 쓰는 순간, 즉 활용하는 곳이 많을수록, 변경사항이 발생 할 확률이 생길수록 담당자의 집중력을 믿고 꼼꼼히 써야한다는 것이다. 그리고 enum값이 수정될 경우 일일히 다 update를 해줘야하는데 놓치는순간 그 부분은 값이 기대값이 아닌 이상한 값이 들어간다. 그래서 불변의 성격을 지닌 데이터를 받을때만 한정적으로 쓴다.

구상

위 환경은 자바17 및 스프링부트 3.2, JPA, POSTGRE 환경이다. 프로세스 흐름은 일반적인 방향을 충실히 따른다.

컨트롤러는 프론트에서 요청 온 파라미터들을 DTO에 바인딩해서 서비스에서 가입 수행 요청을 한다.

서비스는 컨트롤러부터 요청이 온 가입 수행 요청 로직을 수행하며, 리포지터리에게 실제 수행 될 데이터들을 Entity 객체에 맵핑 후 요청을 보낸다.

리포지터리는 서비스에서 요청 온 데이터를 가지고 쿼리에 바인딩 후 sql을 전송한다.

성별 enum
dto의 gender는 entity에서 다음과 같이 바인딩했다.
서비스에선 다음과 같이 dto를 보내주기만 하면 된다. return new 패턴을 이용하였다.
Entity에서 성별 필드는 다음과 같이 설정했다.

1
2
3
-- 성별
CREATE TYPE public.gender AS ENUM ('man''woman');
ALTER TABLE public."member" ALTER COLUMN gender TYPE public.gender USING gender::text::public.gender;
cs

POSTGRE에서 DB는 다음과 같이 수정을 하였다. enum타입을 하나 만들고, 테이블의 gender 컬럼 자료형을 enum으로 바꾼 것.

실행 결과

나의 뇌피셜과 기대에 따르자면 DTO String gender : "man"을 enum으로 바인딩해서 enum의 키값인 "man"이 insert 쿼리에 들어가, 실질적으로 DB에도 보여지는 텍스트는 "man"이지만 보이지않는 값은 숫자가 될 것이라 생각했다. 하지만, 쿼리수행에 실패를 하게 된다.

"man" 값이 varchar로 바인딩 되고있다.

@Column 애노테이션의 columnDefinition을 써도 사용자가 정의한 enum 타입에 대한 바인딩이 불가능 한 듯 하다.

@Enumerated의 경우엔 자바의 enum타입을 바인딩한 값을 쓸때 key(STRING)로 보낼지, index value(ORDINAL)로 보낼지를 정의 할 뿐 실제 쿼리에 필요한 자료형에 맞춰 바인딩이 되진 않는다. 즉 STRING으로 지정하면 man이 가고 ORDINAL로 지정하면 0이 보내질 것이다.

DB의 enum 세팅이 잘못되었는지 테스트해보니 이쪽은 정상 작동을 한다. SQL 에디터에선 순수하게 쿼리만 보내므로 DBMS에서 알아서 바인딩해주기 때문일 것이다.

테스트용 INSERT 쿼리
쿼리 실행 결과 gender에 'man'이 잘 들어 온 것을 확인.

자바는 자바대로 enum을 Key값으로 잘 보냈고, DB는 DB대로 enum 타입이 잘 선언 되었다. 결과만 보자면 이는 JPA ORM의 한계로 보이거나, 내가 무지하여 정확한 맵핑 방법을 몰라서 그런 것이라 본다.

아쉬운대로 DB는 int4로 받고, service에서 Ordinal -> Name, Name -> Ordinal나 Ordinal -> Value / Value -> Ordinal와 같이 enum을 dto와 entity 사이의 converter처럼 써서 계획한 것의 반쪽짜리 enum을 썼다. 근데 이 방식의 단점은, exception 처리를 썩 맘에들게 할 수가 없다. try catch는 지저분해서 마음에 안든다. 장점은 Entity에서 애노테이션을 쓰지않고 int타입 그대로 써서 ORM 맵핑 걱정이 없어 편하다. 이렇게 안하면 enum converter를 또 만들어야한다.

dto -> entity 변환 코드중 일부
클라이언트로 보낼땐 클라이언트에서 필요한 값에 따라 (name or value) 맞추어 보낸다.

반쪽짜리를 굳이 쓴 이유는 여태 한게 아깝고 그나마 대용량으로 갈수록 String-Varchar 타입보단 int 타입이 값이 적어 조회가 빠르기 때문이다. 이에 대한 테스트를 한 다른 분의 글이 아래 링크에 있다.

https://wildeveloperetrain.tistory.com/250

 

Java Enum 타입 데이터베이스 저장 형식은 뭐가 좋을까? (enum, varchar, tinyint)

Java Enum Type 데이터 DB 저장 형식 (enum, varchar, tinyint) 해당 내용은 데이터베이스 설계 과정에서 Java Enum 타입의 데이터는 어떤 형식으로 저장되는 것이 좋은가에 대해 고민하며 정리한 내용입니다.

wildeveloperetrain.tistory.com

 

결론

예상과는 다르게 맵핑의 불일치로 인해 결국 자바의 enum값을 DB의 enum값으로 맵핑하여 insert하는 것에 실패했다.

자바에서 enum을 쓰지않고 일반 String, Int 등으로 보내도 결과는 마찬가지다.

조금이라도 용량을 아끼고 가독성을 살리고 데이터를 일관성있게 가지고있고자 시도 해보았는데 호되게 당해버렸다. mybatis같은 SQL Mapper를 쓴다거나 다른 방식으로 하면 될 수도 있을 것 같긴한데 그렇게까지 다양한 테스트를 시도하기엔 당장 개발 할 것도 많아 이렇게 실패기만 기록을 남기고 추후 여유가 될 때 다른 시나리오를 구상해서 더 시도를 해보아야 겠다. 그냥 돈 좀 더 내고 용량 커져도 varchar로 편하게 쓰자 너무 귀찮은 뻘짓이다.

참고로 RDB에선 enum타입 사용을 권장하진 않는다. 오히려 지양해야한다. 다음의 링크에서 해당 내용의 번역을 확인 할 수 있다. enum타입을 쓰려면 정말로 불변하고 어떠한 연관관계도 없는 데이터인지 확장성까지 고려하여 사용하는 것이 좋다.

https://velog.io/@leejh3224/%EB%B2%88%EC%97%AD-MySQL%EC%9D%98-ENUM-%ED%83%80%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%90%EC%95%84%EC%95%BC-%ED%95%A0-8%EA%B0%80%EC%A7%80-%EC%9D%B4%EC%9C%A0

 

[번역] MySQL의 ENUM 타입을 사용하지 말아야 할 8가지 이유

이 글은 Chris Komlenic의 글 8 Reasons Why MySQL's ENUM Data Type Is Evil을 번역한 글입니다. 원문은 링크에서 찾아보실 수 있습니다.

velog.io

 

반응형