Common response code and message processing

GEONNY·2024년 7월 31일
0

Building-API

목록 보기
12/28
post-thumbnail

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

효율적인 관리를 위해 message.properties 에서 코드들을 제거하고 별도의 Enum 을 생성하여 관리하도록 합니다. 정상 처리 코드와 에러 코드 를 분리하면서도 코드라는 관계성을 맺기 위해 Interface를 작성하고 이을 구현한 Enum 을 작성합니다.
클라이언트로 전달할 코드와 다국어 처리를 위해 message.properties 에서 값을 추출할 수 있도록 messageCode 가 필요합니다. Enum 에서는 해당 값들을 추출할 수 있는 method 가 공통적으로 필요하기 때문에 Interface 에 추가하여 각 Enum 에서 구현하도록 합니다.

📌ResponseCode interface

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 하여 구현합니다.

📌NormalCode enum

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 enum

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

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 탭을 눌러 다국어 처리도 해줍니다.

📌messageConfig

기존에 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 를 수정해줍니다.

📌MemberController

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());
    }
                                            .
                                            .
                                            .

📌GlobalExceptionHandler

우선 오류 부분을 위 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 부분만 수정하면 됩니다.

📙@Slf4j vs LoggerFactory.getLogger

lombok 에서 제공하는 @Slf4j 와 직접 선언하여 쓰는 것은 기본적으로 같은 동작방식입니다. 코드의 간결성 측면에서는 Annotation 을 사용하는 것이 좋지만, 개인적으로 static 필드의 소문자 로거가 별로라.. 취향에 따라 쓰도록 합시다.😅

profile
Back-end developer

0개의 댓글