본문 바로가기

서버 심화 스터디

서블릿 필터와 리스너

[필터란?]

서블릿이나 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 필터가 서블릿 실행 후 후처리 작업을 실행

 


서블릿 리스너 ?  컨테이너에서 발생하는 특정 이벤트를 감지하고, 거기에 자동으로 반응하는 클래스(서블릿)

언제 작동할까?
  • 웹 애플리케이션이 시작될 때
  • 사용자가 요청을 보낼 때
  • 서블릿이 destroy()될 때
  • 사용자가 로그인/로그아웃할 때
  • 세션이 만들어지거나 사라질 때
-> 이런 사건들(event)이 발생하면, 리스너가 자동으로 호출

 

 

사용자(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.");
    }
}