안녕하세요! 이전 포스트에서 서블릿을 구현하여 간단한 요청과 응답을 하는 예제를 살펴봤는데요. 하지만 실제 웹 애플리케이션에서는 요청과 응답만으로는 부족하며, 보다 복잡한 기능을 효과적으로 관리할 필요가 있습니다.
오늘은 서블릿을 활용하여 간단한 회원 관리 시스템을 구현해보겠습니다. 이를 통해 웹 애플리케이션의 기본 동작 원리를 이해할 수 있을 뿐만 아니라, 왜 이후에 템플릿 엔진과 같은 도구들이 등장하게 되었는지도 자연스럽게 알게 될 것입니다.
회원 관리 시스템을 구현하기 위해 먼저 회원을 나타내는 도메인 클래스를 만들어야 합니다. 도메인 클래스는 애플리케이션에서 중요한 데이터를 담고 있으며, 객체의 속성과 동작을 정의합니다.
아래는 회원 정보를 저장할 Member 클래스로, 회원의 id, username, age 정보를 관리합니다. 또한, 객체의 생성과 데이터 관리를 간편하게 하기 위해 롬복(Lombok) 라이브러리를 활용하여 코드의 간결성을 높였습니다.
package hello.servlet.domain.member;
import lombok.Data;
@Data
public class Member {
private Long id; // 회원의 고유 ID
private String username; // 회원 이름
private int age; // 회원 나이
// 기본 생성자 (자바 빈 규약을 위해 필요)
public Member() {
}
// 모든 필드를 초기화하는 생성자
public Member(String username, int age) {
this.username = username;
this.age = age;
}
}
id : 회원의 고유 식별자 (자동 증가 또는 DB 관리)username : 사용자의 이름age : 사용자의 나이username과 age 값을 초기화할 수 있도록 제공.@Data 어노테이션은 getter, setter, toString, equals, hashCode 메서드를 자동으로 생성해줍니다.이렇게 Member 클래스를 정의하면, 이후 서블릿에서 회원정보를 등록하고 조회할 때 이 객체를 활용할 수 있습니다. 다음 단계에서는 회원을 저장하고 관리할 저장소 클래스를 만들어보겠습니다.
이제 회원 정보를 관리할 저장소 클래스를 구현하겠습니다. MemberRepository 클래스는 회원 객체를 저장하고 조회할 수 있도록 도와줍니다. 현재 구현은 동시성 문제가 고려되지 않은 단순한 저장소로, 학습 목적으로 사용됩니다. 실무에서는 동시성을 고려한 ConcurrentHashMap과 AtomicLong을 사용해야 합니다.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>(); // static 사용 (회원 정보 저장소)
private static long sequence = 0L; // static 사용 (회원 ID 자동 증가 시퀀스)
private static final MemberRepository instance = new MemberRepository();
// 싱글톤 패턴 적용 (하나의 인스턴스만 사용)
public static MemberRepository getInstance() {
return instance;
}
private MemberRepository() {
}
/**
* 회원 저장
*/
public Member save(Member member) {
member.setId(++sequence); // 시퀀스를 증가하여 ID 할당
store.put(member.getId(), member); // 저장소에 회원 추가
return member;
}
/**
* ID를 통해 회원 찾기
*/
public Member findById(Long id) {
return store.get(id); // 저장소에서 ID로 회원 검색
}
/**
* 모든 회원 조회
*/
public List<Member> findAll() {
return new ArrayList<>(store.values()); // 모든 회원 정보를 리스트로 반환
}
/**
* 저장소 초기화 (테스트용)
*/
public void clearStore() {
store.clear(); // 저장소 초기화
}
}
MemberRepository는 싱글톤 패턴을 적용하여 애플리케이션 전반에서 하나의 인스턴스만 유지하도록 설계되었습니다.private static final MemberRepository instance = new MemberRepository();getInstance() 메서드를 통해 단 하나의 저장소 인스턴스만 사용하도록 보장합니다.private으로 선언하여 외부에서 객체 생성을 막습니다.💡 싱글톤 패턴을 사용하는 이유 ??
1. 하나의 저장소 인스턴스를 유지하여 데이터 일관성 보장.
2. 불필요한 객체 생성을 방지하고, 메모리 절약.
3. 애플리케이션 전체에서 동일한 저장소를 공유.
💡 하지만, 스프링부트를 사용하면 싱글톤 패턴을 직접 구현할 필요가 없습니다.
스프링 부트는 @Component, @Service, @Repository 등의 애너테이션을 통해 자동으로 싱글톤 빈(Bean)을 생성하고 관리합니다.
store : 회원 정보를 저장하는 HashMap (Key: 회원 ID, Value: Member 객체)sequence : 회원의 ID를 자동 증가시키는 역할save(Member member) :findById(Long id) :findAll() :clearStore() :(회원 저장소 테스트 코드는 다른 포스트에서 작성하겠습니다!!)
회원 정보를 등록하기 위해 웹 브라우저에서 입력할 수 있는 HTML 폼을 제공하는 서블릿을 구현했습니다.
이 서블릿은 클라이언트가 GET 요청을 보내면 HTML 페이지를 생성하여 응답합니다.
package hello.servlet.web.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/newform")
public class MemberFormServlet extends HttpServlet {
@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>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>회원 가입</title>\n" +
"</head>\n" +
"<body>\n" +
"<h1>회원 가입 폼</h1>\n" +
"<form action=\"/servlet/members/save\" method=\"post\">\n" +
" username: <input type=\"text\" name=\"username\" />\n" +
" age: <input type=\"text\" name=\"age\" />\n" +
" <button type=\"submit\">전송</button>\n" +
"</form>\n" +
"</body>\n" +
"</html>\n");
}
}
@WebServlet)@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/newform")
name : 서블릿의 이름 지정urlPatterns: 클라이언트가 /servlet/members/newform 경로로 접근 시 서블릿이 실행됨response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
text/html : 응답이 HTML 문서임을 브라우저에게 알림.utf-8 : 한글 등 다양한 문자를 정상적으로 표시할 수 있도록 인코딩.PrintWriter w = response.getWriter();
w.write("<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>회원 가입</title>\n" +
"</head>\n" +
"<body>\n" +
"<h1>회원 가입 폼</h1>\n" +
"<form action=\"/servlet/members/save\" method=\"post\">\n" +
" username: <input type=\"text\" name=\"username\" />\n" +
" age: <input type=\"text\" name=\"age\" />\n" +
" <button type=\"submit\">전송</button>\n" +
"</form>\n" +
"</body>\n" +
"</html>\n");
PrintWriter를 사용하여 클라이언트에게 HTML을 응답.action 속성 /servlet/members/save로 지정하여 데이터를 POST 방식으로 전송.
이제 회원 정보를 입력받아 저장하는 기능을 구현하겠습니다. 이 서블릿은 회원 가입 폼에서 전송된 데이터를 받아 저장하고, 성공 메시지를 응답하는 역할을 합니다.
package hello.servlet.web.servlet;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@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 ServletException, IOException {
System.out.println("MemberSaveServlet.service");
// 1. 요청 데이터 받기
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
// 2. Member 객체 생성 및 저장
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
// 3. 응답 설정
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
// 4. 저장 결과 HTML 응답
PrintWriter w = response.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"<h1>회원 저장 성공</h1>\n" +
"<ul>\n" +
" <li>ID: " + member.getId() + "</li>\n" +
" <li>Username: " + member.getUsername() + "</li>\n" +
" <li>Age: " + member.getAge() + "</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인으로</a>\n" +
"</body>\n" +
"</html>");
}
}
@WebServlet을 이용한 서블릿 등록@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
/servlet/members/save 경로로 POST 요청을 보내면 이 서블릿이 실행됨.name="memberSaveServlet"은 서블릿의 이름을 명시 (필수 아님).service() 메서드에서 요청 처리protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpServletRequest request : 클라이언트가 보낸 요청 정보.HttpServletResponse response : 클라이언트에게 응답을 보낼 객체.String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
username과 age 값을 request.getParameter()로 가져옴.getParameter()는 항상 문자열을 반환하므로, age 값은 Integer.parseInt()를 사용하여 변환.Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
Member 객체를 생성.memberRepository.save(member)를 통해 저장.response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
HTML 형식임을 명시.utf-8 설정으로 한글이 깨지지 않도록 설정.PrintWriter w = response.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"<h1>회원 저장 성공</h1>\n" +
"<ul>\n" +
" <li>ID: " + member.getId() + "</li>\n" +
" <li>Username: " + member.getUsername() + "</li>\n" +
" <li>Age: " + member.getAge() + "</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인으로</a>\n" +
"</body>\n" +
"</html>");
getWriter()를 사용하여 HTML 응답을 직접 생성.
이제 회원 목록을 조회하는 서블릿을 구현했습니다. 이 서블릿은 저장된 모든 회원 정보를 테이블 형식으로 출력하는 역할을 합니다.
package hello.servlet.web.servlet;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@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 ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
// 1. 회원 목록 조회
List<Member> members = memberRepository.findAll();
// 2. HTML 응답 생성
PrintWriter w = response.getWriter();
w.write("<html>");
w.write("<head>");
w.write(" <meta charset=\"UTF-8\">");
w.write(" <title>회원 목록</title>");
w.write("</head>");
w.write("<body>");
w.write("<h1>회원 목록</h1>");
w.write("<a href=\"/index.html\">메인으로</a>");
w.write("<table border='1'>");
w.write(" <thead>");
w.write(" <tr>");
w.write(" <th>ID</th>");
w.write(" <th>Username</th>");
w.write(" <th>Age</th>");
w.write(" </tr>");
w.write(" </thead>");
w.write(" <tbody>");
// 3. 회원 정보 반복 출력
for (Member member : members) {
w.write(" <tr>");
w.write(" <td>" + member.getId() + "</td>");
w.write(" <td>" + member.getUsername() + "</td>");
w.write(" <td>" + member.getAge() + "</td>");
w.write(" </tr>");
}
w.write(" </tbody>");
w.write("</table>");
w.write("</body>");
w.write("</html>");
}
}
@WebServlet을 이용한 서블릿 등록@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
/servlet/members 경로로 GET 요청을 보내면 이 서블릿이 실행됨.response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
text/html: 응답이 HTML 형식임을 브라우저에게 알림.utf-8: 한글이 깨지지 않도록 인코딩 설정.List<Member> members = memberRepository.findAll();
memberRepository.findAll()을 통해 저장된 모든 회원 데이터를 가져옴.PrintWriter를 사용하여 동적으로 HTML을 생성하고 응답.w.write("<table border='1'>");
w.write(" <thead>");
w.write(" <tr>");
w.write(" <th>ID</th>");
w.write(" <th>Username</th>");
w.write(" <th>Age</th>");
w.write(" </tr>");
w.write(" </thead>");
for (Member member : members) {
w.write(" <tr>");
w.write(" <td>" + member.getId() + "</td>");
w.write(" <td>" + member.getUsername() + "</td>");
w.write(" <td>" + member.getAge() + "</td>");
w.write(" </tr>");
}
for 문을 이용해 List<Member>에 저장된 모든 회원 정보를 반복 출력.
지금까지 서블릿과 자바만으로 HTML을 만들어보았습니다. 서블릿 덕분에 동적(ex. 멤버가 늘어나면 멤버리스트에 따라서 테이블이 늘어나는 것을 동적이라 함.)으로 원하는 HTML을 마음껏 만들 수 있습니다. 정적인 HTML 문서라면 화면이 계속 달라지는 회원의 저장 결과라던가, 회원 목록 같은 동적인 HTML을 만드는 일은 불가능 할 것입니다.
하지만, 코드에서 보이듯이 이것은 매우 복잡하고 비효율적입니다. 자바코드로 HTML을 만들어 내는 것보다 HTML문서에 동적으로 변경해야 하는 부분만 자바코드를 넣는것이 훨씬 편리합니다. 이것이 바로 템플릿 엔진(JSP,Thymeleaf,Freemarker 등등) 이 나온 이유입니다.
따라서, 다음 포스트는 JSP, 더 나아가 스프링과 통합이 잘되는 Thymeleaf를 사용하여 동일한 작업을 진행하는 포스트를 올리겠습니다. 다음시간에 뵈어요~!