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 — 호출 순서: