작업일지

작업 일지#3 카테고리 도메인의 기능 구현 그리고, 상품 도메인과 연계

hj.choi 2021. 10. 20. 03:13

작업 일지#3 카테고리 도메인의 기능 구현 그리고, 상품 도메인과 연계

개요

최근에 정신이 없었다. 화이자 2차 백신을 맞고 한동안 몸이 계속 안좋았다. 살면서, 이렇게 앓아누우면서 고생했던 건 이번이 거의 처음인듯 했다. 그동안 개발 공부한다고, 무리해서 쌓였던 피로도와 2차 백신의 부작용이 큰 원인인 듯 했다. 그래서, 살기위해(?) 한동안 휴식에 전념했던 것 같다. 지금은 컨디션이 매우 좋아져서 다시 개발공부에 전념할 수 있어, 카테고리 도메인 관련 작업을 진행했고, 이렇게 다시 작업일지를 작성할 수 있었다.

본론으로 돌아와서, 이번엔 상품 도메인의 단순 상품 CRUD 기능에 카테고리를 더하여, 좀더 상품을 다채롭게(?) 다룰 수 있는 기능을 구현하고자 했다. 카테고리 도메인을 작업하면서 가장 어려움을 겪고 해결을 했던 이슈는 다음과 같다.

  • 카테고리 스키마 구조
  • DB의 join? java stream map?
  • lombok의 boolean getter? MyBatis의 Reflector getter? Jackson?
  • TypeHandler와 Enum
  • 이번 작업일지에는 위에서 나열한 이슈들을 중심으로 이야기를 진행해볼 예정이다.

카테고리 SCHEMA

카테고리 자체에서 담을 데이터 자체는 매우 간단했다. 상품을 좀더 세밀하게 분류할 수 있는 기준만 제공해주면 되기 때문이다. 그래서 결국 필요한 데이터는 카테고리명이다. 하지만, 카테고리를 좀더 세밀하게 다루기 위해 대 카테고리, 중 카테고리, 소 카테고리로 나눌 경우, 조금 복잡해진다. 식품 이라는 카테고리를 예로 들어보자. 식품에는 상품의 종류에 따라 좀더 세밀하게 분류할 수 있는데, 하나의 경우를 예로 들자면, 식품 -> 축산 -> 소고기가 있다. LinkedList를 연상시키는 구조인듯 하다. 그래서 초기에는 Category 스키마를 다음과 같이 구성했다.

Logical Name Name Type
카테고리 pk id bigserial
카테고리 이름 name varchar(50)
자식 카테고리 fk child_id bigint
환불가능 여부 is_refundable boolean

구성된 스키마를 간략하게 설명하자면, 재귀적 연관관계를 통해, 대,중,소 카테고리의 연결관계 (1:1:m)을 형성하게 된다. 따라서, 특정 상품의 카테고리(소 카테고리)를 조회하고 싶으면 단건 select 쿼리를 사용하면 되고, 특정 카테고리와 연관된 다른 카테고리를 함께 조회하고 싶으면, child_id fk를 통해 join 구문을 사용한 select 쿼리를 사용하면 된다.

하지만 이러한 구조의 스키마에는 한가지의 문제점이 있었다. 바로 복잡하다는 것이다. 복잡하게 만드는 요소는 당연하게도 자식 카테고리 fk 컬럼이었다. 물론, 이 컬럼이 있기에, 카테고리 구조를 좀더 scalable하게 하지만, 전체 조회를 할 경우, 혹은 대 카테고리와 연관된 카테고리들을 조회를 해야할 때, 카테고리 구조가 복잡해질수록 join 구문의 길이가 늘어나며, 이는 결국 DB 서버에 리소스를 더 잡아먹게 할 수도 있고, MyBatis의 경우, 카테고리의 구조가 복잡해질 때마다, SQL 매핑을 새로해야한다. MyBatis와 같은 SQL mapper를 사용해본 사람들은 알겠지만.. 쿼리 한번 바꾸는데, 혹은 컬럼하나 수정하는데 생각보다 시간을 많이 잡아먹는다.

또한, 카테고리는 다른 도메인의 데이터에 비해 비교적 정적이다. 카테고리 데이터의 CRUD 작업은 서비스 관리자에 한해서, 이루어지며, 서비스 제공관련 정책이 바뀌거나 하는 큰일이 일어나지 않는 이상 한 번 등록된 카테고리를 수정하는 경우도 거의 없다. 따라서, scalable에 대비한다는 것 자체로 카테고리에 재귀적 연관관계와 같은 복잡한 설계를 하는 것은 결과적으로 비효율적이라 생각이 들었다. (무엇보다, 카테고리 구조에 변경이 일어날때마다 SQL 매핑 작업을 반복해야하는 구조가 너무도 거슬린다...)

그래서, 카테고리 SCHEMA는 결과적으로 다음과 같이 정했다.

Logical Name Name Type
카테고리 pk id bigserial
카테고리 이름 name varchar(50)
카테고리 타입 category_type varchar(50)
카테고리 레벨 depth_level varchar(50)
환불가능 여부 is_refundable boolean

이전에 구성한 스키마와 차이점이 있다면, fk를 통해 연관관계를 맺는 것이 아니라, 연관관계를 맺는 데 필요한 메타 데이터를 따로 넣는다는 것이다. 이렇게 구성함으로서 얻고자 하는 이점은 다음과 같다.

  • 스키마의 복잡성을 줄여, 컬럼마다 데이터의 가독성을 높인다.
  • fk를 없애서, 경우에 상관없이 연관관계를 고려해야만 하는 경우를 없앤다.
  • 일단 필요한 데이터만 가져오고, 그 데이터간의 연관관계 구성에 대한 선택을 강요하지 않는다. (즉, 데이터 필터링은 애플리케이션 코드로 넘긴다.)

마지막에 데이터 필터링을 애플리케이션 코드로 넘긴다고 언급을 했는데, 이 부분 역시 적지않게 고민한 부분이기에 다음 이슈로 넘겼다.

DB Join vs Java Code

이 이슈는 결국엔 비즈니스 로직을 DB에 넣느냐, 애플리케이션에 넣느냐에 대한 고민인것 같다. 왜냐면, 비즈니스 로직이란 결국 데이터를 서비스의 의도에 맞게 가공하는 것이기 때문이다. 데이터를 가공, 즉 필터링 한다는 것은 사실 DB에서도 가능하고, 애플리케이션 코드로도 가능하다. DB에는 DML에서 제공하는 구문과 DBMS에서 제공하는 함수들을 활용해볼 수 있다. 그리고 애플리케이션에는 코드를 통해 메소드를 직접 짤수도 있고, 프레임워크나 라이브러리에서 제공하는 api를 활용할 수 있다.

오류 발생여부만을 따져보자면, 사실 둘 중 어느 하나를 고르든, 혹은 둘다 사용하든, 큰 문제는 없을 것이다. 사실 데이터 필터링 작업은 DB에서 처리하는 것이 훨씬 쉽다. 하지만, 데이터 필터링 작업을 DB에 몰아 넣으면 DB에 몰리는 리소스의 양이 많아진다. 확장성을 고려한다면, 애플리케이션 리소스가 더 확장하기 쉬운 점을 고려해보면, DB에 필터링 로직을 몰아넣는 것에 대해선 좀더 고민하게 된다.

이 부분에 대해선 블로깅과 간단한 실습을 통해, 비교분석을 해보았는데, 결론은 다음과 같았다.

  • 대규모 데이터를 대상으로 한 쿼리의 경우, 필터링 처리는 DB에 넘긴다.
    • 대량의 데이터를 일단 전부 불러와서 애플리케이션에서 처리하도록 하는 작업은 DB 서버의 리소스를 소진시킬 수 있다.
  • 필요한 데이터의 필터링하는 것과 필요한 데이터만 추출하는 작업을 분리한다.
    • 예를 들자면, 특정 필드를 통해 필요한 데이터를 추출하여 나온 작업을 통해 또 데이터 필터링을 해야할 경우 다음과 같이 작업하면 리소스 분산을 효율적으로 할 수 있다.
      • select id, field1, field2 from item; -> 가져온 데이터를 애플리케이션 코드를 통해 유효한 id만 추출 -> select * from item where id in(1,2,9,323,....);
  • 리턴된 결과를 대상으로 한 작업은 애플리케이션 계층에서 작업하기.
    • 간단히 말하자면, DBMS의 함수를 사용하기보다는, DBMS의 함수로 구현하고자 하는 기능을 애플리케이션 코드로 구현하자는 것이다.
    위에서 언급한 3가지의 기준을 통해, 상품 도메인과 카테고리 도메인을 연관시키는 작업을 진행했다. 가장 간단한 방법은 단건 쿼리를 통해, join 구문으로, 상품과 카테고리의 데이터를 한번에 가져오는 것인데, 필자의 경우, 다음과 같이 진행했다.
@GetMapping("item/{id}")
public ResponseEntity<ItemApiResult> findItem(@PathVariable Long id) {
    Item item = itemService.findItemByIdWithCategory(id);
    Category category = categoryService.findCategoryById(item.getCategoryId());
    return ResponseEntity.ok(new ItemApiResult(item, category));
}

캡슐화를 통해, 세부 로직이 숨겨진 상태지만, 간단히 설명하자면, 단건 쿼리문을 통해 검색하고자 한 상품의 데이터를 가져오고, 그 상품 데이터의 카테고리 fk를 통해 카테고리의 단건 조회를 하여, java에서 인위적으로 연결시켜서 handler의 결과 메세지로 리턴한 것이다. 이렇게 작업을 하는 것은 사실, DB로 한꺼번에 하나, 애플리케이션 코드와 분리해서 작업하나 소모되는 리소스의 차이는 거의 없을 것이다. 하지만, 이렇게 상품과 카테고리를 분리해서 진행함으로서 얻고자 한 이점은 다음과 같다.

  • SQL 매핑 작업을 줄인다.
  • 카테고리와 같이 비교적으로 정적인 부분은 캐싱을 통해 리소스를 좀더 효율적으로 사용할 수 있는 가능성이 있는데, 이렇게 작업을 분리할 경우, 캐싱 기능을 적용하는게 한층 수월해진다. (확장에 열려있는 상태가 된다.)

lombok boolean getter, 그리고 MyBatis의 Reflector getter, 그리고 Jackson...

사실 카테고리의 CRUD 작업은 기존 상품 도메인으 CRUD와 흐름이 거의 동일해서, 그 부분에 대해서 따로 언급할만한건 없었다. 다만, MyBatis를 통해 리턴되는 데이터를 테스트를 하던 도중, 일부 필드가 반영되지 않는 현상이 발생하였다. 사실 이 현상은 예전에 상품 도메인을 작업할 때에도 발생했던 부분인데, 어떻게 된 영문인지, 예외를 던지지않고 조용히 넘어갔던 부분이라, 지금에서야 발견하게 되었다. 해당 문제를 구체적으로 정리하면 다음과 같았다.

  • 서비스 레이어에서 CRUD 단위 테스트를 할 땐, 모든 필드가 의도에 맞게 매핑되어서 리턴된다.
  • 하지만 mockMVC를 통해 리턴되는 response body의 메세지에는 일부 필드가 null로 입력된다.
  • 엄연히 오류임에도 불구하고, 컴파일러는 어떠한 에러메세지도 리턴하지 않는다.

즉, 내가 아는 선에서는 원인을 알 수 없는 오류이기에, 작성한 코드를 하나씩 살펴보아야 했다.

Category entity

@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class Category {
    // 카테고리 pk
    private Long id;

    // 카테고리 이름
    private String name;

    // 카테고리 타입
    private CategoryType categoryType;

    // 카테고리 레벨
    private DepthLevel depthLevel;

    // 환불가능 여부
    private Boolean isRefundable;

    // 카테고리 상태
    private Status status;

    // 카테고리 상태 업데이트 시간
    private LocalDateTime statusUpdateTime;

    // 카테고리 인스턴스 생성 로직
    public static Category convertToCategory(CategoryForm form) {
        return Category.builder()
            .id(form.getId())
            .name(form.getName())
            .categoryType(form.getCategoryType())
            .depthLevel(form.getDepthLevel())
            .isRefundable(form.getIsRefundable())
            .status(form.getStatus())
            .statusUpdateTime(form.getStatusUpdateTime())
            .build();
    }
}

여기서 한가지 주목할 점은 자바빈 스펙에 따르면, boolean의 경우 isRefundable 과 같이 is~로 할경우, getter 메소드로 getIsRefundable() 로 명명되는 것이 아니라 isRefundable()로 getter 메소드 네이밍을 한다. lombok의 경우, 이러한 자바빈 스펙을 따르기 위해@Getter를 사용할 경우 is~로 명명된 boolean 타입의 프로퍼티는 is~()메소드로 getter 메소드를 주입한다. 만약 getIs~()로 명명하고 싶으면, Wrapper class를 사용하면 된다.

처음 디버깅을 할 때, boolean 타입을 Boolean 타입으로 바꾸고 다시 테스트할 때 해당 오류가 해결되어서, 그냥 lombok의 문제인 줄 알았다. 하지만, lombok은 잘못이 없다. 그저 기존 자바빈 스펙에 맞게 @Getter를 통해 코드를 주입해주고 있었을 뿐이니까.. 그럼 뭐가 문제일까?

MyBatis - Reflector

마이바티스는 데이터를 가져올 때, org.apache.ibatis.reflection.Reflector를 사용한다. Reflector의 특징을 파악하기 위해 api 문서를 인용해보겠다.

This class represents a cached set of class definition information that allows for easy mapping between property names and getter/setter methods.

MyBatis로 SQL 데이터를 Java 오브젝트로 매핑하기 위해, 먼저 mapper.xml의 resultType으로 클래스타입을 설정한다. 설정된 클래스타입은, Reflector를 통해 클래스 타입과, 프로퍼티의 이름과 getter/setter 메소드 관련 메타데이터를 저장하여, 바인딩한다.

Reflector는 생성자를 통해, 메타데이터를 저장하는데, 여기서 사용하는 메소드들을 나열해보면 다음과 같다.

public Reflector(Class<?> clazz) {
    type = clazz;
    addDefaultConstructor(clazz);
    addGetMethods(clazz);
    addSetMethods(clazz);
    addFields(clazz);
    readablePropertyNames = getMethods.keySet().toArray(new String[0]);
    writablePropertyNames = setMethods.keySet().toArray(new String[0]);
    for (String propName : readablePropertyNames) {
        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
    for (String propName : writablePropertyNames) {
        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
}

여러가지 메소드가 있지만, 정리해보면, resultType에 매핑된 클래스 타입을 reflect하여, 자바빈 스펙에 맞는 메타 데이터를 가진 프록시 객체를 만들어 캐싱한다음, 바인딩해야할 때 가져다 쓸 수 있도록 한다. 메소드의 세부로직을 살펴보면, SQL 매핑 작업을 할 때 참고할만한 것들이 많았는데, 간단하게 정리해보면 다음과 같다.

필드의 대소문자는 구분하지 않는다.

Reflector의 생성자에서 이루어지는 로직들 중 이 로직을 살펴보자.

for (String propName : readablePropertyNames) {
    caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
    caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}

요약하자면, getter/setter 프로퍼티의 이름의 대문자를 키로, 그리고 원래 프로퍼티의 이름을 값으로 하여, 하나의 맵으로 프로퍼티를 관리한다는 것이다. 이를 통해, 인스턴스 필드를 isFieldName로 명명하든, isfieldname으로 명명하든, 마이바티스는 같은 프로퍼티로 취급한다. 따라서, 마이바티스를 통해 SQL 매핑을 할 경우, 인스턴스 필드를 네이밍 할 때, 카멜케이스를 지키는 것도 중요하지만, 무엇보다 중복되지 않게 네이밍을 하는 것이 가장 중요할 것이다.

DTO는 getter와 setter가 없어도 된다.

일단 addGetMethods(Class<?> clazz)의 세부로직부터 보자.

private void addGetMethods(Class<?> clazz) {
    Map<String, List<Method>> conflictingGetters = new HashMap<>();
    Method[] methods = getClassMethods(clazz);
    Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
        .forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
    resolveGetterConflicts(conflictingGetters);
}

이 중에서 PropertyNamer.isGetter(m.getName())isGetter 메소드의 세부로직을 살펴보자.

public static boolean isGetter(String name) {
    return (name.startsWith("get") && name.length() > 3) || (name.startsWith("is") && name.length() > 2);
}

먼저 addGetMethods(Class<?> clazz)부터 정리하자면, 클래스 타입에 존재하는 모든 메소드 중에서 getter계열의 메소드가 있는지 조회하고, 만약 없다면 프로퍼티의 이름을 참고하여 resolveGetterConflicts(conflictingGetters);에서 getter를 메소드를 만들어서 미리 매핑해둔다. setter 역시 같은 맥락으로 로직을 진행을 한다. 따라서, DTO 클래스에 굳이 getter/setter를 선언하지 않아도 마이바티스가 알아서 매핑해준다.

그리고 isGetter(String name)메소드를 언급한 이유는 name.startsWith("is") && name.length() > 2 코드 때문이었다. 이 코드를 발견하기 전에는 일부 필드가 바인딩되지 않는 오류의 원인이 lombok에서 주입해주는 is~()계열의 getter 메소드 때문이라고 생각했다. 하지만 lombok은 그저 자바빈 스펙에 따라 코드를 주입해주고 있었을 뿐이고, 마이바티스 역시 그 규격에 맞게 필드명을 조회하고, 프록시 객체를 만들어준다. 따라서, 그 당시의 문제의 원인은 lombok이 원인이 아니었다는 것이다.

MappingJackson2MessageConverter

그럼 대체 무엇이 문제일까? 먼저 오류가 발생한 부분을 살펴보면, 서비스 레이어의 로직에서는 큰 문제가 없었으나, 컨트롤러를 거쳐갔을 때, 일부 필드가 매핑되지 않는 것에 주목해보자. 스프링 부트의 경우, @RestController로 핸들러 메소드를 매핑할 경우, 결과를 리턴할 때, viewResolver를 거치지 않고 메세지 그대로 컨버팅하는데, 기본적으로 MessageConverter를 사용하는데, Spring Boot의 경우, JSON 메세지를 다룰 땐, MappingJackson2MessageConverter 라이브러리를 사용한다. 이 라이브러리는 objectMapper를 통해, json과 자바 오브젝트간의 매핑 작업을 매우 단순화하게 해준다. Java 객체를 json으로 컨버팅하는 데 필요한 세부 세팅은 Jackson2ObjectMapperBuilder를 통해 이루어진다.

그런데, 프레젠테이션 레이어에서 발생한 문제가 맞다면, Jackson 라이브러리를 통해 objectMapper로 자바 오브젝트를 json으로 serialization하는 과정에서 문제가 생긴 것이라고 생각할 수 있다. 그런데, Jackson2ObjectMapperBuilderautoDetectGettersSetters(boolean autoDetectGettersSetters) 메소드를 보면, 내 가설도 잘못 되었음을 알 수 있다.

public Jackson2ObjectMapperBuilder autoDetectGettersSetters(boolean autoDetectGettersSetters) {
    this.features.put(MapperFeature.AUTO_DETECT_GETTERS, autoDetectGettersSetters);
    this.features.put(MapperFeature.AUTO_DETECT_SETTERS, autoDetectGettersSetters);
    this.features.put(MapperFeature.AUTO_DETECT_IS_GETTERS, autoDetectGettersSetters);
    return this;
}

MapperFeature

public enum MapperFeature implements ConfigFeature {  
    ...
    AUTO_DETECT_GETTERS(true),
    AUTO_DETECT_IS_GETTERS(true),
    ...
}

위의 설정 메소드를 참고해보면, isXxx()계열의 getter에 대한 설정을 봐주고 있었던 것이다. 그렇다면.. 정말 뭐 때문일까?

원인은 정말 어이없게도, 테스트 코드 였다.

@SpringBootTest
@AutoConfigureMockMvc
class CategoryControllerTest {

    @Test
    @DisplayName("카테고리 추가 성공")
    void newCategory() throws Exception {

        String categoryForm = objectMapper.writeValueAsString(new CategoryForm(Category.builder()
            .name("패션 잡화")
            .categoryType(CategoryType.CLOTH)
            .depthLevel(DepthLevel.DEPTH_ONE)
            .isRefundable(true)
            .status(Status.ACTIVATED)
            .statusUpdateTime(LocalDateTime.now())
            .build()));

        mockMvc.perform(
            post("/api/category")
                .contentType(MediaType.APPLICATION_JSON)
                .content(categoryForm)
                .accept(MediaType.APPLICATION_JSON)
        ).andDo(print())
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name").exists())
            .andExpect(jsonPath("$.categoryType").exists())
            .andExpect(jsonPath("$.depthLevel").exists())
            .andExpect(jsonPath("$.isRefundable").exists())
            .andExpect(jsonPath("$.status").exists())
        ;
    }
}

오류를 발견했던 코드였다. 해당 테스트는 실패 했었는데, 실패 원인은 .andExpect(jsonPath("$.status").exists())였다. 해당 api 핸들러 메소드는 다음과 같다.

@PostMapping("category")
public ResponseEntity<CategoryDTO> newCategory(@RequestBody CategoryForm categoryForm) {
    Category category = categoryService.getInsertedCategory(Category.convertToCategory(categoryForm));
    return ResponseEntity.ok(new CategoryDTO(category));
}

해당 로직을 통해 CategoryDTO를 json으로 serialization 하여 응답 메세지를 리턴한다. CategoryDTO의 코드는 다음과 같다.

@Getter
public class CategoryDTO {

    // 카테고리 pk
    private final Long id;

    // 카테고리 이름
    private final String name;

    // 카테고리 타입
    private final CategoryType categoryType;

    // 카테고리 레벨
    private final DepthLevel depthLevel;

    // 환불가능 여부
    private final Boolean isRefundable;

    public CategoryDTO(Category source) {
        this.id = source.getId();
        this.name = source.getName();
        this.categoryType = source.getCategoryType();
        this.depthLevel = source.getDepthLevel();
        this.isRefundable = source.getIsRefundable();
    }
}

이걸 보고 눈치 챈 사람이 있겠지만, 원인은 CategoryDTO에 Status 인스턴스 멤버변수를 애초에 선언하지 않았다. Status는 후에 데이터 삭제를 배치 프로세스에 활용하기 위해 사용하는 데이터이기 때문에 api 결과에 굳이 보여줄 필요가 없다고 판단해서였다. 그런데 그걸 잠시 잊고, 테스트 코드에 status의 존재여부까지 테스트를 했으니.. 당연히 테스트는 통과하지 않았고, 애플리케이션 코드 자체에는 잘못된 것이 없기 때문에 컴파일러가 예외를 던지지도 않았다. 즉.. 문제는 바로 나 자신이었던 것이다. 즉, .andExpect(jsonPath("$.status").exists()) 한줄로 엄청난 삽질을 했던 것이다. 덕분에 MyBatis의 Reflector의 원리와 Jackson 라이브러리의 적용 방식을 공부할 기회가 됬지만, 만약에 이 작업이 현업에서 진행하는 거였다면, 불필요한 시간비용이 소요되었을 것이다. 지금까지는 작업일지를 특정 작업이 다 끝나면, 이렇게 기록을 하곤 했지만, 앞으로는 짧은 시간 주기로 작업내역을 기록하여, 내가 코드를 사용한 의도를 기억하여, 이번일과 같은 일을 미리 방지하는 습관을 가져야 겠다.

TypeHandler와 Enum

MyBatis를 통해 데이터를 바인딩할 때, Java 오브젝트의 멤버변수의 타입과 SQL 컬럼의 데이터 타입을 매핑해야 한다. 기존에 JDBC로 진행 하였을때, PreparedStatement로 매핑하고, ResultSet을 통해 DB에서 가져온 데이터를 자바 오브젝트에 매핑하여 데이터 액세스 로직을 완성하였다. 이 작업은 매우 반복적이고 비효율적인 면이 많았다. MyBatis에서는 TypeHandler를 통해 이 과정을 직관적으로 사용하도록 한다.(사실 그럼에도 불구하고 쓰기 불편하긴 하다..)

구체적인 방법은 역시 공식문서를 참고하는 것이 좋다. 구체적인 코드는 프레임워크에서 제공하는 기본 BaseTypeHandler 코드를 참고하였다. MyBatis에서 제공하는 EnumTypeHandler는 Enum 필드의 name을 그대로 VARCHAR로 저장하거나, 필드의 ordinal number를 NUMERIC 타입으로 저장하는 방법만을 제공하기 때문에 enum 필드의 구체적인 value를 활용하려면, 어느정도 커스터마이징할 필요가 있다.

마무리

이번 작업은 비즈니스 로직보다는 프레임워크와 라이브러리 관련 api 학습을 많이 했던 것 같다. 또한, 사람의 기억력은 (혹은 나의 기억력) 생각보다 더 voliatile하다는 것을 알게 되었다. 다음 작업을 진행할때는 적어도 40분 단위로 어떤 작업을 했는지 기록하여, 과거에 어떤 의도로 코드를 작성했는지를 파악하며, 이번에 했던 실수를 최대한 방지하는 습관을 가져야 겠다.