[필터란?]
서블릿이나 JSP에 요청이 도달하기 전에 요청과 응답을 가로채서 처리하는 컴포넌트이다. 필터는 요청을 수정하거나, 응답을 변경하거나, 로깅 및 인증 등의 작업을 수행할 수 있다.
1. 사용자가 브라우저에서 /hello 요청
2. Filter1에서 로그 찍음 → 다음 필터 호출
3. Filter2에서 인증 체크 → 통과 시 다음 필터 호출
4. Filter3에서 헤더 추가 → 다음으로 넘어감
5. ExampleServlet 실행 → "Hello" 응답 생성
6. 응답이 Filter3 → Filter2 → Filter1 역순으로 거쳐 나감
7. 클라이언트에게 응답이 도착
필터 : 요청 전/후 공통 로직 처리
필터 체인 : 여러 필터가 연결된 구조
doFilter() : 다음 필터 또는 서블릿 실행
[구현할 구성요소]
MyFilter | 사용자가 구현할 필터 인터페이스 |
FilterChain | 다음 필터를 호출하는 체인 객체 |
LoggingFilter | 요청 전/후 로그 출력 필터 |
ServletServer | 필터들을 실행하도록 수정 |
ServletApplication | 필터 등록 추가 |
1. MyFilter 인터페이스 - 모든 필터가 구현해야 하는 인터페이스 (doFilter() 메서드 포함)
public interface MyFilter {
void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws Exception;
}
2. FilterChain - 등록된 필터들을 순서대로 실행하고, 마지막에는 서블릿을 실행시키는 연결 컨트롤러 역할
public class FilterChain {
private final List<MyFilter> filters;
private final MyServlet servlet;
private int index = 0;
public FilterChain(List<MyFilter> filters, MyServlet servlet) {
this.filters = filters;
this.servlet = servlet;
}
public void doFilter(HttpRequest request, HttpResponse response) throws Exception {
if (index < filters.size()) {
MyFilter nextFilter = filters.get(index++);
nextFilter.doFilter(request, response, this); // 다음 필터 실행
} else {
servlet.service(request, response); // 필터 끝 → 실제 서블릿 실행
}
}
}
3. LoggingFilter - 요청/응답 전후에 로그를 출력하는 예시 필터 구현체
public class LoggingFilter implements MyFilter {
@Override
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws Exception {
System.out.println("[Filter] 요청 전: " + request.getPath());
System.out.println("Processing by: " + Thread.currentThread().getName());
chain.doFilter(request, response); // 다음 필터 or 서블릿
System.out.println("[Filter] 응답 후: " + request.getPath());
}
}
4. ServletServer필터 적용 추가 - 실제 ServerSocket을 열고 요청을 받아 필터 → 서블릿으로 넘겨주는 미니 서블릿 컨테이너 역할
public class ServletServer {
private final Map<String, MyServlet> servletMap = new HashMap<>();
private final List<MyFilter> filters = new ArrayList<>(); // 필터 목록 추가
private final ExecutorService threadPool = Executors.newFixedThreadPool(10); //스레드풀 필드 추가
public void registerServlet(String path, MyServlet servlet) {
servletMap.put(path, servlet); // 경로 등록
}
// 필터 등록 메서드
public void addFilter(MyFilter filter) {
filters.add(filter);
}
public void start(int port) throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server started at port " + port);
while (true) {
Socket socket = serverSocket.accept();
//new Thread(() -> handle(socket)).start(); // 요청마다 새 스레드
threadPool.execute(() -> handle(socket)); // 새 스레드 대신 threadPool 사용
}
}
private void handle(Socket socket) {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))
) {
HttpRequest request = new HttpRequest(in); // 요청 파싱
HttpResponse response = new HttpResponse(out); // 응답 생성
System.out.println("Processing by: " + Thread.currentThread().getName());
// 요청에 해당하는 서블릿 찾기
MyServlet servlet = servletMap.getOrDefault(request.getPath(), new NotFoundServlet());
// servlet.service(request, response); // 서블릿 위임
// 필터 체인 생성 후 실행
FilterChain chain = new FilterChain(filters, servlet);
chain.doFilter(request, response);
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
6. ServletApplication에서 필터 등록 - 프로그램의 시작점 main()이 있는 클래스. 서버를 생성하고, 서블릿과 필터를 등록
public class ServletApplication {
public static void main(String[] args) throws Exception {
ServletServer server = new ServletServer();
server.registerServlet("/hello", new ExampleServlet());
server.addFilter(new LoggingFilter()); // 필터 등록
server.start(8080);
}
}
[Filter] 요청 전: /hello | 클라이언트가 /hello로 요청을 보냈고, 필터가 실행되기 시작 |
Processing by: pool-1-thread-1 | 해당 요청은 스레드풀의 1번 스레드가 처리 중 |
[Filter] 응답 후: /hello | 필터가 서블릿 실행 후 후처리 작업을 실행 |
서블릿 리스너 ? 컨테이너에서 발생하는 특정 이벤트를 감지하고, 거기에 자동으로 반응하는 클래스(서블릿)
언제 작동할까?
-> 이런 사건들(event)이 발생하면, 리스너가 자동으로 호출
- 웹 애플리케이션이 시작될 때
- 사용자가 요청을 보낼 때
- 서블릿이 destroy()될 때
- 사용자가 로그인/로그아웃할 때
- 세션이 만들어지거나 사라질 때
사용자(client) : 브라우저나 앱이 서버로 request를 보냄
컨네이너 : 요청을 받고 서블릿 객체를 생성/실행함
서블릿 객체 : 요청을 실제 처리하는 코드 ( service(), doGet(), doPost() 등)
listener : 이벤트 발생을 감지하고 자동으로 처리되는 클래스( ex. 서버 시작 로그 남기기)
1. 사용자가 /hello 요청을 보냄
2. 컨테이너가 Servlet 객체를 만들고 init() 호출
3. service() → doGet() 등으로 요청 처리
4. 이때 request 이벤트가 발생하면 listener가 감지
5. listener에 등록된 메서드가 자동 실행됨 (예: 로그 찍기)
[구현할 구성요소]
MyRequestListener | 리스너 인터페이스 |
RequestLoggerListener | 요청 들어올 때마다 로그 찍는 실제 리스너 |
Listener 등록/관리 | 여러 리스너 등록하고 실행 |
ServletServer | 요청 시작/종료 시 리스너를 호출 |
1. MyRequestListener - 요청이 시작될 때 / 끝날 때 호출되는 메서드 정의
public interface MyRequestListener {
void onRequestInitialized(HttpRequest request);
void onRequestDestroyed(HttpRequest request);
}
2. RequestLoggerListener - 실제로 요청 전/후에 로그를 찍는 간단한 리스너 구현체
public class RequestLoggerListener implements MyRequestListener {
@Override
public void onRequestInitialized(HttpRequest request) {
System.out.println("[Listener] 요청 시작: " + request.getPath());
}
@Override
public void onRequestDestroyed(HttpRequest request) {
System.out.println("[Listener] 요청 종료: " + request.getPath());
}
}
3. ServletServer 리스너 등록 추가
필드추가
private final List<MyRequestListener> requestListeners = new ArrayList<>();
public void addRequestListener(MyRequestListener listener) {
requestListeners.add(listener);
}
handle() 안에 리스너 호출 추가
private void handle(Socket socket) {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))
) {
HttpRequest request = new HttpRequest(in);
HttpResponse response = new HttpResponse(out);
// 요청 시작 시 리스너 실행
for (MyRequestListener listener : requestListeners) {
listener.onRequestInitialized(request);
}
MyServlet servlet = servletMap.getOrDefault(request.getPath(), new NotFoundServlet());
FilterChain chain = new FilterChain(filters, servlet);
chain.doFilter(request, response);
// 요청 끝났을 때 리스너 실행
for (MyRequestListener listener : requestListeners) {
listener.onRequestDestroyed(request);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
4. ServletApplication에서 리스너 등록
public class ServletApplication {
public static void main(String[] args) throws Exception {
ServletServer server = new ServletServer();
server.registerServlet("/hello", new ExampleServlet());
server.addFilter(new LoggingFilter());
// 리스너 등록
server.addRequestListener(new RequestLoggerListener());
server.start(8080);
}
}
<실행결과>
세션? = 사용자별 정보 저장소
HTTP는 기본적으로 요청 간에 상태를 기억하지 않는다.
즉, 사용자가 로그인했는지, 어떤 장바구니를 갖고 있는지 웹 서버는 기본적으로 기억하지 못한다.
그래서 웹 서버는 사용자마다 상태(정보)를 유지하기 위해 세션을 사용한다.
1. 사용자가 로그인 버튼 클릭 → /login 요청 보냄
2. 서버는 사용자 정보를 확인한 뒤, 세션 ID를 생성
3. 세션 ID를 쿠키에 담아 클라이언트에게 응답
4. 이후 사용자가 다시 요청하면 → 서버는 세션 ID로 사용자 정보 인식
[구현할 구성요소]
Session | 사용자별 데이터를 저장하는 객체 (Map<String, Object>) |
SessionManager | 세션 ID 생성 및 저장소 관리 |
HttpRequest.getSession() | 현재 세션 가져오기 (없으면 생성) |
SessionFilte | 요청마다 세션 ID 확인 + 없으면 생성 |
1. Session
package depth.servlet.session;
import java.util.HashMap;
import java.util.Map;
public class Session {
private final Map<String, Object> data = new HashMap<>();
public void setAttribute(String key, Object value) {
data.put(key, value); // 세션에 key-value쌍 저장
}
public Object getAttribute(String key) {
return data.get(key); // 세션에서 key에 해당하는 값 꺼내기
}
public void removeAttribute(String key) {
data.remove(key); // 세션에서 특정 key 삭제
}
public Map<String, Object> getAllAttributes() {
return data; // 전체 데이터 반환
}
}
2. SessionManager - 세션 ID 생성/조회/저장소 관리
package depth.servlet.session;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class SessionManager {
private final Map<String, Session> sessions = new ConcurrentHashMap<>();
public String createSessionId() {
return UUID.randomUUID().toString();
}
public Session createSession() {
String sessionId = createSessionId();
Session session = new Session();
sessions.put(sessionId, session);
return session;
}
public Session getSession(String sessionId) {
return sessions.get(sessionId);
}
public boolean hasSession(String sessionId) {
return sessions.containsKey(sessionId);
}
public Map<String, Session> getAllSessions() {
return sessions;
}
}
3. SessionFilter - 요청마다 세션 확인 또는 생성
public class SessionFilter implements MyFilter {
private static final String SESSION_HEADER = "X-Session-Id"; // 요청 헤더에서 세션ID추출
private final SessionManager sessionManager;
public SessionFilter(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Override
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws Exception {
String sessionId = request.getHeader(SESSION_HEADER);
Session session;
// 세션 ID가 없거나 잘못되면 새 세션을 만들고 X-Session-Id 헤더로 보냄
if (sessionId == null || !sessionManager.hasSession(sessionId)) {
session = sessionManager.createSession();
sessionId = sessionManager.getAllSessions()
.entrySet().stream()
.filter(entry -> entry.getValue() == session)
.findFirst().get().getKey();
} else {
session = sessionManager.getSession(sessionId);
}
request.setSession(session); // HttpRequest에 세션 연결
response.setHeader(SESSION_HEADER, sessionId); // 클라이언트에 세션ID 전송
chain.doFilter(request, response); //필터 체인 계속 진행
}
}
4. HttpRequest - 세션 지원 기능 추가
public class HttpRequest {
private final String method;
private final String path;
private final Map<String, String> headers = new HashMap<>();
private Session session;
public HttpRequest(BufferedReader reader) throws IOException {
String line = reader.readLine(); // ex: GET /hello HTTP/1.1
String[] parts = line.split(" ");
method = parts[0];
path = parts[1];
while (!(line = reader.readLine()).isEmpty()) {
String[] kv = line.split(": ");
if (kv.length == 2) headers.put(kv[0], kv[1]);
}
}
public String getMethod() { return method; }
public String getPath() { return path; }
public String getHeader(String name) { return headers.get(name); }
public Session getSession() { return session; }
public void setSession(Session session) { this.session = session; }
}
5. HttpResponse - 세션 ID를 응답 헤더에 포함
public class HttpResponse {
private final BufferedWriter writer;
private final Map<String, String> headers = new HashMap<>();
public HttpResponse(BufferedWriter writer) {
this.writer = writer;
}
public void setHeader(String key, String value) {
headers.put(key, value);
}
public void write(String body) throws IOException {
writer.write("HTTP/1.1 200 OK\r\n");
writer.write("Content-Type: text/plain\r\n");
// 새 ID를 클라이언트에게 전달
for (Map.Entry<String, String> entry : headers.entrySet()) {
writer.write(entry.getKey() + ": " + entry.getValue() + "\r\n");
}
writer.write("Content-Length: " + body.length() + "\r\n");
writer.write("\r\n");
writer.write(body);
writer.flush();
}
}
6. ServletApplication - 세션 필터 등록
public class ServletApplication {
public static void main(String[] args) throws Exception {
ServletServer server = new ServletServer();
server.registerServlet("/hello", new ExampleServlet());
server.addFilter(new LoggingFilter());
SessionManager sessionManager = new SessionManager();
server.addFilter(new SessionFilter(sessionManager)); // 세션 필터 등록
server.start(8080);
}
}
7. ExampleServlet - 세션 사용 예시
package depth.servlet.example;
import depth.servlet.*;
public class ExampleServlet implements MyServlet {
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
Object counter = request.getSession().getAttribute("counter");
int count = counter == null ? 1 : ((int) counter) + 1;
request.getSession().setAttribute("counter", count);
response.write("You visited this page " + count + " times.");
}
}
'서버 심화 스터디' 카테고리의 다른 글
의존성 주입(Dependency Injection) 을 구현한 예제<알림 서비스> (0) | 2025.05.09 |
---|---|
의존성 주입(Dependency Injection) (0) | 2025.05.09 |
서블릿 컨테이너 구현 (0) | 2025.05.02 |
스레드 풀(thread pool) (0) | 2025.05.01 |
기본 소켓 프로그래밍 실습 (0) | 2025.04.11 |