๐Ÿ’ป PROJECT/[Spring Boot, React] ๋…์„œ ์Šต๊ด€ ๊ด€๋ฆฌ ์„œ๋น„์Šค

[๊ตฌํ˜„ ๊ธฐ๋ก] CustomException + ErrorCode ๊ตฌ์กฐ ์„ค๊ณ„

devCloud 2026. 5. 22. 18:02
728x90
BACKEND

CustomException + ErrorCode ๊ตฌ์กฐ ์„ค๊ณ„

Spring Boot ํ”„๋กœ์ ํŠธ์—์„œ ์˜ˆ์™ธ ์‘๋‹ต ํ˜•์‹์„ ํ†ต์ผํ•˜๊ธฐ ์œ„ํ•ด CustomException, ErrorCode, GlobalExceptionHandler ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ–ˆ๋‹ค.

1. ๋ฐฐ๊ฒฝ

Spring Boot ํ”„๋กœ์ ํŠธ์—์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๋ณ„๋„๋กœ ์„ค๊ณ„ํ•˜์ง€ ์•Š์œผ๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค.

  • ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋งˆ๋‹ค ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ด ๋‹ฌ๋ผ์ง„๋‹ค.
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ง์ ‘ ์ž‘์„ฑํ•˜๋ฉด ์˜คํƒ€๊ฐ€ ์ƒ๊ธฐ๊ฑฐ๋‚˜ ์ผ๊ด€์„ฑ์ด ๊นจ์ง„๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌ๋˜๋Š” ์—๋Ÿฌ ์‘๋‹ต ํ˜•์‹์ด ์ œ๊ฐ๊ฐ์ด ๋œ๋‹ค.
  • ์–ด๋–ค ์—๋Ÿฌ๊ฐ€ ์–ด๋””์„œ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์ถ”์ ํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

Booktine์—์„œ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ๋“  ์—๋Ÿฌ ์‘๋‹ต์„ ApiResponse ํ˜•์‹์œผ๋กœ ํ†ต์ผํ•˜๊ณ , ๋„๋ฉ”์ธ๋ณ„ ์˜ˆ์™ธ๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ–ˆ๋‹ค.


2. ์ „์ฒด ๊ตฌ์กฐ

๋„ค ๊ฐ€์ง€ ํด๋ž˜์Šค๊ฐ€ ์—ญํ• ์„ ๋‚˜๋ˆ  ๋‹ด๋‹นํ•œ๋‹ค.

ํด๋ž˜์Šค ์—ญํ• 
ApiResponse ์„ฑ๊ณต/์‹คํŒจ ์‘๋‹ต์„ ํ†ต์ผ๋œ ํ˜•์‹์œผ๋กœ ๋ž˜ํ•‘
ErrorCode HTTP ์ƒํƒœ์ฝ”๋“œ + ๋ฉ”์‹œ์ง€๋ฅผ enum์œผ๋กœ ์ค‘์•™ ๊ด€๋ฆฌ
CustomException ErrorCode๋ฅผ ๊ฐ์‹ธ๋Š” RuntimeException
GlobalExceptionHandler ์˜ˆ์™ธ๋ฅผ ์บ์น˜ํ•ด ApiResponse๋กœ ๋ณ€ํ™˜
Service์—์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ → CustomException(ErrorCode) → GlobalExceptionHandler ์บ์น˜ → ApiResponse.fail() ์‘๋‹ต

3. ApiResponse — ๊ณตํ†ต ์‘๋‹ต ๋ž˜ํผ

๋ชจ๋“  API ์‘๋‹ต์— ์‚ฌ์šฉํ•˜๋Š” ๊ณตํ†ต ๋ž˜ํผ๋‹ค. Java record๋กœ ์ •์˜ํ•ด ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.

ApiResponse๋Š” ์„ฑ๊ณต/์‹คํŒจ ์‘๋‹ต ํ˜•์‹์„ ํ•˜๋‚˜๋กœ ํ†ต์ผํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” success ์—ฌ๋ถ€๋งŒ์œผ๋กœ ์‘๋‹ต ์ƒํƒœ๋ฅผ ์ผ๊ด€๋˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

์„ธ ๊ฐ€์ง€ ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

  • ok(data) — ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ์„ฑ๊ณต ์‘๋‹ต
  • ok() — ๋ฐ์ดํ„ฐ ์—†๋Š” ์„ฑ๊ณต ์‘๋‹ต
  • fail(message) — ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ด์€ ์‹คํŒจ ์‘๋‹ต

์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.

// ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
return ResponseEntity.ok(ApiResponse.ok(postService.getPost(postId)));

// ๋ฐ์ดํ„ฐ ์—†๋Š” ์„ฑ๊ณต
return ResponseEntity.ok(ApiResponse.ok());

// ์—๋Ÿฌ ์‘๋‹ต์€ GlobalExceptionHandler์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ
์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ณ  ApiResponse๋กœ ๊ฐ์‹ธ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด ๊ตฌ์กฐ ๋•๋ถ„์— ๋ชจ๋“  API ์‘๋‹ต ํ˜•์‹์ด ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋œ๋‹ค.

4. ErrorCode — ์—๋Ÿฌ ์ค‘์•™ ๊ด€๋ฆฌ

๋„๋ฉ”์ธ ์ „๋ฐ˜์˜ ์—๋Ÿฌ๋ฅผ HTTP ์ƒํƒœ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฌถ์–ด enum์œผ๋กœ ์ •์˜ํ–ˆ๋‹ค.

public enum ErrorCode {
    // ๊ณตํ†ต
    INVALID_INPUT(400, "์ž˜๋ชป๋œ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค."),
    UNAUTHORIZED(401, "์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."),
    FORBIDDEN(403, "์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."),

    // ์‚ฌ์šฉ์ž
    USER_NOT_FOUND(404, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),
    DUPLICATE_EMAIL(409, "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."),
    INVALID_PASSWORD(400, "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),

    // ์ปค๋ฎค๋‹ˆํ‹ฐ
    COMMUNITY_POST_NOT_FOUND(404, "์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),
    COMMUNITY_LIKE_ALREADY_EXISTS(409, "์ด๋ฏธ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅธ ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค."),
    COMMUNITY_REPLY_DEPTH_EXCEEDED(400, "๋Œ€๋Œ“๊ธ€์—๋Š” ๋Œ€๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),

    // ์ง„ํ–‰๋ฅ /๋ชฉํ‘œ
    MONTHLY_GOAL_ALREADY_EXISTS(409, "ํ•ด๋‹น ์›”์˜ ์›”๊ฐ„ ๋ชฉํ‘œ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."),

    // ์„œ๋ฒ„
    INTERNAL_SERVER_ERROR(500, "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");

	// .. ๋“ฑ๋“ฑ
    private final int status;
    private final String message;
}
์ƒํƒœ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ enum์œผ๋กœ ๊ด€๋ฆฌํ•ด ๋ฌธ์ž์—ด ์˜คํƒ€๋‚˜ ์ค‘๋ณต ์ •์˜ ๋ฌธ์ œ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ƒํƒœ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ์šด ์—๋Ÿฌ๊ฐ€ ์ƒ๊ธฐ๋ฉด ErrorCode์—๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค. ๋„๋ฉ”์ธ๋ณ„๋กœ ErrorCode๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ์‹๋„ ์žˆ์ง€๋งŒ, ํ˜„์žฌ Booktine ๊ทœ๋ชจ์—์„œ๋Š” ๋‹จ์ผ ํŒŒ์ผ ๊ตฌ์กฐ๊ฐ€ ๋” ๋‹จ์ˆœํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.


5. CustomException — ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ

ErrorCode๋ฅผ ๋ฉค๋ฒ„๋กœ ๊ฐ€์ง€๋Š” RuntimeException์ด๋‹ค.

CustomException์€ ErrorCode๋ฅผ ๊ฐ์‹ธ๋Š” ์—ญํ• ๋งŒ ๋‹ด๋‹นํ•œ๋‹ค. ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ๋Š” ErrorCode๋งŒ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.

// ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ์†Œ์Šค ์กฐํšŒ
User user = userRepository.findById(userId)
        .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// ์ค‘๋ณต ๊ฒ€์‚ฌ
if (userRepository.existsByEmail(email)) {
    throw new CustomException(ErrorCode.DUPLICATE_EMAIL);
}

// ๊ถŒํ•œ ๊ฒ€์‚ฌ
if (!post.getUser().getId().equals(userId)) {
    throw new CustomException(ErrorCode.FORBIDDEN);
}
๋ฌธ์ž์—ด ๋Œ€์‹  ErrorCode๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ๋ฆ„์ด ํ›จ์”ฌ ๋ช…ํ™•ํ•ด์กŒ๋‹ค.

6. GlobalExceptionHandler — ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

@RestControllerAdvice๋กœ ์ „์—ญ์—์„œ ์˜ˆ์™ธ๋ฅผ ์บ์น˜ํ•˜๊ณ  ApiResponse ํ˜•์‹์œผ๋กœ ์‘๋‹ตํ•œ๋‹ค.

์˜ˆ์™ธ ์‘๋‹ต ํ˜•์‹์„ ์ „์—ญ์—์„œ ํ†ต์ผํ•˜๊ธฐ ์œ„ํ•ด GlobalExceptionHandler๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

7. ์‹ค์ œ ๋™์ž‘ ํ๋ฆ„

์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๋ฌผ์„ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์˜ˆ์‹œ๋กœ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ๋ฆ„
์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด Controller๋Š” Service๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. Service์—์„œ ์ •์ƒ ์ฒ˜๋ฆฌ์— ์„ฑ๊ณตํ•˜๋ฉด ApiResponse.ok()๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด CustomException์ด ๋ฐœ์ƒํ•œ๋‹ค. ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋Š” GlobalExceptionHandler๊ฐ€ ์ „์—ญ์—์„œ ์ฒ˜๋ฆฌํ•œ ๋’ค, ์ตœ์ข…์ ์œผ๋กœ ApiResponse.fail() ํ˜•ํƒœ์˜ ์‘๋‹ต์œผ๋กœ ๋ณ€ํ™˜๋˜์–ด ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌ๋œ๋‹ค.

ํด๋ผ์ด์–ธํŠธ์—๋Š” ์•„๋ž˜ ํ˜•์‹์œผ๋กœ ์‹คํŒจ ์‘๋‹ต์ด ๋‚ด๋ ค๊ฐ„๋‹ค.

{
  "success": false,
  "data": null,
  "message": "๊ฒŒ์‹œ๋ฌผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
}

์„ฑ๊ณต ์‘๋‹ต์€ ์•„๋ž˜ ํ˜•์‹์ด๋‹ค.

{
  "success": true,
  "data": { ... },
  "message": null
}

8. ์ •๋ฆฌ

์ด ๊ตฌ์กฐ์˜ ํ•ต์‹ฌ์€ ์—ญํ•  ๋ถ„๋ฆฌ๋‹ค.

  • ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋Š” ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋Š” ๊ฒƒ๋งŒ ๋‹ด๋‹นํ•œ๋‹ค.
  • GlobalExceptionHandler๋Š” ์‘๋‹ต ๋ณ€ํ™˜๋งŒ ๋‹ด๋‹นํ•œ๋‹ค.
  • ErrorCode๋Š” ์—๋Ÿฌ ์ •์˜๋งŒ ๋‹ด๋‹นํ•œ๋‹ค.

์ƒˆ๋กœ์šด ์—๋Ÿฌ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ErrorCode์— ํ•œ ์ค„ ์ถ”๊ฐ€ํ•˜๊ณ  throw new CustomException(ErrorCode.XXX) ํ˜•ํƒœ๋กœ ๋˜์ง€๋ฉด ๋์ด๋‹ค.

์˜ˆ์™ธ ์‘๋‹ต ํ˜•์‹์ด ๋‹ฌ๋ผ์ง€๋ฉด GlobalExceptionHandler ํ•œ ๊ณณ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋œ๋‹ค. ์ด ๊ตฌ์กฐ ๋•๋ถ„์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ๋ฆ„์ด ๋‹จ์ˆœํ•ด์ง€๊ณ  ์‘๋‹ต ํ˜•์‹๋„ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€๋œ๋‹ค.
728x90