개발자로 후회없는 삶 살기
spring PART.JPA 사용하지 않고 enum 타입 DB에 저장하기 본문
서론
DB에 데이터를 저장하기 위해서는 DB 컬럼 타입과 엔터티 필드 타입을 맞춰줘야 합니다. String을 varchar로 int를 integer로 맞추는 것을 의미합니다.
@Enumerated(value = EnumType.STRING)
자바에서는 enum 타입이 있고 enum 타입의 데이터를 DB에 저장해야 할 필요가 있습니다. JPA에서는 이것을 어노테이션으로 해결할 수 있습니다. 그러면 JPA가 없으면 어떻게 해야 할까요? 이에 대해 알아봅니다.
본론
- 엔터티 필드
@Setter
@Getter
@ToString
public class Item {
private Long id;
private String itemName;
private int price;
private int quantity;
private Boolean open;
private Region[] regions;
private ItemType itemType;
private String deliveryCode;
엔터티 필드에 Region과 ItemType이라는 enum 타입이 있습니다.
public enum Region {
SEOUL("서울"), PUSAN("부산"), JEJU("제주");
private String desc;
Region(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private String desc;
ItemType(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
Region과 ItemType 둘 다 상수와 문자열(desc)을 연결한 형태입니다.
- DB에 저장하는 코드
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity, open, regions, item_type, delivery_code, attach_file, image_files)" +
" values(:itemName, :price, :quantity, :open, :regions, :itemType, :deliveryCode)";
KeyHolder keyHolder = new GeneratedKeyHolder();
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", item.getItemName())
.addValue("price", item.getPrice())
.addValue("quantity", item.getQuantity())
.addValue("open", item.getOpen())
.addValue("regions", item.getRegions())
.addValue("itemType", item.getItemType())
.addValue("deliveryCode", item.getDeliveryCode())
상품 엔터티를 저장하는 insert 쿼리는 jdbc의 이름 기반 파라미터를 사용하여 작성하였습니다.
@Test
void save() throws IOException {
// given
Item item = new Item("A", 1, 1, true, Region.values(), BOOK, "A");
// when
Item saveItem = itemRepository.save(item);
// then
assertThat(item).isEqualTo(saveItem);
}
item 객체를 만들고 Region과 BOOK에 enum 상수를 지정하고 DB에 Save하는 테스트 코드를 작성했습니다.
- 테이블 속성
create table item
(
item_id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
open boolean,
regions varchar(255),
item_type varchar(255),
delivery_code varchar(10),
primary key (item_id),
check ( regions in ('BOOK', 'FOOD', 'ETC') )
check ( item_type in ('BOOK', 'FOOD', 'ETC') )
);
H2에서는 enum 타입을 받기 위해서 regions와 item_type에 check in 구문을 사용하고 type은 varchar()로 해야 합니다. in 구문 다음에 ()에 있는 값 중 하나를 받겠다는 의미입니다.
그렇다면 엔터티의 필드도 String으로 바꿔야할까요? ✅
저의 목적은 필드값을 DB에 저장하는 것이므로 varchar()에 맞는 String으로 바꿔야 할 것 같습니다. 하지만 결과적으로 엔터티는 건들면 안 됩니다. 우리는 항상 엔터티 중심적으로 개발을 해야지 DB에 의존하면 안 됩니다.
.addValue("regions", Arrays.toString(item.getRegions()))
.addValue("itemType", item.getItemType().name())
따라서 엔터티 필드와 상품 객체 생성 시 초기화는 그대로 두고 insert 문에서 Arrays.toString으로 enum 배열을 string으로 변환하여 저장하였고
create table item
(
item_id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
open boolean,
regions varchar(255),
item_type varchar(255),
delivery_code varchar(10),
primary key (item_id),
check ( item_type in ('BOOK', 'FOOD', 'ETC') )
);
enum 배열에 해당하는 쿼리는 문자열을 저장하기 위해 check in 구문이 아닌 단순 varcher로 바꿨습니다. 또한 enum 타입은 ItemType.BOOK을 했을 경우 타입이 ItemType이라서 DB에 Varchar로 저장할 수 없어서 name()으로 string으로 변환 후 저장합니다.
결과를 보면 enum의 desc는 빼고 영어만 저장되어 있습니다. name은 시스템 코드이고, desc가 화면에 보여줄 사용자 코드이므로 DB에는 영어를 저장해야 합니다.
- enum 타입 조회
근데 여기 치명적인 실수가 있었습니다. 지금부터 위의 문제를 해결해봅니다.
문제 : 조회 시 DB에 저장된 Region 배열 값을 가져오지 못하는 상황
저장까지는 완료했는데 조회(findById) 시 '[SEOUL'이 Region의 상수가 아니라는 에러가 발생합니다.
private Region[] regions;
regions는 필드에 배열 형식으로 저장이 되는데 insert 문에서 .addValue("regions", Arrays.toString(item.getRegions()))으로 저장하여
regions = "[SEOUL, PUSAN, JEJU]"
findById를 하면 이렇게 enum 타입에 문자열이 들어갑니다. DB의 regions 속성은 varchar()인데 필드의 regions 속성은 enum 타입이라서 생기는 차이를 해결해야 합니다.
-> 해결 방법
Region[] regions = {BOOK, FOOD};
DB의 regions에 저장된 문자열을 array로 변환하고 ENUM 배열로 만든 후 Regions에 대입해야 하고 최종적으로 findById에서 이런 대입이 일어나야 합니다.
1) '[SEOUL'에 대괄호 없애기
.addValue("regions", Arrays.toString(item.getRegions()))
대괄호가 생기는 이유는 insert 문에서 getRegions로 enum 배열을 받고 [ ]가 있는 채로 문자열로 변환해서 그렇습니다.
.addValue("regions", Arrays.stream(item.getRegions())
.map(Object::toString)
.collect(Collectors.joining(", ")))
따라서 먼저 스트림으로 대괄호를 벗깁니다. 그러면 "SEOUL, PUSAN, JEJU"의 형식이 되어 DB에 저장이 됩니다.
private RowMapper<Member> memberRowMapper() {
return BeanPropertyRowMapper.newInstance(Member.class);
}
이제 DB에 저장된 문자열을 id로 조회하기 위해 mapper를 손봐야 합니다.원래는 위처럼 추상화되어있던 것인데 내부에서 타입변환을 하기 위해 직접 작성합니다.
private RowMapper<Item> itemRowMapper() {
return ((rs, rowNum) -> {
Item item = new Item();
Region[] regions = new Region[3];
item.setId(rs.getLong("item_id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
item.setOpen(rs.getBoolean("open"));
// regions enum 배열 타입
String[] stringRegionArray = rs.getString("regions").split(", ");
for(int i = 0; i < stringRegionArray.length; i++){
regions[i] = Region.valueOf(stringRegionArray[i]);
log.info("regions = {}", stringRegionArray[i]);
}
item.setRegions(regions);
item.setItemType(ItemType.valueOf(rs.getString("item_type")));
item.setDeliveryCode(rs.getString("delivery_code"));
log.info("{}", regions);
return item;
});
}
'regions enum 배열 타입' 주석 부분을 보면 문자열을 split 하고 선언해 둔 Region 배열에 대입한 후 item에 set 합니다.
@Test
void findById() {
// given
Item item = new Item("A", 1, 1, true, Region.values(),
BOOK, "A");
// when
Item saveItem = itemRepository.save(item);
Item findItem = itemRepository.findById(1L);
log.info("item = {}", item);
log.info("saveItem = {}", saveItem);
// then
assertThat(saveItem).isEqualTo(findItem);
}
테스트를 돌려보면 find를 성공하고
원하는 대로 저장이 됩니다.
app에서도 실행해보면 H2 DB에 제대로 저장이 됩니다.
결론
enum 타입을 DB에 저장하기 위해서는 문자열과 enum 타입의 변환이 저장과 조회에 모두 필요하여 상당히 까다로운 과정이 필요했습니다. 엔터티 설계 시 코드의 간결함을 위해 enum 타입을 자주 사용하게 되는데 이번 기회로 확실히 잡을 수 있었습니다.
'[백엔드] > [spring+JPA | 이슈해결]' 카테고리의 다른 글
spring PART.Value Object와 Custom Validator를 이용한 검증 개선 (0) | 2023.07.21 |
---|---|
[Java] Java의 immutable (0) | 2023.07.07 |
(작성중) spring PART.로컬 호스트에서 spring 서버와 flask 서버 통신하기 (0) | 2023.06.16 |
[spring] equals()와 hashCode()를 재정의 해야하는 이유 (0) | 2023.05.03 |
[spring] @NotNull, @NotEmpty, @NotBlank의 차이점 (1) | 2023.04.30 |