Spring Boot 다국어 지원, DB 번역값 처리하기 - AOP 방식으로 response 번역하기

ABL·2024년 7월 3일
2
post-thumbnail
post-custom-banner

Spring Boot 다국어 지원, DB 번역값 처리하기 - 1

이전에 Entity단에서 직접 번역값과 매핑하는 것에 대해,
엔티티마다 Translatable 인터페이스를 구현하고, 번역 필드를 관리하는 코드가 반복되고, 필드가 추가될 때마다 TranslatableField를 함께 추가해주어야 한다는 부분에 대해 번거로움을 느꼈습니다.

따라서 AOP를 사용하여 DTO Response값을 받아와 번역이 요구되는 필드일 경우 번역하는 과정으로 바꾸어보았습니다.

먼저, AOP를 사용하기 때문에 이전처럼 Entity에 Transable을 implements할 이유가 없었습니다. 따라서, 커스텀 어노테이션을 만들어 번역이 필요한 필드에 직접 붙여주는 방식으로 구현했습니다.


Spring Boot 구현 과정

Trans Custom Annotation 선언

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trans {
}

@Data
public class ArtistSearchDto {
    private Long contentId;
    @Trans
    private String artistName;
}

위와 같이 ArtistSearchDto 내 번역이 필요한 artistName에 @Trans 어노테이션을 붙여주었습니다.

Translation Aspect

@Aspect
@Component
@RequiredArgsConstructor
public class TranslationAspect {

    private final TranslationService translationService;

    @Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public Object translateFields(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();

        if (result instanceof ResponseEntity) {
            ResponseEntity<?> responseEntity = (ResponseEntity<?>) result;
            Object body = responseEntity.getBody();
            if (body instanceof Page) {
                Page<?> page = (Page<?>) body;
                page.getContent().forEach(this::translateObject);
            } else {
                translateObject(body);
            }
            return ResponseEntity.status(responseEntity.getStatusCode()).headers(responseEntity.getHeaders()).body(body);
        } else {
            translateObject(result);
        }

        return result;
    }

    private void translateObject(Object obj) {
        if (obj == null) return;

        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Trans.class)) {
                field.setAccessible(true);
                try {
                    String originalValue = (String) field.get(obj);
                    String translatedValue = translationService.translate(originalValue, Language.ofLocale());
                    field.set(obj, translatedValue);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • AOP를 사용하여, @GetMapping 어노테이션이 붙은 모든 메서드에서 실행
  • Response값의 instance 타입에 따라서, translateObject를 확인 후 번역 (@Trans가 붙은 필드 확인 (Trans.class)

기존 방식과의 비교

기존 방식 (Translatable 인터페이스를 사용한 번역)

장점

  1. 명시적 호출: 번역 과정이 명시적으로 서비스 계층에서 호출되므로, 코드의 흐름이 명확합니다.
  2. 구체적인 제어: 특정 상황에서 번역을 제외하거나 커스터마이징할 수 있는 유연성을 제공합니다.

단점

  1. 반복적 코드: 엔티티마다 Translatable 인터페이스를 구현하고, 번역 필드를 관리하는 코드가 반복됩니다.
  2. 유지보수 어려움: 번역 필드가 추가될 때마다 관련 인터페이스와 메서드를 업데이트해야 합니다.
  3. 서비스 의존성: 번역이 필요한 곳마다 TranslationService를 호출해야 하므로 서비스 계층의 의존성이 증가합니다.

AOP 방식

장점

  1. 중앙 집중식 처리: 번역 로직이 하나의 Aspect로 중앙 집중화되어 유지보수가 용이합니다.
  2. 코드 간소화: Response DTO에서 번역 필드에 직접 어노테이션을 붙여주기만 하면 되므로, 번역 관련 코드가 간소화됩니다.
  3. 자동화: @GetMapping 어노테이션이 붙은 모든 메서드에서 자동으로 번역이 수행되므로, 개발자가 일일이 번역을 호출할 필요가 없습니다.

단점

  1. 추적 어려움: AOP로 인해 번역이 자동으로 이루어지므로, 번역 로직이 어디서 호출되는지 추적하기 어렵습니다.
  2. 성능 문제: 모든 @GetMapping 메서드에서 번역을 수행하므로, 불필요한 번역이 발생할 수 있으며, 성능에 영향을 미칠 수 있습니다.

AOP를 사용한 방식은 코드의 간결성과 유지보수 측면에서 큰 장점을 제공합니다. 하지만 @GetMapping 메서드로부터 받은 응답값에 직접 번역을 수행하므로 성능 문제가 발생할 수 있는 단점이 있습니다.
만약 번역이 대부분의 엔드포인트에서 필요하고, 번역 필드가 자주 변경된다면 AOP 방식이 더 적합할 수 있습니다. 반면, 특정 엔드포인트에서만 번역이 필요하거나, 성능이 중요한 경우에는 기존 방식이 더 나을 수 있습니다.
제가 구현중인 서비스의 경우 전자이기 때문에, AOP 방식을 적용하고자 합니다.

profile
💻
post-custom-banner

0개의 댓글