반응형

이번 시간에는 국제화를 적용해보도록 하겠습니다.

 

적용할 부분이 많지는 않고, 프로젝트 내에서 한글로 하드코딩되어있는 메시지들을 별도의 파일로 관리하여, 요청의 Locale 정보에 따라 한글 또는 영어로 응답을 내려주도록 하겠습니다.

 

예외 메시지가 적혀 있는 ExceptionAdvice와 Bean Validation을 사용 중인 Request DTO가 그 대상이 될 것입니다.

코드를 살펴보면 다음과 같습니다.

@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Response exception(Exception e) {
        return Response.failure(-1000, "오류가 발생하였습니다.");
    }

    @ExceptionHandler(AuthenticationEntryPointException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Response authenticationEntryPoint() {
        return Response.failure(-1001, "인증되지 않은 사용자입니다.");
    }

    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public Response accessDeniedException() {
        return Response.failure(-1002, "접근이 거부되었습니다.");
    }

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Response bindException(BindException e) {
        return Response.failure(-1003, e.getBindingResult().getFieldError().getDefaultMessage());
    }

    @ExceptionHandler(LoginFailureException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Response loginFailureException() {
        return Response.failure(-1004, "로그인에 실패하였습니다.");
    }

    @ExceptionHandler(MemberEmailAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Response memberEmailAlreadyExistsException(MemberEmailAlreadyExistsException e) {
        return Response.failure(-1005, e.getMessage() + "은 중복된 이메일 입니다.");
    }

    @ExceptionHandler(MemberNicknameAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Response memberNicknameAlreadyExistsException(MemberNicknameAlreadyExistsException e) {
        return Response.failure(-1006, e.getMessage() + "은 중복된 닉네임 입니다.");
    }

    ...
}
package kukekyakya.kukemarket.dto.sign;

import ...

public class SignUpRequest {

    ...
    @Email(message = "이메일 형식을 맞춰주세요.")
    @NotBlank(message = "이메일을 입력해주세요.")
    private String email;

    @NotBlank(message = "비밀번호를 입력해주세요.")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
            message = "비밀번호는 최소 8자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.")
    private String password;

    ...
    @NotBlank(message = "사용자 이름을 입력해주세요.")
    @Size(min=2, message = "사용자 이름이 너무 짧습니다.")
    @Pattern(regexp = "^[A-Za-z가-힣]+$", message = "사용자 이름은 한글 또는 알파벳만 입력해주세요.")
    private String username;

    ...
    @NotBlank(message = "닉네임을 입력해주세요.")
    @Size(min=2, message = "닉네임이 너무 짧습니다.")
    @Pattern(regexp = "^[A-Za-z가-힣]+$", message = "닉네임은 한글 또는 알파벳만 입력해주세요.")
    private String nickname;

    public static Member toEntity(SignUpRequest req, Role role, PasswordEncoder encoder) {
        return new Member(req.email, encoder.encode(req.password), req.username, req.nickname, List.of(role));
    }
}

메시지가 한글로 하드코딩 되어있습니다.

 

 

스프링에서는 국제화를 손쉽게 적용할 수 있도록 다양한 기능을 지원하고 있습니다.

스프링 빈으로 등록된 MessageSource를 이용하여 별도의 파일에 작성된 메시지를 가져올 수 있고, 요청에 따라 추출된 Locale 정보로 사용자에게 응답해줄 메시지 파일을 선택할 수 있습니다.

 

 

먼저 국제화를 적용하기 위해 필요한 스프링의 MessageSource 인터페이스를 살펴봅시다.

package org.springframework.context;

import ..

public interface MessageSource {
	@Nullable
	String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

	String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

	String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

getMessage로 code와 args(인자)를 전달받으면, 지정해둔 파일에서 메시지를 추출할 수 있을 것입니다.

defaultMessage(기본 메시지)와 Locale 정보도 함께 전달할 수 있습니다.

 

스프링에서는 MessageSource의 다양한 구현체가 지원되고 있으며, 스프링부트에서는 구현체 중 하나인 ResourceBundleMessageSource를 기본적으로 스프링 빈으로 등록해주고 있습니다.

 

 

또, 스프링에서는 요청에서 Locale 정보를 추출하기 위한 LocaleResolver 인터페이스를 제공하고 있습니다.

package org.springframework.web.servlet;

import ...

public interface LocaleResolver {
	Locale resolveLocale(HttpServletRequest request);

	void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);

}

resolveLocale을 이용하여 HttpServletRequest에서 Locale 정보를 추출해낼 수 있습니다.

이에 대한 구현체도 다양하게 지원되고 있으며, 스프링부트에서는 HTTP Accept-Language 헤더에서 Locale 정보를 추출해주는 AcceptHeaderLocaleResolver를 기본적으로 스프링 빈으로 등록해주고 있습니다.

 

Accept-Language 헤더에 대해서는 다음 링크를 참고하시길 바랍니다.

https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Accept-Language

 

Accept-Language - HTTP | MDN

Accept-Language 요청 HTTP 헤더는 어떤 언어를 클라이언트가 이해할 수 있는지, 그리고 지역 설정 중 어떤 것이 더 선호되는지를 알려줍니다. (여기서 언어란 프로그래밍 언어가 아니라 영어같은 자

developer.mozilla.org

 

 

스프링부트에서 기본적으로 등록해주는 ResourceBundleMessageSource와 AcceptHeaderLocaleResolver를 이용하여 국제화를 적용해보도록 하겠습니다.

먼저 application.yml에 Locale 정보에 따라 메시지를 찾을 수 있도록 basename을 지정해주겠습니다.

# application.yml
spring:
  messages:
    basename: i18n/exception
  ...

일단 ExceptionAdvice에서 사용되는 메시지만 별도의 파일로 관리해주도록 하겠습니다.

resources 디렉토리에서 i18n 경로에 메시지 파일을 만들어줍니다.

<basename>-<Locale>.properties로 메시지를 선택할 수 있을 것입니다.

예를 들어,

1. resources/i18n/exception.properties

2. resources/i18n/exception-ko.properties

3. resources/i18n/exception-en.properties

Locale 정보가 없으면 기본 설정으로 1번에서,

Locale 정보가 ko 또는 en이라면 2번 또는 3번에서 메시지를 불러오게 될 것입니다.

 

 

기본 설정은 한글로 적용되도록 하고, Accept Language가 영어로 적혀있을 때는 exception-en.properties로 응답을 내려주도록 하겠습니다.

i18n/exception.properties와 i18n/exception-en.properties를 작성해줍시다.

# exception.properties
exception.code=-1000
authenticationEntryPoint.code=-1001
accessDeniedException.code=-1002
bindException.code=-1003
loginFailureException.code=-1004
memberEmailAlreadyExistsException.code=-1005
memberNicknameAlreadyExistsException.code=-1006
memberNotFoundException.code=-1007
roleNotFoundException.code=-1008
missingRequestHeaderException.code=-1009
categoryNotFoundException.code=-1010
cannotConvertNestedStructureException.code=-1011
postNotFoundException.code=-1012
unsupportedImageFormatException.code=-1013
fileUploadFailureException.code=-1014
commentNotFoundException.code=-1015
messageNotFoundException.code=-1016

exception.msg=오류가 발생하였습니다.
authenticationEntryPoint.msg=인증되지 않은 사용자입니다.
accessDeniedException.msg=접근이 거부되었습니다.
bindException.msg={0}
loginFailureException.msg=로그인에 실패하였습니다.
memberEmailAlreadyExistsException.msg={0}은 중복된 이메일 입니다.
memberNicknameAlreadyExistsException.msg={0}은 중복된 닉네임 입니다.
memberNotFoundException.msg=요청한 회원을 찾을 수 없습니다.
roleNotFoundException.msg=요청한 권한 등급을 찾을 수 없습니다.
missingRequestHeaderException.msg={0} 요청 헤더가 누락되었습니다.
categoryNotFoundException.msg=존재하지 않는 카테고리입니다.
cannotConvertNestedStructureException.msg=중첩 구조 변환에 실패하였습니다.
postNotFoundException.msg=존재하지 않는 게시글입니다.
unsupportedImageFormatException.msg=지원하지 않는 이미지 형식입니다.
fileUploadFailureException.msg=파일 업로드에 실패하였습니다.
commentNotFoundException.msg=존재하지 않는 댓글입니다.
messageNotFoundException.msg=존재하지 않는 쪽지입니다.
# exception-en.properties
exception.msg=exception
authenticationEntryPoint.msg=authenticationEntryPoint
accessDeniedException.msg=accessDeniedException
bindException.msg={0}
loginFailureException.msg=loginFailureException
memberEmailAlreadyExistsException.msg=memberEmailAlreadyExistsException : {0}
memberNicknameAlreadyExistsException.msg=memberNicknameAlreadyExistsException : {0}
memberNotFoundException.msg=memberNotFoundException
roleNotFoundException.msg=roleNotFoundException
missingRequestHeaderException.msg=missingRequestHeaderException : {0}
categoryNotFoundException.msg=categoryNotFoundException
cannotConvertNestedStructureException.msg=cannotConvertNestedStructureException
postNotFoundException.msg=postNotFoundException
unsupportedImageFormatException.msg=unsupportedImageFormatException
fileUploadFailureException.msg=fileUploadFailureException
commentNotFoundException.msg=commentNotFoundException
messageNotFoundException.msg=messageNotFoundException

예외 코드도 exception.properties에서 관리하도록 하겠습니다.

MessageSource.getMessage에서는 파라미터로 오브젝트 배열인 args를 전달받을 수 있었는데, 해당 배열의 인덱스 번호로 메시지에서 값을 사용할 수 있습니다.

예를 들어, "{0}은 중복된 이메일입니다."는 MessageSource.getMessage에 인자로 전달받은 오브젝트 배열의 0번 값으로 치환될 것입니다.

 

 

이제 ExceptionAdvice에 MessageSource를 주입해서 예외 메시지를 응답할 수 있도록 하겠습니다.

package kukekyakya.kukemarket.advice;

import ...

@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class ExceptionAdvice {

    private final MessageSource messageSource;

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Response exception(Exception e) {
        ...
        return getFailureResponse("exception.code", "exception.msg");
    }

    @ExceptionHandler(AuthenticationEntryPointException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Response authenticationEntryPoint() {
        return getFailureResponse("authenticationEntryPoint.code", "authenticationEntryPoint.msg");
    }

    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public Response accessDeniedException() {
        return getFailureResponse("accessDeniedException.code", "accessDeniedException.msg");
    }

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Response bindException(BindException e) {
        return getFailureResponse("bindException.code", "bindException.msg", e.getBindingResult().getFieldError().getDefaultMessage());
    }

    @ExceptionHandler(LoginFailureException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Response loginFailureException() {
        return getFailureResponse("loginFailureException.code", "loginFailureException.msg");
    }

    @ExceptionHandler(MemberEmailAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Response memberEmailAlreadyExistsException(MemberEmailAlreadyExistsException e) {
        return getFailureResponse("memberEmailAlreadyExistsException.code", "memberEmailAlreadyExistsException.msg");
    }
    
    ...

    private Response getFailureResponse(String codeKey, String messageKey) {
        log.info("code = {}, msg = {}", getCode(codeKey), getMessage(messageKey, null));
        return Response.failure(getCode(codeKey), getMessage(messageKey, null));
    }

    private Response getFailureResponse(String codeKey, String messageKey, Object... args) {
        return Response.failure(getCode(codeKey), getMessage(messageKey, args));
    }

    private Integer getCode(String key) {
        return Integer.valueOf(messageSource.getMessage(key, null, null));
    }

    private String getMessage(String key, Object... args) {
        return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
    }
}

MessageSource.getMessage에 key(code)와 args, Locale 정보를 전달하여 메시지를 반환받고 있습니다.

LocaleResolver에 의해서 추출된 Locale 정보는, LocaleContextHolder.getLocale로 얻어낼 수 있습니다.

 

 

코드의 수정이 있었으므로 ExceptionAdvice를 이용하여 테스트하던 ~ControllerAdviceTest의 테스트 코드를 수정해주겠습니다.

package kukekyakya.kukemarket.controller.category;

import ...

@ExtendWith(MockitoExtension.class)
class CategoryControllerAdviceTest {
    @InjectMocks CategoryController categoryController;
    @Mock CategoryService categoryService;
    MockMvc mockMvc;

    @BeforeEach
    void beforeEach() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames("i18n/exception");
        mockMvc = MockMvcBuilders.standaloneSetup(categoryController).setControllerAdvice(new ExceptionAdvice(messageSource)).build();
    }
    ...
}

MockMvc에 ControllerAdvice 등록이 필요하던 모든 테스트에서,

ExceptionAdvice에 ResourceBundleMessageSource를 주입해주도록 하겠습니다.

 

 

글의 주제와는 벗어나지만, 자바의 가변 인자(Object...)는 자주 사용해본 적이 없어서 간단한 학습 테스트도 작성해보았습니다.

package learning;

import ...

public class VarArgsTest {

    @Test
    void varArgsTest() {
        test1();
        System.out.println("===============");
        test1("1", "2", "3");
    }

    private void test1(Object... args) {
        System.out.println("test1 args = " + args.length + " " + args);
        test2(args);

        Object[] args2 = args;
        test2(args2);

        Object args3 = args2;
        test2(args3);
    }

    private void test2(Object... args) {
        System.out.println("test2 args = " + args.length + " " + args);
    }
}
test1 args = 0 [Ljava.lang.Object;@7160fe52
test2 args = 0 [Ljava.lang.Object;@7160fe52
test2 args = 0 [Ljava.lang.Object;@7160fe52
test2 args = 1 [Ljava.lang.Object;@5290b3b1
===============
test1 args = 3 [Ljava.lang.Object;@3d0cba08
test2 args = 3 [Ljava.lang.Object;@3d0cba08
test2 args = 3 [Ljava.lang.Object;@3d0cba08
test2 args = 1 [Ljava.lang.Object;@7c183b70

일단 확인해보고 싶었던 것은,

1. 가변 인자에 아무 것도 전달되지 않을 때, 어떻게 전달 받는지(null 또는 빈 배열) -> 빈 배열

2. 전달 받은 가변 인자를 다른 메소드의 가변 인자로 그대로 전달했을 때, 어떻게 전달 받는지(새로운 배열 또는 기존의 배열 또는 배열 자체를 하나의 오브젝트로 취급) -> 기존의 배열

3. 오브젝트 배열을 하나의 오브젝트로 전달했을 때, 어떻게 전달 받는지(기존의 배열 또는 하나의 오브젝트로 취급) -> 하나의 오브젝트로 취급 

였습니다.

Object의 toString은, hashcode를 통해서 메모리 번지를 응답해주기 때문에, 출력 결과로 보면 위와 같은 결론을 도출해낼 수 있었습니다.

자세한 설명은 생략하고 넘어가겠습니다.

 

 

ExceptionAdvice에 국제화가 잘 적용되었는지 포스트맨을 이용하여 요청해보도록 하겠습니다.

한글예외메시지
영어예외메시지

지정된 Accept-Language에 따라서 다른 메시지를 응답해주고 있습니다.

 

* 혹시 properties 파일의 한글이 깨져서 응답된다면, Settings -> Encoding -> File Encodings에서 properties 파일의 인코딩을 설정해주면 됩니다.

 

 

이어서, Bean Validation에 적용된 메시지들에 국제화를 적용해보도록 하겠습니다.

스프링부트에서는 LocalValidatorFactoryBean를 이용하여 Bean Validation을 수행하고 있습니다.

LocalValidatorFactoryBean에 스프링 빈으로 등록된 ResourceBundleMessageSouce를 주입해주겠습니다.

 

config.WebConfig에 다음과 같은 코드를 작성해줍니다.

package kukekyakya.kukemarket.config;

import ...

@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final MessageSource messageSource;

    ...

    @Override
    public Validator getValidator() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource);
        return bean;
    }
}

getValidator를 오버라이딩하고, 생성한 LocalValidatorFactoryBean에 MessageSource를 주입해주었습니다.

 

 

검증 메시지는 따로 관리할 수 있도록, application.yml에 새로운 basename을 등록해줍시다.

# application.yml
spring:
  messages:
    basename: i18n/exception,i18n/validation
  ...

validation.properties 파일에서 검증 메시지를 관리해주겠습니다.

 

 

i18n/validation.properties와 i18n/validation-en.properties를 작성해줍니다.

# validation.properties
commentCreateRequest.content.notBlank=댓글을 입력해주세요.
commentCreateRequest.postId.notNull=게시글 아이디를 입력해주세요.
commentCreateRequest.postId.positive=올바른 게시글 아이디를 입력해주세요.

categoryCreateRequest.name.notBlank=카테고리 명을 입력해주세요.
categoryCreateRequest.name.size=카테고리 명의 길이는 2글자에서 30글자 입니다.

messageCreateRequest.content.notBlank=쪽지를 입력해주세요.
messageCreateRequest.receiverId.notNull=수신자 아이디를 입력해주세요.
messageCreateRequest.receiverId.positive=올바른 수신자 아이디를 입력해주세요.

postCreateRequest.title.notBlank=게시글 제목을 입력해주세요.
postCreateRequest.content.notBlank=게시글 본문을 입력해주세요.
postCreateRequest.price.notNull=가격을 입력해주세요.
postCreateRequest.price.positiveOrZero=0원 이상을 입력해주세요.
postCreateRequest.categoryId.notNull=카테고리 아이디를 입력해주세요.
postCreateRequest.categoryId.positiveOrZero=올바른 카테고리 아이디를 입력해주세요.

postUpdateRequest.title.notBlank=게시글 제목을 입력해주세요.
postUpdateRequest.content.notBlank=게시글 본문을 입력해주세요.
postUpdateRequest.price.notNull=가격을 입력해주세요.
postUpdateRequest.price.positiveOrZero=0원 이상을 입력해주세요.

signInRequest.email.email=이메일 형식을 맞춰주세요.
signInRequest.email.notBlank=이메일을 입력해주세요.
signInRequest.password.notBlank=비밀번호를 입력해주세요.

signUpRequest.email.email=이메일 형식을 맞춰주세요.
signUpRequest.email.notBlank=이메일을 입력해주세요.
signUpRequest.password.notBlank=비밀번호를 입력해주세요.
signUpRequest.password.pattern=비밀번호는 최소 8자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.
signUpRequest.username.notBlank=사용자 이름을 입력해주세요.
signUpRequest.username.size=사용자 이름이 너무 짧습니다.
signUpRequest.username.pattern=사용자 이름은 한글 또는 알파벳만 입력해주세요.
signUpRequest.nickname.notBlank=닉네임을 입력해주세요.
signUpRequest.nickname.size=닉네임이 너무 짧습니다.
signUpRequest.nickname.pattern=닉네임은 한글 또는 알파벳만 입력해주세요.
commentCreateRequest.content.notBlank=commentCreateRequest.content.notBlank
commentCreateRequest.postId.notNull=commentCreateRequest.postId.notNull
commentCreateRequest.postId.positive=commentCreateRequest.postId.positive

categoryCreateRequest.name.notBlank=categoryCreateRequest.name.notBlank
categoryCreateRequest.name.size=categoryCreateRequest.name.size

messageCreateRequest.content.notBlank=messageCreateRequest.content.notBlank
messageCreateRequest.receiverId.notNull=messageCreateRequest.receiverId.notNull
messageCreateRequest.receiverId.positive=messageCreateRequest.receiverId.positive

postCreateRequest.title.notBlank=postCreateRequest.title.notBlank
postCreateRequest.content.notBlank=postCreateRequest.content.notBlank
postCreateRequest.price.notNull=postCreateRequest.price.notNull
postCreateRequest.price.positiveOrZero=postCreateRequest.price.positiveOrZero
postCreateRequest.categoryId.notNull=postCreateRequest.categoryId.notNull
postCreateRequest.categoryId.positiveOrZero=postCreateRequest.categoryId.positiveOrZero

postUpdateRequest.title.notBlank=postUpdateRequest.title.notBlank
postUpdateRequest.content.notBlank=postUpdateRequest.content.notBlank
postUpdateRequest.price.notNull=postUpdateRequest.price.notNull
postUpdateRequest.price.positiveOrZero=postUpdateRequest.price.positiveOrZero

signInRequest.email.email=signInRequest.email.email
signInRequest.email.notBlank=signInRequest.email.notBlank
signInRequest.password.notBlank=signInRequest.password.notBlank

signUpRequest.email.email=signUpRequest.email.email
signUpRequest.email.notBlank=signUpRequest.email.notBlank
signUpRequest.password.notBlank=signUpRequest.password.notBlank
signUpRequest.password.pattern=signUpRequest.password.pattern
signUpRequest.username.notBlank=signUpRequest.username.notBlank
signUpRequest.username.size=signUpRequest.username.size
signUpRequest.username.pattern=signUpRequest.username.pattern
signUpRequest.nickname.notBlank=signUpRequest.nickname.notBlank
signUpRequest.nickname.size=signUpRequest.nickname.size
signUpRequest.nickname.pattern=signUpRequest.nickname.pattern

이번에도 영문 메시지는 간단하게 적어주도록 하겠습니다.

 

 

이제 위 메시지가 적용될 수 있도록 Request DTO 들을 수정해주겠습니다.

package kukekyakya.kukemarket.dto.sign;

import ...
public class SignUpRequest {

    @Email(message = "{signUpRequest.email.email}")
    @NotBlank(message = "{signUpRequest.email.notBlank}")
    private String email;

    @NotBlank(message = "{signUpRequest.password.notBlank}")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
            message = "{signUpRequest.password.pattern}")
    private String password;

    @NotBlank(message = "{signUpRequest.username.notBlank}")
    @Size(min=2, message = "{signUpRequest.username.size}")
    @Pattern(regexp = "^[A-Za-z가-힣]+$", message = "{signUpRequest.username.pattern}")
    private String username;

    @NotBlank(message = "{signUpRequest.nickname.notBlank}")
    @Size(min=2, message = "{signUpRequest.nickname.size}")
    @Pattern(regexp = "^[A-Za-z가-힣]+$", message = "{signUpRequest.nickname.pattern}")
    private String nickname;

    ...
}

SignUpRequest만 살펴보겠습니다.

message = "{메시지 접근 코드}"만 지정해주면 됩니다.

 

 

잘 적용됐는지 포스트맨으로 확인해보겠습니다.

한글검증메시지
영어검증메시지

이번에도 Accept-Language에 따라서 다른 메시지를 응답받고 있습니다.

 

 

모든테스트통과

다른 코드에 이상은 없는지 테스트를 모두 돌려보고 마치겠습니다.

 

 

이번 시간에는 스프링부트에서 지원해주는 국제화 기능을 이용해서, 요청자의 Locale 정보에 따라 적절한 메시지를 응답해줄 수 있게 되었습니다.

 

 

* 질문 및 피드백은 환영입니다.

* 전체 소스코드에서는 여기에서 확인해볼 수 있습니다.

https://github.com/SongHeeJae/kuke-market

반응형

+ Recent posts