[Spring Boot] Custom Error Code๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ์™€ ์—๋Ÿฌ ์ฝ”๋“œ ๋ฌธ์„œํ™”

ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœํ•˜๋˜ ์ค‘ ํ”„๋ก ํŠธ์—๊ฒŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•˜๋Š” ๊ฒŒ ํž˜๋“ค๋‹ค๋Š” ์—ฐ๋ฝ์„ ๋ฐ›์•˜๋‹ค.  

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๋ฉฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ฒ˜๋ฆฌํ–ˆ๋‹ค. 

 

 

- AppException

@Getter
public class JikgongException extends RuntimeException {

    private final HttpStatus status;
    private final String errorCode;
    private final String errorMessage;

    // ErrorCode ์ƒ์„ฑ์ž
    public JikgongException(ErrorCode errorCode) {
        super(errorCode.getErrorMessage());
        this.status = errorCode.getStatus();
        this.errorCode = errorCode.getCode();
        this.errorMessage = errorCode.getErrorMessage();
    }

    public JikgongException(HttpStatus status, String errorCode, String errorMessage) {
        super(errorMessage);
        this.status = status;
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }
}

 

 

 

- ErrorCode

@Getter
@RequiredArgsConstructor
public enum ErrorCode {
    /**
     * member
     */
    MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "ํšŒ์› ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."),
    ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "๋งŒ๋ฃŒ๋œ access token ์ž…๋‹ˆ๋‹ค."),
    REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "๋งŒ๋ฃŒ๋œ refresh token ์ž…๋‹ˆ๋‹ค."),
    REFRESH_TOKEN_NOT_MATCH(HttpStatus.FORBIDDEN, "์œ ํšจํ•˜์ง€ ์•Š์€ refresh token ์ž…๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜์„ธ์š”."),
    MEMBER_PHONE_EXIST(HttpStatus.CONFLICT, "์ด๋ฏธ ๋“ฑ๋ก๋œ ํ•ธ๋“œํฐ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค."),
	// ...
    
    /**
     * ์•Œ๋ฆผ
     */
    NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "์•Œ๋ฆผ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."),
	// ...
    
    
    /**
     * ์ข‹์•„์š”
     */
    LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "์ข‹์•„์š” ๋ˆ„๋ฅธ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."),
    LIKE_REQUEST_INVALID(HttpStatus.BAD_REQUEST, "์ข‹์•„์š”๋Š” ๊ธฐ์—…์ด ๋…ธ๋™์ž์—๊ฒŒ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."),
    LIKE_ALREADY_EXIST(HttpStatus.CONFLICT, "ํ•ด๋‹น ํšŒ์›์—๊ฒ ์ด๋ฏธ ์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €์Šต๋‹ˆ๋‹ค."),

    /**
     * ์ด๋ฏธ์ง€, ํŒŒ์ผ, ๊ฒฝ๋ ฅ ์ฆ๋ช…์„œ
     */
    FILE_NOT_FOUND_EXTENSION(HttpStatus.BAD_REQUEST, "ํ™•์žฅ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),
	
    // ...
    
    private final HttpStatus status;
    private final String errorMessage;
}

 

 

 

- ExceptionHandler

@ExceptionHandler(JikgongException.class)
public ResponseEntity<?> handleCustomException(JikgongException e) {
    log.error("ํ•ธ๋“ค๋งํ•œ ์—๋Ÿฌ ๋ฐœ์ƒ");
    ErrorResponse errorResponse = ErrorResponse.builder()
        .status(e.getStatus())
        .code(e.getErrorCode())
        .errorMessage(e.getErrorMessage())
        .build();
    return ResponseEntity.status(e.getStatus()).body(new Response<>(errorResponse, "์ปค์Šคํ…€ ์˜ˆ์™ธ ๋ฐ˜ํ™˜"));
}

 

 

 

- ErrorResponse

@Builder
@Getter
public class ErrorResponse {

    private HttpStatus status;
    private String errorMessage;
}

 

์ปค์Šคํ…€ํ•œ ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค๊ณ , status์™€ message๋Š” Enum์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์ด๋‹ค. ๋งŽ์€ ๋ถ„๋“ค์ด ์ด๋Ÿฐ์‹์œผ๋กœ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ๊ณ„์‹ ๋ฐ, ํ•œ ๊ฐ€์ง€ ์•„์‰ฌ์šด ์ ์€ Http Status Code๊ฐ€ 400, 401, 403 404, 409, 500 ๋“ฑ์ด ์žˆ๋Š”๋ฐ, ํ•˜๋‚˜์˜ request์—์„œ ๋™์ผํ•œ code์˜ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ์ƒํƒœ ์ฝ”๋“œ๋งŒ์œผ๋ก  ์—๋Ÿฌ๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ๊ฐ€ ์‰ฝ์ง€ ์•Š๋‹ค.  

 

 

 

๋งŒ์•ฝ ํšŒ์›์„ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•ด์„  ๊ณ ์œ ํ•œ loginId, ๊ณ ์œ ํ•œ phone number์ด ํ•„์š”ํ•˜๋‹ค ๊ฐ€์ •ํ•ด๋ณด์ž. ์•„๋งˆ ์—๋Ÿฌ ์‘๋‹ต์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฐ˜ํ™˜๋  ๊ฒƒ์ด๋‹ค.

// response
{
    "status" : HttpStatus.CONFLICT
    "message" : "์ด๋ฏธ ๋“ฑ๋ก๋œ ํ•ธ๋“œํฐ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค."
}

 

 

 

์ด๋•Œ status๋ฅผ ํ™œ์šฉํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” ๋™์ผํ•œ status๋ฅผ ๊ฐ–๋Š” ์—๋Ÿฌ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ์˜ˆ์™ธ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฒƒ์€ ์ข‹์ง€ ๋ชปํ•˜๋‹ค.

if (response.errorCode == 409)

 

 

๊ทธ๋ ‡๋‹ค๋ฉด message๋กœ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•˜๋Š”๋ฐ message๋Š” ์–ธ์ œ๋“  ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๊ณ  ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ ๋ดค์„ ๋•Œ๋„ ์ข‹์ง€ ๋ชปํ•˜๋‹ค. 

 

 

 

 

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ์ปค์Šคํ…€ ์—๋Ÿฌ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค. 

@Getter
@RequiredArgsConstructor
public enum ErrorCode {
    /**
     * member
     */
    MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER-001", "ํšŒ์› ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."),
    ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "MEMBER-002", "๋งŒ๋ฃŒ๋œ access token ์ž…๋‹ˆ๋‹ค."),
    REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "MEMBER-003", "๋งŒ๋ฃŒ๋œ refresh token ์ž…๋‹ˆ๋‹ค.").
    
    ...
    
    private final HttpStatus status;
    private final String code;
    private final String errorMessage;
}

 

๋‚˜๋Š” [๋„๋ฉ”์ธ]-[Code] ๋ฐฉ์‹์œผ๋กœ ์ปค์Šคํ…€ํ•˜๊ฒŒ ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ–ˆ๋Š”๋ฐ, ๊ฐ์ž ์›ํ•˜๋Š”๋ฐ๋กœ ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

 

 

์ด๋ ‡๊ฒŒ ๋งŒ๋“  Custom Error Code๋Š” ๋…ธ์…˜ ๋“ฑ์„ ์ด์šฉํ•ด ๋”ฐ๋กœ ๋ฌธ์„œํ™” ํ•ด ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ์ž์™€ ๊ณต์œ ํ•ด์•ผํ•œ๋‹ค. ๋˜ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์ˆ˜์ •๋  ๊ฒฝ์šฐ์—๋„ ์—…๋ฐ์ดํŠธ ํ•ด์ค˜์•ผํ•œ๋‹ค.

 

๊ต‰์žฅํžˆ ๋น„ํšจ์œจ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— swagger ์ฒ˜๋Ÿผ  ์ž๋™์œผ๋กœ ๋ฌธ์„œํ™” ์‹œ์ผœ์ฃผ๋ ค ์ด๊ฒƒ์ €๊ฒƒ ์ฐพ์•„๋ดค์—ˆ๋‹ค. 

 

 

๋งŽ์ด ๋ณด์˜€๋˜ ๋ฐฉ์‹์ด RestDocs์— ๋ฌธ์„œํ™”๋ฅผ ํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ๋‹ค. 

Custom .Snippet file, ์‘๋‹ต๊ฐ’ ํ˜•์‹๊ณผ ํ˜•์‹์— ๋งž๋Š” Descriptor ๊ตฌํ˜„, ๊ทธ๋ฆฌ๊ณ  Custom .Snippet file๊ณผ Descriptor๋“ค์„ ์ง€์ •ํ•  Custom Response ํด๋ž˜์Šค ๋“ฑ์ด ํ•„์š”ํ•˜๋‹ค. 

 

๋˜ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ง„ ๋‚ด์šฉ์— Test์ฝ”๋“œ๊นŒ์ง€ ์ž‘์„ฑํ•ด์•ผํ•œ๋‹ค. 

 

์ด๋ฏธ RestDocs๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ์‹œ๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฐฉ์‹๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™์€๋ฐ, ๋‚˜๋Š” Swagger๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ใ„ฑ...๊ท€์ฐฎ์•˜๋‹ค.

๊ทธ๋ƒฅ SSR(ํƒ€์ž„๋ฆฌํ”„)๋กœ ํ™”๋ฉด์„ ๊ฐœ๋ฐœํ•˜์—ฌ ์—๋Ÿฌ ์ฝ”๋“œ ๋ฌธ์„œํ™”๋ฅผ ์‹œ์ผœ์คฌ๋‹ค. 

 

 - ErrorCodeController

@Controller
public class ErrorCodeController {

    /**
     * ์ปค์Šคํ…€ ์—๋Ÿฌ ์ฝ”๋“œ ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ ์ปจํŠธ๋กค๋Ÿฌ
     */
    @GetMapping("/error-codes")
    public String getAllErrorCodes(Model model) {
        Map<String, List<ErrorCodeResponse>> groupedErrorCodes = Stream.of(ErrorCode.values())
            .map(errorCode -> new ErrorCodeResponse(
                errorCode.getStatus(),
                errorCode.getCode(),
                errorCode.getErrorMessage()))
            .collect(Collectors.groupingBy(errorCodeResponse -> errorCodeResponse.getCode().split("-")[0]));

        List<ErrorCodeGroup> errorCodeGroups = groupedErrorCodes.entrySet().stream()
            .map(entry -> new ErrorCodeGroup(entry.getKey(), entry.getValue()))
            .collect(Collectors.toList());

        model.addAttribute("errorCodeGroups", errorCodeGroups);

        return "errorCodes";
    }
}

 

ErrorCode ์ „์ฒด๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ErrorCodeResponse๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๊ณ , ๋„๋ฉ”์ธ๋ณ„๋กœ ๋ฌถ์–ด Key, Value ํ˜•์‹์˜ Map์„ ์ƒ์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

 

์ฆ‰ Key๊ฐ€ Member, Value๊ฐ€ List<ErrorCodeResponse>์ธ Map์„ ์ƒ์„ฑํ•ด์ฃผ์—ˆ๊ณ , ์ด๋ฅผ model์— ๋‹ด์•„ ๋„˜๊ฒจ์ฃผ์—ˆ๋‹ค. 

 

 

์•„๋ž˜ ์‚ฌ์ง„์€ ๋‹ค๋ฅธ ๋ถ„์ด RestDocs๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ด๋‘์‹  ์ปค์Šคํ…€ ์—๋Ÿฌ ์ฝ”๋“œ ๋ฌธ์„œํ™” ํŽ˜์ด์ง€์ด๋‹ค. 

 

 

 

์•„๋ž˜๋Š” ๋‚ด๊ฐ€ ํƒ€์ž„๋ฆฌํ”„๋กœ ๊ตฌ์„ฑํ•œ ์—๋Ÿฌ ์ฝ”๋“œ ๋ฌธ์„œ ํŽ˜์ด์ง€์ด๋‹ค. 

 

 

์ด์ •๋„๋Š” ์ง€ํ”ผํ‹ฐ๊ฐ€ ๊ต‰์žฅํžˆ.. ์ž˜ํ•ด์ค€๋‹ค๐Ÿค”