Spring

[Spring 완전 정복 시리즈 - MVC편] 4편 - 서블릿에서 JSP, 그리고 MVC로

dev-nadan 2025. 8. 11. 20:07

이번 편에서는 서블릿 → JSP → MVC로 이어지는 흐름을 실제 회원 관리 웹 애플리케이션 예제를 통해 단계별로 살펴본다.

서블릿과 JSP를 비교하며, 왜 MVC 패턴이 등장하게 되었는지, 그리고 이를 적용하면 어떤 구조적 장점이 생기는지를 알아본다.


1. 회원 관리 웹 애플리케이션 요구사항

기능 요구사항

  • 회원 저장
  • 회원 목록 조회

도메인 모델

public class Member {
    private Long id;
    private String username;
    private int age;
    // 기본 생성자, getter/setter, 생성자...
}

회원 저장소 (싱글톤)

public class MemberRepository {
    private static 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) { ... }
    public Member findById(Long id) { ... }
    public List<Member> findAll() { ... }
    public void clearStore() { store.clear(); }
}

 

 

  • 스프링 없이 구현하기 위해 직접 싱글톤 패턴을 적용.
  • 실무에서는 ConcurrentHashMap, AtomicLong 등을 사용해 동시성 문제를 해결.

2. 서블릿으로 구현하기

2.1 회원 등록 폼

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter w = response.getWriter();
        w.write("<form action='/servlet/members/save' method='post'>...</form>");
    }
}

 

  • HTML을 전부 자바 코드로 작성해야 하므로 가독성이 매우 떨어진다.

2.2 회원 저장

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        Member member = new Member(username, age);
        memberRepository.save(member);
        // HTML 응답 작성
    }
}

 

2.3 회원 목록

@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
        List<Member> members = memberRepository.findAll();
        // HTML 테이블로 목록 출력
    }
}

 

문제점

  • HTML을 자바 코드로 작성 → 복잡하고 비효율적.
  • 화면 변경이 발생하면 자바 코드를 직접 수정해야 함.

3. JSP로 전환하기 (템플릿 엔진 도입)

JSP를 사용하면 HTML 안에 필요한 곳만 자바 코드를 넣어 동적 렌더링 가능.

3.1 회원 등록 폼 JSP

<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>

 

3.2 회원 저장 JSP

<%
    MemberRepository repo = MemberRepository.getInstance();
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));
    Member member = new Member(username, age);
    repo.save(member);
%>
<ul>
    <li>id=<%=member.getId()%></li>
    <li>username=<%=member.getUsername()%></li>
    <li>age=<%=member.getAge()%></li>
</ul>

 

3.3 회원 목록 JSP

<%
    List<Member> members = MemberRepository.getInstance().findAll();
%>
<c:forEach var="item" items="${members}">
    <tr>
        <td>${item.id}</td>
        <td>${item.username}</td>
        <td>${item.age}</td>
    </tr>
</c:forEach>

장점

  • HTML 중심으로 작성 가능 → 가독성 증가.
  • 동적 데이터 삽입이 간편.

여전히 남는 문제

  • JSP에 비즈니스 로직(저장, 조회)과 뷰 로직이 혼재.
  • 변경 주기가 다른 로직이 한 파일에 있어 유지보수 난이도 상승.

4. MVC 패턴 적용

역할 분리

  • Controller: 요청 처리, 파라미터 검증, 비즈니스 로직 호출, 모델 데이터 생성
  • Model: 뷰에 필요한 데이터 저장
  • View: 화면 렌더링에만 집중

적용 예시

4.1 회원 등록 폼 컨트롤러

@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 {
        RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/new-form.jsp");
        dispatcher.forward(request, response);
    }
}

 

4.2 회원 저장 컨트롤러

@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        Member member = memberRepository.save(new Member(username, age));
        request.setAttribute("member", member);
        RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/save-result.jsp");
        dispatcher.forward(request, response);
    }
}

 

4.3 회원 목록 컨트롤러

@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 {
        request.setAttribute("members", memberRepository.findAll());
        RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/members.jsp");
        dispatcher.forward(request, response);
    }
}

5. MVC 패턴의 한계와 다음 단계

  • 컨트롤러마다 View 호출 코드가 반복됨.
  • 뷰 경로(prefix, suffix) 변경 시 전체 코드 수정 필요.
  • 공통 처리(인증, 로깅 등) 적용이 어려움.

 

이 문제를 해결하기 위해 프론트 컨트롤러 패턴이 도입되며, 스프링 MVC의 핵심 구조가 완성된다.

 


회고

 

이번 과정을 통해 “왜 MVC 패턴이 필요한가”를 실감했다.

서블릿 시절에는 모든 HTML을 자바 코드로 출력해야 해서 유지보수 악몽을 경험할 수밖에 없었고, JSP로 전환하면서 화면 작업은 훨씬 편해졌지만 여전히 비즈니스 로직과 뷰 로직이 뒤섞여 있었다.

MVC로 나누고 나서야 역할이 명확히 분리되고, UI 변경과 로직 변경이 서로 영향을 덜 주는 구조가 가능해졌다.

이제 보니, 내가 과거 프로젝트에서 JSP 안에 모든 로직을 때려 넣었던 경험이 유지보수 지옥의 원인이었음을 알게 됐다. 앞으로는 컨트롤러-서비스-뷰 계층 분리를 철저히 지키는 습관을 가져야겠다.