C
Spring Boot/웹MVC/Lesson 05

웹 MVC — *요청이 응답이 되기까지*

45분·theory
이 챕터
1/2

웹 MVC — *요청이 응답이 되기까지*

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ @RestController + @RequestMapping 으로 REST API 4종 (GET/POST/PUT/DELETE) 구현
  • ✅ @RequestBody / @PathVariable / @RequestParam 차이 + 사용
  • ✅ @RestControllerAdvice 로 전역 예외 처리 패턴

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

요청이 *어떻게* 처리되나 — DispatcherServlet 흐름

핵심 한 줄

브라우저가 GET /users/42 요청을 보내면, Spring 은 DispatcherServlet 이라는 교통 정리원 을 거쳐 맞는 컨트롤러 로 전달합니다.

6단계 흐름

1. HTTP 요청 도착 — 내장 Tomcat 이 받음
2. DispatcherServlet — Spring 의 입구. 모든 요청이 여기로
3. HandlerMapping — "이 URL 은 어떤 컨트롤러어떤 메서드?"
4. 컨트롤러 실행 — 비즈니스 로직 + Service 호출
5. 응답 객체 — DTO 리턴 → Jackson 이 JSON 직렬화
6. HTTP 응답 — 브라우저로

code
요청: GET /api/users/42
       │
       ▼
┌──────────────────────┐
│   Tomcat (내장)       │
└────────┬─────────────┘
         │
         ▼
┌──────────────────────┐
│ DispatcherServlet    │  ← 모든 요청의 입구
└────────┬─────────────┘
         │
         ▼
┌──────────────────────┐
│   HandlerMapping     │  ← "어느 메서드?"
└────────┬─────────────┘
         │
         ▼
┌──────────────────────┐
│  UserController      │
│   .get(42)           │  ← 실제 로직
└────────┬─────────────┘
         │
         ▼  Jackson → JSON
{"id":42, "name":"홍길동"}

각 단계는 Spring 이 알아서 합니다. 개발자는 @RestController 클래스 안에 @GetMapping("/users/{id}") 메서드만 작성하면 됩니다.

Filter vs Interceptor vs AOP — 중간에 끼어들기

요청 처리 중간에 공통 로직 (인증·로깅·압축) 을 끼워 넣고 싶을 때 3가지 선택지가 있습니다.

Filter — 서블릿 표준

DispatcherServlet 이전 에 동작. HTTP 응답 전체를 다룹니다.

java
@Component
public class LoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                     HttpServletResponse res,
                                     FilterChain chain) {
        log.info("요청: {} {}", req.getMethod(), req.getRequestURI());
        chain.doFilter(req, res);     // 다음 필터·서블릿으로
        log.info("응답: {}", res.getStatus());
    }
}

용도: 모든 요청에 공통 — 로깅·압축·CORS·인증 토큰 추출

Interceptor — Spring MVC 단계

DispatcherServlet 이후, 컨트롤러 호출 전·후 끼어듭니다.

java
@Component
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;
    }
}

용도: 특정 컨트롤러 패턴 에만 적용. URL 기반 권한·로깅.

AOP — 메서드 단위

특정 메서드 호출 전후. 가장 세밀.

java
@Aspect
@Component
public class TimingAspect {
    @Around("@annotation(Timed)")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        log.info("{} 실행 {}ms", pjp.getSignature(), System.currentTimeMillis() - start);
        return result;
    }
}

@Service
public class OrderService {
    @Timed
    public void process() { ... }
}

@Transactional·@Cacheable 등이 모두 AOP 기반입니다.

언제 무엇을?

  • 모든 요청 공통 → Filter
  • URL 패턴 기반 → Interceptor
  • 특정 메서드 → AOP

한 번 정리

DispatcherServlet 이 모든 요청을 받아 적절한 컨트롤러로 라우팅합니다. 중간에 공통 로직 을 끼우려면 Filter·Interceptor·AOP 셋 중 선택. 세밀할수록 뒤로.

@RestController + @GetMapping — *REST API 만들기*

@Controller vs @RestController

옛 Spring 의 @ControllerView (HTML) 를 반환 했습니다. JSP·Thymeleaf 같은 템플릿 엔진과 결합해 서버 사이드 렌더링 (SSR) 을 했죠.

@RestController데이터 (JSON) 를 반환 합니다. SPA·모바일 앱과 통신하는 REST API 의 표준. 사실상 @Controller + @ResponseBody 의 결합.

java
@RestController         // 모든 메서드 응답이 JSON
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public UserDto get(@PathVariable Long id) {
        return userService.findById(id);
    }
}

리턴 객체 UserDto 가 Jackson 으로 자동 JSON 변환 → 브라우저에 전송. 간결.

HTTP 메서드 매핑

메서드어노테이션용도
GET@GetMapping조회
POST@PostMapping생성
PUT@PutMapping전체 교체
PATCH@PatchMapping부분 수정
DELETE@DeleteMapping삭제

전부 @RequestMapping(method = RequestMethod.GET) 의 단축형. 명시적 의미 가 있으니 항상 위 단축형을 씁니다.

파라미터 받는 4가지 방법

1. @PathVariable — URL 경로의 변수:

java
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) { ... }
// GET /users/42 → id = 42

2. @RequestParam — 쿼리 스트링:

java
@GetMapping("/users")
public List<User> list(@RequestParam(defaultValue = "0") int page,
                        @RequestParam(required = false) String name) { ... }
// GET /users?page=2&name=홍 → page=2, name="홍"

3. @RequestBody — JSON 본문 (POST·PUT):

java
@PostMapping("/users")
public UserDto create(@RequestBody @Valid CreateUserDto dto) { ... }
// POST /users { "name":"홍" } → dto.name = "홍"

4. @RequestHeader / @CookieValue — HTTP 헤더·쿠키:

java
@GetMapping("/me")
public UserDto me(@RequestHeader("Authorization") String token) { ... }

응답 — HTTP 상태 코드 명시

리턴 값만 주면 200 OK 가 기본. 다른 상태가 필요하면:

java
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)        // 201
public UserDto create(@RequestBody CreateUserDto dto) { ... }

// 또는 더 세밀하게:
@GetMapping("/{id}")
public ResponseEntity<UserDto> get(@PathVariable Long id) {
    return userService.findById(id)
        .map(u -> ResponseEntity.ok(u))                 // 200
        .orElse(ResponseEntity.notFound().build());     // 404
}

예외 처리 — @RestControllerAdvice

각 컨트롤러에서 try-catch 도배하지 말고 전역 예외 처리 를 설정:

java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse notFound(EntityNotFoundException e) {
        return new ErrorResponse("NOT_FOUND", e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse validation(MethodArgumentNotValidException e) {
        return new ErrorResponse("VALIDATION_FAILED",
            e.getBindingResult().getAllErrors().toString());
    }
}

이제 어떤 컨트롤러에서든 EntityNotFoundException 던지면 → 자동으로 404 응답.

입력 검증 — @Valid

java
public record CreateUserDto(
    @NotBlank @Email String email,
    @NotBlank @Size(min=8, max=100) String password,
    @NotBlank String name
) { }

@PostMapping("/users")
public UserDto create(@RequestBody @Valid CreateUserDto dto) { ... }

검증 실패 시 → MethodArgumentNotValidException 자동 발생 → 위 GlobalExceptionHandler 에서 깔끔한 400 응답.

한 번 정리

  • @RestController + @GetMapping 으로 REST API 즉시
  • @PathVariable·@RequestParam·@RequestBody 로 입력
  • @ResponseStatus·ResponseEntity 로 상태 코드
  • @RestControllerAdvice 로 전역 예외 처리
  • @Valid + Bean Validation 으로 입력 검증

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 컨트롤러에 @ControllerAdvice 기반 전역 예외 처리 추가해줘"
  • "User CRUD API 4개 (GET/POST/PUT/DELETE) 컨트롤러 만들어줘"
  • "이 응답을 ResponseEntity 로 감싸서 201 / 204 상태코드 정확히 반환하게 해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

웹 MVC — 요청이 응답이 되기까지 - Spring Boot