ํ˜„์—…์—์„œ ์ž์ฃผ ์“ฐ๋Š” ๋ฐฑ์—”๋“œ ์Šคํ‚ฌ๋“ค (with JAVA)

ํ˜„์—…์—์„œ ์ž์ฃผ ์“ฐ๋Š” ๋ฐฑ์—”๋“œ ์Šคํ‚ฌ — ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ œ๋Œ€๋กœ ํ•˜๊ธฐ. ์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค try-catch๋ฅผ ๋„๋ฐฐํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, API ์—๋Ÿฌ ์‘๋‹ต์ด ์ œ๊ฐ๊ฐ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค. ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, ์ปค์Šคํ…€ ์˜ˆ์™ธ ์„ค๊ณ„, ํ‘œ์ค€ ์—๋Ÿฌ ์‘๋‹ต(ProblemDetail)๊นŒ์ง€ — ์Šคํ”„๋ง์—์„œ ์˜ˆ์™ธ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๋‹ค๋ฃจ๋Š” ๋ฒ•์„ ์‹ค์ œ ์ฝ”๋“œ๋กœ ์ •๋ฆฌํ•œ๋‹ค.


๋“ค์–ด๊ฐ€๋ฉฐ

๊ธฐ๋Šฅ ๊ฐœ๋ฐœ์— ์ซ“๊ธฐ๋‹ค ๋ณด๋ฉด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” ๋Š˜ ๋’ท์ „์œผ๋กœ ๋ฐ€๋ฆฐ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” API ํ’ˆ์งˆ์„ ์ขŒ์šฐํ•˜๋Š” ์Šคํ‚ฌ์ด๋‹ค. ์ž˜๋ชป ๋˜์ ธ์ง„ 500 ์—๋Ÿฌ ํ•˜๋‚˜๊ฐ€ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์„ ๋ฉˆ์ถ”๊ฒŒ ํ•˜๊ณ , ์ œ๊ฐ๊ฐ์ธ ์—๋Ÿฌ ์‘๋‹ต ํ˜•์‹์ด ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ฅผ ์ง€์ €๋ถ„ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.

๊ฐ€์žฅ ํ”ํ•œ ์•ˆํ‹ฐํŒจํ„ด์€ ์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค try-catch๋ฅผ ๋„๋ฐฐํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜๊ณ , ์‘๋‹ต ํ˜•์‹์ด ๋ฉ”์„œ๋“œ๋งˆ๋‹ค ๋‹ฌ๋ผ์ง€๊ณ , ์–ด๋”˜๊ฐ€์—์„œ๋Š” ์ฒ˜๋ฆฌ๋ฅผ ๊นœ๋นกํ•œ๋‹ค. ์ด๊ฑธ ํ•œ ๊ณณ์œผ๋กœ ๋ชจ์œผ๋Š” ๊ฒŒ ์ด ๊ธ€์˜ ํ•ต์‹ฌ์ด๋‹ค.

์ข‹์€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์˜ ๋ชฉํ‘œ๋Š” ๋‘ ๊ฐ€์ง€๋‹ค. ํ•˜๋‚˜, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ ๊ณณ์— ๋ชจ์•„ ์ค‘๋ณต์„ ์—†์•ค๋‹ค. ๋‘˜, ๋ชจ๋“  ์—๋Ÿฌ๊ฐ€ ์ผ๊ด€๋œ ํ˜•์‹์œผ๋กœ ์‘๋‹ต๋˜๊ฒŒ ํ•œ๋‹ค. ๊ทธ๋ž˜์•ผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—๋Ÿฌ๋ฅผ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ํฉ์–ด์ง„ try-catch์˜ ๋ฌธ์ œ๋ถ€ํ„ฐ, ์Šคํ”„๋ง์˜ ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ(@RestControllerAdvice), ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ๋ฅผ ํƒ€์ž…์œผ๋กœ ์„ค๊ณ„ํ•˜๋Š” ๋ฒ•, ํ‘œ์ค€ ์—๋Ÿฌ ํฌ๋งท์ธ ProblemDetail(RFC 9457), ๊ทธ๋ฆฌ๊ณ  ๊ฒ€์ฆ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊นŒ์ง€ ์‹ค์ œ ์ฝ”๋“œ๋กœ ๋ณธ๋‹ค. ์˜ˆ์ œ๋Š” Spring Boot 3.x ๊ธฐ์ค€์ด๋‹ค.


1. ์™œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ „๋žต์ด ํ•„์š”ํ•œ๊ฐ€

๋จผ์ € ์•ˆํ‹ฐํŒจํ„ด๋ถ€ํ„ฐ ๋ณด์ž. ์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค ์ด๋ ‡๊ฒŒ try-catch๋กœ ๊ฐ์‹ธ๋Š” ์ฝ”๋“œ๋‹ค.

// ๋‚˜์œ ์˜ˆ: ์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค try-catch — ์ค‘๋ณต๋˜๊ณ , ์‘๋‹ต ํ˜•์‹๋„ ์ œ๊ฐ๊ฐ์ด๋‹ค
@GetMapping("/users/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
    try {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    } catch (UserNotFoundException e) {
        return ResponseEntity.status(404).body(Map.of("error", "์‚ฌ์šฉ์ž ์—†์Œ"));
    } catch (Exception e) {
        return ResponseEntity.status(500).body(Map.of("message", "์„œ๋ฒ„ ์˜ค๋ฅ˜"));
    }
}

๋ฌธ์ œ๊ฐ€ ํ•œ๋‘˜์ด ์•„๋‹ˆ๋‹ค. ์ด try-catch๊ฐ€ ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ๋ฐ˜๋ณต๋˜๊ณ , ์–ด๋–ค ๊ณณ์€ error, ์–ด๋–ค ๊ณณ์€ message ํ‚ค๋ฅผ ์“ฐ๋ฉฐ ์‘๋‹ต ํ˜•์‹์ด ์ œ๊ฐ๊ฐ์ด๋‹ค. ์ƒˆ ๋ฉ”์„œ๋“œ์—์„œ catch๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด ๊ทธ๋Œ€๋กœ 500์ด ๋…ธ์ถœ๋œ๋‹ค.

ํ•ด๋ฒ•์€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๊ฑท์–ด๋‚ด ํ•œ ๊ณณ์œผ๋กœ ๋ชจ์œผ๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์ •์ƒ ํ๋ฆ„์—๋งŒ ์ง‘์ค‘ํ•˜๊ณ , ์˜ˆ์™ธ๋Š” ๊ทธ๋ƒฅ ๋˜์ง€๋ฉด ๋œ๋‹ค.


2. ์˜ˆ์™ธ๊ฐ€ ๋˜์ ธ์ง€๋ฉด ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๋Š”๊ฐ€

์Šคํ”„๋ง์—์„œ ์ปจํŠธ๋กค๋Ÿฌ๋‚˜ ์„œ๋น„์Šค๊ฐ€ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋ฉด, ๊ทธ ์˜ˆ์™ธ๋Š” ํ˜ธ์ถœ ์Šคํƒ์„ ํƒ€๊ณ  ์œ„๋กœ ์ „ํŒŒ๋œ๋‹ค. ์•„๋ฌด๋„ ์žก์ง€ ์•Š์œผ๋ฉด ์Šคํ”„๋ง์˜ ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ๊ธฐ๊ฐ€ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ, ์ด๋•Œ ๋‚˜์˜ค๋Š” ๊ฒŒ ๊ทธ ์œ ๋ช…ํ•œ Whitelabel ์—๋Ÿฌ ํŽ˜์ด์ง€๋‚˜ ๋ฐ‹๋ฐ‹ํ•œ ๊ธฐ๋ณธ JSON์ด๋‹ค.

{ "timestamp": "...", "status": 500, "error": "Internal Server Error", "path": "/api/users/42" }

์ด ๊ธฐ๋ณธ ์‘๋‹ต์€ ๋‘ ๊ฐ€์ง€๊ฐ€ ์•„์‰ฝ๋‹ค. ์–ด๋–ค ๋น„์ฆˆ๋‹ˆ์Šค์  ์ด์œ ๋กœ ์‹คํŒจํ–ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ณ , ํ˜•์‹์„ ์šฐ๋ฆฌ๊ฐ€ ํ†ต์ œํ•  ์ˆ˜ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์˜ˆ์™ธ๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ์ง€์ ์—์„œ ๊ฐ€๋กœ์ฑ„ ์›ํ•˜๋Š” ํ˜•์‹์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์žฅ์น˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๊ทธ๊ฒŒ ๋‹ค์Œ์˜ @RestControllerAdvice๋‹ค.


3. ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ — @RestControllerAdvice

@RestControllerAdvice๋Š” ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ ํ•œ ๊ณณ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ์Šคํ”„๋ง์˜ ํ•ต์‹ฌ ๋„๊ตฌ๋‹ค. @ExceptionHandler๋กœ "์ด ์˜ˆ์™ธ๋Š” ์ด๋ ‡๊ฒŒ ์‘๋‹ตํ•˜๋ผ"๋ฅผ ์„ ์–ธํ•œ๋‹ค.

@Slf4j
@RestControllerAdvice   // ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ์˜ ์˜ˆ์™ธ๋ฅผ ์—ฌ๊ธฐ์„œ ๊ฐ€๋กœ์ฑˆ๋‹ค
public class GlobalExceptionHandler {

    // ํŠน์ • ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException e) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("USER_NOT_FOUND", e.getMessage()));
    }

    // ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ๋ชจ๋“  ์˜ˆ์™ธ์˜ '์ตœํ›„ ๋ฐฉ์–ด์„ '
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
        log.error("์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ ๋ฐœ์ƒ", e);   // 5xx๋Š” ๋ฐ˜๋“œ์‹œ ๋กœ๊น…
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"));
    }
}

์ด์ œ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์ด๋ ‡๊ฒŒ ๊น”๋”ํ•ด์ง„๋‹ค. ์˜ˆ์™ธ๋Š” ์žก์ง€ ์•Š๊ณ  ๊ทธ๋ƒฅ ๋˜์ง€๋ฉด ์œ„์˜ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);   // try-catch ์—†์ด, ์˜ˆ์™ธ๋Š” ๊ทธ๋ƒฅ ์ „ํŒŒ
}

 

์˜ˆ์™ธ๊ฐ€ ์„œ๋น„์Šค ๊นŠ์€ ๊ณณ์—์„œ ๋˜์ ธ์ ธ๋„, ์Šคํƒ์„ ํƒ€๊ณ  ์˜ฌ๋ผ์™€ @RestControllerAdvice๊ฐ€ ํ•œ ๋ฒˆ์— ์žก๋Š”๋‹ค. ์ฒ˜๋ฆฌ ๋กœ์ง์ด ํ•œ ๊ณณ์— ๋ชจ์—ฌ ์ค‘๋ณต์ด ์‚ฌ๋ผ์ง€๊ณ  ํ˜•์‹์ด ํ†ต์ผ๋œ๋‹ค.


4. ์ปค์Šคํ…€ ์˜ˆ์™ธ ์„ค๊ณ„ — ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ํƒ€์ž…์œผ๋กœ

์˜ˆ์™ธ ์ข…๋ฅ˜๊ฐ€ ๋Š˜์–ด๋‚  ๋•Œ๋งˆ๋‹ค ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ทธ๊ฒƒ๋Œ€๋กœ ๋ฒˆ๊ฑฐ๋กญ๋‹ค. ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์€ ์—๋Ÿฌ๋ฅผ ์ฝ”๋“œ๋กœ ์ •์˜ํ•˜๊ณ , ๊ณตํ†ต ์˜ˆ์™ธ ํ•˜๋‚˜๋กœ ๋ฌถ๋Š” ๊ฒƒ์ด๋‹ค. ๋จผ์ € ์—๋Ÿฌ ์ฝ”๋“œ๋ฅผ enum์œผ๋กœ ์ •์˜ํ•œ๋‹ค.

@Getter
@RequiredArgsConstructor
public enum ErrorCode {
    USER_NOT_FOUND(HttpStatus.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"),
    DUPLICATE_EMAIL(HttpStatus.CONFLICT, "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค"),
    INVALID_INPUT(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค");

    private final HttpStatus status;   // ๊ฐ ์—๋Ÿฌ๊ฐ€ ์–ด๋–ค HTTP ์ƒํƒœ์ธ์ง€
    private final String message;
}

๊ทธ๋ฆฌ๊ณ  ์ด ์ฝ”๋“œ๋ฅผ ๋‹ด๋Š” ๊ณตํ†ต ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ๋ฅผ ๋งŒ๋“ ๋‹ค.

@Getter
public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}

์ด์ œ ์„œ๋น„์Šค์—์„œ๋Š” ์ƒํ™ฉ์— ๋งž๋Š” ์—๋Ÿฌ ์ฝ”๋“œ๋กœ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค.

public User findById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
}

ํ•ธ๋“ค๋Ÿฌ๋„ ๋‹จ ํ•˜๋‚˜๋ฉด ์ถฉ๋ถ„ํ•˜๋‹ค. BusinessException ํ•˜๋‚˜๋งŒ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ๊ฐ€ ์ปค๋ฒ„๋œ๋‹ค.

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
    ErrorCode code = e.getErrorCode();
    return ResponseEntity
        .status(code.getStatus())
        .body(new ErrorResponse(code.name(), code.getMessage()));
}

์—๋Ÿฌ์˜ ์ข…๋ฅ˜(HTTP ์ƒํƒœ + ๋ฉ”์‹œ์ง€)๊ฐ€ enum ํ•œ ๊ณณ์— ๋ชจ์—ฌ ๊ด€๋ฆฌ๋˜๋‹ˆ, ์ƒˆ ์—๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ enum์— ํ•œ ์ค„๋งŒ ๋”ํ•˜๋ฉด ๋œ๋‹ค. ์–ด๋–ค ์ƒํ™ฉ์— ์–ด๋–ค HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์“ธ์ง€๋„ ์—ฌ๊ธฐ์„œ ์ผ๊ด€๋˜๊ฒŒ ์ •ํ•ด์ง„๋‹ค.


5. ์ผ๊ด€๋œ ์—๋Ÿฌ ์‘๋‹ต — ProblemDetail (RFC 9457)

์—๋Ÿฌ ์‘๋‹ต ํ˜•์‹์„ ์ง์ ‘ ๋งŒ๋“ค์–ด๋„ ๋˜์ง€๋งŒ, ์‚ฌ์‹ค ํ‘œ์ค€์ด ์žˆ๋‹ค. RFC 9457(Problem Details for HTTP APIs)์€ API ์—๋Ÿฌ ์‘๋‹ต์˜ ๊ณตํ†ต ํฌ๋งท์„ ์ •์˜ํ•˜๊ณ , Spring 6(๋ถ€ํŠธ 3)๋ถ€ํ„ฐ ProblemDetail๋กœ ๊ธฐ๋ณธ ์ง€์›ํ•œ๋‹ค. ์ง์ ‘ ๋งŒ๋“  ErrorResponse ๋Œ€์‹  ์ด๊ฑธ ์“ฐ๋ฉด ํ‘œ์ค€์„ ๋”ฐ๋ฅด๋ฉด์„œ ์ฝ”๋“œ๋„ ์ค€๋‹ค.

@ExceptionHandler(BusinessException.class)
public ProblemDetail handleBusiness(BusinessException e) {
    ErrorCode code = e.getErrorCode();

    // status์™€ detail๋กœ ํ‘œ์ค€ ์—๋Ÿฌ ๋ฐ”๋”” ์ƒ์„ฑ
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        code.getStatus(), code.getMessage());
    problem.setTitle(code.name());
    problem.setProperty("errorCode", code.name());   // ํ‘œ์ค€ ์™ธ ์ปค์Šคํ…€ ํ•„๋“œ ์ถ”๊ฐ€

    return problem;
}

์‘๋‹ต์€ ์ด๋ ‡๊ฒŒ, ํ‘œ์ค€ ํ˜•์‹์œผ๋กœ ๋‚˜๊ฐ„๋‹ค. Content-Type๋„ ์ž๋™์œผ๋กœ application/problem+json์ด ๋œ๋‹ค.

{
  "type": "about:blank",
  "title": "USER_NOT_FOUND",
  "status": 404,
  "detail": "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
  "instance": "/api/users/42",
  "errorCode": "USER_NOT_FOUND"
}

 

type·title·status·detail·instance๊ฐ€ ํ‘œ์ค€ ํ•„๋“œ์ด๊ณ , setProperty๋กœ errorCode ๊ฐ™์€ ์ปค์Šคํ…€ ํ•„๋“œ๋ฅผ ์–น์„ ์ˆ˜ ์žˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์ด์ œ ์–ด๋–ค API๋ฅผ ํ˜ธ์ถœํ•˜๋“  ๊ฐ™์€ ๊ตฌ์กฐ๋กœ ์—๋Ÿฌ๋ฅผ ํŒŒ์‹ฑํ•˜๋ฉด ๋œ๋‹ค. ์ฐธ๊ณ ๋กœ application.properties์— spring.mvc.problemdetails.enabled=true๋งŒ ์ผœ๋„ ์Šคํ”„๋ง์˜ ๊ธฐ๋ณธ ์˜ˆ์™ธ๋“ค์ด ์ด ํฌ๋งท์œผ๋กœ ์‘๋‹ต๋œ๋‹ค.


6. ๊ฒ€์ฆ ์—๋Ÿฌ ์ฒ˜๋ฆฌ — @Valid

๋˜ ํ•˜๋‚˜ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๊ฒŒ ์ž…๋ ฅ ๊ฒ€์ฆ์ด๋‹ค. ์š”์ฒญ DTO์— ๊ฒ€์ฆ ์• ๋„ˆํ…Œ์ด์…˜์„ ๋ถ™์ด๊ณ  @Valid๋ฅผ ๊ฑธ๋ฉด, ์Šคํ”„๋ง์ด ์ž๋™์œผ๋กœ ๊ฒ€์ฆํ•˜๊ณ  ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋˜์ง„๋‹ค.

public record SignupRequest(
    @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
    String name,

    @Email(message = "์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค")
    String email,

    @Min(value = 1, message = "๋‚˜์ด๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
    int age
) {}

@PostMapping("/users")
public User signup(@Valid @RequestBody SignupRequest request) {
    return userService.signup(request);   // ๊ฒ€์ฆ ํ†ต๊ณผํ•œ ๊ฐ’๋งŒ ์—ฌ๊ธฐ ๋„๋‹ฌ
}

๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด MethodArgumentNotValidException์ด ๋˜์ ธ์ง„๋‹ค. ์ด๊ฑธ ์ „์—ญ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์žก์•„, ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์™œ ํ‹€๋ ธ๋Š”์ง€ ๋‹ด์•„ ์‘๋‹ตํ•œ๋‹ค.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException e) {
    ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
    problem.setTitle("VALIDATION_FAILED");

    // ํ•„๋“œ๋ณ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ชจ์•„์„œ ๋‹ด๋Š”๋‹ค
    Map<String, String> errors = new HashMap<>();
    e.getBindingResult().getFieldErrors().forEach(err ->
        errors.put(err.getField(), err.getDefaultMessage()));
    problem.setProperty("errors", errors);

    return problem;
}

์ด์ œ ์ž˜๋ชป๋œ ์ž…๋ ฅ์—๋Š” "์–ด๋А ํ•„๋“œ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ‹€๋ ธ๋Š”์ง€"๊ฐ€ ์นœ์ ˆํ•˜๊ฒŒ ์‘๋‹ต๋˜์–ด, ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ํผ ์—๋Ÿฌ๋ฅผ ๋ฐ”๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค.


7. ์‹ค๋ฌด ํŒ ์ •๋ฆฌ

๋งˆ์ง€๋ง‰์œผ๋กœ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์—์„œ ํ”ํžˆ ๋†“์น˜๋Š” ๊ฒƒ๋“ค์ด๋‹ค.

  • 4xx์™€ 5xx๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๋ผ. ํด๋ผ์ด์–ธํŠธ ์ž˜๋ชป(์ž˜๋ชป๋œ ์ž…๋ ฅ, ์—†๋Š” ๋ฆฌ์†Œ์Šค)์€ 4xx, ์„œ๋ฒ„ ์ž˜๋ชป(๋ฒ„๊ทธ, ์™ธ๋ถ€ ์—ฐ๋™ ์‹คํŒจ)์€ 5xx๋‹ค. ์ด ๊ตฌ๋ถ„์ด HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์ œ๋Œ€๋กœ ์“ฐ๋Š” ์ถœ๋ฐœ์ ์ด๋‹ค.
  • 5xx๋Š” ๋กœ๊น…ํ•˜๊ณ , 4xx๋Š” ๋Œ€๊ฐœ ํ•˜์ง€ ๋งˆ๋ผ. 5xx๋Š” ์šฐ๋ฆฌ๊ฐ€ ๊ณ ์ณ์•ผ ํ•  ๋ฌธ์ œ๋ผ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ๋‚จ๊ฒจ์•ผ ํ•œ๋‹ค. ๋ฐ˜๋ฉด 4xx(์‚ฌ์šฉ์ž์˜ ์ž˜๋ชป๋œ ์š”์ฒญ)๊นŒ์ง€ error ๋ ˆ๋ฒจ๋กœ ๋กœ๊น…ํ•˜๋ฉด ๋กœ๊ทธ๊ฐ€ ๋…ธ์ด์ฆˆ๋กœ ๊ฐ€๋“ ์ฐฌ๋‹ค.
  • ์—๋Ÿฌ ์‘๋‹ต์— ๋ฏผ๊ฐ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•˜์ง€ ๋งˆ๋ผ. ์Šคํƒ ํŠธ๋ ˆ์ด์Šค, ๋‚ด๋ถ€ ๊ฒฝ๋กœ, SQL ๋“ฑ์ด ์‘๋‹ต ๋ฐ”๋””์— ์„ž์—ฌ ๋‚˜๊ฐ€๋ฉด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋œ๋‹ค. ์ƒ์„ธ ์›์ธ์€ ๋กœ๊ทธ์—๋งŒ ๋‚จ๊ธฐ๊ณ , ํด๋ผ์ด์–ธํŠธ์—๋Š” ์•ˆ์ „ํ•œ ๋ฉ”์‹œ์ง€๋งŒ ์ค€๋‹ค.
  • ์˜ˆ์™ธ๋ฅผ ํ๋ฆ„ ์ œ์–ด๋กœ ๋‚จ์šฉํ•˜์ง€ ๋งˆ๋ผ. "๊ฐ’์ด ์—†์œผ๋ฉด ์˜ˆ์™ธ"๊ฐ€ ํ•ญ์ƒ ์˜ณ์€ ๊ฑด ์•„๋‹ˆ๋‹ค. ์ •์ƒ์ ์œผ๋กœ ์žˆ์„ ์ˆ˜ ์žˆ๋Š” ๋ถ€์žฌ(๋นˆ ๋ชฉ๋ก ๋“ฑ)๋Š” ์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ •์ƒ ๋ฐ˜ํ™˜์œผ๋กœ ๋‹ค๋ฃจ๋Š” ๊ฒŒ ๋งž๋‹ค.
  • @Transactional ๋กค๋ฐฑ ๊ทœ์น™์— ์ฃผ์˜ํ•˜๋ผ. ์Šคํ”„๋ง์€ ๊ธฐ๋ณธ์ ์œผ๋กœ RuntimeException์—๋งŒ ๋กค๋ฐฑํ•œ๋‹ค. ์ปค์Šคํ…€ ์˜ˆ์™ธ๋ฅผ RuntimeException ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“  ์ด์œ ์ด๊ธฐ๋„ ํ•˜๋‹ค(ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์ฐธ๊ณ ).

๋งˆ์น˜๋ฉฐ

์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” "์—๋Ÿฌ๊ฐ€ ๋‚ฌ์„ ๋•Œ ๋Œ€์ถฉ ๋ง‰๋Š”" ๋ฐฉ์–ด๊ฐ€ ์•„๋‹ˆ๋ผ, API๊ฐ€ ์‹คํŒจ๋ฅผ ์ผ๊ด€๋˜๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ‘œํ˜„ํ•˜๋Š” ์„ค๊ณ„๋‹ค. ์ปจํŠธ๋กค๋Ÿฌ์˜ try-catch๋ฅผ ๊ฑท์–ด๋‚ด @RestControllerAdvice๋กœ ๋ชจ์œผ๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ๋ฅผ ์—๋Ÿฌ ์ฝ”๋“œ๋กœ ํƒ€์ž…ํ™”ํ•˜๊ณ , ์‘๋‹ต์„ ProblemDetail ํ‘œ์ค€์œผ๋กœ ํ†ต์ผํ•˜๋ฉด, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ฝ”๋“œ๊ฐ€ ๊ทน์ ์œผ๋กœ ๋‹จ์ˆœํ•ด์ง€๋ฉด์„œ ํ’ˆ์งˆ์€ ์˜ฌ๋ผ๊ฐ„๋‹ค.

ํ•ต์‹ฌ๋งŒ ๋‹ค์‹œ ์งš์œผ๋ฉด, ์˜ˆ์™ธ๋Š” ๋˜์ง€๊ณ , ์ฒ˜๋ฆฌ๋Š” ํ•œ ๊ณณ์— ๋ชจ์œผ๊ณ , ์‘๋‹ต์€ ํ‘œ์ค€ ํ˜•์‹์œผ๋กœ ํ†ต์ผํ•œ๋‹ค. ์ด ์„ธ ๊ฐ€์ง€๋งŒ ์ง€์ผœ๋„ "์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค try-catch"์˜ ์ง€์˜ฅ์—์„œ ๋ฒ—์–ด๋‚  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ผ๊ด€๋œ ์—๋Ÿฌ ์‘๋‹ต์€ ๋ฐฑ์—”๋“œ๋งŒ์˜ ์ด๋“์ด ์•„๋‹ˆ๋ผ, ๊ทธ API๋ฅผ ์“ฐ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋‹ค๋ฅธ ์„œ๋น„์Šค ๋ชจ๋‘์˜ ์ด๋“์ด๋‹ค.


์ฐธ๊ณ  ์ถœ์ฒ˜