package hello.servlet.domain.member;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Member {
private Long id;
private String username;
private int age;
public Member() {
}
public Member(String username, int age) {
this.username = username;
this.age = age;
}
}
package hello.servlet.domain.member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MemberRepository {
private Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
private static final MemberRepository instance = new MemberRepository();
public static MemberRepository getInstance() {
return instance;
}
private MemberRepository() {
}
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id) {
return store.get(id);
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
Member 클래스는 id, username, age 필드로 이루어진 단순한 구조이며 Repository또한 DB를 사용하지 않고 간단한 메모리 저장으로 Map으로 구현(동시성 문제 고려X, 싱글톤 패턴)
로컬호스트 URL로 바로 접근시 보이는 html문서로 여러 버전의 회원가입, 목록조회가 가능하다.

이번 포스팅에서는 서블릿, JSP, 서블릿 MVC를 다룬다.
@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/servlet/members/save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
""");
}
}
위의 urlPattern에 나타난 URI로 요청이 들어오면 service 메서드가 호출된다. 이 폼 서블릿은 회원가입 폼을 제공하는 HTML을 반환해야 하므로, 적절한 헤더를 설정한 뒤 response.getWriter()를 사용해 HTML을 작성한다. 해당 메서드에서 w.write()를 통해 HTML을 직접 작성하며, 회원정보 저장 요청(POST) 처리나 목록 조회(GET) 처리에서도 HTML을 보내는 방식은 동일하다.
서블릿 덕분에 HTTP 요청과 응답 처리가 편리해졌지만, w.write()와 같은 방식으로 HTML을 작성하는 것은 부담스러우며, 비즈니스 로직과 HTML 작성 코드가 혼재되어 역할이 겹치는 문제가 있다. 다만, HTML 요소를 동적으로 조정하는 것은 가능하다. 이러한 문제를 해결하기 위해 등장한 것이 템플릿 엔진(예: JSP, Thymeleaf)으로, Java 코드로 HTML 전체를 작성하지 않고 HTML 문서 내 동적으로 변화가 필요한 부분만 Java 코드를 삽입할 수 있도록 도와준다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/jsp/members/save.jsp" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
위는 회원가입 입력 폼으로 사용할 HTML이 담긴 JSP 파일이다. JSP 파일은 일반 HTML과 크게 다르지 않으며, 맨 윗줄에 자바 코드를 포함할 수 있는 것이 특징이다. 이를 통해 동적으로 데이터를 처리하거나 로직을 삽입하여 HTML 문서를 유연하게 생성할 수 있다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// request, response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("save.jsp");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
%>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
성공
<ul>
<li>id=<%=member.getId()%></li>
<li>username=<%=member.getUsername()%></li>
<li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
위의 JSP 코드는 회원정보 저장(save) 부분으로, 자바 코드가 HTML 내부에 뭉텅이로 포함되어 있다. JSP는 이러한 방식으로 HTML과 자바 코드 영역이 위/아래로 나뉘며, HTML에서 동적 처리가 필요한 부분을 JSP 문법으로 작성할 수 있다.
다만, JSP는 구식 기술로 평가받으며, 현재는 잘 사용되지 않는다. 이번에는 MVC의 기초를 이해하고자 JSP를 사용 중이며, JSP는 학습이 간단하므로 이 정도만 공부하고 넘어갈 계획이다.
JSP가 사장된 이유는 HTML을 다루는 템플릿에서 비즈니스 로직을 포함한 자바 코드까지 처리하기 때문이다. 우리의 목적은 JSP처럼 혼재된 구조를 피하고, 서블릿의 자바 코드와 렌더링 영역(JSP)을 분리하여 각 역할이 독립적으로 동작하도록 만드는 것이다.
서블릿으로만 개발할 때는 View 화면(렌더링)을 위한 HTML 작성이 자바 코드와 섞여 코드가 지저분해지는 문제가 있었다. 반면, JSP로만 개발할 때는 HTML 작성 영역에 자바 코드가 과도하게 포함되어 유지보수가 어려운 상황이 발생했다.
실제로, JSP로만 개발하던 특정 기업에서는 하나의 JSP 파일이 수천 줄을 넘기면서 아무도 해당 파일을 수정하거나 관리할 수 없게 된 사례가 있다. 결국, 해당 파일은 그것을 개발한 한 명의 개발자만 유지보수할 수 있었으며, 아마 당사자도 이를 다루는 데 매우 어려움을 겪었을 것이다.
이러한 문제는 새로운 기술에 인색할 경우 벌어지는 현상으로, 좋은 신기술에 대해 항상 열린 마음으로 공부하고 수용할 필요성을 보여준다.
하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하면 너무 많은 역할을 담당하게 되어 유지보수가 어려워진다. 특히, 비즈니스 로직과 뷰 렌더링은 보통 변경의 라이프 사이클이 다르기 때문에, 하나의 파일에서 서로 다른 기능을 계속 수정해야 하는 상황이 자주 발생한다. 예를 들어, UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 성격과 발생 시점이 다르다.
M(Model): 뷰에 출력할 데이터를 담는다.V(View): 모델에 담긴 데이터를 사용해 화면을 렌더링하는 역할(HTML 생성).C(Controller): HTTP 요청을 받아 파라미터 검증, 비즈니스 로직 처리,실제 실무에서는 컨트롤러에 비즈니스 로직을 두지 않고,
Service라는 별도의 계층을 만들어 처리한다.

후에 더 발전시킬 계획이 있으므로, 현재는 서블릿을 컨트롤러, JSP를 뷰, HttpServletRequest의 내부 저장소를 모델로 활용한다.
HttpServletRequest는 내부에 데이터 저장소를 가지고 있어:
.setAttribute()를 통해 데이터를 저장..getAttribute()를 통해 데이터를 조회할 수 있다.이러한 기능을 일시적으로 모델로 활용하여 데이터를 뷰로 전달하는 데 사용한다.
// 회원저장 폼
@WebServlet(name = "MvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
// List(get) 요청시
@WebServlet(name= "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
// save(POST) 요청시
@WebServlet(name= "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
코드가 훨씬 간결해졌다. 우리는 viewPath에 해당 경로를 설정하고, 이 경로에 JSP 파일로 렌더링에 필요한 HTML 리소스들을 담아둔다.
request의 Dispatcher 메서드를 활용하여:
viewPath를 전달.request와 response 객체를 forward로 JSP에 전달하여, 렌더링 작업을 JSP에 위임한다.위의 코드에서 중복을 쉽게 확인할 수 있다.
항상 dispatcher.forward 과정을 반복하며, response 객체는 더 이상 사용되지 않는다.
이러한 반복은 코드의 공통 처리가 어렵게 만들며,
반복되는 과정을 메서드로 추출해도 결국 해당 메서드를 계속 호출해야 한다.
이 문제를 해결하기 위해 컨트롤러 호출 전에 요청을 받아 공통 작업을 처리하는,
일종의 "수문장 역할"을 하는 프론트 컨트롤러(Front Controller) 패턴을 도입할 수 있다.
스프링 MVC의 핵심 역시 바로 이 프론트 컨트롤러에 있다.
출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1