0. ํ์์ฑ
์ ๋ฒ์ ํ๋ 6์ธ ํ๋ก์ ํธ์ ๋ฐํ ํ๊ฒฝ ๋ชจ์ต์ด๋ค. ๋น์ ResultResponse
๋ผ๋ ํ ๋ด์ ๊ณตํต ์๋ต ๊ท์น์ ์ ํ๊ณ , ResponseEntity
๋ก ๋ฐํํ๊ธฐ ์ ์ DTO๋ฅผ ResultResponse
๋ก ํ ๋ฒ ๋ ๊ฐ์ธ์ ๋ฐํํ๋ค.
๊ทธ ๋น์์๋ ํฐ ๋ถํธํจ ์์ด ํ๋ ๊ฒ ๊ฐ์ง๋ง, ๋ค์ ๋ณด๋ 2๊ฐ์ง ๋ถํธ ์ฌํญ์ด ์์๋ค.
- 1๏ธโฃ ๋งค์๋์ ๋ฐํ ํ์
์ ํญ์
ResponseEntity
์ResultResponse
๋ก ๊ฐ์ธ์ค์ผ ํ๋ค. ์ค์ ํ์๊ฐ ํ๋ก ํธ ๊ฐ๋ฐ์์๊ฒ ์ค์ผํ JSON ๋ฐํ ๊ฐ์ฒด๋TokenDTO
์ธ๋ฐ, ๊ทธ์ ๋ฐํ ๊ท์น์ ์ํด์ ํ๋ก์ธ์ค์์๋ ๋ถํ์ํ ๊ฐ์ฒด๋ฅผ ์ถ๊ฐํด์ผ๋ง ํ๋ค. - 2๏ธโฃ ๊ฐ์ ์ด์ ๋ก ๋ฐํ๊ฐ ๋ํ ๋ ๊ฐ์ฒด๋ฅผ ํ์ฉํด ๊ฐ์ธ์ค์ผ ํ๋ค.
์ด๋ 3์ธ์ ๋ฐฑ์๋ ๊ฐ๋ฐ์ ์ค ํ ๋ช ์ด๋ผ๋ ์ค์๋ก ResultResponse๋ฅผ ํ์ฉํ์ง ์์ผ๋ฉด ๊ณตํต ์๋ต์์ ๋ฒ์๋๋ ํด๋จผ ์๋ฌ๋ฅผ ์ผ์ผ์ผฐ๋ค.
๊ทธ๋์ ์ด๋ฒ ๋ชฉํ๋ ํ์๋ค์ด ์ ๊ฒฝ ์ฐ์ง ์๊ณ , ๋ฐํํด์ผํ ๊ฐ
์์ฒด๋ฅผ ๋ฆฌํดํ๋ฉด ์๋์ผ๋ก ๊ณตํต ๋ฐํ ๋ก์ง์ ๊ฐ์ธ์ง๋๋ก ๋ง๋ค์ด๋ณด๋ ค ํ๋ค.
1. ๊ณํ
Spring์ ์์ฒญ์ด ๋ค์ด์์ ์ฒ๋ฆฌ๋๋ ์ ๋ฐ์ ๊ณผ์ ์ ๊ธฐ๋กํด๋ณด์๋ค. ์ฌ๊ธฐ์ ์ ์ฒ๋ฆฌ
ํน์ ํ์ฒ๋ฆฌ
๊ณผ์ ์ด ๊ฐ๋ฅํ ๊ณณ์ ํ์๊ฐ ์๊ณ ์๋ ํ์์๋ 3๊ณณ์ด๋ค.
- 1๏ธโฃ
์๋ธ๋ฆฟ ํํฐ ๋ ๋ฒจ
: ์๋ธ๋ฆฟ ํํฐ ๋ ๋ฒจ์์ ์ฌ์ฉ์์ ์๋ต์ ๊ฐ๋ก์ฑ์ ๊ณตํต ์๋ต ํ์์ผ๋ก ๋ณํ ์ฒ๋ฆฌ ํ์ ๋ค์ ๋๋ ค์ฃผ๊ธฐ - 2๏ธโฃ
Interceptor ๋ ๋ฒจ
: postHandle()์ ํ์ฉํด, Controller์์ ๋ฐ์ ๋ฐํ๊ฐ์ ๊ณตํต ์๋ต ํ์์ผ๋ก ๋ณํ ์ฒ๋ฆฌ ํ Handler Adapter์๊ฒ ๋๋ ค์ฃผ๊ธฐ - 3๏ธโฃ
AOP ๋ ๋ฒจ
: AOP์@AfterReturning
๋ก์ง์ ํ์ฉํ์ฌ ์ปจํธ๋กค๋ฌ ๋ ์ด์ด์์ ๋ฐํ ๋ฐ์ ๊ฐ์ ๋ํด ๊ณตํต ์๋ต ํ์ ๋ณํ ์์ ์ ํด์ฃผ๊ธฐ
๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด ์์ 3๊ฐ์ง ๊ณผ์ ์ ๋นํจ์จ์ ์ด๊ฑฐ๋ ๋ถ๊ฐ๋ฅํด์ ํ์ฉํ์ง ์์๋ค. ์ดํ ์๋ ์ ๋ฐฐ์ ๊ด๋ จ ๋ธ๋ก๊ทธ ๊ธ๊ณผ GPT๋ฅผ ํตํด @ResponseBodyAdvice
๋ผ๋ ์ด๋
ธํ
์ด์
์ ์กด์ฌ์ ์ฌ์ฉ๋ฒ์ ์๊ฒ๋์๊ณ , ์ด๋ฅผ ํ์ฉํ๋ค.
์ด๋ฒ ๊ธ์์๋ (1) ์ 3๊ฐ์ง ๊ณผ์ ์ด ์ ๋นํจ์จ์ ์ด๊ฑฐ๋ ๋ถ๊ฐ๋ฅํ์ง ์์๋ณด๊ณ , (2) @ResponseBodyAdvice
์ ํ์ฉ๋ฒ๊ณผ ํจ์จ์ฑ์ ๋งํด๋ณด๊ฒ ๋ค.
2. ์๋ (1) Servlet Level
public class FilterResponse {
private final ObjectMapper mapper = new ObjectMapper();
// Filter ์ ์ ์ข
๋ฃ์ ๋ฐ๋ฅธ Response ์ค์
public <T> HttpServletResponse ok(HttpServletResponse response, T data)
throws IOException {
HashMap<String, Object> responseBody = new HashMap<>();
responseBody.put("status", "success");
responseBody.put("message", "์ฑ๊ณตํ์์ต๋๋ค.");
responseBody.put("data", data);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(mapper.writeValueAsString(responseBody));
return response;
}
public void error(HttpServletResponse response, ErrorCode errorCode) throws IOException {
HashMap<String, Object> responseBody = new HashMap<>();
responseBody.put("status", "fail");
responseBody.put("message", errorCode.getMessage());
responseBody.put("data", null);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(mapper.writeValueAsString(responseBody));
response.setStatus(errorCode.getStatus().value());
}
public void error(HttpServletResponse response, String msg) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, msg);
}
}
์๋ธ๋ฆฟ ๋ ๋ฒจ์์ ๊ณตํต ์๋ต์ ๋ณํํด์ฃผ๊ธฐ ์ํด์ ์์ ๊ฐ์ด ๋งค์๋๋ฅผ ์์ฑํ๋ค.
@RequestMapping("/api")
public class UserController {
private final FilterResponse filterResponse = new FilterResponse();
@GetMapping("/user")
public void getUser(HttpServletResponse response) throws IOException {
User user = new User("John Doe", "johndoe@example.com");
filterResponse.ok(response, user);
}
}
๋ชจ๋ ์๋ต ๊ฐ์ ํด๋น ๊ณตํต ์๋ต์ผ๋ก ๋ณํํ๊ธฐ ์ํด์๋ ์ปจํธ๋กค๋ฌ ๋ ์ด์ด์์ ๋ฐํ๊ฐ์ ๋ฐ๊พธ๋ ํํฐ ๋งค์๋๋ฅผ ๊ผญ ํธ์ถํด์ผ ํ๋ค. ๋ฐฐ๋ณด๋ค ๋ฐฐ๊ผฝ์ด ๋ ์ปค์ง ๊ฒฝ์ฐ๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ฐํ ํ์ ์ ๋ํ ์ ๊ฒฝ์ ์จ์ค ํ์๊ฐ ์์ง๋ง, Error ์์๋ Error Filter Method, ์ฑ๊ณต ์์๋ Success Filter Method๋ฅผ ์ฌ์ฉํด์ผ ํด์ ๋ฒ๊ฑฐ๋ก์์ ๋ณํ์ง ์์๋ค.
์ฌ์ค ์์ ๊ฐ์ ๋ฐฉ๋ฒ์ ์ฌ์ฉ์์ ์์ฒญ์ด ๊ตณ์ด Spring MVC๊น์ง ๊ฐ ํ์๊ฐ ์์ ๋ ์์ฃผ ํ์ฉํ๋ ๋ฐฉ์์ด๋ค. ์๋ฅผ ๋ค๊ธฐ ์ํด, ์ ๋ฒ ํ๋ก์ ํธ์์ ์์ฑํ๋ ํ ํฐ ์ ํจ์ฑ ์ฒดํฌ ๋ก์ง์ ํ ๋ฒ ๋ณด๊ฒ ๋ค.
// ํ ํฐ ์ ํจ์ฑ ์ฒดํฌ -> ํต๊ณผ ๋ชปํ๋ฉด ๋ฐ๋ก ๋ค์ filter ๋ก ๋์ด๊ฐ๋ค.
switch (jwtUtil.validateToken(token)) {
case -1:
request.setAttribute(ConstantUtil.EXCEPTION_ATTRIBUTE, ErrorCode.EXPIRED_TOKEN);
filterChain.doFilter(request, response);
return;
case -2:
case -3:
case -4:
request.setAttribute(ConstantUtil.EXCEPTION_ATTRIBUTE, ErrorCode.INVALID_TOKEN);
filterChain.doFilter(request, response);
return;
case -5:
filterResponse.error(response, ErrorCode.ALREADY_LOGOUT);
return;
}
ํด๋น ๋ก์ง์ AuthFilter
์์ ์๋ ๋ก์ง์ผ๋ก case -5:
์ ๊ฐ์ด ์ด๋ฏธ ๋ก๊ทธ์์ํ ์ ์ ์ ํ ํฐ์ด๋ฉด, ๊ทธ ์ดํ ๊ณผ์ ์ ๊ฑฐ์น์ง ์๊ณ ๋ฐ๋ก Return ํ๋๋ก ๋ฐ๊พธ์๋ค. ์ด๋ ๋ฏ Spring MVC ๋ด๋ถ๋ฅผ ๋ค์ฌ๋ค๋ณด์ง ์๊ณ ํํฐ์์ ์๋ต ๋ฐํ์ด ๊ฐ๋ฅํ ์์ฒญ์์๋ ์ฐ์ด๋ ๊ณตํต ์๋ต ๋ฐํ์ ์ํด์๋ ๋ฐ๋์งํ์ง ์๋ค.
3. ์๋ (2) Interceptor Level
์ธํฐ์
ํฐ๋ preHandle()
๊ณผ postHandle()
์ด๋ผ๋ ์ ,ํ์ฒ๋ฆฌ์ฉ ํจ์๋ฅผ ๊ฐ์ง๊ณ ์๋ค. ์ด๋ ์ด๊ฒ์ ๋จผ์ ํ์ฉํด ๊ณตํต ์๋ต ํจ์๋ฅผ ์์ฑํ ์ ๋ฐฐ๊ฐ ์์ด ํด๋น ๋ธ๋ก๊ทธ๋ฅผ ์ฐธ๊ณ ํ๋ค.
๊ฒฐ๋ก ์ ์ด ๋ฐฉ๋ฒ์ ๋ถ๊ฐ๋ฅ ํ๋ค. ์ด์ ๋ Dispatcher Servlet์ด ์ปจํธ๋กค๋ฌ์ ์๋ต๊ฐ ์์ฒด๋ฅผ JSON์ผ๋ก ๋ณํ ํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ฐ๋ผ์ ์ธํฐ์ ํฐ์์ ๋ฐํ ๊ฐ ์์ฒด๋ฅผ ๋ณํํด๋ ์๋ต JSON์ ์ํฅ์ ์ฃผ์ง ๋ชปํ๋ค.
4. ์๋ (3) AOP ํ์ฉ
Spring AOP์์๋ ํ์ฒ๋ฆฌ๋ฅผ ์ํ ์ด๋
ธํ
์ด์
์ด ์กด์ฌํ๋ค. @After
ํน์ @AfterReturning
์ด๋ค. @AfterReturning
์ ๊ฒฝ์ฐ, ๋ฐํ ์งํ ํ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํด์ ๊ณตํต ์๋ต ๋งค์๋ ํ์ฉ์ด ๊ฐ๋ฅํ ๋ฏ ํ๋ค.
ํ์ง๋ง ๊ฒฐ๋ก ์ AOP ๋ํ ๊ณตํต ์๋ต ํฌ๋งท ๋ณํ์ด ๋ถ๊ฐ๋ฅ ํ๋ค. ์ด์ ๋ Interceptor Level๊ณผ ๋๊ฐ์ด Dispatcher Servlet์ Controller์์ ๋ฐํํ ๊ฐ ์์ฒด๋ง ํน์ ํด์ HttpMessageConverter๋ก ๋ณํํ ์ ์๊ณ , AOP๊ฐ ํ ๋ฒ ๋ณํํ ๋ด์ฉ์ ํ์ธํ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ฐ๋ผ์ ํ์๊ฐ ์๊ฐํ์ ๋ ๋ค์๊ณผ ๊ฐ์ด ์ ๋ฆฌํ ์ ์์ ๊ฒ ๊ฐ๋ค.
์ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด Controller๊ฐ ๊ฐ์ ๋ฐํํ ์๊ฐ ํด๋น ๊ฐ์ Dispatcher Servlet
์๊ฒ ์งํํ๋ค. Interceptor
์ AOP
๊ฐ ํ ์ ์๋ ํ์ฒ๋ฆฌ๋ ์ฌ์ฉ์์๊ฒ ์ค ์๋ต์ ์ํฅ์ ์ฃผ๋ ํ์ฒ๋ฆฌ๊ฐ ์๋๋ผ, ๋ฐํ ๊ฐ์ ๋ฐ๋ผ ๋ฐฑ์๋ Level์ ๊ฐ๋ค์๊ฒ ์ํฅ์ ์ฃผ๋ ํ์ฒ๋ฆฌ์ธ ๊ฒ์ด๋ค.
5. ์ฐพ์ ๋ฐฉ๋ฒ
3๊ฐ์ง ์คํจ๋ก ๋๋ดํ๋ ์ค ์ ๋ฐฐ์ ๋ธ๋ก๊ทธ์ GPT๋ฅผ ํตํด @ResponseBodyAdvice
๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ์๊ฒ ๋์๋ค. ์ฒ์์ ์ด๋ฆ๋ง ๋ฃ๊ณ AOP์ ์ผ์ข
์ด๋ผ ์๊ฐํ์ง๋ง ๊ทธ๊ฒ์ ์๋์๊ณ , ๊ทธ์ ํน์ํ ์ด๋
ธํ
์ด์
์ด์๋ค.
๊ทธ๋ ๋ค๋ฉด ์ @ResponseBodyAdvice
๋ ๊ณตํต์๋ต์ ๋ฐ๊ฟ ์ ์์๊น?
ํ์ฒ๋ฆฌ๋ฅผ ์ํด ํ์ด๋ Interceptor
์ AOP
๋ ๋ชปํ ์์ญ์ด๋ค. ๊ทธ๊ฒ์ด ๊ฐ๋ฅํ ์ด์ ๋ฅผ ์ค๋ช
ํ๊ฒ ๋ค.
@ResponseBodyAdvice
๊ฐ ์คํ๋๋ ์์ ์ ํ๋์ ์ฒดํฌ๋ก ํ์ํ HtppMessageConverter
์ ์์ญ์ด๋ค. ์ฆ HttpMessageConverter๊ฐ ์ปจํธ๋กค๋ฌ์ ๋งค์๋ ๋ฐํ ๊ฐ์ JSON์ผ๋ก ๋ณํํ๊ธฐ ์ ์ ์คํ๋์ด ๋งค์๋ ๋ฐํ๊ฐ์ ์ง์ ๊ฐ์
ํ๋ค. ์ด๊ฒ์ด ๊ฐ๋ฅํ ์ด์ ๋ DispatcherServlet์๊ฒ ์ผ์ ์์๋ฐ์ Handler Adapter
๊ฐ HttpMessageConverter
๋ฅผ ํ์ฉํด ๊ฐ์ ๋ฐ๊พธ๊ธฐ ์ ์, @ResponseBodyAdvice ๊ตฌํ์ฒด๋ฅผ ๋ค์ฌ๋ค๋ณด๊ณ , ํด๋น ์ปจํธ๋กค๋ฌ์ ๋ฐํ๊ฐ์ ํฌ๋งท ์์ ์ด ํ์ํ์ง๋ฅผ ํ์ธํ๋ค. ๋ง์ฝ ํ์ ๋ณํ์ด ํ์ํ ๋ฐํ ๊ฐ์ด๋ผ๋ฉด, ๊ตฌํ์ฒด์ ์ ํ ๋ช
์ธ๋๋ก ํ์์ ์์ ํ ๋ค JSON์ผ๋ก ๋ณํํ๋ค. ๊ทธ๋ ๋ค๋ฉด ์ด์ ํ๋์ฉ ์ฝ๋๋ฅผ ๋ฏ์ด๋ณด๋ฉด์ ํ์ธํด๋ณด๊ฒ ๋ค.
(1) ํด๋ ๊ตฌ์กฐ
- 1๏ธโฃ
GlobalResponseHandler
: @ResponseBodyAdvice ๊ตฌํ์ฒด - 2๏ธโฃ
ResultResponse
: ํ์์ ์ ํ ๊ณตํต ์๋ต ๋ก์ง - 3๏ธโฃ
ErrorCode
: Enum์ผ๋ก ์ด๋ฃจ์ด์ง ์ปค์คํ ์๋ฌ์ ๋ด์ฉ๋ฌผ - 4๏ธโฃ
GlobalException
: ํ์์ ์ฌ์ฉํ ์ปค์คํ ์๋ฌ, ์ปค์คํ ์๋ฌ๋ ๋ฑ ํ๋๋ง ๋๊ณ , ๊ทธ ๋ด์ฉ๋ฌผ์ ErrorCode๋ก ๋ฐ๊พธ๋ฉฐ ์ฌ์ฉํ ์์ - 5๏ธโฃ
Global Exception Handler
: ์ปค์คํ ์๋ฌ๊ฐ ์ปจํธ๋กค๋ฌ ๋ ์ด์ด ์ดํ ๋ ์ด์ด์์ ๋ฐ์ํ์์ ์ ์ด๋ป๊ฒ ์๋ต์ ๋์ง ๊ฑด์ง๋ฅผ ๊ตฌํํ ๋ก์ง
(2) ResultResponse
@Builder
@Getter
@AllArgsConstructor
public class ResultResponse<T> { // ์ ๋ค๋ฆญ ํ์
์ ํด๋์ค ์ ์ฒด์์ ์ฌ์ฉํ ๊ฑด๋ฐ, ์ ๋ค๋ฆญ ํ์
์ T๋ผ๊ณ ๋ช
๋ช
ํ๊ฒ ๋ค.
private String status;
private String message;
private T data;
// static ํจ์๋ ์ธ์คํด์ค ์์ด ํธ์ถ๋๋ฏ๋ก, ํด๋์ค์ ์ ๋ค๋ฆญ ํ์
์ด ๋ญ์ง ํ์ธ ๋ถ๊ฐ, ๋ฐ๋ผ์ ๋ฉ์๋ ๋ด๋ถ์์๋ง ์ฌ์ฉํ ์ ๋ค๋ฆญ ํ์
์ ์ ์ธ (์ ๋ถ T๋ผ๊ณ ํ์ง๋ง, ๋ ์๋ฏธ๋ ๊ฐ์ง๋ง ๋ช
๋ช
ํ๋ ๋ฐฉ๋ฒ์ ๋ฌ๋ฆฌ ํด๋ ๋จ.)
// ex - public static<U> ResultResponse<U> success (T data)๋ผ๊ณ ์์ฑ๊ฐ๋ฅ
public static<T> ResultResponse<T> success (T data) {
return new ResultResponse<>("success", "์ ์์ ์ผ๋ก ์ฒ๋ฆฌํ์์ต๋๋ค.", data);
}
public static <T> ResultResponse<T> fail (String message) {
return new ResultResponse<>("fail", message, null);
}
}
(3) GlobalResponseHeader
@RestControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
// ํด๋น Advice ์ ์ฉ ๋ฒ์
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
// ์๋ต ๋ณํ ๋งค์๋
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
// ๋ง์ฝ ๋ฐํ ํ์
์ด void์ด๋ฉด data ์์ด ์๋ต
if (Void.TYPE.equals(returnType.getParameterType())) {
return new ResultResponse<>("success", "์ฒ๋ฆฌ๊ฐ ์๋ฃ๋์์ต๋๋ค.", null);
}
// ๋ง์ฝ String์ด๋ฉด ์์ธ ๋ฐ์
if (body instanceof String) {
throw new GlobalException(ErrorCode.NOT_ALLOW_STRING);
}
if(body instanceof ResultResponse){
return body;
}
return ResultResponse.success(body);
}
}
์๋ ์ด๋ฒ ๊ตฌํ์ ํต์ฌ์ธ @ResponseBodyAdvice
์ด๋ค. ํ๋์ฉ ์์์๋ถํฐ ์ดํด ๋ณด์.
1๏ธโฃ @ResponseBodyAdvice
์ด๋
ธํ
์ด์
์ ๋ป
ResponseBodyAdvice ์ธํฐํ์ด์ค๋ฅผ ๋ฐ์์ ๊ตฌํ ํ๋ค๊ณ ํด์, ์๋ ๋ฑ๋ก์ด ๋์ง๋ ์๋๋ค. ๋ฐ๋ผ์ Bean
๋ฑ๋ก์ ์ํด ํด๋น ์ด๋
ธํ
์ด์
์ ์ ์ฅํด๋์ด์ผ ํ๋ค. Bean ๋ฑ๋ก์ ํด๋์ด์ผ์ง ์ฑ๊ธํค ํจํด์ด ์ ์ฉ๋์ด, ๋ชจ๋ ์ค๋ ๋์์ ํ๋์ ResponseBodyAdvice ๊ฐ์ฒด๋ง์ ๊ณต์ ํ ์ ์๋ค. ์ด๋ฅผ ํ์ง ์์ผ๋ฉด ๋งค ์์ฒญ๋ง๋ค new GlobalResponseHandler ()
๋ฅผ ์ฐ๋ฉด์ ๋ถ๋ฌ์์ผ ํ๋ค.
2๏ธโฃ supports
๋งค์๋
// ํด๋น Advice ์ ์ฉ ๋ฒ์
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
ํด๋น ๋งค์๋๋ '์ด ๋ดํฌ ํจํด ๋์ ์ ์ด๋ ์ปจํธ๋กค๋ฌ๊น์ง ์ ์ฉํ ๊ฒ์ธ๊ฐ' ๋ฒ์๋ฅผ ์ ํ๋ ํจ์์ด๋ค. ํ์๋ ๋ชจ๋ ๋ฒ์์ ์ ์ฉํ๊ธฐ ์ํด true๋ฅผ ๋ฐํํ๋ค.
3๏ธโฃ beforeBodyWrite
๋งค์๋
// ์๋ต ๋ณํ ๋งค์๋
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
// ๋ง์ฝ ๋ฐํ ํ์
์ด void์ด๋ฉด data ์์ด ์๋ต
if (Void.TYPE.equals(returnType.getParameterType())) {
return new ResultResponse<>("success", "์ฒ๋ฆฌ๊ฐ ์๋ฃ๋์์ต๋๋ค.", null);
}
// ๋ง์ฝ String์ด๋ฉด ์์ธ ๋ฐ์
if (body instanceof String) {
throw new GlobalException(ErrorCode.NOT_ALLOW_STRING);
}
if(body instanceof ResultResponse){
return body;
}
return ResultResponse.success(body);
}
์ ๋งค์๋๋ ์ปจํธ๋กค๋ฌ ๋ฐํ ๊ฐ์ ๋ํ์ฌ ์๋ต ํ์ ๋ณํ์ ์ด๋ป๊ฒ ์ ์ฉํ ๊ฒ์ธ๊ฐ๋ฅผ ๋ํ๋ด๋ ๋งค์๋์ด๋ค. ์ฌ๊ธฐ์ ์์ธ์ฒ๋ฆฌ ํด์ค ๋ถ๋ถ์ ๋ ๋ถ๋ถ์ด๋ค.
a. ๋ฐํ ํ์
์ด void์ด๋ฉด ResultResponse(T data)์ ๋ค์ด๊ฐ data
๋ผ๋ ์ธ์๊ฐ ์กด์ฌํ์ง ์๊ฒ ๋๋ค. ์ด๋ NullPointerException์ด ๋ ์ ์์์ผ๋ก, type์ด void์ธ ๊ฒฝ์ฐ, ๋ชจ๋ ์ธ์๋ฅผ ๊ฐ์ง ์์ฑ์๋ฅผ ๋ถ๋ฌ์ ์ฒ๋ฆฌํ๋ค.
b. ๋ฐํ ํ์
์ด String ์ผ ๋, ๋ฐ๋ก ์ฒ๋ฆฌํด์ฃผ์ง ์๊ณ , ResultResponse.success(T body)
๋ฅผ ์ฐ๋ฉด CastException์ด ๋๋ค. ์ด์ ๋ฅผ ์์๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค. String ๋ฐํ ํ์
์ ๊ฐ์ง Controller์์ String์ ๋ฐํํ๋ ์์๋ฅผ ๋ค์ด๋ณด์!
a. Controller๊ฐ ๊ฐ์ ๋ฐํํ๋ ์์ ์ ์ด๋ฏธ HttpMessageConverter ๊ตฌํ์ฒด ์ค์์ ๋ญ ์ธ์ง ๊ณ ๋ฅธ๋ค.
b. ์ด์ ์ปจํธ๋กค๋ฌ ๋ ๋ฒจ์์ ๊ณ ๋ฅธ Conveter๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ๊ฐ๊ธฐ ์ง์ ์ @ResponseBodyAdvice
๊ฐ ๊ฐ์ ๊ฐ๋ก์ฑ์ String
โ ResultResponse ๊ฐ์ฒด
๋ก ๋ณํํ๋ค.
c. ์ด์ StringHttpMessageConverter
๋ฅผ ๋ง๋๊ฒ ๋๋ฉด Type Cast ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ์๋๋ฉด ์ธ์๋ก ๋ค์ด์จ ๊ฐ์ ๊ฐ์ฒด์ด๊ธฐ ๋๋ฌธ์ด๋ค. ๊ฐ์ฒด๋ฅผ JSON ์ง๋ ฌํ ํ๊ธฐ ์ํด์๋ MappingJackson2HttpMessageConverter
๊ฐ ํ์ํ๋ฐ, ํ์ฌ ์ฌ์ฉ ์ค์ธ ๋งค์๋๋ String ์ง๋ ฌํ๋ง ๊ฐ๋ฅํ converter์ด๊ธฐ ๋๋ฌธ์ด๋ค. ๋ฐ๋ผ์ String ๋ฐํ์ ๋ง๊ณ , ๊ฐ์ฒด๋ก ๋ต์ ์ถ๋ ฅํ๋๋ก ์ ๋ ํด์ผ ํ๋ค.
(4) ErrorCode
@Getter
@AllArgsConstructor
public enum ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "ํด๋น ๋ฉค๋ฒ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค."),
NOT_ALLOW_STRING(HttpStatus.INTERNAL_SERVER_ERROR, "๋ฐฑ์๋ ๋ด๋น์๊ฐ String์ผ๋ก ๋ฐํ์ ์ค์ ํ์ต๋๋ค. String ๋ฐํ์ ํ์ฉ๋์ง ์์ต๋๋ค. ๋ด๋น์์๊ฒ ๋ฌธ์ํ์ธ์!")
;
private final HttpStatus status;
private final String message;
// global (๊ณตํต)
}
ํด๋น Enum Class๋ ์๋น์ค ๋ด์์ ํ์ฉํ ์ ์ผํ ์ปค์คํ
์๋ฌ์ธ Global Exception
์ ๋ด์ฉ๋ฌผ๋ก ์ฌ์ฉํ Enum์ด๋ค. Enum์ ๊ตฌ์ฑ์์๋ status
์ message
๊ฐ ๋๊ฒ ๋ค.
(5) Global Exception
@Getter
public class GlobalException extends RuntimeException{
private final ErrorCode errorCode;
public GlobalException(ErrorCode errorCode) { this.errorCode = errorCode; }
}
๋ค์์ ์ด๋ฒ ํ๋ก์ ํธ์์ ์ฐ์ด๋ ์ ์ผํ Custom Exception์ด๋ค. ๋ชจ๋ ์๋ฌ๋ง๋ค Custom Exception์ ๋ง๋ค์ด์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ๋ ์๊ฒ ์ผ๋, ๊ทธ๋ ๊ฒ ํด๋ ์ค๋ฅ ๋ด์ฉ๋ฌผ๋ง ๋ฐ๋๋ ์
์ฅ์์ ๊ตณ์ด ํ์ํ ์ ์ฐจ์ธ์ง ๋ชจ๋ฅด๊ฒ ๋ค. ๊ทธ๋์ Custom Exception์ ํ๋๋ง ํ๊ณ , ๊ทธ๊ฒ์ ๋ด์ฉ๋ฌผ์ ErrorCode
๋ก ๋ฐ๊ฟ๊ฐ๋ฉฐ ์ฌ์ํ๊ธฐ๋ก ํ๋ค.
(6) Global Exception Handler
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import spot.spot.global.dto.response.ResultResponse;
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private static final HttpHeaders jsonHeaders;
static {
jsonHeaders = new HttpHeaders();
jsonHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
}
// ์ฌ์ฉ์๊ฐ ์์ธก ๊ฐ๋ฅํ ์๋ฌ ๋ฐ์ ์
@ExceptionHandler(GlobalException.class)
public ResponseEntity<ResultResponse<Object>> handleGlobalException ( GlobalException globalException) {
return ResponseEntity
.status(globalException.getErrorCode().getStatus())
.headers(jsonHeaders)
.body(ResultResponse.fail(globalException.getErrorCode().getMessage()));
}
// ์๊ธฐ์น ๋ชปํ ์๋ฌ ๋ฐ์ ์ (์ผ๋จ ์๋ฌ ๋ด์ฉ์ด front ํํ
๋ ๋ณด์ด๊ฒ ๋์ต๋๋ค. ๋ฐฐํฌํ ๋ ๊ณ ์น๊ฒ ์ต๋๋ค.
@ExceptionHandler(Exception.class)
public ResponseEntity<ResultResponse<Object>> handleUnExpectException (Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.headers(jsonHeaders)
.body(ResultResponse.fail("์๋ฒ ๋ด๋ถ ์ค๋ฅ ๋ฐ์: " + e.getMessage()));
}
}
๋ค์์ ํด๋น GlobalException
์ด ๋ฌ์ ๋, ์ด๋ฅผ ํต์ ํ Exception Handler์ด๋ค. ์ผ๋จ ๋ฏธ๋ฆฌ jsonHeader๋ฅผ ๋ง๋ค์ด์ ์์๋ก ๊ฐ์ง๊ณ ์๊ณ , ResponseEntity์ ์ฌ์ฉ์๊ฐ ์ค์ ํ ์๋ก ์ฝ๋๊ฐ ๋ค์ด๊ฐ ์ ์๋๋ก ํ๋ค.
Exception handling ํ ์ํฉ์ ๋ ๊ฐ์ง๋ก ์ฐ์ ํ๋๋ฐ, ์ฒซ ๋ฒ์งธ๋ก ๊ฐ๋ฐ์๊ฐ ์์ธก ๊ฐ๋ฅํ ํน์ ์๋ํ ์๋ฌ ๋ฐ์ ์ ๊ทธ๊ฒ์ ๋ค๋ฃจ๋ @ExceptionHandler(GlobalException.class)
์ด๋ค. ๋๋ฒ์งธ๋ ๊ฐ๋ฐ์๊ฐ ์์ธกํ์ง ๋ชปํ ์๋ฌ ๋ฐ์ ์์ ๊ทธ๊ฒ์ ๋ค๋ฃจ๋ @ExceptionHandler(Exception.class)
์ด๋ค . (ํ์๋ ๊ฐ๋ฐ ์ค์ front ์ชฝ์์ error reporting์ ํด์ฃผ๋ฉด ์๋ฒ๋ฅผ ๋ค์ ๊น์ ๋ณด๋ ๊ฒ ๊ท์ฐฎ์์ ์ผ๋จ ์๋ฌ ๋ด์ฉ๋ ๊ฐ์ด ๋ฐํํ๋๋ก ํด์คฌ๋ค. - ์ค ๋ฐฐํฌํ๋ฉด ์ง์์ผ๊ฒ ์ง...)
6. ๊ฒฐ๊ณผ
์ด์ ํ ์คํธ์ฉ ์ปจํธ๋กค๋ฌ๋ฅผ ์ง๊ณ ์ง์ ํด๋ณด์.
// โ
1๏ธโฃ ๊ธฐ๋ณธํ ๋ฐํ (int, double, boolean ๋ฑ)
@GetMapping("/primitive")
public int getPrimitive() {
return 42; // ์๋์ผ๋ก ResultResponse.success(42)๋ก ๊ฐ์ธ์ ธ์ผ ํจ
}
// โ
2๏ธโฃ ์ฐธ์กฐํ ๋ฐํ (String)
@GetMapping("/string")
public String getString() {
return "Spring Boot is running!";
}
// โ
3๏ธโฃ DTO ๊ฐ์ฒด ๋ฐํ
@GetMapping("/dto")
public SampleDto getDto() {
return new SampleDto(1, "Test DTO");
}
// โ
4๏ธโฃ List<T> ๋ฐํ
@GetMapping("/list")
public List<String> getList() {
return List.of("Apple", "Banana", "Cherry");
}
// โ
5๏ธโฃ Map<K, V> ๋ฐํ
@GetMapping("/map")
public Map<String, Object> getMap() {
return Map.of("name", "John", "age", 30, "active", true);
}
// โ
6๏ธโฃ ์์ธ ๋ฐ์ ํ
์คํธ (์ ์๋ ์์ธ)
@GetMapping("/exception")
public void throwGlobalException() {
throw new GlobalException(ErrorCode.MEMBER_NOT_FOUND);
}
// โ
7๏ธโฃ ์๊ธฐ์น ์์ ์์ธ ํ
์คํธ (ArithmeticException)
@GetMapping("/unexpected-error")
public void throwUnexpectedError() {
int result = 10 / 0; // ArithmeticException ๋ฐ์
}
// โ
8๏ธโฃ ResponseEntity<T> ์ง์ ๋ฐํ (ResponseBodyAdvice๊ฐ ์ ์ฉ๋์ง ์์์ผ ํจ)
@GetMapping("/response-entity")
public ResponseEntity<SampleDto> getResponseEntity() {
return ResponseEntity.ok(new SampleDto(2, "ResponseEntity ์ฌ์ฉ"));
}
// โ
9๏ธโฃ POST ์์ฒญ์ผ๋ก DTO ํ
์คํธ
@PostMapping("/post-dto")
public SampleDto postDto(@RequestBody SampleDto dto) {
return dto; // ๊ณตํต ์๋ต์ผ๋ก ๊ฐ์ธ์ ธ์ผ ํจ
}
์ต๋ํ ๋ชจ๋ ์ ํ์ ํ ์คํธ ํ ์ ์๊ฒ ํด๋ฌ๋ผ๊ณ , GPT์ ์์ฒญํด์ ์ฝ๋๋ฅผ ์งฐ๋ค. ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ ์ ์๋ฏ์ด ๋ถํ์ํ ResponseEntity ํน์ ๊ณตํต ์๋ต ํ์ (์ด๋ฒ ํฌ์คํ ์์๋ ResultResponse)๋ฅผ ๋งค ์ปจํธ๋กค๋ฌ ๋ฐํ ํ์ ๊ณผ ๋ฐํ๊ฐ๋ง๋ค ๊ฐ์ ํ์๊ฐ ์๋ค.
์ด์ ํ ๋ฒ ํ ์คํธ๋ฅผ ํด๋ณผ๊น?
a. ์ ์์ ์ธ ์์ฒญ - ์ ์์ ์ธ ์๋ต
@PostMapping("/post-dto")
public SampleDto postDto(@RequestBody SampleDto dto) {
return dto; // ๊ณตํต ์๋ต์ผ๋ก ๊ฐ์ธ์ ธ์ผ ํจ
}
์ ํจ์์ ๋ํ ๋ด์ฉ์ด ์ ๋๋ก ๋์๋ค.
b. ์ ์์ ์ด์ง ์์ ์์ฒญ - Error ๋ฐํ
c. ๋ฐฑ์๋ ๊ฐ๋ฐ์๊ฐ String์ ๋ฐํ ํ์ ์ผ๋ก ์ปจํธ๋กค๋ฌ ๋งค์๋๋ฅผ ์งฐ์ ์, ์๋ฌ ๋ฉ์์ง ๋ฐํ
String ์์ฒด๋ฅผ ๋ฐํ ํ๋ ๊ฒฝ์ฐ Error ๋ฉ์์ง๋ฅผ ์ค์ DTO ํํ๋ก ๊ฐ์ ๋ฐํํ๋๋ก ์ ๋
์ฐธ๊ณ ์๋ฃ ๐
[[Spring] ์คํ๋ง API ๊ณตํต ์๋ต ํฌ๋งท ๋ฐํ ๊ธฐ๋ฅ์ ํจ์จ์ ์ผ๋ก ๋ง๋ค๊ธฐ ( with ResponseBodyAdvice )
๊ฐ๋ฐ ๊ณ๊ธฐ SSAFY 2๋ฒ์งธ ํ๋ก์ ํธ ์ด๊ธฐ์, ์ด๋ค ๋ฐฉ์์ผ๋ก ์๋ต์ ๋ด๋ณด๋ผ ์ง ํ์ํ ์ ์ ์์ง๋ง, ๋ค๋ฅธ ๋ฐฑ์ค๋ ํ์๋ค์ด ์๋์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ต์ ๋ด๋ณด๋ด๋ ๊ฒ์ ์ ํธํ๋ค. ์์ธ์ง๋ ๋ชจ๋ฅด๊ฒ ์ง๋ง
whalesbob.tistory.com](https://whalesbob.tistory.com/18)