이 포스팅은 김영한 강사님의 강의를 참고하여 포스팅을 한 것입니다. 문제가 생길 경우 즉시 처리하겠습니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
이전 포스팅을 통해 기본적으로 Servlet 은 어떻게 동작되는지 간단히 확인해 봤고, Request Response 객체의 사용법도 확인해 봤다.
그럼 이제 더 발전시켜서, 단순한 Text가 아닌 실제 HTML Form을 주고받을 때 서블릿은 어떻게 코드를 짜야하는지 알아보고, 단점을 느껴보자.
서블릿
Member 도메인 생성
먼저 나는 간단하게 클라이언트로부터 전송받을 유저명과 나이를 전달받고, 각 객체를 Id로 구분될 수 있도록 다음과 같이 작성했다.
@Getter
@Setter
public class Member {
private Long id;
private String username;
private int age;
Member() {
}
public Member(String username, int age) {
this.username = username;
this.age = age;
}
}
참고 : 도메인 객체와 DTO 객체
생각해 보면, 우리가 회원 가입을 할 때 필요한 정보와 아이디를 찾을 때 입력하는 정보는 다르다.
예를 들어, 회원 가입을 할 땐 사용자의 개인정보와 주소, 여러 약관 동의에 관한 정보들이 서버에서 필요하겠지만, 아이디를 찾으려고 할 땐 내가 본인이 맞는지, 변경할 아이디가 무엇인지 정도가 필요할 것이다. 이런 경우에서 어떤 상황에서든지 모두 하나의 도메인 객체만을 사용한다면 발생할 수 있는 문제점은 아이디를 찾는 비즈니스 로직을 실행할 때, 전혀 관계없는 메서드나 필드를 호출하거나 변질이 될 수 있다는 문제점이 생길 수도 있고, 쓸데없는 정보들이 왔다 갔다 하면서 서버에 부담이 가게 될 수 있다.
물론, 지금 하는 것과 같이 아주 간단한 프로젝트에선 굳이 이를 구분해서 하는 건 선택 사항이겠지만, 규모가 있는 프로젝트에선 해당 비즈니스 로직에서 딱 필요한 메서드나 필드가 뭔지 파악하고 따로 DTO 객체를 만드는 것이 좋다.
그리고 MemberRepository와 같은 비즈니스 로직을 처리할 클래스도 작성했다.
코드는 보여주기 어려워서 store(), findById(), findAll() 메서드 정도를 구현했다.
서블릿 객체 생성
여기에서 양식 폼을 클라이언트에게 보여주고 데이터를 받으려면 다음과 같은 서블릿 객체가 필요하다.
- 클라이언트로부터 회원 가입 양식을 보여줄 URI를 매핑하고 응답할 수 있는 서블릿
- 클라이언트가 Form을 전송한 경우에 이를 매핑하고 비즈니스 로직을 처리하는 서블릿
- etc..
나는 이를 처리하기 위해 각각의 요청을 처리할 서블릿을 구현했지만, 저장된 모든 멤버들의 리스트를 가져오는 내가 작성한 서블릿 코드 정도만 공유해 보겠다.
@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
PrintWriter w = resp.getWriter();
w.println("<!DOCTYPE html>");
w.println("<html>");
w.println("<head>");
w.println("<title>전체 멤버 목록</title>");
w.println("<style>");
w.println("table {");
w.println(" border-collapse: collapse;");
w.println(" width: 100%;");
w.println("}");
w.println("th, td {");
w.println(" border: 1px solid #dddddd;");
w.println(" text-align: left;");
w.println(" padding: 8px;");
w.println("}");
w.println("tr:nth-child(even) {");
w.println(" background-color: #f2f2f2;");
w.println("}");
w.println("</style>");
w.println("</head>");
w.println("<body>");
w.println("<h1>전체 멤버 목록</h1>");
w.println("<table>");
w.println("<tr>");
w.println("<th>이름</th>");
w.println("<th>나이</th>");
w.println("</tr>");
for (Member member : members) {
w.println("<tr>");
w.println("<td>" + member.getUsername() + "</td>");
w.println("<td>" + member.getAge() + "</td>");
w.println("</tr>");
}
w.println("</table>");
w.println("</body>");
w.println("</html>");
}
}
(일부러 단점이 보이도록 하드 코딩 했습니다.)
이렇게 하면 for-each 문을 사용해서 동적으로 <table> 태그를 생성해서 클라이언트에게 보여줄 수 있다. 회원가입을 하고 /servlet/members를 호출하면 다음과 같이 랜더링 되어 클라이언트에게 뿌려지는 것을 확인할 수 있다.
서블릿의 한계
하지만 눈에 보이듯이 이렇게 일일이 문자열로 작성하고 있는 것은 개발자 입장에서 정말 고된 작업이고 비효율적이다. 더군다나 이는 문자열이라서 틀린 코드를 찾아내기도 힘들다.. 이는 고작해야 2~30줄 정도 되지만 실무에선 이것보다 훨씬 코드가 길어질 텐데 이렇게 작성하고 있으면 개발 때려치우고 싶다는 생각이 들지 않을까..?
서블릿은 이러한 문제점이 있지만, 이를 보다 편리하게 작성할 수 있도록 하는 것이 템플릿 엔진이다.
템플릿 엔진은 HTML 문서에서 딱 프로그래밍이 필요한 부분만 코드를 적용해서 동적으로 변경할 수 있다.
템플릿 엔진으로 JSP, Thymeleaf, Freemarker, Velocity 등이 있다.
*참고로 JSP는 타 템플릿 엔진과의 경쟁에서 밀리게 되면서 사장되어 가는 추세이다.
MVC 패턴
MVC 패턴 개요
위에서 본 바와 같이 하나의 Servlet이나 JSP로 작성된 코드들을 보면 한 파일에 비즈니스 코드와 서비스 로직이 포함되어 있다. 이렇게 되면 너무 많은 역할을 하게 되고, 유지보수가 어려워진다. 진짜 중요한 건 변경의 라이프 사이클이 다르다는 점이다. 예를 들어서 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분들을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다.
Model View Controller
MVC 패턴은 하나의 서블릿이나 JSP로 처리하던 것을 Controller와 View 영역으로 역할을 분담한 것을 말한다. (Web Application은 보통 MVC 패턴을 사용)
- Controller : HTTP 요청의 파라미터를 받고 검증하며, 비즈니스 로직을 실행한다. 또한 뷰에 전달할 결과 데이터를 Model에 담는다.
- Model : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해 줌으로써 뷰는 화면을 랜더링 하는 일에만 집중할 수 있다.
- View : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. (HTML을 생성하는 부분)
참고 : 컨트롤러의 역할
컨트롤러가 비즈니스 로직을 실행한다고 했지만, 사실은 이렇게 되면 컨트롤러가 너무 많은 역할을 담당하게 된다. 그래서 비즈니스 로직만을 담당하는 Service 계층을 별도로 생성하여 처리하는 것이 좋다.
즉, 보통 컨트롤러는 HTTP 요청 파라미터를 받아 검증하고, Service 계층을 호출하여 Service에서 비즈니스 로직을 수행하고 컨트롤러에 전달. 이 결과를 Model에 담아 View로 전달하는 정도가 좋다.
MVC 패턴 그림
JSP로 MVC 패턴 적용
우선 나는 처음 SSR 기술을 배울 때, Thymeleaf로 시작해서 JSP에 대해서 깊게 알고 있진 않다. 따라서 아래의 내용은 가볍게 훑어두는 정도면 된다고 생각한다. (제가 JSP를 좋아하지 않습니다)
아래의 Servlet은 먼저 클라이언트가 회원가입 페이지를 요청한 경우, 회원가입 Form이 작성된 jsp 파일로 이동하는 코드이다.
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
}
}
- viewPath : resource의 경로
- RequestDispatcher : 주어진 경로에 위치하는 resource를 감싸는 객체. 이 객체는 주어진 경로로 이동하는 역할을 하는 데 사용된다.
- forward() : 요청을 Servlet 에서 서버에 위치하는 다른 resource(Servlet, jsp, HTML file)로 이동하는 메서드.
Redirect vs Forward
간단하게 말하자면,
- Redirect : 클라이언트 -> 서버 -> 클라이언트 -> 서버 -> 클라이언트. 클라이언트에 응답이 도착하는 시점에 Redirect 경로로 클라이언트가 서버로 다시 요청해서 응답을 받게 된다.
- Forward : 클라이언트 -> 서버 -> 클라이언트. 클라이언트가 서버로 요청을 보내면, 서버 내부에서 다른 resource로 경로를 바꾸어서 클라이언트에게 응답한다.
참고 : WEB-INF, META - INF
WEB-INF, META-INF 디렉터리 내부의 파일들은 파일의 absolute path를 클라이언트에서 호출하여도 접근이 불가능하다.
즉, 파일 경로를 통한 direct access가 불가능하고, 오직 RequestDispatcher의 forward() 통해서만 접근할 수 있다. 이를 통해 보안을 강화할 수 있다.
MVC 패턴의 한계
확실히, MVC 패턴이 있고 없음의 차이는 복잡성이 높은 프로젝트일수록 그 차이가 뚜렷해진다. 하지만, 아직 아래와 같은 점들을 개선하면 좋다고 생각한다.
- 하나의 매핑이 추가될 때 마다, Servlet 을 생성하여야 하고, 매번 중복되는 viewPath로 forward() 를 호출하고 있다.
- 경우에 따라서 HttpServletRequest , HttpServletResponse 를 사용하지 않고 있으며, 이 객체들을 사용하는 코드는 TC를 작성하기도 어렵다.
- 공통 처리가 어렵다. 예를 들어 요청한 클라이언트의 로그인 상태를 여러 곳에서 알아야 하는 경우, 여러 Servlet에 이 검사를 하는 로직이 필요하다. 메서드로 뽑아버리면 복잡성은 줄어들 순 있겠으나, 여전히 동일한 메서드를 호출해야 하기 때문에 더 나은 방법이 필요하다.
즉, 제주에서 서울로 가는 모든 사람들을 검문해야 하는 경우, 다양한 경로가 존재하기 때문에 모든 경로에 대해 검문소를 설치하여야 제대로 된 검문이 가능하다. 하지만, 제주에서 서울로 가는 경로를 하나로 줄일 수 있다면 단 한곳에만 검문소를 설치해도 모든 사람들을 검문할 수 있게 된다. 이렇게 하려면 모든 url에 대해서 하나의 서블릿(검문소)이 받도록 하면 되지 않을까? 이것을 Front Controller 패턴이라고 하며, 위의 문제점들을 깔끔히 해결할 수 있다. 다음 포스팅에서 이를 자세히 알아보겠다
'스프링 > 스프링 MVC' 카테고리의 다른 글
스프링 MVC 기본 기능 (0) | 2023.05.16 |
---|---|
스프링 MVC 구조 (0) | 2023.05.15 |
MVC 프레임워크 (0) | 2023.05.14 |
서블릿 (0) | 2023.05.12 |
Spring MVC : 컨테이너 (2) | 2023.05.11 |