SpringMVCRequestMapping
1️⃣ 요청 매핑 기본
요청 매핑이란? 🎯
정의: 클라이언트 요청 URL을 애플리케이션의 특정 메서드에 매핑하는 과정
목적: HTTP 요청이 들어왔을 때 어떤 컨트롤러가 처리할지 결정
핵심:
@RequestMapping
어노테이션과 그 파생 어노테이션들을 사용
기본 매핑 어노테이션 📌
// 기본 형태
@RequestMapping("/hello")
public String hello() {
return "hello";
}
// HTTP 메서드 지정
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String helloGet() {
return "hello-get";
}
// 간편 어노테이션
@GetMapping("/hello")
@PostMapping("/hello")
@PutMapping("/hello")
@DeleteMapping("/hello")
@PatchMapping("/hello")
매핑 조건 다양화 🔀
// URL 경로 변수
@GetMapping("/users/{userId}")
public String user(@PathVariable("userId") String userId) {
return "user " + userId;
}
// 다중 경로 변수
@GetMapping("/users/{userId}/orders/{orderId}")
public String getOrder(@PathVariable String userId,
@PathVariable Long orderId) {
return "user=" + userId + ", order=" + orderId;
}
// 특정 파라미터 조건
@GetMapping(value = "/users", params = "type=admin")
public String usersAdmin() {
return "admin users";
}
// 특정 헤더 조건
@GetMapping(value = "/users", headers = "mode=dark")
public String usersDarkMode() {
return "dark mode users";
}
// 미디어 타입 조건 (Content-Type)
@PostMapping(value = "/users", consumes = "application/json")
public String createUserJson() {
return "create user with JSON";
}
// Accept 헤더 조건
@GetMapping(value = "/users", produces = "application/json")
public String getUsersJson() {
return "get users as JSON";
}
2️⃣ HTTP 요청 매핑 패턴
URL 패턴 매칭 방식 🧩
// 1. 정확한 경로 매칭
@GetMapping("/users/login") // /users/login
// 2. 경로 변수 매칭
@GetMapping("/users/{id}") // /users/1, /users/abc 등
// 3. 와일드카드 매칭
@GetMapping("/files/**") // /files, /files/a, /files/a/b 등
// 4. 정규식 패턴
@GetMapping("/files/{filename:.+}") // .이 포함된 파일명 매칭
// 5. 접미사 패턴 매칭 (스프링 부트 기본 비활성화)
@GetMapping("/files/*.png") // /files/image.png 등
요청 매핑 우선순위 📊
정확한 경로 매칭이 가장 높은 우선순위
경로 변수가 다음 우선순위
와일드카드 매칭이 가장 낮은 우선순위
@GetMapping("/users/new") // 우선순위 1: 정확한 경로
@GetMapping("/users/{id}") // 우선순위 2: 경로 변수
@GetMapping("/users/*") // 우선순위 3: 와일드카드
3️⃣ 요청 매핑 실제 API 예시
RESTful 리소스 설계 예시 🌐
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 전체 상품 목록 조회
@GetMapping
public List<ProductDto> getAllProducts() {
// ...
}
// 특정 상품 조회
@GetMapping("/{id}")
public ProductDto getProduct(@PathVariable Long id) {
// ...
}
// 상품 생성
@PostMapping
public ResponseEntity<ProductDto> createProduct(@RequestBody ProductDto productDto) {
// ...
return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
}
// 상품 수정
@PutMapping("/{id}")
public ProductDto updateProduct(@PathVariable Long id,
@RequestBody ProductDto productDto) {
// ...
}
// 상품 부분 수정
@PatchMapping("/{id}")
public ProductDto patchProduct(@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
// ...
}
// 상품 삭제
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
// ...
return ResponseEntity.noContent().build();
}
// 상품 검색
@GetMapping("/search")
public List<ProductDto> searchProducts(@RequestParam String keyword) {
// ...
}
}
계층형 리소스 관계 매핑 예시 📊
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// 주문 목록 조회
@GetMapping
public List<OrderDto> getOrders() {
// ...
}
// 특정 주문 조회
@GetMapping("/{orderId}")
public OrderDto getOrder(@PathVariable Long orderId) {
// ...
}
// 주문 내 상품 목록 조회
@GetMapping("/{orderId}/items")
public List<OrderItemDto> getOrderItems(@PathVariable Long orderId) {
// ...
}
// 주문 내 특정 상품 조회
@GetMapping("/{orderId}/items/{itemId}")
public OrderItemDto getOrderItem(@PathVariable Long orderId,
@PathVariable Long itemId) {
// ...
}
// 주문 상태 변경
@PatchMapping("/{orderId}/status")
public OrderDto updateOrderStatus(@PathVariable Long orderId,
@RequestBody String status) {
// ...
}
}
4️⃣ 컨트롤러 구현 패턴
전통적인 @Controller 패턴 📝
@Controller
public class HomeController {
// 뷰 이름 반환
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome home!");
return "home"; // ViewResolver가 home.html 찾아 렌더링
}
// 리다이렉트
@PostMapping("/redirect-demo")
public String redirectDemo() {
return "redirect:/home"; // 리다이렉트 응답 (300번대 상태 코드)
}
// 포워드
@GetMapping("/forward-demo")
public String forwardDemo() {
return "forward:/home"; // 서버 내부에서 다른 URL로 전달
}
}
@RestController 패턴 📊
@RestController // @Controller + @ResponseBody 결합
@RequestMapping("/api")
public class ApiController {
// 단순 문자열 반환
@GetMapping("/hello")
public String hello() {
return "Hello World"; // 응답 바디에 직접 출력
}
// 객체 반환 (JSON 변환)
@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) {
UserDto user = new UserDto();
user.setId(id);
user.setName("User " + id);
return user; // JSON으로 자동 변환
}
}
요청/응답 유형별 패턴 📋
@RestController
@RequestMapping("/api/patterns")
public class PatternDemoController {
// 1. 경로 변수 + 쿼리 파라미터
@GetMapping("/users/{userId}")
public UserDto getUserWithParams(
@PathVariable Long userId,
@RequestParam(required = false) String fields) {
UserDto user = userService.findById(userId);
// fields 파라미터로 필요한 필드만 선택 (예: ?fields=id,name)
if (fields != null) {
// 필드 필터링 로직
}
return user;
}
// 2. 요청 바디 + 응답 상태 코드
@PostMapping("/resources")
public ResponseEntity<ResourceDto> createResource(
@RequestBody @Valid ResourceDto resourceDto) {
ResourceDto created = resourceService.create(resourceDto);
// 생성 응답 (201 Created)
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity
.created(location) // 201 + Location 헤더
.body(created);
}
// 3. 검증 오류 처리
@PostMapping("/validate")
public ResponseEntity<?> validateData(
@RequestBody @Valid DataDto dataDto,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity
.badRequest() // 400 Bad Request
.body(errors);
}
// 검증 통과 후 처리
return ResponseEntity.ok(dataService.process(dataDto));
}
// 4. 조건부 응답
@GetMapping("/conditional/{id}")
public ResponseEntity<ResourceDto> getConditional(
@PathVariable Long id,
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
ResourceDto resource = resourceService.findById(id);
String eTag = "\"" + resource.getVersion() + "\"";
// ETag가 일치하면 304 Not Modified
if (eTag.equals(ifNoneMatch)) {
return ResponseEntity
.status(HttpStatus.NOT_MODIFIED)
.eTag(eTag)
.build();
}
// 변경된 리소스 반환
return ResponseEntity
.ok()
.eTag(eTag)
.body(resource);
}
}
5️⃣ 요청 처리 고급 기법
핸들러 메서드 인터셉터 🔍
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/admin/**");
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/user/**", "/api/**");
}
}
public class LoggingInterceptor implements HandlerInterceptor {
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
log.info("Request URI: {}", request.getRequestURI());
return true; // true: 계속 진행, false: 요청 중단
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
log.info("Response status: {}", response.getStatus());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (ex != null) {
log.error("Exception: {}", ex.getMessage());
}
}
}
URL 경로 커스터마이징 🛣️
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseSuffixPatternMatch(false) // *.html 같은 접미사 패턴 비활성화
.setUseTrailingSlashMatch(true); // 후행 슬래시 매칭 활성화 (/users == /users/)
}
@Override
public void addFormatters(FormatterRegistry registry) {
// URL 경로 변수 변환을 위한 컨버터 등록
registry.addConverter(new StringToEnumConverter());
}
}
// 경로 변수를 enum 값으로 변환하는 커스텀 컨버터
public class StringToEnumConverter implements Converter<String, UserType> {
@Override
public UserType convert(String source) {
return UserType.valueOf(source.toUpperCase());
}
}
// API URL 버전 관리
@RestController
@RequestMapping("/api/v1") // API 버전을 URL에 포함
public class ApiV1Controller {
// ...
}
@RestController
@RequestMapping(path = "/api",
headers = "API-Version=2") // 헤더로 버전 구분
public class ApiV2Controller {
// ...
}
예외 처리 @ExceptionHandler 🚨
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 컨트롤러 내부의 예외 처리
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<Map<String, String>> handleProductNotFound(
ProductNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", "Product not found");
error.put("message", ex.getMessage());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(error);
}
@GetMapping("/{id}")
public ProductDto getProduct(@PathVariable Long id) {
return productService.findById(id)
.orElseThrow(() -> new ProductNotFoundException("Product not found: " + id));
}
}
// 전역 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGlobalException(Exception ex) {
Map<String, String> error = new HashMap<>();
error.put("error", "Internal Server Error");
error.put("message", ex.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errors);
}
}
6️⃣ 원격 API 호출 및 통합 구현
RestTemplate 활용 🌐
@Service
public class ExternalApiService {
private final RestTemplate restTemplate;
public ExternalApiService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
public UserDto getUserFromExternalApi(Long id) {
String url = "https://api.example.com/users/{id}";
return restTemplate.getForObject(url, UserDto.class, id);
}
public ResponseEntity<UserDto> createUserInExternalApi(UserDto user) {
String url = "https://api.example.com/users";
return restTemplate.postForEntity(url, user, UserDto.class);
}
}
// 컨트롤러에서 활용
@RestController
@RequestMapping("/api/proxy")
public class ProxyController {
private final ExternalApiService externalApiService;
@Autowired
public ProxyController(ExternalApiService externalApiService) {
this.externalApiService = externalApiService;
}
@GetMapping("/users/{id}")
public UserDto proxyGetUser(@PathVariable Long id) {
return externalApiService.getUserFromExternalApi(id);
}
}
WebClient 활용 (비동기 API 호출) 🚀
@Service
public class ReactiveApiService {
private final WebClient webClient;
public ReactiveApiService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<UserDto> getUserReactive(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserDto.class);
}
public Flux<UserDto> getAllUsersReactive() {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(UserDto.class);
}
}
// 컨트롤러에서 활용
@RestController
@RequestMapping("/api/reactive")
public class ReactiveController {
private final ReactiveApiService reactiveApiService;
@Autowired
public ReactiveController(ReactiveApiService reactiveApiService) {
this.reactiveApiService = reactiveApiService;
}
@GetMapping("/users/{id}")
public Mono<UserDto> getUser(@PathVariable Long id) {
return reactiveApiService.getUserReactive(id);
}
@GetMapping("/users")
public Flux<UserDto> getAllUsers() {
return reactiveApiService.getAllUsersReactive();
}
}
Last updated