간단한 회원 관리 애플리케이션을 서블릿, JSP로 만들어보면서 이 둘의 한계를 알아보고 MVC패턴까지 구현해본다.
회원관리 - 서블릿
servlet/domain/member에 해당 클래스를 만든다.
member에 대한 save, find 기능 등이 있는 memberRepository 클래스도 만든다.
웹 애플리케이션 만들기
서블릿으로 회원 등록 HTML 폼을 제공한다.
response 객체로 html을 하나하나 입력해 위와 같은 폼을 제공한다.
이 상태에서는 전송을 눌러도 form action /servlet/members/save 를 안만들어줬기 때문에 오류가 발생한다.
save를 만들어보자.
save로 가게 되면 username과 age를 getParameter로 받을 수 있게 되고 해당 데이터를 바탕으로 Member를 생성해 save 해주었다.
결과를 바로 볼 수 있도록 html도 보내준다. 자바코드이기 때문에 member.getAge() 등으로 동적으로 만들 수 있다.
이제는 저장된 회원을 조회하는 기능을 만들어본다.
for문을 이용해 회원 목록을 html로 보여준다.
서블릿을 이용해보면 html을 직접 하나하나 입력하면서 사용하는 것이 매우 불편하다는 것을 알 수 있다.
HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있다면 좋을 것이다.
그래서 템플릿 엔진을 사용한다.
템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해 동적으로 변경할 수 있다.
템플릿 엔진에는 JSP, Thymeleaf, Freemarker, Velocity 등이 있다.
JSP를 통해 동일한 작업을 해본다.
회원관리 - JSP
JSP를 사용하기 위해선 아래와 같은 라이브러리를 추가해야 한다.
1
2
3
4
5
6
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' // 스프링부트 3.0 이상
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
//JSP 추가 끝
먼저 jsp로 회원 생성 폼을 나타내본다.
webapp/jsp/members에 new-form.jsp 를 추가한다.
webapp 아래에 있는 것은 바로 불러올 수 있다.
따라서 /jsp/members/new-form.jsp를 부르면 아래와 같은 화면이 나온다.
이제 저장 부분을 만들어보자
save.jsp이다.
<%@ ~~ %@> 안에는 자바코드를 사용할 수 있다.
Member와 MemberRepository를 끌어쓰기 위해서는 위의 2,3줄 처럼 import를 해주어야 한다.
request와 response 객체는 그냥 바로 쓸 수 있다.
성공적으로 나타나는 것을 볼 수 있다.
이제 JSP로 회원목록 조회를 만들어보자.
결과는 당연히 같다.
결국 JSP는 서블릿 코드와 같지만 HTML을 중심으로 하고, 자바 코드를 부분부분 입력해 쓸 수 있었다는 것이 특징이다.
서블릿으로 개발할 때는 뷰 화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 지저분하고 복잡했다.
JSP를 사용함으로써 뷰를 생성하는 HTML 작업을 깔끔하게 가져가고, 중간중간 동적으로 변경이 필요한 부분에만 자바 코드를 적용했다.
하지만
회원 저장 JSP를 보면 코드의 상위 절반은 회원을 저장하기 위한 비즈니스 로직이고, 나머지 하위 절반은 결과를 보여주기 위한 HTML 뷰 영역이다.
코드를 잘 보면 JAVA 코드, 데이터를 조회하는 리포지토리 등 다양한 코드가 모두 JSP에 노출되어 있다. JSP가 너무 많은 역할을 하게 된다.
하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링 모두 처리하게 되면 비즈니스 로직을 호출하는 부분에 변경이 발생해도, UI를 변경할 일이 생겨도 해당 파일을 수정해야 한다. HTML코드 하나 수정하는데 수백줄의 자바 코드가 함께 있을 수 있는 것이다.
위와 같이 작은 프로젝트면 상관이 없지만 프로젝트가 커질수록 유지 보수가 어려워진다.
MVC 패턴 개요
서블릿과 JSP는 유지보수가 힘들다는 단점도 있지만 가장 큰 부분이 있다.
변경의 라이프 사이클이 다르다는 점이다.
예를 들어 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 높고 대부분 서로에게 영향을 주지 않는다. 이런 부분을 하나의 코드로 관리하는 것은 더더욱 유지보수에 어려움을 준다.
그리고 JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적이다.
이래서 나온 것이 MVC 패턴이다.
- 모델 : 뷰에 출력할 데이터를 담아둔다.
- 뷰가 필요한 데이터를 모두 모델에 담아 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링하는데 집중한다.
- 뷰 : 모델에 담겨있는 데이터를 사용해 화면을 그리는 일에 집중한다.
- 컨트롤러 : HTTP 요청을 받아 파라미터를 검증하고, 비즈니스 로직을 실행한다. 뷰에 전달할 결과 데이터를 조회해 모델에 담는다.
(비즈니스 로직은 사실 서비스에 담긴다.)
회원관리 - MVC 패턴
서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해 MVC패턴을 적용해본다.
모델은 HttpServletRequest 객체를 사용한다. request는 내부에 데이터 저장소를 가지고 있는데 request.setAttribute(), request.getAttribute()를 사용하면 데이터를 보관하고, 조회할 수 있다.
회원 저장 폼을 제공해본다.
(컨트롤러)
dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.
리다이렉트가 아닌 서버 내부에서의 호출이다.
WEB-INF 디렉토리 내부의 jsp 파일은 외부에서 직접적으로 (url) 호출할 수 없다.
redirect vs forward
리다이렉트는 실제 클라이언트에 응답이 나갔다가 클라이언트가 redirect 경로로 다시 요청한다. 그렇기 때문에 리다이렉트는 클라이언트가 인지하고 URL도 실제로 변경된다.
반면 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다. (servlet-mvc/members/new-form -> /WEB-INF/views/new-form.jsp 이지만 인지하지 못했다.)
(뷰)
여기서 form action=”save” 인데 /save 이런식이면 절대경로로 /save로 가지만 위처럼 쓰면 해당 url에서 뒤에 /save만 추가되는 상대경로가 된다.
보통은 절대경로를 사용하는 것이 좋다.
이제 회원 저장 부분을 봐보자.
<%= request.getAttribute(“member”).getID() %> 등으로 모델에 저장한 member 객체를 꺼낼 수도 있지만, 너무 복잡해진다.
JSP %[] 문법을 제공하는데 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다.
이제 회원목록을 조회해보자.
members를 조회하는 for문은 jsp가 제공하는 forEach 문법을 사용해 출력한다.
사용하기 위해서는 2번째 줄 (taglib ~~ )이 필요하다.
서블릿 + JSP로 만든 MVC패턴의 한계
MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
특히 뷰는 화면을 그리는 역할에 충실한 덕분에 코드가 깔끔하고 직관적이다.
하지만 컨트롤러는 중복이 많고, 필요하지 않는 코드들도 많이 보인다.
- forward 중복
- view로 이동하는 코드가 항상 중복 호출되어진다.
- 이 부분을 메서드로 공통화해도 되지만, 메서드도 항상 직접 호출해야한다.
- viewPath 중복
- /WEB-INF/views
- .jsp
- 사용하지 않는 코드
- request, response를 사용하지 않는 경우가 있다.
- 얘네는 사용한다해도 테스트 케이스를 작성하기 힘들어지는 단점이 있다.
- 공통 처리의 어려움
- 공통 기능을 메서드로 뽑으면 될 것 같지만 결과적으로 해당 메서드를 항상 호출해야 된다.
- 호출하는 것 자체도 중복이다.
결국 공통 처리가 어렵다는 문제가 있는 것이다.
이러한 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다.
프론트 컨트롤러 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다. 스프링 MVC의 핵심도 이 프론트 컨트롤러이다.
이는 다음 포스트에 다룬다.