서블릿은 동시에 여러 요청을 처리하는데, 그때 같은 변수(메모리)를 여러 스레드가 동시에 만지면 값이 섞인다. 그게 사고다.
아래는 “서블릿 맥락”에서 감이 오게끔 정리한 설명이다.
서블릿은 요청마다 새로 만들어지지 않는다
많이 착각하는게
“요청 1개 오면 서블릿 객체 1개 생성해서 처리하고 버리겠지?”
대부분의 서블릿 컨테이너(톰캣)는 서블릿 인스턴스를 보통 1개만 만들어 놓고 재사용한다.
- /hello → HelloServlet 객체 1개
- 요청이 1만 번 와도 그 객체 1개를 계속 쓴다.
2) 동시에 요청이 오면 “스레드”가 여러 개가 된다
톰캣은 동시에 여러 클라이언트 요청을 처리해야 하니까 보통 이렇게 한다.
- 요청 A → 스레드 T1이 처리
- 요청 B → 스레드 T2가 처리
- 요청 C → 스레드 T3가 처리
문제는 여기서:
T1, T2, T3가 같은 HelloServlet 객체(1개)를 동시에 호출한다.
즉, doGet()이 동시에 여러 번 실행될 수 있다.
인스턴스 필드가 위험한 이유: “서블릿 객체 1개를 공유하기 때문”
예를 들어
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
private String currentUser; // 인스턴스 필드
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
currentUser = req.getParameter("user");
resp.getWriter().println("Hello " + currentUser);
}
}
이게 왜 문제냐면:
- 사용자 A가 /hello?user=alice 요청 (스레드 T1)
- 사용자 B가 /hello?user=bob 요청 (스레드 T2)
실제로는 이렇게 섞일 수 있다.
- T1: currentUser = "alice" 저장
- (바로 이어서) T2: currentUser = "bob"로 덮어씀
- T1: 응답 출력 → "Hello bob"가 나가버림 (alice가 bob으로 보임)
즉, 요청별로 따로 있어야 할 값이 “공유 변수”에 들어가면서 섞이는 것이 사고다.
여기서 중요한 포인트:
- currentUser는 “요청마다 달라지는 값”인데
- 서블릿 객체가 공유되니까 인스턴스 필드도 공유된다
- 그래서 멀티스레드에서 값이 엉킨다
그럼 static은 왜 “더” 위험하다고 하나?
인스턴스 필드는 최소한 “그 서블릿 객체 1개”에서 공유되는 거다.
그런데 static은 더 넓게 공유된다.
static 필드 예시
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
private static String currentUser; // static 필드
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
currentUser = req.getParameter("user");
resp.getWriter().println("Hello " + currentUser);
}
}
이 경우는 공유 범위가 이렇게 된다.
- HelloServlet 인스턴스가 1개든 2개든 상관없이,
- JVM 안에서 HelloServlet.currentUser는 딱 1개다.
즉, 서블릿 인스턴스를 여러 개로 늘리는 설정을 한다거나, 다른 방식으로 로딩되더라도,
static은 “클래스 자체”에 붙어 있으니 더 강한 전역 공유가 된다.
그리고 결과는 똑같다. 더 쉽게 섞인다.
“요청마다 달라지는 값”은 어디에 둬야 안전하나?
요청별 값은 요청 스레드마다/요청마다 독립적으로 있어야 한다. 대표적으로:
지역 변수(Local variable)로 두기 (가장 안전)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String user = req.getParameter("user"); // 지역변수: 요청마다 스택에 따로 생김
resp.getWriter().println("Hello " + user);
}
지역 변수는
- 메서드 호출(요청 처리) 동안만 존재하고
- 요청마다 스레드마다 각자 스택에 따로 생긴다
그래서 섞일 수가 없다.
요청 범위 저장소 사용
- request.setAttribute(...) : “이 요청 안에서만”
- session : “이 사용자(브라우저) 세션 안에서만”
- DB/캐시/외부 저장소 : 공유 데이터는 저장소가 책임지고 동시성도 별도 설계
사고가 “거의 무조건” 난다는 표현의 의미
요청이 한 번에 하나씩만 들어오면(동시 요청 없음) 운 좋게 안 터진다.
하지만 웹 서버는 현실적으로:
- 브라우저 탭 2개
- 새로고침 연타
- 여러 사용자 동시 접속
- 정적 리소스 요청도 동시 발생(css/js/image)
이런 상황에서 동시 요청이 기본으로 생긴다.
그래서 요청별 데이터를 인스턴스 필드/static에 넣는 순간,
언젠가는 섞이거나 레이스 컨디션이 터진다.
그 의미에서 “거의 무조건 사고”라고 말하는 거다.
한 문장으로 정리
- 서블릿은 보통 인스턴스 1개를 공유한다 → 인스턴스 필드도 공유 변수가 된다
- static은 클래스에 딱 1개다 → 더 강한 전역 공유 변수가 된다
- 요청마다 달라지는 값은 공유 변수에 두면 → 동시 요청에서 섞여서 터진다
- 요청별 값은 지역 변수나 request/session 같은 “범위가 분리된 곳”에 둬야 한다
'Backend > Servlet' 카테고리의 다른 글
| forward와 JSP 실행 흐름 (0) | 2026.02.06 |
|---|---|
| 멀티스레드 환경에서 Servlet 사용 (0) | 2026.02.06 |
| Servlet Container와 Servlet의 관계 (0) | 2026.02.05 |
| RequestMapping static Map은 괜찮은 이유 (0) | 2026.02.05 |
| 서블릿 요청 흐름 : Request가 채워지는 시점과 service() 호출 위치 (0) | 2026.02.05 |