API ์์ธ์ฒ๋ฆฌ
์์ธ์ฒ๋ฆฌ๊ฐ ์ค์ํ ์ด์
1. ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋ฌ๋ฅผ ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ๋ชจ๋ ์ ์์ง ๋ชปํ๋ฉด, ์๋น์ค ํ๊ฒฝ์์ ๋ฐ์ํ๋ ์๋ฌ์ ๋ํด ์ ๋๋ก ๋์ํ ์ ์์
2. ์๋ฌ ์ฒ๋ฆฌ๋ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ(AOP)๋ฅผ ํตํด ๋ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌ ๊ฐ๋ฅ
์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋ฌ

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 ์ ๊ณตํ๊ณ ์์

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๋ ํด๋์ค ๋ ๋ฒจ ๋จ์์ ์ ๋ํ ์ด์ ์

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

