서블릿 생명주기와 스레드 공유: 서블릿은 톰캣이 하나만 만들어 돌려쓰는 객체였다

서블릿은 톰캣이 하나만 만들어 여러 스레드가 공유한다. 필드에 값을 담으면 안 되는 이유, doGet을 누가 부르는지, doPost를 안 만들면 405가 나는 이유까지.

조회
목차

서블릿 게시판의 첫 단계. 브라우저에서 주소를 치면 내가 만든 자바 클래스가 글자 하나를 응답하는 것까지다.

코드는 짧았는데 모르는 게 계속 나왔다. 특히 틀리게 알고 있던 것들이 많았다.

이름이 세 개인데 뭐가 뭔지 몰랐다

WAS, 톰캣, 서블릿 컨테이너가 다 같은 걸 말하는 것 같기도 하고 다른 것 같기도 했다. "서블릿은 JVM에 톰캣 안에서 도는 거 아니야?"라고 물었는데 순서가 틀렸다.

JVM 프로세스
 └ 톰캣 (자바로 만든 프로그램)
    └ 서블릿 컨테이너
       └ 내 서블릿 객체
  • WAS는 역할 이름
  • 톰캣은 그 역할을 하는 제품 이름
  • 서블릿 컨테이너는 톰캣 안에서 서블릿을 만들고 부르고 없애는 부품

서블릿이 스레드를 만드는 줄 알았다

이런 문장을 읽었다.

"프로세스가 아닌 스레드를 생성하는 자바의 Servlet이 등장했다"

서블릿이 스레드를 만든다고 이해했는데 반대였다.

  • 스레드는 톰캣이 만든다. 요청마다 하나씩 붙여준다
  • 서블릿 객체는 하나뿐이다. 톰캣이 하나 만들어두고 계속 재사용한다

개수가 안 맞는다. 스레드는 여러 개인데 객체는 하나다.

그래서 "서블릿은 단일 스레드"라고 생각하면 안 된다. 한 번에 한 명만 들어온다고 착각하게 되는데, 실제로는 여러 스레드가 같은 객체를 동시에 지나간다.

객체를 하나만 쓰는 이유

객체를 매번 새로 만들고 버리는 게 비싸서다. 예전 CGI 방식이 요청마다 프로세스를 만들어서 느렸던 것과 같은 이유가 객체에도 적용된다.

싸게 얻은 대신 대가가 있다. 여럿이 같은 객체를 만지니까 값을 담아두면 안 된다.

"필드에 값을 담지 마라"를 상수 얘기로 오해했다

상수 얘기가 아니라 위치 얘기였다.

누구 것언제 사라지나
필드 (클래스 안, 메서드 밖)객체 것 — 하나뿐이라 모두가 같은 걸 본다서버 끌 때
지역 변수 (메서드 안)그 요청 것 — 스레드마다 따로 생긴다메서드 끝나면

정확한 규칙은 "필드를 만들지 마라"가 아니라 **"바뀌는 값을 필드에 두지 마라"**다. 한 번 정하고 안 바뀌는 값(static final)은 여럿이 동시에 봐도 덮어쓸 게 없어서 안전하다.

숫자가 왜 틀리는지 몰랐다

필드에 숫자를 두고 요청마다 1씩 올리면, 세 명이 동시에 들어왔을 때 3이 나와야 할 것 같다. 1이 될 수 있다.

"1씩 올린다"가 한 동작이 아니라 세 단계라서다.

1. 지금 값을 읽는다
2. 1을 더한다
3. 결과를 쓴다

스레드는 이 사이에 끼어든다.

스레드A: 0 을 읽음
스레드B: 0 을 읽음        ← A가 아직 안 썼다
스레드A: 1 을 씀
스레드B: 1 을 씀          ← A가 쓴 걸 덮어쓴다

두 번 올렸는데 1이다.

doGet을 누가 부르는지 몰랐다

내 코드에는 doGet을 부르는 줄이 없는데 불린다. 톰캣이 부른다.

요청 도착 → 톰캣이 service() 를 부름
          → service() 가 GET/POST 를 보고 doGet/doPost 를 고름

service()HttpServlet이 이미 만들어놨다. 내가 할 일은 doGet만들어 두는 것뿐이다.

내 코드가 남의 코드를 부르는 게 아니라, 내가 만들어 둔 걸 남이 부른다. 서블릿의 많은 게 이 구조에서 나온다 — 왜 main이 없는지, 왜 등록이 필요한지 전부.

POST로 보내면 500일 줄 알았는데 405였다

doPost를 안 만들었으니 아무 일도 안 나거나 에러가 날 줄 알았다.

HttpServlet은 빈 껍데기가 아니었다. doGet·doPost·doPut·doDelete를 다 갖고 있고 기본 구현이 전부 405(허용되지 않는 메서드)를 응답한다. 내가 재정의한 것만 실제로 일한다.

500이 아닌 이유: 500은 "내 코드가 돌다가 터졌다"인데, 여기선 내 코드가 아예 안 돌았다.

그래서 @Override가 중요하다. 이름을 doGett으로 잘못 쓰면 부모 것을 덮은 게 아니라 새 메서드를 만든 게 되고, 그러면 GET도 405가 난다. @Override가 컴파일 때 잡아준다.

응답에 글자 쓰는 법

response.println(...)이 아니다. response.getWriter()로 통로를 꺼내서 거기에 쓴다.

response.setContentType("text/html; charset=UTF-8");  // 먼저
PrintWriter out = response.getWriter();               // 그다음
out.println("Hello");

응답 본문은 한 덩이 값이 아니라 브라우저로 흘러가는 통로다. 그 통로를 잡아야 글자를 흘려보낼 수 있다.

setContentTypegetWriter()보다 먼저 해야 한다. 통로를 잡은 뒤에 인코딩을 바꾸면 늦어서 한글이 깨진다.

서블릿을 왜 등록해야 하나

톰캣은 내 클래스가 있는지 모른다. .java 파일은 그냥 파일이다. 잘 만들어져 있어도 등록이 없으면 아무 일도 안 일어난다.

web.xml에 두 덩이가 필요하다.

<servlet>          클래스에 이름을 붙인다
<servlet-mapping>  그 이름을 주소에 붙인다

이름을 중간에 두는 이유는 한 클래스를 여러 주소에 붙일 수 있게 하려고다.

<servlet-mapping>만 지우면 404가 뜬다. 서블릿은 만들어져 있는데 아무도 못 부른다.

폴더 이름이 자유롭지 않았다

자바는 패키지 이름과 폴더 경로가 같아야 한다. 점이 폴더 구분이다.

package board;   →  src/main/java/board/

파일 이름과 클래스 이름도 같아야 한다. 프론트에서 폴더 자유롭게 두던 거랑 다르다.

스프링부트는 서블릿을 안 쓰는 줄 알았다

쓴다. 서블릿이 딱 하나 있고 이름이 DispatcherServlet이다. 모든 요청이 일단 거기로 들어가고, 그 안에서 주소를 보고 내가 만든 컨트롤러로 갈라낸다.

톰캣도 그대로 쓴다. 다만 내가 설치한 톰캣에 올리는 게 아니라 앱 안에 톰캣이 들어 있다. 없어진 게 아니라 안 보이게 된 거다.

어떻게 넘어갔나

아는 것에 대응시킨 게 제일 빨랐다.

Next.js서블릿
route.ts서블릿 클래스
export function GETdoGet
파일 경로가 주소가 됨web.xml이 주소를 붙임
Next가 불러줌톰캣이 불러줌

route handler 파일 최상단에 변수를 두고 요청마다 고치면 안 되는 것도 같은 이유였다. 그 파일도 한 번 로드되고 재사용되니까.

그리고 직접 찔러봤다. POST가 405라고 듣기만 한 게 아니라 curl -X POST로 보내서 톰캣이 준 405 페이지를 봤다. 예측이 틀렸던 게 더 오래 남는다.

남길 것

  • 서블릿은 담는 게 아니라 하는 거다. 요청이 오면 실행되는 처리 코드지, 값을 담아두는 통이 아니다.
  • 내가 부르는 게 아니라 내가 만들어 둔 걸 남이 부른다.
  • 객체 하나를 여럿이 쓰는 구조에서는 에러 없이 틀린다. 터지지 않는 버그가 제일 찾기 어렵다.
  • 프론트 지식이 자산이다. 모르는 걸 아는 것에 대응시키는 게 설명 듣는 것보다 빨랐다.
  • 틀린 예측이 맞는 설명보다 오래 남는다.

더 봐야 할 것

  • 바뀌는 값을 필드에 못 두면, 요청 사이에 값을 이어가야 할 때는 어디에 두나
  • 필드에 카운터를 두고 탭 여러 개로 동시에 요청해서 숫자가 어긋나는 걸 눈으로 보기
  • DispatcherServlet 하나가 어떻게 여러 컨트롤러로 갈라내나

사이트 내 검색

제목·태그·설명·본문으로 발행 글을 찾습니다