자바 백엔드 입문 27편. @Controller·@RequestMapping의 표준 패턴, 클래스 레벨 + 메서드 레벨 매핑 합성, HTTP 메서드별 단축 어노테이션 5종을 우편함 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 27편이에요. 25편에서 DispatcherServlet의 9단계 흐름을 풀었다면, 이번 27편은 @Controller + @RequestMapping — 그 흐름의 가장 중요한 한 부품인 컨트롤러 작성의 표준 패턴을 풀어 가요.
@RequestMapping이 헷갈리는 이유
처음 Spring 컨트롤러 코드를 보면 @RequestMapping·@GetMapping·@PostMapping 등이 클래스에도 박히고 메서드에도 박혀 있어요. "둘이 합쳐지는 건가? 우선순위가 있나?" 가 안 잡혀요.
이 글에서는 우편함 비유로 풀어요. 클래스 @RequestMapping("/orders") = 우편함 주소(아파트 동), 메서드 @GetMapping("/{id}") = 우편함 호수(305호). 두 개가 합쳐져서 "/orders/{id}" 완성 주소. 끝까지 따라오시면 컨트롤러 매핑 5가지 옵션이 한 그림에 들어와요.
기본 컨트롤러 패턴
@Controller 가 박힌 클래스 안에 메서드 + @GetMapping 같은 HTTP 메서드 매핑. 이게 모든 컨트롤러의 기본 골격이에요.
@Controller
public class OrderController {
@GetMapping("/orders/{id}")
public String getOrder(@PathVariable Long id, Model model) {
model.addAttribute("order", orderService.findById(id));
return "order-view"; // 뷰 이름 반환 (HTML 렌더)
}
}
이 한 메서드가 하는 일 — "GET /orders/123 요청이 오면 getOrder(123, ...) 호출, 결과를 order-view 라는 HTML 템플릿으로 렌더". 컨트롤러 한 클래스 안에 메서드 여러 개가 있으면 — 각 메서드마다 별도 URL 매핑 등록.
@RequestMapping vs @GetMapping 단축형
매핑 어노테이션의 원조는 @RequestMapping 이에요. "이 메서드를 이런 HTTP 요청에 매핑해라" 모든 옵션을 다 받는 만능 어노테이션.
@RequestMapping(value = "/orders", method = RequestMethod.GET)
public String list() { ... }
이게 너무 길어서 Spring 4.3부터 HTTP 메서드별 단축형 5종이 도입됐어요.
| 단축형 | 원본 |
|---|---|
@GetMapping("/path") | @RequestMapping(value="/path", method=GET) |
@PostMapping("/path") | @RequestMapping(value="/path", method=POST) |
@PutMapping("/path") | @RequestMapping(value="/path", method=PUT) |
@PatchMapping("/path") | @RequestMapping(value="/path", method=PATCH) |
@DeleteMapping("/path") | @RequestMapping(value="/path", method=DELETE) |
현대 Spring 코드는 단축형이 거의 100%. @RequestMapping 단독은 "한 메서드에 여러 HTTP 메서드 묶음 처리" 같은 특수 케이스에만 봐요.
클래스 레벨 + 메서드 레벨 매핑 합성 — 우편함 주소
@RequestMapping 은 클래스에도 박을 수 있어요. 그러면 클래스 매핑이 모든 메서드 매핑의 접두사가 돼요.
@Controller
@RequestMapping("/orders") // ← 클래스 레벨, "아파트 동 주소"
public class OrderController {
@GetMapping // → /orders
public String list() { ... }
@GetMapping("/{id}") // → /orders/{id}
public String detail(@PathVariable Long id) { ... }
@PostMapping // → /orders (POST)
public String create(...) { ... }
@DeleteMapping("/{id}") // → /orders/{id} (DELETE)
public String delete(@PathVariable Long id) { ... }
}
클래스에 /orders 를 박으면 — 안의 모든 메서드 매핑이 자동으로 /orders 접두사를 갖게 돼요. 반복 제거 + 한눈에 보기 쉬움이 핵심 이점.
비유로 풀면 — 클래스 @RequestMapping("/orders") = "이 컨트롤러는 주문 관련 우편함 전체 담당", 메서드 @GetMapping("/{id}") = "이 메서드는 그 우편함 안에서 특정 호수 담당". 두 개가 합쳐져 "/orders/{id} 완성 주소".
@RequestMapping의 5가지 옵션
가장 자주 만나는 5가지 옵션. 단축형 어노테이션에도 다 적용 가능.
1. value (또는 path) — URL 패턴
@GetMapping("/orders/{id}") // 단일
@GetMapping({"/orders", "/order"}) // 여러 경로
@GetMapping("/files/**") // 와일드카드
{id} 같은 PathVariable, ** 같은 와일드카드, 정규식까지 지원.
2. params — 쿼리 스트링 조건
@GetMapping(value = "/orders", params = "status=active")
public String activeOrders() { ... }
// /orders?status=active 만 매칭
특정 쿼리 파라미터가 있을 때만 매칭. 같은 URL에 여러 메서드를 분기할 때 유용.
3. headers — HTTP 헤더 조건
@PostMapping(value = "/api/orders", headers = "X-API-Version=v2")
public Order createV2(...) { ... }
// X-API-Version: v2 헤더가 박혀 있을 때만 매칭
API 버전 분기에 자주 쓰여요.
4. consumes — 요청 본문의 Content-Type
@PostMapping(value = "/orders", consumes = MediaType.APPLICATION_JSON_VALUE)
public Order create(@RequestBody OrderRequest req) { ... }
// Content-Type: application/json 요청만 매칭
JSON·XML·multipart 분기.
5. produces — 응답의 Content-Type
@GetMapping(value = "/orders/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Order json(@PathVariable Long id) { ... }
@GetMapping(value = "/orders/{id}", produces = MediaType.APPLICATION_XML_VALUE)
public Order xml(@PathVariable Long id) { ... }
같은 URL인데 클라이언트의 Accept 헤더에 따라 분기. "콘텐츠 협상(Content Negotiation)" 이라고 불러요.
매핑 우선순위 — 더 구체적인 게 이김
같은 URL에 매칭되는 매핑이 여러 개 있으면 Spring이 가장 구체적인 매핑 을 골라요. 룰:
- 정확한 경로 > 와일드카드
- params/headers/consumes/produces 조건 많은 게 우선
- HTTP 메서드 명시 > 미명시
예를 들어:
@GetMapping("/orders/{id}") // 일반
public Order getById(@PathVariable Long id) { ... }
@GetMapping(value = "/orders/{id}", params = "preview=true") // 더 구체적
public Order getPreview(@PathVariable Long id) { ... }
/orders/123?preview=true 요청이 오면 — 두 메서드 다 매칭되지만 params 조건이 있는 두 번째가 우선. /orders/123 만 요청이면 첫 번째 매칭.
@PathVariable — URL 안의 변수 추출
{id} 같은 PathVariable을 메서드 매개변수로 받는 패턴.
@GetMapping("/orders/{orderId}/items/{itemId}")
public Item getItem(
@PathVariable Long orderId,
@PathVariable Long itemId) { ... }
매개변수 이름이 URL 변수 이름과 같으면 자동 매핑. 다르면 명시:
@GetMapping("/orders/{id}")
public Order get(@PathVariable("id") Long orderId) { ... }
22편에서 깊이 다룰 부분.
REST API 표준 컨벤션은 명확해요. 리소스 단위 복수형 URL(/orders) + HTTP 메서드로 동작 분기(GET 조회·POST 생성·PUT 전체 수정·PATCH 부분 수정·DELETE 삭제). URL 안에 /getOrder·/createOrder 같은 동사를 박지 않아요.
실전 예 — REST API 컨트롤러 전체 골격
위 다 합쳐서 "주문 관리 REST API" 한 컨트롤러를 짜면 이렇게 돼요.
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping
public List<Order> list(@RequestParam(required = false) String status) {
return orderService.findAll(status);
}
@GetMapping("/{id}")
public Order detail(@PathVariable Long id) {
return orderService.findById(id);
}
@PostMapping
public Order create(@RequestBody @Valid OrderRequest req) {
return orderService.create(req);
}
@PatchMapping("/{id}")
public Order update(@PathVariable Long id, @RequestBody OrderUpdateRequest req) {
return orderService.update(id, req);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
orderService.delete(id);
}
}
5개 메서드로 "주문 CRUD 전체" 가 완성. 이게 한국 회사 백엔드의 표준 컨트롤러 패턴이에요.
한 줄 정리 — @Controller 클래스 안에 @GetMapping·@PostMapping 메서드. 클래스 레벨 @RequestMapping("/path") 가 모든 메서드의 접두사. 5가지 옵션(params·headers·consumes·produces)으로 정교한 분기.
시험 직전 한 번 더 — 컨트롤러 매핑 입문자가 매번 헷갈리는 것
@Controller= HTML 뷰 렌더 (ViewResolver 거침)@RestController=@Controller+@ResponseBody, JSON 직접 반환@RequestMapping= 매핑 어노테이션의 원조. 모든 옵션 받음- HTTP 메서드별 단축형 5종 =
@GetMapping·@PostMapping·@PutMapping·@PatchMapping·@DeleteMapping - 현대 Spring = 단축형이 거의 100%
- 클래스 + 메서드 매핑 합성 = 클래스
/orders+ 메서드/{id}→/orders/{id} - 클래스 레벨 매핑 = 반복 제거 + 가독성 향상
- 5가지 옵션 =
value·params·headers·consumes·produces params= 쿼리 스트링 조건,headers= HTTP 헤더 조건consumes= 요청 Content-Type,produces= 응답 Content-Typeproduces활용 = 콘텐츠 협상 (JSON vs XML vs Protobuf)- 매핑 우선순위 = 더 구체적인 매핑이 우선
- 정확한 경로 > 와일드카드, 조건 많은 게 > 적은 것
@PathVariable= URL 변수 추출 (/{id}→ 메서드 매개변수)- 매개변수 이름이 URL 변수와 같으면 자동 매핑
- 다르면
@PathVariable("id")명시 - REST 컨벤션 = 복수형 리소스 URL + HTTP 메서드로 동작 분기
- URL에
/getOrder·/create동사 박지 X - 같은 URL 여러 컨트롤러 매핑 충돌 시 — 시작 시 즉시 예외
- 한 컨트롤러 = 한 리소스 도메인 (
OrderController=/orders영역) @RequestMapping단독은 거의 안 쓰임 — 다중 HTTP 메서드 묶음에만
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 22편 — SpEL 표현식 언어
- 23편 — AOP 횡단 관심사가 뭔가
- 24편 — @Aspect로 첫 AOP 박기
- 25편 — DispatcherServlet 요청 처리 흐름
- 26편 — Filter vs Interceptor 비교
다음 글: