🙋♀️ Spring MVC란?
Spring MVC라고 불리는 Spring Web MVC는 서블릿을 기반으로 만들어진 웹 프레임워크이다.
서블릿을 기반으로 만들어진 프레임워크라고 말문을 열었다.
서블릿에 대한 이해가 없다면 아래 포스팅을 먼저 보는 것을 추천한다.
2024.05.11 - [우아한테크코스/레벨2] - [Java] 서블릿(Servlet) 이란? Java Application에서 어떻게 사용될까?
Spring MVC는 스프링 프레임워크에 유연함과 확장성을 확보하기 위해서 기존 서블릿을 사용한 구조에 MVC 아키텍처 패턴을 적용한 것이다. 어떻게 MVC 아키텍처 패턴의 도입이 스프링 프레임워크에 유연함과 확장성이라는 강력한 구조적 이점을 주게 되었을까?
기존에 웹 프로그래밍을 위해 사용한 방식을 알아보고 불편함을 해결하기 위해 Spring MVC가 등장한 배경에 대해 알아보자.
🤒 Spring MVC가 등장하기 이전
🤬 MVC 패턴 도입 이전
Spring MVC가 등장하기 이전에는 어떤 기술을 사용하여 동적 페이지를 클라이언트에게 제공할 수 있었을까?
그 중 하나인 서블릿을 사용한 개발을 살펴보자. 서블릿은 클라이언트로부터 요청을 받았을 때 로직을 처리한 결과를 HTML 형태로 반환하였다. 즉 비즈니스 로직과 뷰의 출력이 분리되지 않고 하나의 서블릿 클래스에서 처리된 셈이다. 아래 코드를 보자.
이 코드는 요청에서 토큰 값을 꺼내어 사용자의 정보를 조회하는 로직을 서블릿 환경에서 작성한 것이다.
비즈니스 로직은 단 두 줄인데 뷰에 대한 처리 때문에 코드가 매우 길어진 것을 볼 수 있다. 서블릿의 등장 덕분에 동적 페이지를 제공할 수 있게 되었지만 코드에서 볼 수 있듯이 매우 비효율적인 방식임을 알 수 있다. 이러한 간단한 기능의 코드가 몇 십줄을 넘어간다면 하나의 큰 애플리케이션을 만들고자 한다면 유지보수가 매우 힘들어질 것 같다.
🌸 MVC 패턴 도입 이후
이러한 문제를 해결하고자 MVC 패턴이 도입되었다. MVC 아키텍처 패턴은 구성 요소의 정보를 담은 모델 (Model)과 화면 출력 로직을 담은 뷰 (View) 그리고 애플리케이션 제어 로직을 담은 컨트롤러 (Controller)를 분리하여 세 가지 요소가 협력하도록 설계한 패턴이다. 위에서 본 서블릿 클래스처럼 비즈니스 로직과 뷰 로직이 한 클래스에 담겨 있는 현상을 해결하기 위해 등장하였다.
여기서 템플릿 엔진 이야기를 곁들일 수 있을 것 같다. 템플릿 엔진 (Template Engine)은 뷰의 고정된 양식에서 정적인 부분만 따로 비워놓고, 외부에서 가져온 데이터로 해당 부분을 채워 동적인 페이지를 만드는 것을 의미한다. 대표적으로 JSP나 Thymeleaf 등이 있다.
이렇게 템플릿 엔진 중 JSP를 사용해서 위의 코드를 개선해보자.
짜잔 ~
비즈니스 로직에 의해 뷰에 출력할 값을 HttpServletRequest 객체에게 담아서 전송하였다. 이제 JSP는 요청 객체에 담은 회원 데이터를 꺼내어 클라이언트에게 회원 정보가 담긴 뷰를 전송할 수 있다. 😎
이것만으로도 충분히 개선된 것 같다. 하지만 기능이 조금 더 복잡해지면 어떻게 될까? 아주 많은 메서드를 작성해야한다고 가정했을 때 위에서 사용한 코드에서 눈엣가시를 찾아보자.
크게 세 부분이 보인다.
뷰를 호출하는 로직도 중복이기 때문에 메서드를 하나 만들 때마다 뷰의 경로와 실행 로직을 작성해주어야 한다. 그리고 매개변수가 요청 응답 객체로 고정되어있기 때문에 해당 메서드를 테스트하기도 어렵다. 더욱이 인증 확인이나 보안 처리, 로깅 등 모든 메서드마다 공통으로 처리해야할 로직이 생기는 경우 모든 메서드에 다 구현을 해주거나 중복으로 호출해야하는 일이 생기는 문제가 있다. 마지막으로 서블릿 설정에 대한 부분도 메서드에 항상 포함이 되므로 불편함이 생길 것 같다.
이러한 문제를 해결하기 위해 공통으로 로직을 처리하는 친구가 필요할 것 같다. Spring MVC에서는 이러한 문제를 프론트 컨트롤러 패턴이 도입함으로써 해결하였다.
🎵 프론트 컨트롤러 패턴의 도입
프론트 컨트롤러 패턴을 도입해서 어떻게 세 부분의 문제를 해결할 수 있었을까? 코드를 통해 알아보자.
프론트 컨트롤러가 조금 복잡해 보인다. 이 프론트 컨트롤러가 어떤 문제를 해결하였는 지 살펴보자.
🍕 각 컨트롤러가 자신의 View를 반환하도록
먼저 뷰를 호출하는 부분부터 분리할 수 있을 것 같다. 컨트롤러가 자신의 뷰 이름을 반환하기만 하면 프론트 컨트롤러가 이를 받아 자동으로 뷰를 호출한다. 즉, 컨트롤러는 뷰의 작동을 직접 호출할 필요가 없다.
public void render(HttpServletRequest request, HttpServletResponse response) {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewName);
dispatcher.forward(request, response);
}
ModelAndView의 render 메서드에서 일괄적으로 뷰를 호출하고 있다.
🍕 각 컨트롤러가 자신의 Model을 반환하도록
동적 웹 환경을 제공하기 위해서 서버에서 뷰에 데이터를 전달해야한다. 이를 위해 각 컨트롤러 메서드가 별도의 Model 객체를 만들어서 전달해주자. 그럼 프론트 컨트롤러는 이를 받아 뷰에 전달하기만 하면 된다.
public class MemberSaveController implements Controller {
private final MemberService memberService = MemberService.getInstance();
@Override
public void process(final Map<String, String> params) {
String name = params.get("name");
String email = params.get("email");
String password = params.get("password");
Member savedMember = memberService.save(new SignupRequest(name, email, password));
ModelAndView mv = new ModelAndView("members-view");
mv.getModel().put("savedMember", savedMember);
return mv;
}
}
컨트롤러 메서드의 예시 중 하나이다. 자체적으로 Model 객체를 만들어서 반환하고 있다.
이를 통해 얻을 수 있는 부가적인 장점은 HttpServletRequest와 HttpServletResponse를 컨트롤러 메서드 파라미터에서 제거할 수 있다는 것이다. 이제 컨트롤러 메서드가 서블릿에 대한 정보를 몰라도 잘 동작하게 된다. 이전에 HttpServletRequest에 담아서 보내던 데이터를 Model에 담아서 서블릿이라는 것과 무관하게 동작하게 된다.
🍕 서블릿 설정을 하지 않아도 잘 동작하도록
기존에는 컨트롤러 메서드마다 서블릿 컨테이너에게 나 여기있어요! 하고 등록해주는 과정이 필요했다.
이를 프론트 컨트롤러에게 위임하여 A라는 URL로 접근했을 때 지정된 컨트롤러 메서드가 실행되도록 하였다.
아래 코드가 이를 구현한 부분이다.
private Map<String, Controller> controllers = new HashMap<>();
public FrontControllerServlet() {
controllers.put("/login", new LoginController());
controllers.put("/signup", new SignupController());
controllers.put("/list", new MemberListController());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) {
String requestURI = request.getRequestURI();
Controller controller = controllers.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
...
프론트 컨트롤러가 모든 컨트롤러들의 접근 가능 경로와 구현체를 담고 있기 때문에 URI와 매칭되는 컨트롤러로 한번에 포워딩하는 작업을 담당해준다.
⭐ Spring MVC의 탄생
이렇게 프론트 컨트롤러의 도입을 공부해보니 현재 Spring MVC의 구조가 탄생한 배경이 이해되었다.
핸들러 어댑터에 대한 내용은 우선 제외하였다.
이렇게 클라이언트는 FrontController에게 요청을 보내는 것만으로도 모든 컨트롤러의 정보를 몰라도 원하는 응답을 받을 수 있게 되었다. 반대로 컨트롤러도 서블릿 컨테이너에 대한 정보를 모르고 자신이 원하는 데이터만 전달 받고 비즈니스 로직 만을 처리해도 요청 및 응답이 잘 처리되도록 하였다. 즉 하나의 컨트롤러는 ModelAndView만 잘 전달한다면 그로써 역할을 다하게 되는 것이다. 기존에 작성했던 서블릿 클래스를 다시 보면 얼마나 큰 진전인지 확인할 수 있다 🎯
😎 마무리하며
스프링 애플리케이션을 여러 번 경험하였지만 서블릿이라는 개념에 대해 무지하였다. 깊게 공부하지 않고 단순히 스프링을 사용하기만 하였다면 평생 몰랐을 것이다. 그러나 스프링 애플리케이션이 동작하기에 서블릿은 상당히 중요한 역할을 하고 있었고 이렇게 중요한 친구를 몰랐다는 것에서, 서블릿이 Spring MVC 뒤에 아주 잘 은닉하였다는 증거가 된다.
즉 스프링 컨테이너의 동작 방식이 프로덕션 레벨로부터 완벽하게 캡슐화가 되었다고 볼 수 있다.
프론트 컨트롤러에 해당되는 친구는 DispatcherServlet 이라고 한다.핸들러 어댑터나 핸들러 매핑, 뷰 리졸버 등 자세한 Spring MVC 구조에 해당하는 설명을 많이 누락하였는데 다음 포스팅에서 보충해보려고 한다.
참고
https://docs.spring.io/spring-framework/reference/web/webmvc.html
https://www.baeldung.com/spring-mvc-tutorial
+ 영한님의 스프링 MVC 강의
'우아한테크코스 > 레벨2' 카테고리의 다른 글
[회고] 우아한테크코스 6기 백엔드 레벨2 4주차 회고 (2) | 2024.05.19 |
---|---|
[회고] 내가 꿈꾸는 개발자의 삶은 무엇일까? 내가 생각하는 좋은 개발자의 기준 (2) | 2024.05.19 |
[Java] 서블릿(Servlet) 이란? Java Application에서 어떻게 사용될까? (0) | 2024.05.11 |
[회고] 우아한테크코스 6기 백엔드 레벨2 3주차 회고 (3) | 2024.05.06 |
[Spring] 에러 응답 (Error Response) 을 반환하는 이유는? 어떤 정보를 담아 보내야할까? (7) | 2024.05.03 |