
message.properties 에는 다국어 처리가 불필요한 코드도 포함되어 있었습니다.

효율적인 관리를 위해 message.properties 에서 코드들을 제거하고 별도의 Enum 을 생성하여 관리하도록 합니다. 정상 처리 코드와 에러 코드 를 분리하면서도 코드라는 관계성을 맺기 위해 Interface를 작성하고 이을 구현한 Enum 을 작성합니다.
클라이언트로 전달할 코드와 다국어 처리를 위해 message.properties 에서 값을 추출할 수 있도록 messageCode 가 필요합니다. Enum 에서는 해당 값들을 추출할 수 있는 method 가 공통적으로 필요하기 때문에 Interface 에 추가하여 각 Enum 에서 구현하도록 합니다.
common.code.ResponseCode
public interface ResponseCode {
String code(); // 클라이언트 응답 코드 리턴
String messageCode(); //messageCode 리턴
}
정상 응답 코드 Enum 을 생성합니다. Enum 의 name 이 message.properties의 message code 가 되도록 두 값을 맞춰 주어야 합니다. ResponseCode를 implements 하고 response code 와 message code 를 리턴하는 interface method 를 override 하여 구현합니다.
common.code.NormalCode
public enum NormalCode implements ResponseCode {
SEARCH_SUCCESS("OK"),
CREATE_SUCCESS("OK"),
MODIFY_SUCCESS("OK"),
DELETE_SUCCESS("OK");
private final String code;
NormalCode(String code) {
this.code = code;
}
@Override
public String code() {
return this.code;
}
@Override
public String messageCode() { // Enum 의 name 으로 message 추출
return this.name();
}
}
ErrorCode 는 서버사이드와 클라이언트사이드, 그외의 오류를 구분할 수 있도록 코드 중간 SV (SerVer), CE (CliEnt), DT (DaTa) 구분 값을 추가해 줬습니다. ERR 을 prefix 하고 구분과 인덱스를 붙여 작성하였습니다.
코드가 필요하다면 규칙에 맞게 추가해서 사용하도록 합니다.
common.code.ErrorCode
public enum ErrorCode implements ResponseCode {
SERVICE_ERROR("ERR_SV_01"), //runtime exception 처리용
DATA_PROCESSING_ERROR("ERR_SV_02"), // 데이터 처리 관련 에러
INVALID_PARAMETER("ERR_CE_01"), // 잘못된 파라미터 전달 에러
NO_DATA("ERR_DT_01"); // 데이터 없음
private final String code;
ErrorCode(String code) {
this.code = code;
}
@Override
public String code() {
return this.code;
}
@Override
public String messageCode() {
return this.name();
}
}
message.properties 를 열어 앞에서 추가한 Enum code name 에 맞게 메시지를 추가해 줍니다.
resources/messages/message.properties
SEARCH_SUCCESS=데이터를 조회하는데 성공하였습니다.
CREATE_SUCCESS=데이터를 추가하는데 성공하였습니다.
MODIFY_SUCCESS=데이터를 수정하는데 성공하였습니다.
DELETE_SUCCESS=데이터를 삭제하는데 성공하였습니다.
DATA_PROCESSING_ERROR=데이터를 처리하는데 실패하였습니다.
SERVER_ERROR=요청한 서비스에 문제가 있습니다. 관리자에게 문의하세요.
INVALID_PARAMETER=적합하지 않은 값이 전달되었습니다.
NO_DATA=데이터가 존재하지 않습니다.
message.properties 의 Resource Bundle 탭을 눌러 다국어 처리도 해줍니다.

기존에 messageCode 를 받아 message.properties 에서 코드와 메시지를 리턴해주던 MessageConfig 를 수정합니다. ResponseCode 를 인자로 받고, 코드는 받은 Enum의 code 를, 메시지는 Enum 의 name 으로 message.properties 에서 추출해 리턴해 주도록 합니다.
@Configuration
@RequiredArgsConstructor
public class MessageConfig {
private final MessageSource messageSource;
public String getMessage(ResponseCode responseCode) {
return messageSource.getMessage(
responseCode.messageCode(), null, LocaleContextHolder.getLocale()
);
}
public String getCode(ResponseCode responseCode) {
return responseCode.code();
}
}
messageConfig 변경으로 오류가 발생하는 Controller 와 GlobalExceptionHandler 를 수정해줍니다.
status 는 messageConfig 의 getCode 메소드를 호출하도록, 그리고 적절한 코드를 해당 메소드의 인자로 전달하도록 합니다.
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final MessageConfig messageConfig;
@GetMapping(value = "/members/{memberId}",
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ItemResponse<MemberSearchResponse>> getMemberById(
@PathVariable("memberId") String memberId) {
return ResponseEntity.ok()
.body(ItemResponse.<MemberSearchResponse>builder()
.status(messageConfig.getCode(NormalCode.SEARCH_SUCCESS))
.message(messageConfig.getMessage(NormalCode.SEARCH_SUCCESS))
.item(memberService.getMemberById(memberId))
.build());
}
@GetMapping(value = "/members", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ItemsResponse<MemberSearchResponse>> getMembers() {
return ResponseEntity.ok()
.body(ItemsResponse.<MemberSearchResponse>builder()
.status(messageConfig.getCode(NormalCode.SEARCH_SUCCESS))
.message(messageConfig.getMessage(NormalCode.SEARCH_SUCCESS))
.items(memberService.getMembers())
.build());
}
.
.
.
우선 오류 부분을 위 Controller 와 같이 수정하고, 약간의 기능개선을 합니다. ExceptionHandler 가 추가 되더라도 ErrorCode 만 변경되고 리턴되는 객체의 구조는 같을 것이기 때문에 응답객체를 생성하는 메서드를 추가하여 기능을 분리해 줍니다. 그리고 Exception 에 대한 로그도 출력 될 수 있도록 합니다.
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final MessageConfig messageConfig;
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEntityNotFound(EntityNotFoundException e) {
return generateErrorResponse(ErrorCode.NO_DATA, e);
}
private ResponseEntity<ErrorResponse> generateErrorResponse(
ErrorCode errorCode, Exception e) {
log.error(errorCode.messageCode(), e);
return ResponseEntity.ok()
.body(ErrorResponse.builder()
.status(messageConfig.getCode(errorCode))
.message(messageConfig.getMessage(errorCode))
.detailMessage(e.getMessage())
.build());
}
}
자 이제 테스트를 해보면
http://localhost:13713/my-api/members/member1 요청
{
status: "OK",
message: "데이터를 조회하는데 성공하였습니다.",
item: {
memberId: "member1",
memberName: "회원 명"
}
}
Locale 변경
{
status: "OK",
message: "Data retrieved successfully.",
item: {
memberId: "member1",
memberName: "회원 명"
}
}
http://localhost:13713/my-api/members/member2 요청
{
status: "ERR_DA_01",
message: "The data does not exist.",
detailMessage: "회원 ID 가 존재하지 않습니다. -> member2"
}
console
jakarta.persistence.EntityNotFoundException: 회원 ID 가 존재하지 않습니다. -> member2
이하생략..
정상적으로 응답이 전달되고, 출력되지 않던 로그도 정상 출력됩니다.
프로젝트의 보안 레벨에 따라 상세 메시지를 출력하면 안되는 경우가 있습니다. 하지만 개발 단계에서는 필요한 부분이기 때문에 profiles.active 에 따라 detailMessage 를 추가하거나 빼기도 합니다. 일반 메시지의 경우에도 추상화 하여 전달해야 할 경우도 있습니다. 모든 ErrorCode message 를 SERVER_ERROR 메시지로 변경해야 하는데, 이 경우 GlobalExceptionHandler 의 generateErrorReponse 부분만 수정하면 됩니다.
lombok 에서 제공하는 @Slf4j 와 직접 선언하여 쓰는 것은 기본적으로 같은 동작방식입니다. 코드의 간결성 측면에서는 Annotation 을 사용하는 것이 좋지만, 개인적으로 static 필드의 소문자 로거가 별로라.. 취향에 따라 쓰도록 합시다.😅