안녕하세요! JSP와 서블릿은 클라이언트의 요청을 처리하는 웹 애플리케이션의 핵심입니다. 그런데 서블릿을 개발할 때 초보자가 가장 흔하게 실수하는 부분이 바로 인스턴스 변수의 사용입니다. 이번 포스팅에서는 서블릿의 인스턴스 변수가 왜 위험한지, 그리고 어떻게 해결해야 하는지 알아보겠습니다.
대부분의 서블릿 컨테이너(예: Apache Tomcat, Jetty)는 효율성을 위해 서블릿을 단 하나의 인스턴스만 생성하여 재사용합니다. 즉, 웹 애플리케이션에 특정 서블릿이 아무리 많이 요청되어도, 그 서블릿 클래스의 객체는 메모리에 딱 하나만 존재한다는 의미입니다.
그리고 여러 클라이언트의 요청이 들어오면, 서블릿 컨테이너는 각 요청마다 새로운 스레드를 생성하고, 이 모든 스레드가 동일한 서블릿 인스턴스를 공유하여 사용합니다.
비유: 여러 명의 손님(클라이언트)이 하나의 주방(서블릿 인스턴스)에 몰려와 각자 주문(요청)을 합니다. 이 주문을 처리하기 위해 주방에서는 여러 명의 요리사(스레드)를 투입하는데, 이 요리사들은 모두 동일한 주방 공간과 도구, 재료를 공유하며 작업을 수행합니다.
만약 이 하나의 주방(서블릿 인스턴스)에 모두가 공유하는 공동 재료(인스턴스 변수)가 있다면 어떻게 될까요?
한 요리사(스레드)가 이 재료를 사용해 값을 바꾸는 도중에, 다른 요리사(스레드)가 갑자기 그 재료를 가져가서 값을 바꿔버릴 수 있습니다. 이처럼 여러 스레드가 동시에 하나의 자원(인스턴스 변수)에 접근하여 읽거나 쓰려고 할 때, 스레드 간의 실행 순서에 따라 데이터의 일관성이 깨지거나 예상치 못한 결과가 발생하는 현상을 스레드 안정성 문제(Thread-Safety Issue) 또는 경쟁 조건(Race Condition)이라고 합니다.
아래 간단한 예시를 통해 문제를 살펴봅시다.
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.io.PrintWriter;
@WebServlet("/counter") // 이 어노테이션으로 서블릿을 쉽게 등록할 수 있습니다.
public class MyCounterServlet extends HttpServlet {
// 경고! 인스턴스 변수로 선언된 카운터입니다.
// 이 변수는 모든 클라이언트(스레드)가 공유하게 됩니다.
private int counter = 0;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// HTTP 응답 헤더 설정 (한글 깨짐 방지 등)
response.setContentType("text/html;charset=UTF-8");
counter++; // !!! 이 부분이 여러 스레드에 의해 동시에 실행될 수 있어 문제가 발생합니다. !!!
PrintWriter out = response.getWriter();
out.println("<html><head><title>방문 카운터</title></head><body>");
out.println("<h1>현재 방문 횟수: " + counter + "</h1>");
out.println("<p>새로고침 할 때마다 카운터가 증가합니다.</p>");
out.println("</body></html>");
out.close(); // PrintWriter 닫기
}
}
그렇다면 어떻게 스레드 안정성 문제를 해결해야 할까요? 답은 간단합니다. 클라이언트마다 다른 값을 저장해야 하거나, 요청별로 독립적인 데이터를 처리해야 할 때는 인스턴스 변수를 사용하지 않는 것입니다.
지역 변수 사용: doGet(), doPost()와 같은 메서드 내부에 변수를 선언하면, 그 변수는 해당 요청을 처리하는 스레드에서만 사용되는 지역 변수가 됩니다. 따라서 다른 스레드가 접근할 수 없어 스레드 안정성 문제가 발생하지 않습니다. 마치 요리사가 자기 요리에만 필요한 재료를 개인 트레이에 담아 사용하는 것과 같습니다.
// 올바른 예시: 지역 변수 사용
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int localCounter = 0; // 요청마다 새롭게 생성되는 지역 변수
localCounter++; // 이 변수는 현재 스레드만 접근
// ... (이후 localCounter 사용)
}
내장 객체 사용: 클라이언트의 상태를 유지해야 하는 값(로그인 정보, 사용자별 설정 등)은 request 객체나 session 객체에 저장합니다. 이 객체들은 각각의 요청(request)이나 세션(session)에 고유하게 생성되거나 스레드와 1:1로 매핑되므로 안전합니다. 서블릿 컨테이너가 이 객체들을 스레드 안전하게 관리해 줍니다.
서블릿의 인스턴스 변수는 모든 스레드가 공유하므로, 클라이언트마다 다른 값을 저장해야 하거나 요청별로 독립적인 데이터를 처리할 때는 절대 사용하면 안 됩니다. 이것이 서블릿 개발의 기본이자 '황금률'입니다.
만약 여러 스레드가 공유해야 하지만 동시에 변경되는 변수가 꼭 필요하다면, volatile 키워드나 synchronized 블록, 또는 java.util.concurrent 패키지의 동시성 유틸리티 클래스(예: AtomicInteger) 등을 사용하여 동기화 처리를 해야 합니다. 하지만 이는 코드를 복잡하게 만들고 성능에 영향을 줄 수 있으므로, 초보자에게는 "서블릿 인스턴스 변수는 특별한 경우가 아니면 위험하다!"는 규칙을 따르는 것이 가장 안전하고 좋은 방법입니다.