스프링에 들어가기 전에, 기본적인 JSP와 서블릿 개념을 살펴보자.
JSP는 Java Server Page의 약어로 HTML 코드에 자바 코드를 넣어 동적 웹 페이지를 생성하는 웹 애플리케이션 도구다. JSP과 같이 등장하는 아파치 톰캣이라는 친구도 알아보자.
우리가 사용하는 웹 페이지는 아파치와 톰캣으로 이루어져있다. 하나씩 단어를 뜯어보자.
아파치는 소프트웨어 재단의 오픈소스 프로젝트다. 웹 서버라고 불리며 클라이언트에서 요청하는 HTTP 요청이 왔을 때만 응답하는 정적 웹 페이지에 사용된다.
톰캣은 마찬가지로 소프트웨어 재단의 오픈소스 프로젝트다. 컨테이너, 웹 컨테이너, 서블릿 컨테이너라고 불리며 동적인 웹을 만들기 위해 사용된다. (우리가 자주 듣는 WAS(Web Application Server)라는 아이다! 😉)
웹 서버에서 정적으로 처리해야할 데이터를 제외한 JSP, ASP, PHP 등을 이용하여 동적인 웹을 만들기 위한 웹 컨테이너, 서블릿 컨테이너 역할을 수행한다. 아파치 서버와는 다르게 DB 연결과 같은 다른 응용 프로그램과의 상호 작용 등 동적인 기능을 사용할 수 있다.
근데 왜 아파치 톰캣이라고 하는가?
이유는 톰캣이 아파치의 기능 일부를 가져와 제공해주는 형태이기 때문에 합쳐서 부른다.
우아한 Tech - 10분 테코톡 🐶 코기님의 영상이 이해하기에 너무 좋아서 이를 토대로 살펴보자.
하단의 이미지처럼 요청과 응답에는 정말 많은 내용이 담겨있다. 개발자는 들어온 요청에 대해서 모든 규약을 확인하고 분석해야하고, 규약에 맞춰 처리해 응답을 전달해주어야 한다.
굉장히 귀찮은 작업들임을 알 수 있다. 하지만 이를 HttpServletRequest
의 메소드를 호출하면 훨씬 간단하게 정보를 다룰 수 있다. 즉, 서블릿이 요구하는 규칙을 지켜주면서 서블릿을 정의해주면, HTTP 요청과 응답을 쉽게 처리할 수 있다는 의미다.
서블릿을 이용하게 되면 - 위에서 말했듯, 개발자가 요청이나 응답을 확인하고 분석할 필요가 없어지고 비즈니스 로직만 집중할 수 있게 된다.
영상에서는 간단한 서블릿 코드를 가져와서 설명했다.
init()
메소드 호출destroy()
메소드 호출service()
메소드 호출서블릿이 요청을 처리할 때 호출하는 service()
메소드는 간단하게 다음과 같은 이미지로 처리된다.
결국, 서비스 메소드만 재정의해서 처리 방법을 개발자의 부담이 덜해진다는 것이다! 🎉
service()
메소드를 호출한 후 클리이언트의 요청 메소드에 따라 do*()
호출do*()
메소드는 동적 페이지를 생성한 후 HttpServletResponse객체에 응답을 전송서블릿의 동작 방식을 이해했다면, 컨테이너는 쉽게 이해할 수 있다. 서블릿 컨테이너는 서블릿을 관리해주는 아이다. 서블릿 컨테이너가 수행하는 역할은 다음과 같다.
웹 서버와의 통신 지원
일반적으로 통신을 위해서는 소켓을 만들고 listen, accept를 해야하지만, 서블릿 컨테이너는 이러한 기능을 API로 제공하여 복잡한 과정을 생략하고 비즈니스 로직에만 집중할 수 있다.
서블릿 생명주기 관리
서블릿 컨테이너는 서블릿의 생성과 소멸을 관리한다. 서블릿 클래스를 로딩하여 인스턴스화하고 초기화 메소드를 호출하고 요청이 들어오면 적절한 서블릿 메소드를 호출한다. 그리고 생명을 다하게된 순간에는 GC를 진행하여 편의를 제공한다.
멀티스레드 지원 및 관리
서블릿 컨테이너는 요청이 들어올 때마다 새로운 스레드를 생성하는데, HTTP 서비스 메소드를 실행하고 나면 스레드는 자동으로 죽게된다. 원래는 스레드를 관리해야하지만, 서버가 다중 스레드를 생성 및 운영해주기 때문에 스레드의 안정성에 대해 걱정할 필요가 없어진다.
선언적인 보안 관리
서블릿 컨테이너를 사용하면 개발자는 보안에 관련된 내용을 서블릿이나 자바 클래스에 구현하지 않아도 된다. 일반적으로 보안관리는 XML에 기록하기에 보안에 대해 수정할 일이 생겨도 자바 코드를 수정하여 다시 컴파일하지 않아도 보안관리가 가능하다.
서블릿은 다양한 시점에 발생하는 이벤트와 이벤트를 처리하기 위한 인터페이스를 정의하고 있다. 이들 이벤트와 인터페이스를 이용하면 웹 어플리케이션에서 필요로 하는 데이터의 초기화와 요청 처리 등을 추적할 수 있게 된다.
즉, 서블릿 컨테이너가 우리가 원하는 내용을 같이 작업을 해줬으면 좋겠다 싶을 때 사용하는 거라고 이해하면 된다.
💡 스프링의 애플리케이션 Context는 IoC 컨테이너와 관련이 있을 뿐, 서블릿 Context와 전혀 관계가 없다.
JDBC는 자바 프로그램이 DB에 연결되어 데이터를 주고 받을 수 있게 도와준다. JDBC API를 사용하면 DBMS에 알맞는 드라이버만 있으면 어떤 데이터베이스라도 사용할 수 있다.
커넥션은 결국 운영체제가 맺어주는 개념이다. 따라서 OS의 자원을 사용하기 때문에 매번 커넥션을 생성하는 비용은 비효율적이다.
커넥션 풀은 데이터베이스와 연결된 커넥션을 미리 만들어두어 풀 속에 저장해두고 필요할 때 커넥션을 풀에서 꺼내와 쓰고 다시 풀에 반환하는 것을 의미한다. 풀 속에 미리 커넥션이 생성되어있기 때문에 커넥션을 생성하는 비용이 절감되고 빠르다. 또한 커넥션을 재사용하기 때문에 생성되는 커넥션의 수가 일정하게 유지되어 더 안정적으로 사용할 수 있다는 장점을 가지고 있다.
Statement와 PreparedStatement의 차이는 캐시 사용 여부다. 쿼리 실행은 크게 3 단계로 나뉜다.
Statement는 이러한 단계들을 매번 쿼리를 수행할 때마다 거치는 반면에, PreparedStatement는 처음 한 번만 이 단계들을 수행하고 캐시에 담아둔다.
따라서 PreparedStatement는 캐시에 담아 재사용하기 때문에 매번 실행할 때마다 쿼리를 파싱하는 Statement에 비해 성능적으로 좋다.
또한 PreparedStatement는 미리 파싱한 내용을 제외하고 나머지는 모두 쿼리로 취급하지 않기 때문에 기능적 편리함(값을 전처리하는 과정을 생략 - 대표적으로 '
)을 제공하고 SQL Injection의 대응 방안이 된다. (참고: https://jaredablon-31568.medium.com/how-to-prevent-sql-injection-vulnerabilities-how-prepared-statements-work-f492c369614f
)
클라이언트가 요청을 하면, 서블릿 컨테이너가 받는데, 가장 앞에서 서버로 들어오는 모든 요청을 처리하는 프론트 컨트롤러라는 것을 스프링에서 정의했고, 이를 Dispatcher Servlet 이라고 한다.
어떤 점 때문에 등장했을까? 🤔
앞에서 말했듯, 서블릿 컨테이너는 요청당 새로운 스레드를 생성한다. 따라서 한 개의 요청을 처리할 때 다른 요청을 처리해야하는 일이 생기면 멀티스레드로 요청을 처리하게 된다. 각 스레드에는 서로 다른 서블릿이 처리할 수도 있고, 혹은 각 스레드에서 동일한 서블릿을 이용할 수도 있다.
여기서 멀티스레드의 스레드 생성에 드는 비용, 다른 스레드로 전환하는 Context Switching, 스레드 생성 제한 등과 더불어 핸들러의 로직이 중복된다는 점에서 요청당 서블릿을 정해주는 행동은 굉장히 비효율적인 면이 있다.
하지만 Dispatcher Servlet을 사용하면, 공통된 로직을 묶어서 처리하기 때문에 효율성이 증가해진다.
Dispatcher Servlet의 전체적인 흐름