F-lab 1~14์ฃผ์ฐจ ์ดํ Claude๊ฐ ์์๋ก ๊ตฌ์ฑํ ํ์ต ๊ฒฝ๋ก.
F-lab์์ ๋ค๋ฃจ์ง ์์ ๊ฐ์ฅ ํฐ ๊ณต๋ฐฑ ์์ญ์ธ Spring MVC์ ๋ด๋ถ ๋ฉ์ปค๋์ฆ ์ ์ ๋ณตํ๊ณ , 8-9์ฃผ์ฐจ AOP์์ ๊ด๊ณ๋ฅผ ๋ช ํํ ์ ๋ฆฌํ๋ค.
- DispatcherServlet ๋์ ์๋ฆฌ (9๋จ๊ณ ์์ฒญ ์ฒ๋ฆฌ ํ๋ฆ)
- ์์ฒญ/์๋ต ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ๋ฉ์ปค๋์ฆ
- Filter vs Interceptor vs AOP (๋ฉด์ ๋จ๊ณจ)
- ์์ธ ์ฒ๋ฆฌ์ Validation ํ์ค ํจํด
- REST API ์ค๊ณ์ ๊ตฌํ
4๋ ์ฐจ Spring Boot ๊ฐ๋ฐ์๊ฐ "๋งค์ผ ์ฐ์ง๋ง ๋ด๋ถ๋ ๋ชจ๋ฅด๋" ์์ญ์ ์ฑ์ฐ๋ ์ฃผ์ฐจ๋ค.
1~14์ฃผ์ฐจ์ ๊ฐ์ฅ ํฐ ๊ณต๋ฐฑ:
| ์์ญ | ์ฃผ์ฐจ | ์ํ |
|---|---|---|
| Java ์ธ์ด | 1~3์ฃผ์ฐจ | โ |
| ๋์์ฑ | 4์ฃผ์ฐจ | โ |
| Spring IoC/DI | 5์ฃผ์ฐจ | โ |
| ์น ์ธํ๋ผ ์ ๋ฌธ (Servlet/JSP) | 6์ฃผ์ฐจ | โ |
| JPA + ํธ๋์ญ์ | 7์ฃผ์ฐจ | โ |
| Spring AOP | 8-9์ฃผ์ฐจ | โ |
| ํธ๋์ญ์ ์ ๋ฆฌ + ๊ฒฉ๋ฆฌ ์์ค | 10์ฃผ์ฐจ | โ |
| JPA ๊น์ด | 11-12์ฃผ์ฐจ | โ |
| DB ํ๋๋ฉํธ + ์ด์ | 13-14์ฃผ์ฐจ | โ |
| Spring MVC ๋ด๋ถ ๋ฉ์ปค๋์ฆ | ๋ฏธ๋ฑ์ฅ | โ |
์ ์ด ์ฃผ์ ๊ฐ ๊ฒฐ์ ์ ์ธ๊ฐ:
1. ๋ฉด์ ๋จ๊ณจ โ DispatcherServlet 9๋จ๊ณ, Filter/Interceptor/AOP ๋น๊ต๋ ๊ฑฐ์ 100%
2. 8-9์ฃผ์ฐจ AOP์ ์์ฑ โ Filter/Interceptor์ ๋น๊ตํด์ผ AOP์ ์ง์ง ์์น๊ฐ ๋ณด์
3. 6์ฃผ์ฐจ Servlet์ ์์ฐ์ค๋ฌ์ด ํ์ฅ โ Front Controller ํจํด์ผ๋ก ํตํฉ
4. ์ค๋ฌด ๋๋ฒ๊น
๋ฅ๋ ฅ โ ์์ฒญ์ด ์ด๋์ ์ด๋ป๊ฒ ์ฒ๋ฆฌ๋๋์ง ์์์ผ ๋ฌธ์ ์ถ์ ๊ฐ๋ฅ
[Phase 1] ์น ์์ฒญ์ ์ฌ์ โ Servlet์์ Spring MVC๊น์ง
โ
[Phase 2] DispatcherServlet 9๋จ๊ณ ์ฒ๋ฆฌ ํ๋ฆ โ ์ ์ 1
โ
[Phase 3] ์์ฒญ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ (@RequestBody ๋ฑ์ ๋ด๋ถ)
โ
[Phase 4] ์๋ต ์ฒ๋ฆฌ์ ViewResolver
โ
[Phase 5] Filter vs Interceptor vs AOP โ ์ ์ 2 (โ
๋ฉด์ ๋จ๊ณจ)
โ
[Phase 6] ์์ธ ์ฒ๋ฆฌ์ Validation
โ
[Phase 7] REST API ์ค๊ณ์ ์ค์
์ด 7 Phase ร 24 Unit โ ์ ์ 2๊ฐ๋ฅผ ๊ฐ์ง ๋จ์ผ ์ฃผ์ฐจ.
| ์ฃผ์ฐจ | ์ฃผ์ | ์๋ฏธ |
|---|---|---|
| 1~3์ฃผ์ฐจ | Java ์ธ์ด์ ํํ๋ ฅ | ๊ธฐ์ด |
| 4์ฃผ์ฐจ | ๋์์ฑยท๋ฉํฐ์ค๋ ๋ฉ | ๋์์ฑ |
| 5์ฃผ์ฐจ | Spring IoC/DI | Spring ์ ๋ฌธ |
| 6์ฃผ์ฐจ | ์น ์ธํ๋ผ + DB ์ ๊ทผ | ์ค์ ํ๊ฒฝ ์ ๋ฌธ |
| 7์ฃผ์ฐจ | JPA + ํธ๋์ญ์ ์ ๋ฌธ | ๋ฐ์ดํฐ ์ถ์ํ |
| 8-9์ฃผ์ฐจ | AOP ๋ฉ์ปค๋์ฆ + ํธ๋์ญ์ ์ ํ | AOP ์ ๋ณต |
| 10์ฃผ์ฐจ | ํธ๋์ญ์ ์ ๋ฆฌ + ๊ฒฉ๋ฆฌ ์์ค | ํธ๋์ญ์ ๋ง๋ฌด๋ฆฌ |
| 11-12์ฃผ์ฐจ | JPA ์์์ฑ + ์ฐ๊ด๊ด๊ณ + N+1 | JPA ์ ๋ณต |
| 13-14์ฃผ์ฐจ | DB ํ๋๋ฉํธ + ์ด์ | DB ์ ๋ณต |
| 15์ฃผ์ฐจ (์ง๊ธ) | Spring MVC ๋ด๋ถ + REST API | ์น ๊ณ์ธต ์ ๋ณต |
โ ์ด์ ๋ฐฑ์๋ ํ์คํ์ ๋ชจ๋ ์์ญ์ ๋ณธ ์ .
| Day | Phase | ํ์ต ๋ชฉํ |
|---|---|---|
| 1์ผ์ฐจ | Phase 1 | Servlet โ Spring MVC ์งํ |
| 2-3์ผ์ฐจ | Phase 2 | DispatcherServlet 9๋จ๊ณ (โ ) |
| 4์ผ์ฐจ | Phase 3 | ์์ฒญ ๋ฐ์ธ๋ฉ ๋ฉ์ปค๋์ฆ |
| 5์ผ์ฐจ | Phase 4 | ์๋ต๊ณผ ViewResolver |
| 6์ผ์ฐจ | Phase 5 | Filter vs Interceptor vs AOP (โ ) |
| 7์ผ์ฐจ | Phase 6 + 7 | ์์ธ ์ฒ๋ฆฌ + REST API + ์ข ํฉ ์๊ธฐ ์ ๊ฒ |
์ฌ์ ์ผ์ (10์ผ): Phase 2, 5์ ๊ฐ +1์ผ. ๋ ์ ์ ์ ์ง์ ๋๋ฒ๊ฑฐ๋ก step-through ๊ถ์ฅ.
๋ชฉํ: 6์ฃผ์ฐจ์ Servlet/JSP ํ์ต์ ๋ค์ ์ง๊ณ , Spring MVC๊ฐ ์ด๋ค ์งํ์ ๊ฒฐ๊ณผ์ธ์ง ์ดํดํ๋ค.
์ ์ ์ง์: 6์ฃผ์ฐจ Phase 7
ํต์ฌ ๋ณต์ต
Servlet:
HttpServletRequest, HttpServletResponse ์ฌ์ฉServlet Container (Tomcat ๋ฑ):
์ ํ์ ์ธ Servlet ์ฝ๋ (์๋ ๋ฐฉ์):
@WebServlet("/users/*")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
String path = req.getPathInfo(); // "/123"
// ๋ถ๊ธฐ ์ฒ๋ฆฌ...
// JSON ์ง์ ์์ฑ...
res.getWriter().write(json);
}
}
Servlet์ ํ๊ณ:
1. URL๋ง๋ค Servlet์ ๋ง๋ค๋ฉด Servlet ํญ์ฆ
2. ๊ณตํต ์ฒ๋ฆฌ (์ธ์ฆ, ๋ก๊น
) ๋ฐ๋ณต ์ฝ๋
3. URL ๋ผ์ฐํ
๋ถ๊ธฐ ์๋ ์์ฑ
4. JSON ๋ณํ ์๋ ์์ฑ
โ Front Controller ํจํด ํ์
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 1.1, 8์ฃผ์ฐจ Phase 6 (๋์์ธ ํจํด)
ํต์ฌ ๊ฐ๋
Front Controller ํจํด:
"๋ชจ๋ ์์ฒญ์ ํ ๊ณณ์์ ๋ฐ์์ ์ ์ ํ ์ฒ๋ฆฌ๊ธฐ๋ก ์์"
๊ตฌ์กฐ:
Before: After (Front Controller):
[Client] โ [Servlet A] [Client] โ [Front Controller]
[Client] โ [Servlet B] โ (๋ถ๊ธฐ)
[Client] โ [Servlet C] [Handler A/B/C]
ํจ๊ณผ:
๋น์ :
"์ ํ ๊ตํ์ โ ๋ชจ๋ ์ ํ๊ฐ ํ ๋ช ์๊ฒ ์์, ๊ทธ ์ฌ๋์ด ์ ์ ํ ๋ถ์๋ก ์ฐ๊ฒฐ"
8์ฃผ์ฐจ Phase 1๊ณผ ์ฐ๊ฒฐ:
โ Spring MVC์ DispatcherServlet ์ด ์ ํํ ์ด ์ญํ
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 1.2
ํต์ฌ ๊ทธ๋ฆผ
[Client]
โ HTTP Request
[Servlet Container (Tomcat)]
โ
[Filter Chain] โ Servlet ํ์ค
โ
[DispatcherServlet] โ Spring์ Front Controller โญ
โ
[Interceptor Chain] โ Spring ์ ๊ณต
โ
[Controller (@Controller)]
โ
[Service / Repository] โ AOP ์ ์ฉ
โ
[Database]
DispatcherServlet์ ์ ์ฒด:
/ ๊ฒฝ๋ก ๋งคํ)Spring Boot์ ์๋ ์ค์ :
// ์๋์ผ๋ก ๋์ (๊ฐ๋ฐ์๊ฐ ์ง์ ๋ฑ๋ก X)
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
ํต์ฌ ํต์ฐฐ:
"DispatcherServlet๋ ๊ฒฐ๊ตญ์ ํ๋์ Servlet ์ผ ๋ฟ์ด๋ค.
๊ทธ๋ฌ๋ ๊ทธ ์์์ Spring์ ๋ชจ๋ ๋ง๋ฒ์ด ์ผ์ด๋๋ค."
ILIC ๊ด์ :
์๊ธฐ ์ ๊ฒ
๋ชฉํ: ๋ฉด์ ๋จ๊ณจ โ Spring MVC๊ฐ ์์ฒญ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์ง 9๋จ๊ณ๋ก ์๋ฒฝํ ์ดํดํ๋ค.
์ ์ ์ง์: Phase 1
ํต์ฌ 9๋จ๊ณ (์ธ์์ผ ํจ):
1. [DispatcherServlet] HTTP ์์ฒญ ์์
โ
2. [HandlerMapping] ์ด๋ค ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ธ์ง ์ฐพ๊ธฐ
โ
3. [HandlerAdapter] ๊ทธ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ฅผ ํธ์ถ
โ
4. [Interceptor preHandle] ์ปจํธ๋กค๋ฌ ํธ์ถ ์ ์ฒ๋ฆฌ
โ
5. [Controller] ๋น์ฆ๋์ค ๋ก์ง ์คํ โ ModelAndView ๋ฐํ
โ
6. [Interceptor postHandle] ์ปจํธ๋กค๋ฌ ํธ์ถ ํ ์ฒ๋ฆฌ
โ
7. [ViewResolver] View ์ด๋ฆ โ ์ค์ View ๊ฐ์ฒด๋ก ๋ณํ
โ
8. [View.render()] HTML ๋๋ JSON ์๋ต ์์ฑ
โ
9. [Interceptor afterCompletion] ์๋ต ์๋ฃ ํ ์ฒ๋ฆฌ
โ
[Client]
REST API ํ๋ฆ (View ๋จ๊ณ ๋จ์ํ):
1~6. (์์ ๋์ผ)
โ
7. [HttpMessageConverter] ๊ฐ์ฒด โ JSON ๋ณํ
โ
8. JSON ์๋ต
โ
9. afterCompletion
ํต์ฌ ํต์ฐฐ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 2.1
ํต์ฌ ์ญํ :
"URL โ Controller ๋ฉ์๋ ๋งคํ ์ ๋ณด๋ฅผ ๊ด๋ฆฌ"
๋ํ์ HandlerMapping:
| ๊ตฌํ์ฒด | ๋งคํ ๋ฐฉ์ |
|---|---|
RequestMappingHandlerMapping | @RequestMapping, @GetMapping ๋ฑ โญ |
BeanNameUrlHandlerMapping | ๋น ์ด๋ฆ์ด URL์ธ ๊ฒฝ์ฐ |
SimpleUrlHandlerMapping | ๋ช ์์ URL-๋น ๋งคํ |
ํ๋ Spring Boot์์๋ RequestMappingHandlerMapping ์ด ์ฌ์ค์ ํ์ค.
๋์ ์์:
@RestController
@RequestMapping("/api/bookings")
public class BookingController {
@GetMapping("/{id}")
public Booking getBooking(@PathVariable Long id) { ... }
}
์์ฒญ: GET /api/bookings/123
โ RequestMappingHandlerMapping์ด BookingController.getBooking ๋ฉ์๋๋ฅผ ๋ฐํ
๋ด๋ถ ๊ตฌ์กฐ:
@RequestMapping ์ด๋
ธํ
์ด์
์ค์บ๋๋ฒ๊น ํ:
/actuator/mappings ์๋ํฌ์ธํธ์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 2.2
ํต์ฌ ๊ฐ๋
๋ฌธ์ : Controller๋ ๋ค์ํ ํํ๋ก ์์ฑ ๊ฐ๋ฅ
@Controller)โ ํธ์ถ ๋ฐฉ๋ฒ์ด ๋ค ๋ค๋ฆ
HandlerAdapter์ ํด๊ฒฐ:
"๋ค์ํ ํธ๋ค๋ฌ๋ฅผ ํต์ผ๋ ๋ฐฉ์์ผ๋ก ํธ์ถ"
๋ํ์ HandlerAdapter:
| ๊ตฌํ์ฒด | ๋์ |
|---|---|
RequestMappingHandlerAdapter | @RequestMapping ๋ฉ์๋ โญ |
HttpRequestHandlerAdapter | HttpRequestHandler |
SimpleControllerHandlerAdapter | ์ Controller ์ธํฐํ์ด์ค |
๋์์ธ ํจํด:
ํต์ฌ ๋์ โ ์ด๋ ธํ ์ด์ ๊ธฐ๋ฐ์ ๋ง๋ฒ:
@GetMapping("/users/{id}")
public User getUser(
@PathVariable Long id,
@RequestParam String type,
HttpServletRequest req,
@AuthenticationPrincipal User currentUser
) { ... }
โ HandlerAdapter๊ฐ:
1. @PathVariable โ URL์์ ์ถ์ถ
2. @RequestParam โ ์ฟผ๋ฆฌ ์คํธ๋ง์์ ์ถ์ถ
3. HttpServletRequest โ ์์ฒญ ๊ฐ์ฒด ๊ทธ๋๋ก ์ ๋ฌ
4. @AuthenticationPrincipal โ Security Context์์ ์ถ์ถ
ArgumentResolver:
RequestParamMethodArgumentResolver, PathVariableMethodArgumentResolver ๋ฑ์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 2.3
ํต์ฌ ๊ฐ๋
HandlerInterceptor:
"Controller ํธ์ถ ์ ยทํยท์๋ฃ ํ ๊ฐ๋ก์ฑ๊ธฐ"
3๊ฐ์ง ๋ฉ์๋ โญ :
public interface HandlerInterceptor {
boolean preHandle(req, res, handler); // Controller ํธ์ถ ์
void postHandle(req, res, handler, mav); // Controller ํธ์ถ ํ, View ๋ ๋๋ง ์
void afterCompletion(req, res, handler, ex); // ์๋ต ์๋ฃ ํ (์์ธ ํฌํจ)
}
์์ โ ์ธ์ฆ ์ฒดํฌ:
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String token = req.getHeader("Authorization");
if (!isValid(token)) {
res.setStatus(401);
return false; // ์ปจํธ๋กค๋ฌ ํธ์ถ ์ ํจ!
}
return true;
}
}
๋ฑ๋ก:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**");
}
}
preHandle false ๋ฐํ ์:
ํ์ฉ ์์:
์๊ธฐ ์ ๊ฒ
๋ชฉํ:
@RequestBody,@RequestParam,@PathVariable๋ฑ์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ์ดํดํ๋ค.
์ ์ ์ง์: Phase 2
ํต์ฌ ๋น๊ต โญ :
| ์ด๋ ธํ ์ด์ | ์ด๋์ ์ถ์ถ | ํ์ |
|---|---|---|
@PathVariable | URL ๊ฒฝ๋ก | /users/{id} |
@RequestParam | ์ฟผ๋ฆฌ ์คํธ๋ง / ํผ | ?name=Alice |
@RequestBody | HTTP body | JSON, XML |
@ModelAttribute | ์ฟผ๋ฆฌ + ํผ โ ๊ฐ์ฒด ๋ฐ์ธ๋ฉ | (์๋) |
์์ ์ข ํฉ:
// 1. PathVariable
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
// ์์ฒญ: GET /users/42
// 2. RequestParam
@GetMapping("/users")
public List<User> searchUsers(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "10") int size
) { ... }
// ์์ฒญ: GET /users?name=Alice&size=20
// 3. RequestBody
@PostMapping("/users")
public User createUser(@RequestBody UserDto dto) { ... }
// ์์ฒญ: POST /users
// Body: {"name": "Alice", "email": "..."}
// 4. ModelAttribute
@GetMapping("/search")
public List<User> search(@ModelAttribute SearchCriteria criteria) { ... }
// ์์ฒญ: GET /search?name=Alice&age=25
// โ SearchCriteria ๊ฐ์ฒด์ ์๋ ๋ฐ์ธ๋ฉ
์ธ์ ๋ฌด์์ โญ :
| ์ํฉ | ์ถ์ฒ |
|---|---|
| ๋ฆฌ์์ค ์๋ณ์ (id) | @PathVariable |
| ๊ฒ์ ์กฐ๊ฑด, ํํฐ | @RequestParam |
| ๋ณต์กํ ๊ฐ์ฒด (POST/PUT) | @RequestBody |
| GET์ ๋ค์ค ๊ฒ์ ์กฐ๊ฑด โ ๊ฐ์ฒด | @ModelAttribute |
์๊ธฐ ์ ๊ฒ
@RequestParam์ ์ธ ์ ์๋๊ฐ? (ํํธ: form-encoded๋ฉด ๊ฐ๋ฅ)์ ์ ์ง์: Unit 3.1
ํต์ฌ ์ญํ :
"HTTP body(JSON/XML) โ ์๋ฐ ๊ฐ์ฒด ๋ณํ"
@RequestBody / @ResponseBody ์ ์ง์ง ์ ์ฒด:
๋ํ Converter:
| Converter | ์ฒ๋ฆฌ |
|---|---|
MappingJackson2HttpMessageConverter | JSON โ ๊ฐ์ฒด โญ |
StringHttpMessageConverter | text/plain |
Jaxb2RootElementHttpMessageConverter | XML |
ByteArrayHttpMessageConverter | byte[] |
๋์ ํ๋ฆ (@RequestBody):
[HTTP Body]
{ "name": "Alice", "age": 25 }
โ
[MappingJackson2HttpMessageConverter.read()]
โ
[Jackson ObjectMapper]
โ
[์๋ฐ ๊ฐ์ฒด]
new UserDto("Alice", 25)
โ
[Controller ๋ฉ์๋ ํ๋ผ๋ฏธํฐ๋ก ์ฃผ์
]
@ResponseBody (์ ํํ ๋ฐ๋):
[Controller ๋ฐํ๊ฐ]
โ
[Jackson ObjectMapper.writeValueAsString()]
โ
[JSON ๋ฌธ์์ด]
โ
[HTTP Response Body]
์ปค์คํฐ๋ง์ด์ง:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule()) // LocalDateTime
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.setSerializationInclusion(Include.NON_NULL);
}
}
ILIC ํ์ฉ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 3.2
ํต์ฌ ๊ฐ๋
Content Negotiation:
"ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ ์๋ต ํ์ ์ ๋ง์ถฐ ์๋ต"
HTTP Accept ํค๋:
GET /api/users/1 HTTP/1.1
Accept: application/json
โ ์๋ฒ: JSON์ผ๋ก ์๋ต
GET /api/users/1 HTTP/1.1
Accept: application/xml
โ ์๋ฒ: XML๋ก ์๋ต (ํด๋น Converter ๋ฑ๋ก ์)
Spring์ ๊ฒฐ์ ํ๋ฆ:
1. ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ๋ฐํ ํ์
ํ์ธ
2. ์๋ต ๊ฐ๋ฅํ Converter ๋ชฉ๋ก ํ์ธ
3. Accept ํค๋์ ๋งค์นญ ๋๋ Converter ์ ํ
4. ๋ณํ
produces ๋ช
์:
@GetMapping(value = "/users/{id}", produces = "application/json")
public User getUser(@PathVariable Long id) { ... }
โ JSON๋ง ์๋ต (๊ทธ ์ธ Accept๋ 406 Not Acceptable)
consumes ๋ช
์:
@PostMapping(value = "/users", consumes = "application/json")
public User createUser(@RequestBody UserDto dto) { ... }
โ Content-Type์ด JSON์ผ ๋๋ง ์ฒ๋ฆฌ
ILIC ์๋๋ฆฌ์ค:
produces = "application/json" ๋ช
์ ๊ฐ๋ฅ์๊ธฐ ์ ๊ฒ
๋ชฉํ: REST API ์๋์ ViewResolver ์๋ฏธ์ ResponseEntity ํ์ฉ์ ์ ๋ฆฌํ๋ค.
์ ์ ์ง์: Phase 3
ํต์ฌ ๊ฐ๋
ViewResolver:
"Controller๊ฐ ๋ฐํํ View ์ด๋ฆ โ ์ค์ View ๊ฐ์ฒด ๋ณํ"
์ ํต์ ํ๋ฆ (View ๊ธฐ๋ฐ):
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("user", currentUser);
return "home"; // View ์ด๋ฆ ๋ฐํ
}
}
โ ViewResolver: "home" โ home.html ๋๋ home.jsp ์ฐพ์์ ๋ ๋๋ง
๋ํ ViewResolver:
| ๊ตฌํ์ฒด | ์ฒ๋ฆฌ ๋์ |
|---|---|
ThymeleafViewResolver | Thymeleaf ํ ํ๋ฆฟ |
InternalResourceViewResolver | JSP |
BeanNameViewResolver | ๋น ์ด๋ฆ์ด View |
ํ๋ Spring Boot โ REST API ์๋:
@RestController ์ ๋์:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.find(id); // ๊ฐ์ฒด ๋ฐํ
}
}
โ ViewResolver๋ฅผ ๊ฑฐ์น์ง ์๊ณ HttpMessageConverter๋ก ์ง์ JSON ๋ณํ
ILIC ์๋๋ฆฌ์ค:
@RestController์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 4.1
ํต์ฌ ์ฐจ์ด โญ :
@Controller | @RestController | |
|---|---|---|
| ์ ์ | View ์ด๋ฆ ๋ฐํ | ๋ฐ์ดํฐ ์ง์ ๋ฐํ |
| ์๋ต | ๋ณดํต HTML | ๋ณดํต JSON |
@ResponseBody | ๋ฉ์๋๋ง๋ค ๋ช ์ ํ์ | ์๋ ์ ์ฉ |
| ์ฉ๋ | SSR, ์ ํต ์น | REST API |
์ค์ ์ ์:
@Controller
@ResponseBody // โ ํฉ์น ๊ฒ RestController
public @interface RestController { ... }
์์ ๋น๊ต:
@Controller // ์ ํต (SSR)
public class TraditionalController {
@GetMapping("/users")
public String list(Model model) {
model.addAttribute("users", users);
return "user-list"; // View ์ด๋ฆ
}
@GetMapping("/api/users")
@ResponseBody // ๋ช
์ ํ์
public List<User> apiList() {
return userService.findAll();
}
}
@RestController // ํ๋ (REST API) โญ
public class ApiController {
@GetMapping("/users")
public List<User> list() {
return userService.findAll(); // ์๋ JSON ๋ณํ
}
}
ILIC: ๋ชจ๋ ์ปจํธ๋กค๋ฌ๊ฐ @RestController
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 4.2
ํต์ฌ ๊ฐ๋
ResponseEntity:
"HTTP ์ํ ์ฝ๋, ํค๋, ๋ฐ๋ ๋ฅผ ์ง์ ์ ์ด"
๊ธฐ๋ณธ ๋ฐํ vs ResponseEntity:
// ๊ธฐ๋ณธ: ํญ์ 200 OK
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.find(id);
}
// ResponseEntity: ์ํ ์ฝ๋ ๋ช
์ ๊ฐ๋ฅ
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.find(id);
if (user == null) {
return ResponseEntity.notFound().build(); // 404
}
return ResponseEntity.ok(user); // 200
}
์์ฃผ ์ฐ๋ ํจํด:
// 201 Created (POST)
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/users/" + user.getId())
.body(user);
// 400 Bad Request
return ResponseEntity.badRequest().body(errorMessage);
// 204 No Content (DELETE ํ)
return ResponseEntity.noContent().build();
// 400๋ with body
return ResponseEntity.status(409) // Conflict
.body(Map.of("error", "์ด๋ฏธ ์กด์ฌํฉ๋๋ค"));
REST API ํ์ค ์ํ ์ฝ๋ โญ :
| ์ฝ๋ | ์๋ฏธ | ์ฌ์ฉ ์์ |
|---|---|---|
| 200 OK | ์ฑ๊ณต | ์ผ๋ฐ ์กฐํ/์์ |
| 201 Created | ์์ฑ๋จ | POST ํ |
| 204 No Content | ์ฑ๊ณต (์๋ต X) | DELETE ํ |
| 400 Bad Request | ์๋ชป๋ ์์ฒญ | Validation ์คํจ |
| 401 Unauthorized | ์ธ์ฆ ํ์ | ๋ก๊ทธ์ธ ์ ๋จ |
| 403 Forbidden | ๊ถํ ์์ | ์ธ์ฆ์ OK, ๊ถํ X |
| 404 Not Found | ๋ฆฌ์์ค ์์ | ์๋ชป๋ ID |
| 409 Conflict | ์ถฉ๋ | ์ค๋ณต ๋ฑ๋ก |
| 500 Internal Server Error | ์๋ฒ ์ค๋ฅ | ์์ธ ๋ฐ์ |
ILIC ์ ์ฉ:
@PostMapping("/bookings")
public ResponseEntity<Booking> create(@RequestBody BookingDto dto) {
Booking saved = bookingService.create(dto);
return ResponseEntity
.status(HttpStatus.CREATED)
.header("Location", "/bookings/" + saved.getId())
.body(saved);
}
์๊ธฐ ์ ๊ฒ
๋ชฉํ: ๊ฐ์ฅ ์์ฃผ ๋ฌป๋ ๋ฉด์ ์ง๋ฌธ โ ์ธ ๊ฐ์ง ํก๋จ ๊ด์ฌ์ฌ ์ฒ๋ฆฌ ๋๊ตฌ์ ์ฐจ์ด๋ฅผ ๋ช ํํ ์ก๋๋ค.
์ ์ ์ง์: Phase 1
ํต์ฌ ๊ฐ๋
Servlet Filter:
"DispatcherServlet ๋๋ฌ ์ ยทํ ์์ ๋์ โ Servlet ํ์ค"
์์น:
[Client] โ [Filter] โ [DispatcherServlet] โ [Interceptor] โ [Controller]
๊ตฌํ:
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// ์์ฒญ ์
long start = System.currentTimeMillis();
chain.doFilter(req, res); // ๋ค์ Filter / DispatcherServlet
// ์๋ต ํ
long duration = System.currentTimeMillis() - start;
log.info("Request took {}ms", duration);
}
}
ํน์ง:
@Component + Filter Bean ๋ฑ๋ก์ ๊ฐ๋ฅํ์ฉ ์ฌ๋ก:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 2.4, 5.1
ํต์ฌ ์ฐจ์ด:
Interceptor:
"DispatcherServlet ์์์ Controller ์ ยทํ ๋์ โ Spring ์ ๊ณต"
์์น:
[Client] โ [Filter] โ [DispatcherServlet] โ [Interceptor] โ [Controller]
Filter์์ ๊ฒฐ์ ์ ์ฐจ์ด โญ :
@Override
public boolean preHandle(req, res, handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
// ์ด๋
ธํ
์ด์
ํ์ธ ๊ฐ๋ฅ
if (method.hasMethodAnnotation(Auditable.class)) {
// ๊ฐ์ฌ ๋ก๊ทธ
}
}
return true;
}
ํ์ฉ ์ฌ๋ก:
์ ์ ์ง์: 8-9์ฃผ์ฐจ, Unit 5.2
ํต์ฌ ์์น:
[Client] โ [Filter] โ [DispatcherServlet] โ [Interceptor] โ [Controller] โ [Service (AOP)]
AOP์ ์์น:
8-9์ฃผ์ฐจ ๋ณต์ต:
ํ์ฉ ์ฌ๋ก:
@Transactional) โ 7์ฃผ์ฐจ์ ์ ์ง์: Unit 5.1~5.3
์์ ๋น๊ต โญ :
| Filter | Interceptor | AOP | |
|---|---|---|---|
| ์์น | DispatcherServlet ์ธ๋ถ | DispatcherServlet ๋ด๋ถ | ๋ฉ์๋ ๋จ์ |
| ํ์ค | Servlet ํ์ค | Spring | Spring |
| Spring ๋น ์ ๊ทผ | ์ ํ์ | ๊ฐ๋ฅ | ๊ฐ๋ฅ |
| Handler ์ ๋ณด | X | โ | โ |
| req/res ๋ณ๊ฒฝ | โ (Wrapper) | ์ด๋ ต | ๋ฉ์๋ ์ธ์/๋ฐํ |
| ๋ฉ์๋ ๋จ์ ์ ์ฉ | X | URL ๋จ์ | ๋ฉ์๋ ๋จ์ โญ |
| ์์ธ ์ฒ๋ฆฌ ์์น | ์ธ๊ณฝ | ์ปจํธ๋กค๋ฌ ์ ํ | ๋ฉ์๋ ํธ์ถ ์ |
| ๋ํ ํ์ฉ | Security, CORS, ์ธ์ฝ๋ฉ | ์ธ์ฆ, ๋ก๊น | ํธ๋์ญ์ , ๋น์ฆ๋์ค ๋ก๊น |
์ ํ ๊ธฐ์ค โญ:
| ์ํฉ | ์ ํ |
|---|---|
| ๋ชจ๋ ์์ฒญ์ ์ธ์ฝ๋ฉ / CORS | Filter |
| Spring Security | Filter (Security๊ฐ Filter ๊ธฐ๋ฐ) |
| Request/Response ์์ฒด ์กฐ์ | Filter |
| URL ํจํด๋ณ ์ธ์ฆ | Interceptor |
| ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ์ง์ ์ ๊ถํ ํ์ธ | Interceptor |
| ๋ฉ์๋์ ํธ๋์ญ์ | AOP (@Transactional) |
| ๋น์ฆ๋์ค ๋ก์ง ๋ก๊น | AOP |
| ๋ฉ์๋ ์ธ์/๋ฐํ๊ฐ ๋ณ๊ฒฝ | AOP |
ํธ์ถ ์์ (๋๋ฒ๊ฑฐ๋ก ํ์ธ ๊ถ์ฅ):
[Filter] preHandle
โ
[Interceptor] preHandle
โ
[AOP] before
โ
[Controller method]
โ
[AOP] after
โ
[Interceptor] postHandle
โ
[Interceptor] afterCompletion
โ
[Filter] postHandle (์ญ์)
ILIC ์ ์ฉ:
๋ฉด์ ๋ชจ์ ๋ต๋ณ:
"Filter๋ Servlet ํ์ค์ผ๋ก DispatcherServlet ์ธ๊ณฝ์์ ๋์ํฉ๋๋ค. Interceptor๋ Spring ์ ๊ณต์ผ๋ก ์ปจํธ๋กค๋ฌ ์ ํ๋ฅผ ๊ฐ๋ก์ฑ๋ฉฐ Handler ์ ๋ณด๋ฅผ ์ ์ ์์ต๋๋ค. AOP๋ ๋ฉ์๋ ๋จ์๋ก ๋์ํ๋ฉฐ ๊ฐ์ฅ ์ธ๋ฐํ ์ ์ด๊ฐ ๊ฐ๋ฅํฉ๋๋ค. Spring Security์ฒ๋ผ ๊ฐ๋ ฅํ ์ธ๊ณฝ ์ฐจ๋จ์ด ํ์ํ๋ฉด Filter, ์ปจํธ๋กค๋ฌ ๋จ์ ์ธ์ฆ/๋ก๊น ์ด๋ฉด Interceptor, ๋ฉ์๋ ๋จ์ ํก๋จ ๊ด์ฌ์ฌ๋ฉด AOP ๋ฅผ ์ฌ์ฉํฉ๋๋ค."
์๊ธฐ ์ ๊ฒ
๋ชฉํ: REST API์ ํ์ค ์๋ฌ ์๋ต ํจํด๊ณผ Bean Validation ํ์ฉ์ ๋ง์คํฐํ๋ค.
์ ์ ์ง์: Phase 2
ํต์ฌ ๊ฐ๋
HandlerExceptionResolver:
"Controller์์ ๋์ง ์์ธ๋ฅผ ์ฒ๋ฆฌ ํ๋ ์ปดํฌ๋ํธ"
๊ธฐ๋ณธ ๋์:
HandlerExceptionResolver ๋ค์๊ฒ ์์๋ํ ๊ตฌํ์ฒด:
| ๊ตฌํ์ฒด | ์ฒ๋ฆฌ |
|---|---|
ExceptionHandlerExceptionResolver | @ExceptionHandler ๋ฉ์๋ โญ |
ResponseStatusExceptionResolver | @ResponseStatus ์ด๋
ธํ
์ด์
|
DefaultHandlerExceptionResolver | Spring ๊ธฐ๋ณธ ์์ธ (404, 405 ๋ฑ) |
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 6.1
ํต์ฌ ํจํด
์ปจํธ๋กค๋ฌ ๋จ์ ์ฒ๋ฆฌ (@ExceptionHandler):
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.find(id); // ์์ผ๋ฉด UserNotFoundException
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(UserNotFoundException ex) {
return ResponseEntity.status(404)
.body(new ErrorResponse(ex.getMessage()));
}
}
์ ์ญ ์ฒ๋ฆฌ (@ControllerAdvice ๋๋ @RestControllerAdvice) โญ :
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
// Validation ์๋ฌ ์ฒ๋ฆฌ
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(500)
.body(ErrorResponse.of("INTERNAL_ERROR", "์๋ฒ ์ค๋ฅ"));
}
}
@ControllerAdvice ์ ๊ฐ์ :
ํ์ค ์๋ฌ ์๋ต ์ค๊ณ โญ :
public record ErrorResponse(
String code, // "USER_NOT_FOUND"
String message, // ์ฌ๋์ด ์ฝ์ ๋ฉ์์ง
LocalDateTime timestamp,
String path, // ์์ฒญ URL
List<FieldError> errors // Validation ์๋ฌ (์์ ๋)
) {}
@Getter @AllArgsConstructor
class FieldError {
private String field;
private String message;
}
์์ ์๋ต:
{
"code": "USER_NOT_FOUND",
"message": "User with id 42 not found",
"timestamp": "2026-05-04T10:30:00",
"path": "/api/users/42",
"errors": []
}
ILIC ํ์คํ:
์๊ธฐ ์ ๊ฒ
@ControllerAdvice ์ @RestControllerAdvice ์ ์ฐจ์ด๋? (ํํธ: ํ์๋ @ResponseBody ์๋)์ ์ ์ง์: Unit 6.2
ํต์ฌ ๊ฐ๋
Bean Validation (JSR-380):
"์ด๋ ธํ ์ด์ ๊ธฐ๋ฐ ๊ฒ์ฆ โ ์๋ํ๋ Validation"
๊ธฐ๋ณธ ์ด๋ ธํ ์ด์ :
| ์ด๋ ธํ ์ด์ | ๊ฒ์ฆ |
|---|---|
@NotNull | NULL ์๋ |
@NotEmpty | NULL ์๋ + ๋น์ด์์ง ์์ (String/Collection) |
@NotBlank | NULL ์๋ + ๊ณต๋ฐฑ ์๋ (String) |
@Size(min, max) | ๊ธธ์ด/ํฌ๊ธฐ ๋ฒ์ |
@Min / @Max | ์ซ์ ๋ฒ์ |
@Pattern(regexp) | ์ ๊ทํํ์ |
@Email | ์ด๋ฉ์ผ ํ์ |
@Past / @Future | ๋ ์ง |
์์:
public record UserCreateRequest(
@NotBlank(message = "์ด๋ฆ์ ํ์์
๋๋ค")
@Size(min = 2, max = 50)
String name,
@NotBlank
@Email
String email,
@Min(0) @Max(150)
int age
) {}
Controller์์ ์ฌ์ฉ โญ :
@PostMapping("/users")
public ResponseEntity<User> create(
@Valid @RequestBody UserCreateRequest request // โ @Valid ํ์
) {
return ResponseEntity.ok(userService.create(request));
}
@Valid ๊ฐ ์์ผ๋ฉด ๊ฒ์ฆ ์ ๋จ!
Validation ์คํจ ์:
MethodArgumentNotValidException ๋ฐ์@RestControllerAdvice ์์ ์ฒ๋ฆฌ์ค์ฒฉ ๊ฒ์ฆ (@Valid ์ฌ๊ท):
public record OrderRequest(
@NotNull
@Valid // ์ค์ฒฉ ๊ฒ์ฆ
AddressDto address,
@NotEmpty
@Valid // ๋ฆฌ์คํธ์ ๊ฐ ์์ ๊ฒ์ฆ
List<OrderItemDto> items
) {}
์ปค์คํ Validator (์ฐธ๊ณ ):
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
String message() default "์ ํจํ์ง ์์ ์ ํ๋ฒํธ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ILIC ํ์ฉ:
์๊ธฐ ์ ๊ฒ
@Valid ์ @Validated ์ ์ฐจ์ด๋? (ํํธ: ํ์๋ ๊ทธ๋ฃน ์ง์ ๊ฐ๋ฅ)์ ์ ์ง์: Unit 6.2~6.3
ํต์ฌ ์์น
1. ์ผ๊ด๋ ๊ตฌ์กฐ:
2. ์๋ฌ ์ฝ๋ + ๋ฉ์์ง ๋ถ๋ฆฌ:
USER_NOT_FOUND)3. ๋ฏผ๊ฐ ์ ๋ณด ๋ ธ์ถ X:
์ ์ญ ์์ธ ์ฒ๋ฆฌ ํ ํ๋ฆฟ:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 1. ๋น์ฆ๋์ค ์์ธ (์ฌ์ฉ์ ์
๋ ฅ ๋ฌธ์ )
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException ex) {
log.warn("Business error: {}", ex.getMessage());
return ResponseEntity.status(ex.getStatus())
.body(ErrorResponse.of(ex.getCode(), ex.getMessage()));
}
// 2. Validation ์คํจ
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> new FieldError(e.getField(), e.getDefaultMessage()))
.toList();
return ResponseEntity.badRequest()
.body(ErrorResponse.builder()
.code("VALIDATION_FAILED")
.message("์
๋ ฅ๊ฐ์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค")
.errors(errors)
.build());
}
// 3. ๋ฐ์ดํฐ ์์
@ExceptionHandler({EntityNotFoundException.class, NoSuchElementException.class})
public ResponseEntity<ErrorResponse> handleNotFound(Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
}
// 4. ๋ชจ๋ ์์ธ์ ์ต์ข
์ฒ๋ฆฌ (Fallback)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(500)
.body(ErrorResponse.of("INTERNAL_ERROR", "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค"));
}
}
ILIC ์ ์ฉ ๊ถ์ฅ:
์๊ธฐ ์ ๊ฒ
USER_NOT_FOUND)๋ชฉํ: 4๋ ์ฐจ ํ์คํ ๊ฐ๋ฐ์๊ฐ ์์์ผ ํ REST API ์ค๊ณ ์์น๊ณผ ํ์ด์ง/๋ฌธ์ํ๋ฅผ ์ ๋ฆฌํ๋ค.
์ ์ ์ง์: 6์ฃผ์ฐจ Phase 7
ํต์ฌ 6๊ฐ์ง ์์น:
REST API URL ์ค๊ณ ์์น โญ :
| ์ข์ ์ | ๋์ ์ |
|---|---|
GET /users | GET /getUsers (๋์ฌ X) |
GET /users/123 | GET /user?id=123 |
POST /users | POST /createUser |
DELETE /users/123 | POST /deleteUser/123 |
GET /users/123/orders | GET /userOrders?userId=123 |
ํต์ฌ:
/users)/users/{id}/orders)/order-items)์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 7.1, 4.3
ํต์ฌ ๋งคํ โญ :
| ์์ | HTTP ๋ฉ์๋ | URL | ์ฑ๊ณต ์ฝ๋ |
|---|---|---|---|
| ๋ชฉ๋ก ์กฐํ | GET | /users | 200 OK |
| ๋จ๊ฑด ์กฐํ | GET | /users/{id} | 200 OK |
| ์์ฑ | POST | /users | 201 Created |
| ์ ์ฒด ์์ | PUT | /users/{id} | 200 ๋๋ 204 No Content |
| ๋ถ๋ถ ์์ | PATCH | /users/{id} | 200 ๋๋ 204 |
| ์ญ์ | DELETE | /users/{id} | 204 No Content |
Spring ์ด๋ ธํ ์ด์ :
@GetMapping("/users") // GET
@PostMapping("/users") // POST
@PutMapping("/users/{id}") // PUT
@PatchMapping("/users/{id}") // PATCH
@DeleteMapping("/users/{id}") // DELETE
PUT vs PATCH โญ :
์์ โ PATCH์ ํจ์จ์ฑ:
// PUT โ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฒด
PUT /users/1
{
"name": "Alice",
"email": "alice@example.com",
"age": 25,
"address": "Seoul"
// ... ๋ชจ๋ ํ๋
}
// PATCH โ ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง
PATCH /users/1
{
"email": "alice.new@example.com"
}
Idempotent (๋ฉฑ๋ฑ์ฑ) โญ :
์ ์ค์ํ๊ฐ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: 11-12์ฃผ์ฐจ Phase 9 (ํ์ด์ง ํจ์ )
ํต์ฌ ๊ฐ๋
Spring Data JPA์ ํ์ด์ง ์ถ์ํ:
// Repository
public interface BookingRepository extends JpaRepository<Booking, Long> {
Page<Booking> findByCustomerId(Long customerId, Pageable pageable);
}
// Controller
@GetMapping("/bookings")
public Page<Booking> list(
@RequestParam Long customerId,
Pageable pageable // ์๋ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ
) {
return bookingRepository.findByCustomerId(customerId, pageable);
}
์์ฒญ ์์:
GET /bookings?customerId=1&page=0&size=20&sort=createdAt,desc
Pageable ์๋ ํ์ฑ:
page: ํ์ด์ง ๋ฒํธ (0๋ถํฐ)size: ํ์ด์ง ํฌ๊ธฐsort: ์ ๋ ฌ (field,asc/desc)Page vs Slice โญ :
| Page | Slice | |
|---|---|---|
| ์ ์ฒด ๊ฐ์ | โ (count ์ฟผ๋ฆฌ ์ถ๊ฐ) | โ |
| ๋ค์ ํ์ด์ง ์กด์ฌ ์ฌ๋ถ | ์์ (์ ์ฒด๋ก ๊ณ์ฐ) | ์์ (size+1 ์กฐํ) |
| ์ฌ์ฉ ์ฌ๋ก | ํ์ด์ง ๋ฒํธ UI | ๋ฌดํ ์คํฌ๋กค |
| ์ฑ๋ฅ | ์ฝ๊ฐ ๋๋ฆผ (count ์ถ๊ฐ) | ๋น ๋ฆ |
Slice ํ์ฉ:
public interface BookingRepository extends JpaRepository<Booking, Long> {
Slice<Booking> findByCustomerId(Long customerId, Pageable pageable);
}
โ ๋ชจ๋ฐ์ผ ๋ฌดํ ์คํฌ๋กค์ ์ ํฉ
Cursor-based Pagination (์ฐธ๊ณ ):
GET /bookings?afterId=100&size=20
ILIC ์๋๋ฆฌ์ค:
12์ฃผ์ฐจ N+1 ํจ์ ์ฃผ์ โญ :
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: ์ ์ฒด
ํต์ฌ ๋๊ตฌ
SpringDoc OpenAPI (Swagger ํ์):
์์กด์ฑ:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
์๋ ์์ฑ:
http://localhost:8080/swagger-ui.html โ UIhttp://localhost:8080/v3/api-docs โ JSON์ด๋ ธํ ์ด์ ์ถ๊ฐ (์ ํ):
@RestController
@Tag(name = "Booking", description = "์์ฝ ๊ด๋ฆฌ API")
public class BookingController {
@Operation(summary = "์์ฝ ์์ฑ", description = "์ ์ด์ ์์ฝ์ ๋ฑ๋กํฉ๋๋ค")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "์ฑ๊ณต"),
@ApiResponse(responseCode = "400", description = "์๋ชป๋ ์์ฒญ")
})
@PostMapping("/bookings")
public ResponseEntity<Booking> create(@RequestBody BookingRequest request) { ... }
}
์ข์ API ๋ฌธ์์ ์์:
1. ๋ช
ํํ ์ค๋ช
(Operation, Parameter)
2. ์์ฒญ/์๋ต ์์
3. ์๋ฌ ์๋ต ์์
4. ์ธ์ฆ ๋ฐฉ๋ฒ
5. ํ์ด์ง ๊ท์น
API ๋ฒ์ ๋ ์ ๋ต:
| ๋ฐฉ์ | ์ |
|---|---|
| URL Path | /api/v1/users, /api/v2/users |
| ํค๋ | X-API-Version: 2 |
| ์ฟผ๋ฆฌ ์คํธ๋ง | /api/users?version=2 |
โ URL Path ๋ฐฉ์์ด ๊ฐ์ฅ ๋ช ํ โญ
ILIC ๊ถ์ฅ:
์๊ธฐ ์ ๊ฒ
โ โ โ ๋ฉด์ ๋จ๊ณจ (๋ฐ๋์):
โ โ ๋งค์ฐ ๊ถ์ฅ:
Phase 2 (DispatcherServlet 9๋จ๊ณ):
Phase 5 (Filter/Interceptor/AOP ๋น๊ต):
์ด์ ์๋ฐ ๋ฐฑ์๋ ํ์คํ ๊ฐ๋ฐ์๊ฐ ์์์ผ ํ ๊ฑฐ์ ๋ชจ๋ ๊ฒ ์ ๋ณธ ์ :
| ์์ญ | ์ฃผ์ฐจ |
|---|---|
| Java ์ธ์ด | 1-3์ฃผ์ฐจ |
| ๋์์ฑ | 4์ฃผ์ฐจ |
| Spring Core (IoC, AOP) | 5, 8-9์ฃผ์ฐจ |
| ํธ๋์ญ์ | 7, 10์ฃผ์ฐจ |
| JPA | 7, 11-12์ฃผ์ฐจ |
| ๋ฐ์ดํฐ๋ฒ ์ด์ค | 13-14์ฃผ์ฐจ |
| Spring MVC + REST API | 15์ฃผ์ฐจ |
๋ค์ ์ถ์ฒ ์ฃผ์ (์๊ธฐ ํ์ต์ฉ):
์ด๋ฒ ์ฃผ์ฐจ๋ ๋๋ฒ๊ฑฐ step-through ๊ฐ ๊ฐ์ฅ ๋น ๋ฅธ ํ์ต๋ฒ์ ๋๋ค:
Phase 2 โ DispatcherServlet:
DispatcherServlet.doDispatch ์ ๋ธ๋ ์ดํฌํฌ์ธํธPhase 3 โ HttpMessageConverter:
MappingJackson2HttpMessageConverter.read() ์ ๋ธ๋ ์ดํฌPhase 5 โ ํธ์ถ ์์: