@Transient를 이용한 Mapstruct 매핑 간소화

배경

소상공인 앱을 개발하며 다양한 언어로의 번역 작업은 필수적이었다. 이 과정에서, 특정 테이블의 세 개 필드에 걸쳐 번역이 추가되면서, Entity와 DTO 간의 매핑 작업이 복잡해졌다.

👉다국어 지원을 위한 데이터베이스 설계와 Spring Boot 구현 전략

처음에는 Translation Entity에서 번역 값을 가져오는 방식으로 작업했지만, 해당 값이 없을 경우 대체 언어로 전환하는 추가 작업이 필요했다. 이러한 과정이 비즈니스 로직보다 더 긴 코드를 요구하게 되어, 전체 로직의 파악이 어려워졌다.
또한, 현재 프로젝트에서 Mapstruct 라이브러리를 사용하고 있었지만, 필드 수만큼 매개변수가 늘어나는 문제로 인해 @AfterMapping을 수동으로 적용해야 하는 상황이 발생했다. 이는 Mapstruct의 기본적인 매핑 장점을 상실하게 만들었다.
그러던 중, 이전에 검토했지만 도입하지 않았던 JPA의 @Transient 어노테이션이 떠올랐다. @Transient를 적용하자, 매핑 작업이 훨씬 간편해졌다. @Transient 어노테이션을 사용함으로써, Mapstruct의 단순 매핑 이점을 최대한 활용하여 번역 작업을 보다 효율적이고 편리하게 할 수 있게 되었다. 이 두 결합은 복잡한 언어 처리 과정을 단순화시키고, 비즈니스 로직에 집중할 수 있는 환경을 마련해주었다.
이 글에서는 JPA @Transient와 Mapstruct의 결합이 어떻게 번역 작업을 간소화하고, 프로젝트의 효율성을 높일 수 있는지에 대해 자세히 설명하고자 한다.

JPA @Transient?

엔티티들이 저장되고 관리되는 환경인 영속성 컨텍스트에서 관리되는 객체는 보통 데이터베이스의 테이블과 매핑되며, 이들의 속성들은 테이블의 컬럼과 연결된다. 그러나 @Transient 어노테이션이 적용된 필드는 이러한 매핑 과정에서 제외된다. 즉, 해당 필드는 데이터베이스에 저장되거나 조회되지 않으며, JPA의 영속성 컨텍스트에서도 관리되지 않음을 뜻한다.

@Transient 어노테이션이 유용한 상황은 아래와 같은 예시가 있다.

  1. 임시 데이터 처리: 계산 필드나 로직 처리를 위한 임시 데이터를 저장할 때 사용된다. 이러한 필드는 비즈니스 로직에는 필요하지만, 데이터베이스에 영구적으로 저장할 필요가 없는 경우에 적합하다.
  2. 보안: 민감한 정보나 보안상 저장되면 안 되는 데이터를 다룰 때 유용하다. 예를 들어, 비밀번호의 해시값은 저장해야 하지만, 원본 비밀번호는 저장되면 안 되는 경우에 @Transient 를 사용할 수 있다.
  3. 복잡한 로직 분리: 데이터베이스와 관련 없는 복잡한 비즈니스 로직을 처리하는 필드에 사용하여, 로직을 더 명확하게 분리할 수 있다.

@Transient 사용하기 전 서비스 레이어

번역을 위해 작업했던 Translation 테이블은 Key-Value 구조로 복합키인 TranslationId를 조회하면 나오는 결과 값을 DTO에 매핑해주었다. 이 작업은 테이블에 번역 데이터가 추가되면 될 수록 많은 매개변수가 필요했으며, Mapstruct 사용을 어렵게했다.

변경 전 코드

public StoreInfoDto getStore(SecurityUser securityUser, Long storeId) {
    Store store = storeRepositoryFacade.findById(storeId);
    ...

    String description = translationRepositoryFacade.findTranslation(store.getDescriptionColumn());
    List<StoreDayOff> dayOffs = storeRepositoryFacade.findAllDayOffByStore(store);
    return storeMapper.toInfoDto(store, descripiton, dayOffs);
}
@Mapper
public interface StoreMapper {
    StoreDayOffMapper STORE_DAY_OFF_MAPPER = Mappers.getMapper(StoreDayOffMapper.class);
	...
    default StoreInfoDto toInfoDto(Store store, @Context String description, List<StoreDayOff> dayOffs) {
          return new StoreInfoDto(toDto(store),STORE_DAY_OFF_MAPPER.toDtoList(dayOffs));
       }
	
    @AfterMapping
    default void setDescription(@MappingTarget StoreInfoDto storeInfoDto, @Context String description) {
	    storeInfoDto.setDescription(description);
	}
}

변경 후 코드

public StoreInfoDto getStore(SecurityUser securityUser, Long storeId) {
      Store store = storeRepositoryFacade.findById(storeId);
      ...

      String description = translationRepositoryFacade.findTranslation(store.getDescriptionColumn());
      store.updateDescription(description);
      List<StoreDayOff> dayOffs = storeRepositoryFacade.findAllDayOffByStore(store);
      return storeMapper.toInfoDto(store, dayOffs);
  }
@Mapper
public interface StoreMapper {
    StoreDayOffMapper STORE_DAY_OFF_MAPPER = Mappers.getMapper(StoreDayOffMapper.class);
	...
    default StoreInfoDto toInfoDto(Store store, List<StoreDayOff> dayOffs) {
      return new StoreInfoDto(toDto(store),STORE_DAY_OFF_MAPPER.toDtoList(dayOffs));
    }
}

간단한 예시였지만, 번역 데이터가 3~4개가 되는 경우에 발생하던 매핑의 복잡성은 @Transient를 사용하면서 줄어들었다. 하지만 @Transient를 도입하면서 발생하는 문제점 몇가지가 존재했다.

@Transient 사용의 이유

각 필드마다 다국어 지원을 위한 Key-Value 구조를 가진 Translation 테이블은 가게의 설명, 판매되는 상품의 이름 등 실제 엔티티와 밀접하게 연관된 데이터였다. 특히, Accept-Language 헤더 값을 활용하여 매번 값을 검색한 후 복잡한 매핑 과정은 @Transient 을 사용하며 간단해졌다. 즉 이를 사용한 이유는 크게 3가지로 볼 수 있었다.

  1. 엔티티와의 밀접한 연관성: @Transient를 통해 엔티티 내에 직접 포함시키는 것은 논리적으로 타당하다고 생각했다. 이는 엔티티의 완전성을 유지에 도움이 된다고 생각했기 때문이다.
  2. 국제화(i18n) 지원의 효율성: 다양한 언어를 지원하기 위해 Accept-Language에 따라 동적으로 번역 데이터를 로드하고 처리하고 있었기에, @Transient를 사용하여 엔티티 수준에서 직접적이고 효율적인 처리가 가능했다.
  3. 매핑 로직의 단순화: 이 방식은 서비스 레이어나 매퍼에서 복잡한 로직을 처리하는 것보다 간결하고 직관적이었다. 엔티티 자체가 필요한 번역 데이터를 적절히 로드하고 관리함으로써 매핑 과정을 단순화했다.

@Transient 사용에 대한 고려 사항

  1. 역할과 책임의 분리: @Transient를 사용하여 엔티티 내에서 DTO에 관련된 로직을 처리하는 것은 엔티티와 DTO 간의 역할과 책임을 혼동할 수 있다. 엔티티는 비즈니스 로직과 데이터베이스와의 매핑에 집중해야 하며, DTO 변환은 별도의 계층(예: 서비스 또는 매퍼 계층)에서 처리하는 것이 바람직하다.
  2. 유지보수와 확장성: 엔티티에 @Transient 필드를 추가하면, 이 필드가 변경될 때마다 관련 엔티티와 매퍼 코드를 모두 수정해야 할 수 있다. 이는 유지보수와 확장성 측면에서 비효율적일 수 있는 선택이다.
  3. DTO 매핑의 명확성: DTO 매핑은 가능한 명확하고 예측 가능해야 한다. @Transient를 사용하면 매핑 로직이 엔티티 내부로 숨겨질 수 있으며, 이는 매핑의 명확성을 해칠 수 있다.

정리

지금까지 소상공인 앱 개발 과정에서 번역 작업의 복잡성을 줄이기 위해 @Transient 어노테이션과 Mapstruct 라이브러리를 사용한 경험을 공유해보았다.
번역 작업을 위해 사용되는 Translation 테이블의 Key-Value 구조는 매번 번역 데이터가 추가될 때마다 많은 매개변수가 필요하고, 매핑 작업이 복잡해짐을 야기했다. 이러한 문제를 해결하기 위해 JPA의 @Transient 어노테이션을 도입하여 엔티티 내에서 번역 데이터를 직접 로드하고 처리하게 함으로써 매핑 과정을 단순화했다.
그러나 @Transient를 사용하면서 엔티티와 DTO 간의 역할과 책임이 혼동되는 문제, 유지보수와 확장성이 저하되는 문제, 그리고 DTO 매핑의 명확성이 해치는 문제 등이 존재한다는 것을 인지하고 @Transient 어노테이션을 간편하게 사용할 수 있지만, 그 사용에 있어서의 고려 사항과 함께 여러 트레이드 오프가 존재함을 인식했다.
엔티티의 책임을 명확히 하고, 매핑 과정의 복잡성을 줄이며, 전체 시스템의 구조를 깔끔하게 유지할 수 있는 방법을 찾아 또 다시 리펙토링의 길로 들어선다..

@Transient를 이용한 Mapstruct 매핑 간소화

댓글 남기기

Scroll to top