Controller의 역할
Spring에서 Controller는 어플리케이션의 프레젠테이션 계층을 담당한다.
웹 브라우저나 모바일 어플 등 클라이언트 단에서 보내는 요청을 처리하고 응답을 반환하는 역할이다.
주요 핵심 역할은 다음과 같다.
- 클라이언트 요청 처리: HTTP 요청 데이터를 받아 적절한 Service layer로 전달.
- Response 생성: 처리결과를 기반으로 클라이언트에 반환할 데이터 생성
- URI 매핑: 특정 요청 URI와 method를 매핑하여 적절한 처리로직이 실행되도록 한다.
Controller 코드 작성방법
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping(value = "/", name = "유저 목록 조회")
public ResponseEntity<UserListRpDto> getAllUsers() {
UserListRpDto userInfoRpDtoList = userService.getAllUserInfo();
return ResponseEntity.ok(userInfoRpDtoList);
}
@PostMapping(value = "/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody UserDto userDto) {
Long userId = userService.registerUser(userDto);
return ResponseEntity.status(HttpStatus.CREATED).body(userId);
}
@DeleteMapping(value = "/{userId}/delete")
public ResponseEntity<Long> deleteUser(@PathVariable Long userId) {
Long id = userService.deleteUser(userId);
return ResponseEntity.ok(id);
}
@GetMapping(value = "/{userId}/info")
public ResponseEntity<UserInfoRpDto> getUserInfo(@PathVariable Long userId) {
UserInfoRpDto rpDto = userService.getUserInfo(userId);
return ResponseEntity.ok(rpDto);
}
}
Spring의 Controller는 클래스 단에 @RestController나 @Controller 어노테이션을 이용하여 정의한다.
REST API 중심의 어플리케이션에서는 @RestController를 사용하고, @Controller와 @ResponseBody가 결합된것이다.
그 다음 @RequestMapping 어노테이션으로 클래스 및 메소드 레벨의 URI 매핑을 해준다.
주로 Controller는 특정 엔티티나 도메인 기준으로 분류하여 생성하기 때문에 URI 매핑에 이를 구분해주면 된다.
예를 들어 위 코드는 User에 대한 요청을 처리하는 API를 작성하기 때문에 UserController 클래스이고, @RequestMapping("/api/users")라는 URI를 매핑하였다.
Controller 클래스 내부의 메소드는 각 API를 선언한다고 생각하면 된다.
이 때 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 어노테이션으로 HTTP 메소드별로 요청을 매핑한다.
API URI를 설계할 때는 RESTful API의 일관성을 유지하기 위하여 명사형 URI를 사용하고, 계층적으로 쌓아가면 된다.
예를 들어 유저 관련된 API라면 "api/users/{id}"와 같이 유저에 대한기능을 users에 몰아넣고 뒤에 id 값을 입력받는다.
여기서 특정 유저의 주식종목에 대한 요청이 있다면 계층적으로 "api/users/{id}/stocks/{stockCode}/buy" 이런식으로 URI를 설계하여 유저의 주식 구매 요청을 처리할 수 있다.
Request 파라미터 처리 방법
Controlloer에서 데이터를 입력받는 방법은 크게 3가지로 나뉜다.
바로 RequestParam, PathVariable, RequestBody이다.
@ReuqestParam
RequestParam은 쿼리 파라미터나 폼 데이터를 처리할 때 사용된다.
간단한 데이터를 전달할 때 적합하며 검색 필터나 페이지 번호 등을 전달받을 때 많이 사용한다.
예를 들어 한국에 살고있는 유저의 목록을 조회한다면, RequestParam에 국가 정보를 넣어 "api/users/search?country=ko" 이런식으로 파라미터를 전달받을 수 있다.
@GetMapping("/search")
public String search(@RequestParam(name="key") String keyword, @RequestParam(defaultValue = "10") int limit) {
return "검색 키워드: " + keyword + ", 제한: " + limit;
}
구현은 위와 같이 API method의 parameter 앞에 @RequestParam 어노테이션을 붙여주면 동일한 이름의 변수에 값이 매핑된다.
만약 name을 지정해준다면 URL에 붙는 파라미터 이름과 메소드의 파라미터 이름을 다르게 매핑할 수 있다.
또한 defaultValue를 지정하면 URL에 값이 입력되지 않았을 때의 기본값을 설정할 수 있다.
@PathVariable
PathVariable은 URI 경로의 일부를 변수로 받아서 처리할 때사용한다.
RESTful URI 설계에 적합하며 리소스 ID나 계층적인 구조를 표현할 때 사용한다.
예를 들어 Tommy라는 회원의 정보를 조회하기 위해서 "api/users/Tommy" 이런식으로 URI를 구성할 수 있다.
@GetMapping("/users/{id}")
public String getUserById(@PathVariable Long id) {
return "사용자 ID: " + id;
}
@*Mapping 어노테이션의 경로에서 curly barcket으로 감싸게 되면 해당 부분을 pathvariable로 치환하겠다는 의미이다.
따라서 "users/16"으로 API 요청이 들어오게 된다면, 메소드의 파라미터인 Long id 값에는 16이 매핑되게 되는것이다.
@RequestBody
@PostMapping(value = "/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody UserDto userDto) {
Long userId = userService.registerUser(userDto);
return ResponseEntity.status(HttpStatus.CREATED).body(userId);
}
POST API에서는 특정 로직을 수행하기 위한 복잡한 입력값들이 들어오는 경우가 다양하다.
예를 들어 새로운 신규 유저의 회원가입 로직을 처리하기 위해서는 로그인 아이디/패스워드, 이메일, 인적사항 등 다양한 정보가 들어오게 된다.
이러한 변수값들을 @RequestParam이나 @PathVariable로 처리하기에는 URI가 너무 더러워지기 때문에, 이러한 복잡한 데이터 구조의 입력값을 받을 때는 @RequestBody를 사용한다.
RequestBody는 JSON이나 XML 형식의 HTTP 요청 바디를 객체로 매핑해준다.
위 코드에서는 UserDto라는 DTO 클래스를 정의하여 회원 정보를 입력할 수 있게하고, 입력받은 body를 해당 객체로 생성해준다.
@Valid를 어노테이션을 붙이면 입력한 요청이 해당 DTO 클래스의 형식과 일치하는지 검증도 가능하다.
요약하자면 다음과 같은 용도구분으로 세 가지 request 파라미터 처리방법이 있다.
- @RequestParam: 검색 조건을 위한 query parameter
- @PathVariable: 리소스 ID나 계층적 구조
- @RequestBody: 복잡한 데이터 구조
ResponseEntity 응답 처리
Controller의 response 반환은 ResponseEntity 객체를 통해서 수행한다.
@GetMapping(value = "/", name = "유저 목록 조회")
public ResponseEntity<UserListRpDto> getAllUsers() {
UserListRpDto userInfoRpDtoList = userService.getAllUserInfo();
return ResponseEntity.ok(userInfoRpDtoList);
}
@PostMapping(value = "/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody UserDto userDto) {
Long userId = userService.registerUser(userDto);
return ResponseEntity.status(HttpStatus.CREATED).body(userId);
}
@DeleteMapping(value = "/{userId}/delete")
public ResponseEntity<Long> deleteUser(@PathVariable Long userId) {
Long id = userService.deleteUser(userId);
return ResponseEntity.ok(id);
}
위 코드를 다시 참고하자면, 각 API 메소드 타입은 ResponseEntity에 대한 제너릭 타입을 사용하여 응답 데이터의 타입을 지정하는 것을 확인할 수 있다.
ResponseEntity<?>로 정의하면 response object 값에 아무 값이나 들어올 수 있다.
ResponseEntity<Long> 처럼 java primitive data type으로 지정하거나
ResponseEntity<UserListRpDto> 정의한 DTO 클래스를 반환할 수도 있다.
주로 세 번째 방법처럼 Response 마다 적절한 DTO 클래스를 설계하여 반환하는 편이다.
또한 ResponseEntity를 사용하면 response body 뿐만 아니라 HTTP 상태코드를 명시적으로 설정할 수 있다.
@GetMapping("/users/{id}")
public ResponseEntity<String> getUser(@PathVariable Long id) {
try {
User user = userService.findById(id);
if (user != null) {
return ResponseEntity.ok("사용자 ID: " + user.getId());
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("사용자를 찾을 수 없습니다.");
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("에러 발생: " + e.getMessage());
}
}
위 예시처럼 userService에서 특정 ID를 가지는 유저 정보를 반환하는 API가 있다고 해보자.
정상적으로 User를 가져왔을 경우 ok로 응답하고, 유저가 없을 경우 404 NOT_FOUND를 반환한다.
또한 내부적으로 에러가 발생하여 exception이 throw 될 수 있으니 try-catch 구문으로 예외처리를 할 수 있다.
주로 아래 4개 메서드를 이용하여 API method 반환을 수행한다.
- ResponseEntity.ok(T body): HTTP 200 OK와 함께 본문 데이터를 반환.
- ResponseEntity.status(HttpStatus): 특정 HTTP 상태 코드를 설정.
- ResponseEntity.notFound(): HTTP 404 Not Found 응답.
- ResponseEntity.badRequest(): HTTP 400 Bad Request 응답.