๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๋ฐฑ์—”๋“œ/spring&springboot

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„_Spring Boot ์‹ฌํ™”์ฃผ์ฐจ] API ์˜ˆ์™ธ์ฒ˜๋ฆฌ

by alswlfl 2026. 7. 1.

API ์˜ˆ์™ธ์ฒ˜๋ฆฌ

์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ฐ€ ์ค‘์š”ํ•œ ์ด์œ 
1. ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์—๋Ÿฌ๋ฅผ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ๋ชจ๋‘ ์ž˜ ์•Œ์ง€ ๋ชปํ•˜๋ฉด, ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ์— ๋Œ€ํ•ด ์ œ๋Œ€๋กœ ๋Œ€์‘ํ•  ์ˆ˜ ์—†์Œ
2. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋„ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ(AOP)๋ฅผ ํ†ตํ•ด ๋” ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์—๋Ÿฌ

HTTP ๋ฉ”์‹œ์ง€ ์˜ˆ์‹œ

Response ๋ฉ”์‹œ์ง€

  • start-line (์ƒํƒœ์ค„): API ์š”์ฒญ ๊ฒฐ๊ณผ (์ƒํƒœ ์ฝ”๋“œ, ์ƒํƒœ ํ…์ŠคํŠธ)
HTTP/1.1 404 Not Found
  • HTTP ์ƒํƒœ ์ฝ”๋“œ ์ข…๋ฅ˜
    • 2xx Success: 200๋ฒˆ๋Œ€์˜ ์ƒํƒœ์ฝ”๋“œ๋Š” ์„ฑ๊ณต ์˜๋ฏธ
    • 4xx Client Error: 400๋ฒˆ๋Œ€์˜ ์ƒํƒœ์ฝ”๋“œ๋Š” ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ, ์ฆ‰ ์ž˜๋ชป๋œ ์š”์ฒญ ์˜๋ฏธ
    • 5xx Server Error: 500๋ฒˆ๋Œ€์˜ ์ƒํƒœ์ฝ”๋“œ๋Š” ์„œ๋ฒ„ ์—๋Ÿฌ, ์ฆ‰ ์ •ํ™•ํ•œ ์š”์ฒญ์— ์„œ๋ฒ„์ชฝ ์‚ฌ์œ ๋กœ ์—๋Ÿฌ๊ฐ€ ๋‚œ ์ƒํ™ฉ

Spring์—์„œ๋Š” HttpStatus๋ผ๋Š” ํŽธ๋ฆฌํ•œ enum ์ œ๊ณตํ•˜๊ณ  ์žˆ์Œ

HttpStatus Enum

Spring ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•

  • ํด๋” ์ถ”๊ฐ€ API์— ํด๋” ์ค‘๋ณต ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ ์ƒํƒœ์ฝ”๋“œ 400๊ณผ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜
  • ์‘๋‹ต๋„ ๋” ์‹ ๊ฒฝ์จ์„œ return ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด, Spring์—์„œ ์ œ๊ณตํ•˜๋Š” ResponseEntity ํด๋ž˜์Šค ์‚ฌ์šฉ
ResponseEntity๋Š” HTTP response object๋ฅผ ์œ„ํ•œ Wrapper๋กœ, ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒƒ๋“ค์„ ๋‹ด์•„์„œ response๋กœ ๋‚ด๋ ค์ฃผ๋ฉด ๊ฐ„ํŽธํ•˜๊ฒŒ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
- HTTP status code
- HTTP headers
- HTTP body
  • Spring์—์„œ๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ AOP ๊ธฐ๋Šฅ์ด ๋™์ž‘ํ•˜๊ณ  ์žˆ๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ• ์• ๋„ˆํ…Œ์ด์…˜์„ ์ œ๊ณตํ•ด์คŒ → ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜ ์ œ๊ณต
  • ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜์€ Controller์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ ์žก์•„์ฑ”
  • ๋˜ํ•œ, ๋ชจ๋“  ๋ฉ”์„œ๋“œ์— try-catch๋ฌธ์œผ๋กœ ์˜ˆ์™ธ์ฒ˜๋ฆฌํ•  ํ•„์š” ์—†์Œ

1. Controller ์ฝ”๋“œ ์ˆ˜์ •

  • RestApiException ํด๋ž˜์Šค ์ƒ์„ฑ
package com.sparta.myselectshop.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class RestApiException {
    private String errorMessage;
    private int statusCode;
}
  • FolderController์— ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜ ์ถ”๊ฐ€
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class FolderController {

    private final FolderService folderService;

    @PostMapping("/folders")
    public void addFolders(@RequestBody FolderRequestDto folderRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        List<String> folderNames = folderRequestDto.getFolderNames();

        folderService.addFolders(folderNames, userDetails.getUser());
    }

    @GetMapping("/folders")
    public List<FolderResponseDto> getFolders(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        return folderService.getFolders(userDetails.getUser());
    }

    @ExceptionHandler({IllegalArgumentException.class}) // Controller์—์„œ IllegalArgumentException์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ˆ˜ํ–‰
    public ResponseEntity<RestApiException> handleException(IllegalArgumentException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.BAD_REQUEST
        );
    }
}

์˜ˆ์™ธ์ฒ˜๋ฆฌ ์ ์šฉ ์˜ˆ์‹œ


Spring์˜ Global ์˜ˆ์™ธ์ฒ˜๋ฆฌ

Spring์—์„œ ์ „์ฒด Controller์— ExceptionHandler๋ฅผ ๊ฐ๊ฐ ๋งŒ๋“ค์–ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Œ

// exception > GlobalExceptionHandler.java

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({IllegalArgumentException.class})
    public ResponseEntity<RestApiException> handleException(IllegalArgumentException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.BAD_REQUEST
        );
    }
}
  • @RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์ด ๊ณตํ†ต์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๊ธฐ ์œ„ํ•œ ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ์• ๋„ˆํ…Œ์ด์…˜์ž„
  • ํ•ด๋‹น ์• ๋„ˆํ…Œ์ด์…˜์„ ๋‹ฌ๋ฉด, ๋ชจ๋“  Controller์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋“ค์„ ์ „๋ถ€ ์žก์•„์˜ด

@ControllerAdvice

  • @ControllerAdvice๋Š” ํด๋ž˜์Šค ๋ ˆ๋ฒจ ๋‹จ์œ„์˜ ์• ๋„ˆํ…Œ์ด์…˜์ž„


Error ๋ฉ”์‹œ์ง€ ๊ด€๋ฆฌํ•˜๊ธฐ

Spring์˜ Properties ํŒŒ์ผ์„ ์ด์šฉํ•ด์„œ Error ๋ฉ”์‹œ์ง€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

  • key=value ํ˜•์‹์œผ๋กœ ์›ํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋„ฃ์–ด๋†“๊ณ  ์‚ฌ์šฉํ•˜๋ฉด๋จ

1. resources > messages.properties ์ถ”๊ฐ€

below.min.my.price=์ตœ์ € ํฌ๋ง๊ฐ€๋Š” ์ตœ์†Œ {0}์› ์ด์ƒ์œผ๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”.
not.found.product=ํ•ด๋‹น ์ƒํ’ˆ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

2. ProductService ๋‚ด์— ์ ์šฉ

  • messages.properties์— ์žˆ๋Š” ๊ฐ’์€ messageSource ํด๋ž˜์Šค ์‚ฌ์šฉํ•ด์„œ ์ ์šฉ ๊ฐ€๋Šฅ
private final MessageSource messageSource; // Bean ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉ
@Service
@RequiredArgsConstructor
public class ProductService {

    ...
    private final MessageSource messageSource;

    public static final int MIN_MY_PRICE = 100;

	...

    @Transactional // Dirty Checking ์ˆ˜ํ–‰
    public ProductResponseDto updateProduct(Long id, ProductMypriceRequestDto requestDto) {
        int myprice = requestDto.getMyprice(); // myprice ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ(์กฐ๊ฑด: myprice๊ฐ€ 100์› ์ด์ƒ์ด์–ด์•ผํ•จ)

        if(myprice < MIN_MY_PRICE) {
            throw new IllegalArgumentException(
                    messageSource.getMessage(
                           "below.min.my.price", // key ๋ถ€๋ถ„
                            new Integer[]{MIN_MY_PRICE}, // value์ธ ๋ฉ”์‹œ์ง€ ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ์ „๋‹ฌํ•  ๊ฐ’
                            "Wrong Price", // defaultMessage๋กœ code ๋ถ€๋ถ„์„ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ๋จ
                            Locale.getDefault() // ์–ธ์–ด ์„ค์ •
                    )
            );
        }

        ...

        return new ProductResponseDto(product);
    }

   	...
}
  • IllegalArgumentException์ด๋‚˜ NullPointException๊ณผ ๊ฐ™์ด ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด์ ธ ์žˆ๋Š” RuntimeException ๋ง๊ณ ๋„ Custom ๊ฐ€๋Šฅํ•จ
    - Exception ํด๋ž˜์Šค ์ƒ์†๋ฐ›์•„ Custom
package com.sparta.myselectshop.exception;

public class ProductNotFoundException extends RuntimeException{

    public ProductNotFoundException(String message) {
        super(message);
    }
}
@Transactional // Dirty Checking ์ˆ˜ํ–‰
public ProductResponseDto updateProduct(Long id, ProductMypriceRequestDto requestDto) {
    int myprice = requestDto.getMyprice(); // myprice ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ(์กฐ๊ฑด: myprice๊ฐ€ 100์› ์ด์ƒ์ด์–ด์•ผํ•จ)

    if(myprice < MIN_MY_PRICE) {
        throw new IllegalArgumentException(
                messageSource.getMessage(
                       "below.min.my.price", // key ๋ถ€๋ถ„
                        new Integer[]{MIN_MY_PRICE}, // value์ธ ๋ฉ”์‹œ์ง€ ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ์ „๋‹ฌํ•  ๊ฐ’
                        "Wrong Price", // defaultMessage๋กœ code ๋ถ€๋ถ„์„ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ๋จ
                        Locale.getDefault() // ์–ธ์–ด ์„ค์ •(๋ฒˆ์—ญ)
                )
        );
    }

    // ํ•ด๋‹น ์ƒํ’ˆ์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
    Product product = productRepository.findById(id).orElseThrow(() ->
            new ProductNotFoundException(messageSource.getMessage(
                    "not.found.product",
                    null,
                    "Not Found Product",
                    Locale.getDefault()
            )));

    product.update(requestDto); // ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ˆ˜ํ–‰

    return new ProductResponseDto(product);
}

 

3. GlobalExceptionHandler์— ์ถ”๊ฐ€

package com.sparta.myselectshop.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({IllegalArgumentException.class})
    public ResponseEntity<RestApiException> illegalArgumentExceptionHandler(IllegalArgumentException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.BAD_REQUEST
        );
    }

    @ExceptionHandler({NullPointerException.class})
    public ResponseEntity<RestApiException> nullPointerExceptionHandler(NullPointerException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.NOT_FOUND.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.NOT_FOUND
        );
    }

    @ExceptionHandler({ProductNotFoundException.class})
    public ResponseEntity<RestApiException> notFoundProductExceptionHandler(ProductNotFoundException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.NOT_FOUND.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.NOT_FOUND
        );
    }
}

์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ