
지난 포스트에서 MVC 패턴을 대략적으로 알아보았으니, C를 담당하는 컨트롤러에 대해 알아보겠습니다.
웹 애플리케이션(외 대부분의 애플리케이션)들은 사용자가 내부 로직(비즈니스 코드, 모델 등)을 알 필요가 없으며 직접적인 상호작용을 하지 않습니다.
네이버 로그인을 한다고 하면 사용자는 아이디와 비밀번호를 입력하고 로그인 버튼을 누를 뿐, 사용자가 폼 데이터를 전송하는 것을 직접 보거나 DB 쿼리를 조회하는 등의 동작을 보거나 보여주지 않습니다.
또한 대부분의 경우 사용자에게 보여지는 웹 화면은 HTML/CSS/JS를 통해 렌더링되고, 비즈니스 코드는 Java 등을 사용하므로 언어 체계나 동작 과정에서도 차이가 있습니다.
이때 사용자의 요청과 비즈니스 코드 등을 연결(View - Model을 연결)해주는 역할이 바로 컨트롤러 Controller입니다.
스프링 환경에서 사용자의 요청이 들어오면 컨트롤러가 가로채어 해당 업무를 수행합니다.
요청 처리를 지시받은 컨트롤러는 서비스를 호출하여 요청을 처리하게 됩니다. 이후 서비스가 로직을 수행해서 반환된 결과물을 Model에 담고 적절한 View를 호출해서 결과를 사용자에게 보여주게 됩니다.
스프링 프레임워크에서 이러한 컨트롤러를 구현하기 위해 @Contoller 어노테이션을 사용합니다.
@Controller
public class AuthController {...}
위와 같이 특정 클랙스에 @Controller 어노테이션을 붙이면 스프링이 시작되고 Component Scan의 컨테이너 등록 과정에서 해당 클래스를 컨트롤러로 인식하고 스프링 빈으로 등록합니다.
등록 후에는 스프링의 Front Controller인 DispatcherServlet가 이 클래스를 컨트롤러로 인식하여 요청을 보낼 수 있게 됩니다.
Request Mapping은 컨트롤러에 어떤 요청이 들어왔을 때 어떤 컨트롤러 메소드를 실행할 지 결정하는 규칙입니다. DispatcherServlet이 이 매핑 정보들을 확인하고 요청을 처리할 컨트롤러 메소드를 호출합니다.
다음과 같은 Request Mapping 어노테이션들이 있습니다. 현재는 요청에 맞는 HTTP 메소드 별 어노테이션을 주로 사용합니다.
@RequestMapping
별도 설정이 없는 경우 모든 HTTP 메소드를 허용하는 어노테이션입니다.
보통 메소드보다는 클래스 레벨에 사용하여 공통 요청 URL 설정에 사용됩니다.
@GetMapping
GET 요청을 처리하고자 하는 메소드에 사용됩니다. 주로 데이터 조회나 페이지 불러오기 등에 사용됩니다.
@PostMapping
POST 요청을 처리하고자 하는 메소드에 사용됩니다. 주로 회원가입과 같은 데이터 저장에 사용됩니다.
@PatchMapping
PATCH 요청을 처리하고자 하는 메소드에 사용됩니다. 주로 비밀번호 변경과 같이 데이터를 일부 수정하는 경우에 사용됩니다.
@PutMapping
PUT 요청을 처리하고자 하는 메소드에 사용됩니다. PATCH 처럼 데이터 갱신 등에 사용하지만 PATCH가 데이터 일부를 수정했다면, PUT은 데이터를 전체 수정하는 경우 사용합니다.
PUT은 클라이언트가 리소스의 정확힌 URL(id 포함)을 알고 요청을 보내야합니다.
POST는 동일한 요청을 보내면 동일한 정보를 갖지만 서로 다른(id를 갖는다던가) 데이터를 생성하게 됩니다.
그러나 PUT은 멱등성(idempotence)을 갖기 때문에 동일한 요청을 보내도 동일한 결과를 유지합니다. 그래서 사용자 데이터 수정, 설정 정보 변경 등에 사용합니다.
결국 PATCH와 PUT 둘 다 수정인가 할 수도 있겠지만 다음과 같은 차이가 있습니다.
- PATCH는 멱등성을 보장하지 않음, PUT은 멱등성을 보장함
- PATCH는 데이터의 일부분만 알고 요청을 보내면 됨, PUT으로도 일부 수정을 할 수 있으나 데이터 전체를 보내야함. 즉, 클라이언트가 데이터의 전체를 알고 있어야 요청을 제대로 보낼 수 있음
컨트롤러에서 비즈니스 로직(서비스 등)을 거친 결과는 모델 Model에 담겨 View로 전달됩니다. 즉, 컨트롤러가 서비스의 결과물을 뷰로 전달하는 객체라고 생각하시면 됩니다.
Model은 [키-값]쌍으로 데이터를 저장하고 전달합니다. 요청이 컨트롤러로 들어올 때 생성되어 응답으로 나가고 나면 소멸합니다.
스프링에서는 Model의 데이터가 Object 타입으로 저장되므로 자바에서 사용할 수 있는 모든 형태의 객체를 담아서 전달할 수 있습니다.
모델에 담긴 키-값쌍 데이터는 보통 다음과 같이 사용됩니다.
String str = "apple";
model.addAttribute("message", str);
<!-- Thymeleaf Template 사용을 가정 -->
<p>안녕하세요! <span th:text="${message}"></span></p>
지금까지 알아본 내용으로 간단한 컨트롤러를 만들어봅시다.
@Controller
@RequestMapping("/my") //해당 컨트롤러가 "/my"로 시작하는 모든 요청 받음
public class MyController {
@GetMapping("/name") // GET "/my/name"
public String getMyName(
@RequestParam(name = "name"), //사용자가 ?name="apple" 파라미터 전송
Model model //데이터
) {
String str = "현재 사용자: " + name; //일종의 비즈니스 로직(원래는 서비스에서 처리)
model.addAttribute("message", str); //모델에 처리된 데이터를 담음 [키-값]
return "mypage"; //mypage.html 등을 찾아 사용자에게 응답
}
}
사용자가 GET /my/name?name="apple"이라는 요청을 보내면 클래스(컨트롤러)가 요청을 받아 GET /my/name을 처리하는 메소드를 컨트롤러 안에서 찾고 그 결과를 응답하는 간단한 컨트롤러입니다.
return 구문에서 "mypage"는 mypage라는 이름을 가진 html or jsp(application.properties 설정에 따라 다름)을 찾아 반환합니다.
위에서 본 @Controller는 화면 그러니까 html/jsp와 같은 뷰를 반환하였습니다.
최근 웹 어플리케이션은 프론트엔드 React.js와 같은 프레임워크를 사용하여 백엔드에서 한 번에 뷰를 보여주는 일이 점점 줄고 있습니다. 따라서 백엔드를 처리 후 뷰가 아니라 프론트엔드 뷰에서 사용할 데이터(주로 JSON)를 보내주어야하는 경우가 잦은데 이럴 때 @RestController를 사용합니다.
기존처럼 @Controller를 사용했을 때도 @ResponseBody를 사용하면 데이터를 보낼 수 있습니다.
@Controller
public class MyController {
@GetMapping("/my/{id}")
@ResponseBody
public User getMyInfo(@PathVariable Long id) { //사용자가 GET /my/1 등 요청
//User라는 객체(DTO/Entity)와 userQueryService 서비스가 있다고 가정
User user = userQueryService.findById(id);
return user;
}
}
위와 같이 회원 정보를 조회 후 User 객체에 담아 JSON(또는 xml)의 형태로 반환합니다.
@Controller만 사용했을 경우 return에 오는 문자열이 ViewResolver를 통해 뷰로 변환되어 나갔는데, @ResponseBody 어노테이션을 사용하게되면 ViewResolver의 동작을 건너뛰고 응답 바디에 데이터만 실어져 보내집니다.
그럼 다시 돌아와서 @RestController가 뭐길래 @ResponseBody를 설명했을까요?
바로 @RestController가 @Controller + @ResponseBody 사용으로 인해 길어지는 코드를 간결하게 해주는 역할을 합니다.
위 예제에서 응답 바디를 반환하는 경우 모든 메소드에 일일이 @ResponseBody를 붙여주어야 하지만, @RestController로 선언하면 붙일 필요 없이 해당 어노테이션 하나만으로 끝나기 때문입니다.
@RestController
public class MyController {
@GetMapping("/my/{id}")
public User getMyInfo(@PathVariable Long id) { //사용자가 GET /my/1 등 요청
//User라는 객체(DTO/Entity)와 userQueryService 서비스가 있다고 가정
User user = userQueryService.findById(id);
return user;
}
}
만약 하나의 컨트롤러에 뷰 반환, JSON 등 데이터 반환을 섞어서 사용해야한다면 @Controller로 선언하고 필요한 부분에 @ResponseBody를 붙여주는 쪽으로 갑니다.
요청에는 크게 두 가지 방식을 이용해 데이터를 컨트롤러에 전달합니다.
쿼리 스트링을 이용해 전달하는 방식은 가장 간단하고 오래 사용된 방식입니다.
URL의 맨 끝에 ?를 붙이고 key=value로 표현합니다.
/search?keyword=a&page=1
이렇게 URL에 노출되기 때문에 실제로 로그인 같은 중요한 데이터를 보낼 때는 사용하지 않고 GET 요청에서 간단한 값을 보내고자할 때 사용됩니다.
다음은 @RequestParam을 사용한 간단한 코드입니다.
//GET /search?keyword=a&page=1
@GetMapping("/search)
public String searchByKeyword(
@RequestParam("keyword") String keyword,
@RequestParam(value = "page", required = true) Integer page
) {
(...)
}
다른 옵션이 없는 경우 value는 생략하고 쿼리 스트링의 키 값만 입력하고, 다른 옵션이 있으면 value 등으로 키 값을 명시합니다.
또 다른 방식은 HTTP의 요청 Body에 데이터를 JSON 형태로 담아서 컨트롤러로 보내어 받는 형태입니다. 이 방식은 쿼리 스트링처럼 URL에 노출되지 않고 전송이 됩니다.
데이터를 숨겨야하거나, 복잡하거나, 많은 양을 전송하고자 할 때 사용합니다. GET 요청에도 사용은 할 수 있으나 주로 GET을 제외한 나머지 요청에서 주로 사용됩니다.
@PostMapping("/user/join")
public String join(@RequestBody UserJoinDTO userJoinDto) {
(...)
}
//다음과 같은 DTO를 생성 및 사용
public class UserJoinDto {
String username;
String password;
(....)
}
DTO(Data Transfer Object)의 설명은 이 포스트를 참조해주세요.
이 과정에서 스프링이 요청 바디의 JSON 데이터를 DTO와 매핑하여 자동으로 변환해줍니다. 스프링의 편리한 점이 또 등장하네요.
@RequestBody는 생략하면 제대로 변환이 되지 않는 다는 것과 DTO가 없으면 동작하지 않는다는 점 주의하셔야 합니다.