본 글은 인프런 김영한님의 스프링 완전 정복 로드맵을 기반으로 정리했습니다.
웹 서버와 웹 애플리케이션 서버(WAS)의 경계는 조금 모호하다. 간단하게 말하자면 웹 서버는 정적 리소스를 제공하는 서버고, 기타 부가기능을 제공한다. WAS는 프로그램 코드를 실행해서 애플리케이션 로직을 수행할 수 있는 서버를 말한다. 그런데 웹 서버도 프로그램을 실행하는 기능을 포함할 수 있으며, WAS도 웹 서버의 정적 리소스 제공기능을 포함할 수 있다. 즉, WAS만으로도 시스템 구성이 가능하단 것이다. 그런데도 굳이 둘을 구분하는 이유는 다음과 같다.
구분하지 않으면, WAS의 역할이 너무 많기 때문에 과부하가 올 수 있다.
구분하지 않으면, 복잡하고 중요한 로직 실행이 간단한 정적 리소스 제공 때문에 수행이 어려워 진다.
구분하지 않으면, 장애 상황시 정적 리스소 제공이 막히기 때문에 에러 페이지조차 띄우지 못한다.
구분하면, 웹 서버와 WAS를 독립적으로 scale-out/scale-in 할 수 있는 유연함이 생긴다.
위와 같은 이유로, 대부분의 시스템은 웹 서버와 WAS를 독립적으로 사용한다. 웹 서버는 정적 리소스 제공이나 기타 부가기능을 처리하고 복잡한 서비스 로직이나 DB 접근 같은 경우는 WAS로 위임해서 처리한다.
웹 애플리케이션 서버를 로직처리에 특화되어 있다고 했다. 일반적으로 HTTP 프로토콜 위에서 동작하는 웹 애플리케이션의 로직을 처리하기 위해 무엇이 필요한지 생각해보자.
우리가 관심있는건 4번이다. 나머지는 직접 구현하기 쉽지 않고 매번 구현하기엔 중복 또한 매우 심할 것이다. 개발자가 핵심 로직만 작성할 수 있도록 도와주는것이 서블릿이다.
서블릿은 위와 같이 @WebServlet
애노테이션을 통해 등록한다. xml 파일을 통해 등록할 수도 있다.
WAS는 등록한 서블릿들을 서블릿 컨테이너 내부에 싱글톤으로 관리해준다. 서블릿은 싱글톤이기 때문에 공유변수를 사용하면 안 된다. 각각 다른 HTTP 요청은 멀티쓰레드를 통해 처리되는데, 이 때 동시에 서블릿의 공유변수에 접근하면 동시성 이슈가 발생할 수 있기 때문이다.
핵심로직은 HttpServlet
클래스를 상속해서 오버라이딩한 protected service(HttpServletRequest req, HttpServletResponse resp)
내부에 작성하면 된다.
HttpServletRequest
타입은 인터페이스로 HTTP 요청을 파싱하고 처리하는데 필요한 다양한 메서드를 제공한다. HttpServletResponse
타입도 인터페이스로 HTTP 응답을 생성하는데 필요한 다양한 메서드를 제공한다. 두 타입의 객체 모두 요청마다 HTTP 메시지를 기반으로 새로 생성된다. 즉, 서블릿은 싱글톤이고 서블릿의 메서드가 사용하는 HttpServletRequest, HttpServletResponse 타입의 객체는 HTTP 요청마다 새로 생성된다.
서블릿을 사용해서 HTTP 요청/응답을 보다 쉽게 처리하고 핵심 비즈니스 로직 처리에 집중할 수 있다.
서블릿 컨테이너는 등록한 서블릿들을 생성, 초기화, 호출, 관리한다. 고객의 요청마다 서블릿을 새로 생성하는건 매우 비효율적이기 때문에 서블릿 컨테이너는 최초 로딩시 등록한 서블릿들을 생성 및 초기화해서 싱글톤으로 재사용 한다. 컨테이는 고객의 요청을 처리할 수 있는 서블릿을 HttpServletRequest, HttpServletResponse 타입의 객체를 통해 호출하는 책임도 가진다.
위에서 서블릿에서 공유 필드를 사용하면 동시성 이슈가 발생할 수 있다고 했다. 동시성 이슈가 발생할 수 있다는건 서블릿은 공유 객체라는 것이다. 좀 더 자세히 말하자면, HTTP 요청 하나마다 쓰레드 하나씩 배정되고 여러 쓰레드가 서블릿에 동시에 접근해서 로직을 처리한다.
쓰레드들은 쓰레드 풀에서 일정 숫자만큼 생성되어 관리된다. 최대 쓰레드 개수는 직접 설정할 수 있으며 톰캣의 경우 최대 200개까지 관리할 수 있다. 만약 모든 쓰레드가 사용중이라면 새로운 사용자 요청은 쓰레드가 쓰레드 풀에 반납되기를 기다려야 된다. 사용자 요청마다 쓰레드를 새로 생성하지 않고 쓰레드를 반납/재사용하는 이유는 다음과 같다.
WAS의 주요 튜닝 포인트중 하나가 바로 이 최대 쓰레드 개수(max thread size)이다. 적절한 쓰레드 개수는 애플리케이션 마다 상이하기 때문에 성능 테스트툴 (JMeter, nGrinder ..) 과 모니터링을 통해 적절한 값을 찾아나가야 한다.
멀티쓰레드 프로그램을 동기화하는건 매우 어려운 일이다. WAS가 멀티쓰레드 동기화를 알아서 해주기 때문에 개발자는 마치 싱글쓰레드 프로그램을 작성하듯이 로직을 짜면 된다. 하지만, 사용자 요청들이 멀티쓰레드로 동시에 처리된다는걸 인지하는건 중요하다. 이를 모르고 싱글톤인 서블릿에 공유 필드를 사용했다가 동시성 이슈로 큰 버그가 발생할 수 있다.
서블릿과 멀티쓰레드 개념을 이해하는것은 중요하다. 그러나, 요즘 서블릿을 직접 등록할 일은 거의 없다. 현대에는 대부분 디스패쳐 서블릿과 애노테이션 기반의 SpringMVC 를 사용하기 때문에 서블릿 대신 SpringMVC의 기능을 사용해서 개발하게 된다. ex) @Controller, @RequestParam, ...
다음 글에서 디스패쳐 서블릿과 스프링에서 제공하는 MVC 기능들을 다루도록 하겠다.