[Spring] Spring MVC: @RestController, @RequestMapping
정의
Spring MVC는 Servlet 기반 동기 웹 프레임워크. DispatcherServlet이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. WebFlux는 reactive 대안.
기본 컨트롤러
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> list() {
return userService.findAll();
}
@GetMapping("/{id}")
public User get(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User create(@RequestBody @Valid CreateUserDto dto) {
return userService.create(dto);
}
@PatchMapping("/{id}")
public User update(@PathVariable Long id, @RequestBody @Valid UpdateUserDto dto) {
return userService.update(id, dto);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
@RestController = @Controller + @ResponseBody (반환값을 응답 본문으로 직렬화).
Mapping 어노테이션
@RequestMapping의 단축형:
@GetMapping,@PostMapping,@PutMapping,@PatchMapping,@DeleteMapping
@RequestMapping(value = "/users", method = RequestMethod.GET, produces = "application/json")
@GetMapping(value = "/users", produces = "application/json") // 동등
파라미터
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) { ... }
@GetMapping("/users/{userId}/posts/{postId}")
public Post getPost(@PathVariable Long userId, @PathVariable Long postId) { ... }
// path variable 이름 다르면
public User get(@PathVariable("id") Long userId) { ... }
// 쿼리스트링
@GetMapping("/search")
public List<User> search(
@RequestParam String q,
@RequestParam(defaultValue = "1") int page,
@RequestParam(required = false) String sort,
@RequestParam Map<String, String> all
) { ... }
// 헤더
@GetMapping("/me")
public User me(@RequestHeader("Authorization") String auth) { ... }
// 쿠키
public ... (@CookieValue("session") String session) { ... }
// JSON body
@PostMapping("/users")
public User create(@RequestBody CreateUserDto dto) { ... }
// 폼
@PostMapping("/login")
public void login(@ModelAttribute LoginForm form) { ... }
Validation
@PostMapping
public User create(@RequestBody @Valid CreateUserDto dto) { ... }
public record CreateUserDto(
@NotBlank @Email String email,
@NotBlank @Size(min = 1, max = 100) String name,
@Min(0) @Max(150) Integer age
) { }
검증 실패 시 MethodArgumentNotValidException. @ControllerAdvice로 422/400 응답.
ResponseEntity
상태 코드, 헤더 명시 제어.
@GetMapping("/{id}")
public ResponseEntity<User> get(@PathVariable Long id) {
return userService.findOptional(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> create(@RequestBody CreateUserDto dto) {
User user = userService.create(dto);
URI location = URI.create("/users/" + user.id());
return ResponseEntity.created(location).body(user);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
예외 처리: @ControllerAdvice
전역 예외 핸들러.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ProblemDetail> notFound(EntityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ProblemDetail> validation(MethodArgumentNotValidException ex) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
var problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
problem.setProperty("errors", errors);
return ResponseEntity.badRequest().body(problem);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ProblemDetail> generic(Exception ex) {
log.error("unexpected error", ex);
return ResponseEntity.internalServerError()
.body(ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR));
}
}
ProblemDetail (RFC 7807)
3.0+ 표준 에러 응답.
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Invalid input");
pd.setType(URI.create("https://example.com/probs/validation"));
pd.setProperty("errors", List.of(...));
응답:
{
"type": "https://example.com/probs/validation",
"title": "Bad Request",
"status": 400,
"detail": "Invalid input",
"instance": "/api/users",
"errors": [...]
}
Content-Type: application/problem+json.
파일 업로드
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
Path target = Paths.get("uploads/" + file.getOriginalFilename());
Files.copy(file.getInputStream(), target);
return "uploaded";
}
@PostMapping("/upload-multi")
public List<String> uploadMulti(@RequestParam("files") MultipartFile[] files) {
return Arrays.stream(files)
.map(MultipartFile::getOriginalFilename)
.toList();
}
application.yml:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
Content negotiation
@GetMapping(value = "/users/{id}", produces = {"application/json", "application/xml"})
public User get(@PathVariable Long id) { ... }
Accept 헤더 또는 URL 확장자로 분기.
CORS
@RestController
@CrossOrigin(origins = "https://app.example.com")
public class UserController { ... }
@GetMapping("/users")
@CrossOrigin
public List<User> list() { ... }
// 전역
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
Interceptor
public class AuditInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
log.info("{} {}", req.getMethod(), req.getRequestURI());
return true; // false면 요청 중단
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuditInterceptor());
}
}
Filter와 비슷하나 Spring MVC 컨텍스트에서 동작.
정적 리소스
기본적으로 src/main/resources/static/, public/, META-INF/resources/가 자동 서빙.
spring:
web:
resources:
static-locations: classpath:/static/, file:./uploads/
cache:
period: 1d
자주 보는 패턴
DTO 변환
@PostMapping
public UserResponse create(@RequestBody @Valid CreateUserDto dto) {
User user = userService.create(dto.toEntity());
return UserResponse.from(user);
}
엔티티 직접 노출 X. DTO로.
Pagination
@GetMapping
public Page<User> list(Pageable pageable) {
return userRepository.findAll(pageable);
}
쿼리: ?page=0&size=20&sort=name,asc.
Spring Data가 자동.
Bulk
@PostMapping("/bulk")
public List<User> bulkCreate(@RequestBody @Valid List<@Valid CreateUserDto> dtos) {
return userService.bulkCreate(dtos);
}
Streaming
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter();
executor.submit(() -> {
for (int i = 0; i < 10; i++) {
emitter.send("event " + i);
Thread.sleep(1000);
}
emitter.complete();
});
return emitter;
}
SSE. WebFlux의 Flux가 더 자연스러움.
함정
1. @RequestBody는 한 번만 읽음
HttpServletRequest의 input stream은 1회. 같은 요청을 여러 곳에서 읽으면 빈 결과.
2. @PathVariable 이름
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) { ... } // 컴파일 시 -parameters 옵션
public User get(@PathVariable("id") Long userId) { ... } // 명시
-parameters 컴파일러 옵션이 없으면 명시 필요.
3. JSON 역직렬화
Jackson 기본. record 같이 final 필드는 @JsonCreator 또는 builder 필요한 경우 있음.
public record CreateUserDto(String email, String name) { } // OK
4. 트랜잭션과 controller
@Transactional은 service에. controller에 두면 view 렌더링까지 트랜잭션 → lazy loading 문제.
5. exception handler 순서
@ExceptionHandler(Exception.class) // 가장 일반
@ExceptionHandler(RuntimeException.class)
@ExceptionHandler(EntityNotFoundException.class) // 가장 구체
가장 구체적인 핸들러가 먼저 매치. Spring이 알아서 처리.
요청 처리 흐름 (전체 그림)
HTTP 요청
→ Servlet Filter (Spring Security 포함)
→ DispatcherServlet
→ HandlerMapping → handler 결정
→ HandlerInterceptor.preHandle
→ Controller 메서드 실행
→ HandlerInterceptor.postHandle
→ ViewResolver → View 렌더링 (또는 JSON 직렬화)
→ HandlerInterceptor.afterCompletion
→ Filter 역순
→ HTTP 응답
각 단계의 상세:
- Filter: java-servlet-filter (서블릿 컨테이너 레벨)
- DispatcherServlet 내부: spring-dispatcher-servlet
- Interceptor: spring-handler-interceptor
관련 위키
이 글의 용어 (8개)
- [Java] Servlet API: HttpServletRequest, HttpServletResponsejava
- 정의 Servlet 은 Java 진영의 표준 HTTP 처리 API. 컨테이너 (Tomcat / Jetty / Undertow) 가 HTTP 요청을 객체 ( ) 로 만들어 serv…
- [Java] Servlet Filterjava
- 정의 Filter 는 servlet 호출 전/후 에 끼어들어 요청과 응답을 가로채는 컴포넌트. 인증, 로깅, 압축, CORS, 인코딩 변환 같은 횡단 관심사 (cross-cutt…
- [Spring] Bean Validation: @Valid, @NotBlank, customspring
- 정의 Spring은 Jakarta Bean Validation (JSR-380)을 표준으로 사용. , , 등 어노테이션으로 선언적 검증. Spring MVC가 로 자동 통합. 설…
- [Spring] DispatcherServlet: Front Controller 내부spring
- 정의 가 / 사용법 을 다룬다면, 이 페이지는 그 모든 요청을 받아 처리하는 의 내부 메커니즘 을 다룬다. 은 본질적으로 하나의 Servlet (). Front Controlle…
- [Spring] HandlerInterceptor: preHandle, postHandle, afterCompletionspring
- 정의 HandlerInterceptor 는 내부에서 handler (Controller 메서드) 실행 전후 에 끼어드는 Spring MVC 전용 메커니즘. Filter 와 비슷하…
- [Spring] RequestContextHolder, LocaleContextHolder: ThreadLocal 기반 요청 컨텍스트spring
- 정의 Spring 은 HTTP 요청 / 응답 / locale / 트랜잭션 / 인증 같은 요청 범위 데이터를 ThreadLocal 에 저장 해 전역 노출한다. 메서드 인자로 일일이…
- [Spring] Security: Filter Chain, JWT, OAuth2spring
- 정의 Spring Security는 인증·인가의 사실상 표준. 서블릿 필터 체인 위에 다양한 인증/권한 처리를 모듈식으로. 6.0+ 부터 Lambda DSL 표준, Bean 방식…
- [Spring] WebFlux: Reactive, Mono, Fluxspring
- 정의 Spring WebFlux는 Project Reactor 기반의 비동기·non-blocking 웹 프레임워크. Spring MVC와 별개. Netty 기본 (Tomcat도 …
이 개념을 다룬 위키 페이지 (20)
- wiki[Java] JavaBean 프로퍼티 규약
- wiki[Java] Servlet API: HttpServletRequest, HttpServletResponse
- wiki[Java] Servlet 생명주기
- wiki[Frontend] SPA Architecture: CSR, SSR, SSG, hydration
- wiki[Spring] Bean Scope 심화: Singleton, Prototype, Web Scope
- wiki[Spring Boot] Embedded Web Server 설정과 튜닝
- wiki[Spring] DispatcherServlet: Front Controller 내부
- wiki[Spring] HandlerInterceptor: preHandle, postHandle, afterCompletion
- wiki[Spring MVC] Argument Resolver: Custom HandlerMethodArgumentResolver
- wiki[Spring MVC] Content Negotiation: Accept 헤더, MediaType, ContentNegotiationStrategy
- wiki[Spring MVC] Exception Handling: @ExceptionHandler, ControllerAdvice, ProblemDetail
- wiki[Spring MVC] Form Handling: @ModelAttribute, DataBinder, conversion
- wiki[Spring MVC] Model + BindingResult: 뷰 모델과 바인딩 오류 처리
- wiki[Spring] RequestContextHolder, LocaleContextHolder: ThreadLocal 기반 요청 컨텍스트
- wiki[Spring] Security: Filter Chain, JWT, OAuth2
- wiki[Spring Security] OAuth2 Authorization Code Flow + PKCE
- wiki[Spring Security] Testing: @WithMockUser, MockMvc, jwt(), oauth2Login()
- wiki[Spring] Stereotype 어노테이션: @Component, @Service, @Repository, @Controller
- wiki[Spring MVC] Thymeleaf: 서버 사이드 템플릿 엔진
- wiki[Spring] Virtual Threads (Project Loom): Spring 6.1+
💬 댓글