[SECTION 02. View를 분리하자 — JSP와 MVC 패턴의 등장]
| 서블릿 | JSP | |
|---|---|---|
| 기본 형태 | Java 코드 안에 HTML | HTML 안에 Java 코드 |
| 주요 역할 | 요청 처리, 비즈니스 로직 | 화면 출력 (View) |
| 파일 위치 | src/main/java/ | src/main/webapp/ |
| 확장자 | .java | .jsp |
JSP는 마법이 아닙니다. 결국 서블릿으로 변환되어 실행됩니다.
hello.jsp
↓ (Tomcat이 자동 변환)
hello_jsp.java (서블릿)
↓ (컴파일)
hello_jsp.class
↓ (실행)
HTML 응답
즉 JSP는 "서블릿을 더 편하게 쓰기 위한 도구"입니다. 내부적으로는 똑같이 서블릿입니다.
<dependencies>
<!-- 기존 서블릿 API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP API 추가 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<!-- JSTL 추가 (JSP 태그 라이브러리) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
| 태그 | 문법 | 용도 |
|---|---|---|
| 지시자 | <%@ %> | JSP 설정 (인코딩, import 등) |
| 선언 | <%! %> | 멤버 변수/메서드 선언 |
| 스크립틀릿 | <% %> | Java 코드 실행 |
| 표현식 | <%= %> | 값을 HTML에 출력 |
| 주석 | <%-- --%> | JSP 주석 (HTML에 미포함) |
src/main/webapp/hello.jsp 생성:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>첫 번째 JSP</title>
</head>
<body>
<h1>안녕하세요!</h1>
<%-- JSP 주석: HTML 소스에 보이지 않음 --%>
<%
// 스크립틀릿: Java 코드 실행
String name = "홍길동";
int age = 25;
%>
<p>이름: <%= name %></p> <%-- 표현식: 값 출력 --%>
<p>나이: <%= age %>세</p>
<%
// 조건문도 사용 가능
if (age >= 20) {
%>
<p>성인입니다.</p>
<%
} else {
%>
<p>❌ 미성년자입니다.</p>
<%
}
%>
</body>
</html>
브라우저에서 직접 접속:
http://localhost:8080/hello.jsp
체감 포인트: HTML 안에 Java 코드가
<% %>로 들어가니까 훨씬 읽기 쉬워졌습니다. 하지만 코드가 복잡해질수록<% %>가 난무해서 또 지저분해집니다.
서블릿에서 JSP로 요청을 포워드(forward) 하는 도구입니다.
[브라우저] → 요청 → [서블릿] → forward → [JSP] → 응답 → [브라우저]
데이터 준비 화면 출력
forward vs redirect
forward: 서버 내부에서 이동. URL이 바뀌지 않음. request 객체 공유 가능redirect: 브라우저에게 "다른 URL로 가세요" 지시. URL이 바뀜. request 객체 새로 생성
MemberListServlet.java 작성:
package com.example;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@WebServlet("/members")
public class MemberListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 데이터 준비 (나중에는 DB에서 가져옴)
List<String[]> members = new ArrayList<>();
members.add(new String[]{"홍길동", "hong@email.com", "서울"});
members.add(new String[]{"김철수", "kim@email.com", "부산"});
members.add(new String[]{"이영희", "lee@email.com", "대구"});
// 2. request에 데이터 저장 (JSP에서 꺼내 쓸 수 있음)
req.setAttribute("members", members);
req.setAttribute("totalCount", members.size());
// 3. JSP로 포워드 (화면 출력은 JSP에게 위임)
RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/memberList.jsp");
dispatcher.forward(req, resp);
}
}
webapp/
├── WEB-INF/
│ └── views/
│ └── memberList.jsp ← ✅ 직접 URL 접근 불가 (보안)
└── hello.jsp ← ❌ 브라우저에서 직접 접근 가능
WEB-INF폴더 안의 파일은 브라우저에서 직접 URL로 접근할 수 없습니다. 반드시 서블릿을 통해서만 접근 가능하게 만들어 JSP가 단독으로 실행되는 상황을 방지합니다.
src/main/webapp/WEB-INF/views/memberList.jsp 생성:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.List" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원 목록</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 30px auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
</style>
</head>
<body>
<h2>회원 목록</h2>
<%-- 서블릿이 저장한 데이터 꺼내기 --%>
<%
List<String[]> members = (List<String[]>) request.getAttribute("members");
Integer totalCount = (Integer) request.getAttribute("totalCount");
%>
<p>총 <strong><%= totalCount %></strong>명의 회원이 있습니다.</p>
<table>
<tr>
<th>번호</th>
<th>이름</th>
<th>이메일</th>
<th>지역</th>
</tr>
<%
for (int i = 0; i < members.size(); i++) {
String[] member = members.get(i);
%>
<tr>
<td><%= i + 1 %></td>
<td><%= member[0] %></td>
<td><%= member[1] %></td>
<td><%= member[2] %></td>
</tr>
<%
}
%>
</table>
</body>
</html>
http://localhost:8080/members접속해서 확인해 봅시다.
| 역할 | 담당 | 하는 일 |
|---|---|---|
| Model | Java 객체 (데이터) | 비즈니스 데이터 표현 (담아서 이동시키고 표현하는 것) |
| View | JSP, Thymeleaf, mustache | 화면 출력 |
| Controller | 서블릿 | 요청 받기, 데이터 준비, View 선택 |
[브라우저]
↓ 요청
[Controller: 서블릿]
↓ 데이터 요청
[Model: Java 객체]
↓ 데이터 반환
[Controller: 서블릿]
↓ Model을 request에 담아 forward
[View: JSP]
↓ HTML 생성
[브라우저] ← 응답
섹션 1의 회원가입 예제를 MVC 패턴으로 리팩토링합니다.
src/main/java/com/example/model/Member.java 생성:
package com.example.model;
public class Member {
private String name;
private String email;
private String password;
// 생성자
public Member(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
}
// Getter
public String getName() { return name; }
public String getEmail() { return email; }
public String getPassword() { return password; }
}
src/main/java/com/example/MemberController.java 생성:
package com.example;
import com.example.model.Member;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
// URL 패턴을 /member/* 로 지정해서 여러 기능을 하나의 서블릿에서 처리
@WebServlet("/member/*")
public class MemberController extends HttpServlet {
// 임시 저장소 (실제로는 DB 사용)
private List<Member> memberStore = new ArrayList<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String pathInfo = req.getPathInfo(); // /list, /new 등
if (pathInfo == null || pathInfo.equals("/list")) {
// 회원 목록 조회
req.setAttribute("members", memberStore);
forward(req, resp, "/WEB-INF/views/member/list.jsp");
} else if (pathInfo.equals("/new")) {
// 회원가입 폼
forward(req, resp, "/WEB-INF/views/member/form.jsp");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String pathInfo = req.getPathInfo();
if (pathInfo.equals("/new")) {
String name = req.getParameter("name");
String email = req.getParameter("email");
String password = req.getParameter("password");
// 유효성 검증
if (name == null || name.trim().isEmpty()) {
req.setAttribute("error", "이름을 입력해주세요.");
forward(req, resp, "/WEB-INF/views/member/form.jsp");
return;
}
// Model 저장
memberStore.add(new Member(name, email, password));
// ✅ POST 후 redirect (PRG 패턴: 새로고침 시 중복 제출 방지)
resp.sendRedirect("/member/list");
}
}
// forward 공통 메서드
private void forward(HttpServletRequest req, HttpServletResponse resp, String path)
throws ServletException, IOException {
RequestDispatcher dispatcher = req.getRequestDispatcher(path);
dispatcher.forward(req, resp);
}
}
src/main/webapp/WEB-INF/views/member/form.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; }
input { width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box; }
button { margin-top: 12px; padding: 10px 20px; background: #4CAF50; color: white; border: none; cursor: pointer; }
.error { color: red; margin-bottom: 10px; }
</style>
</head>
<body>
<h2>회원가입</h2>
<%-- 에러 메시지 출력 --%>
<% if (request.getAttribute("error") != null) { %>
<p class="error"><%= request.getAttribute("error") %></p>
<% } %>
<form action="/member/new" method="post">
<label>이름 *<br>
<input type="text" name="name">
</label><br><br>
<label>이메일<br>
<input type="email" name="email">
</label><br><br>
<label>비밀번호<br>
<input type="password" name="password">
</label><br>
<button type="submit">가입하기</button>
</form>
</body>
</html>
src/main/webapp/WEB-INF/views/member/list.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.List" %>
<%@ page import="com.example.Member" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원 목록</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 30px auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background-color: #4CAF50; color: white; }
a { display: inline-block; margin-bottom: 12px; padding: 8px 16px; background: #2196F3; color: white; text-decoration: none; }
</style>
</head>
<body>
<h2>회원 목록</h2>
<a href="/member/new">+ 회원 추가</a>
<%
List<Member> members = (List<Member>) request.getAttribute("members");
%>
<% if (members == null || members.isEmpty()) { %>
<p>등록된 회원이 없습니다.</p>
<% } else { %>
<p>총 <strong><%= members.size() %></strong>명</p>
<table>
<tr>
<th>번호</th><th>이름</th><th>이메일</th>
</tr>
<% for (int i = 0; i < members.size(); i++) {
Member m = members.get(i); %>
<tr>
<td><%= i + 1 %></td>
<td><%= m.getName() %></td>
<td><%= m.getEmail() %></td>
</tr>
<% } %>
</table>
<% } %>
</body>
</html>
1. http://localhost:8080/member/new → 회원가입 폼
2. 이름/이메일/비밀번호 입력 후 제출
3. http://localhost:8080/member/list → 등록된 회원 목록 확인
4. 이름 비워두고 제출 → 에러 메시지 확인
| 개념 | 핵심 내용 |
|---|---|
| JSP | HTML 안에 Java 코드를 삽입할 수 있는 기술, 내부적으로 서블릿으로 변환 |
| RequestDispatcher | 서블릿에서 JSP로 요청을 포워드하는 도구 |
| request.setAttribute | 서블릿 → JSP로 데이터를 전달하는 방법 |
| WEB-INF | 브라우저 직접 접근을 막는 보안 폴더, JSP는 여기에 보관 |
| MVC 패턴 | Model(데이터), View(JSP), Controller(서블릿)로 역할 분리 |
| PRG 패턴 | POST 후 Redirect하여 새로고침 중복 제출 방지 |
| forward vs redirect | forward는 서버 내부 이동, redirect는 브라우저에게 새 요청 지시 |