[Spring] Part 1-4-2. 나만의 MVC 프레임워크 만들기 | CGI 프로그램과 서블릿 (Servlet)
2023.01.13
-
반응형
1. CGI 프로그램과 서블릿
CGI란?
CGI는 웹 서버와 애플리케이션 사이에 데이터를 주고 받는 규약입니다.
CGI 규칙에 따라서 만들어진 프로그램을 CGI 프로그램이라고 할 수 있는 것입니다.
CGI 프로그램 종류로는 컴파일 방식 (C, C++, Java 등)과 인터프리터 방식(PHP, Python 등)이 존재합니다.
위 그림은 웹서버와 CGI 규칙을 통해서 데이터를 주고 받는 프로그램을 도식화한 것입니다.
인터프리터 방식 CGI 프로그램
인터프리터 방식 CGI 프로그램에서 웹서버는 Script engine(스크립트 엔진)을 실행시키며, 스크립트 엔진은 스크립트 파일을 해석하여 웹서버에게 결과값을 리턴하는 방식의 프로그램입니다.
위 그림에서 웹서버와 스크립트 엔진 사이 CGI 규칙을 통해 통신을 한다고 볼 수 있습니다.
서블릿과 서블릿 컨테이너
서블릿(Servlet)과 서블릿 컨테이너(Servlet Container) 역시 동일하게 규칙이 적용되는데, 웹서버와 서블릿 컨테이너 사이에 CGI 규칙에 따라서 데이터를 주고 받습니다.
즉, 개발자는 CGI 규칙에 대해서 자세히 알 필요가 없어졌습니다. 대신 서블릿 컨테이너와 서블릿 파일 사이의 규칙을 알고 있어야 합니다.
서블릿(Servlet)이란?
그렇다면 서블릿이란 무엇일까요?
Servlet (Server + Applet의 합성어)
서블릿이란, 자바에서 웹 애플리케이션을 만드는 기술이라고 간단하게 말할 수 있습니다.
즉, 동적인 웹페이지를 구현하기 위한 표준이라고 보시면 됩니다.
서블릿 컨테이너(Servlet Container)란?
그렇다면 서블릿 컨테이너란 무엇일까요?
앞서 두 가지 방식은 모두 CGI 규칙을 통해서 데이터를 주고 받는다고 했는데, 어떤 것은 engine이라는 표현을 사용하고 어떤 것은 Container라는 표현을 사용하는 이유가 무엇일까요?
Container라 함은 Life Cycle을 관리한다고 할 때 사용됩니다. 그래서 서블릿 컨테이너는 서블릿의 Life Cycle을 관리하기 때문에 그러한 이름이 붙여졌다고 생각할 수 있습니다.
그래서 Servlet Conatiner는 다음과 같은 특징을 가지고 있습니다.
서블릿의 생성부터 소멸까지의 Life Cycle(라이프 사이클, 생명주기)을 관리하는 역할
서블릿 컨테이너는 웹서버와 소켓을 만들고 통신하는 과정을 대신 처리해 준다.
우리는 지난번 포스팅에서 WAS를 직접 만들어 보았는데 그 과정을 대신해서 해 준다고 생각하면 됩니다.
따라서 개발자는 비지니스 로직에만 집중하면 된다.
서블릿 객체를 싱글톤으로 관리 (싱글톤: 인스턴스 하나만 생성하여 공유하는 방식)
상태를 유지(stateful)하게 설계하면 안 됨(Thread safety 하지가 않기 때문)
2. WAS vs. Servlet Container
그렇다면 WAS라는 표현과 Servlet Conatiner의 차이는 무엇일까요?
WAS는 서블릿 컨테이너를 포함하는 개념이라고 보면 편합니다. 즉, WAS가 조금 더 큰 개념인 것이죠.
지난번 WAS를 구현해 보면서 알게된 점은 WAS는 매 요청마다 Thread pool에서 기존 스레드를 재사용하는데, WAS의 주요 튜닝 포인트(tuning point)는 max thread 수 인 것입니다. (max thread 수에 따라서 최대 처리 스레드 수가 달라지기 때문)
그리고 대표적인 WAS로 톰캣(Tomcat)이 있습니다.
위 사진은 WAS가 Thread pool에서 Thread를 하나 꺼내서 요청에 맞는 처리를 수행하고 과정을 통해 웹서버에 결과값을 전달하며, 클라이언트에게 다시 그 값을 최종적으로 반환하는 모습입니다.
그림에서 Servlet에 해당하는 부분을 보시면 init(), service(), destroy()에 해당하는 것들이 있는데 이것들은 추후에 서블릿을 학습하면서 계속해서 추가적으로 설명을 하도록 할 것입니다.
3. 코드로 살펴보기
앞서 서블릿 객체를 싱글톤으로 관리하기 때문에 상태를 유지(stateful)하게 설계하면 안 된다고 했는데요. (Thread safety 하지가 않기 때문) 왜 그런 것인지 코드로 한번 살펴보겠습니다.
package org.example.counter;
public class Counter implements Runnable{
private int count = 0;
public void increment() {
count++;
}
public void decrement() {
count--;
}
public int getValue() {
return count;
}
@Override
public void run() {
this.increment();
System.out.println("Value for Thread After increment " + Thread.currentThread().getName() + " " + this.getValue()); // 1
this.decrement();
System.out.println("Value for Thread at last " + Thread.currentThread().getName() + " " + this.getValue()); // 0
}
}
위 코드와 같이 간단한 Counter 라는 스레드를 하나 만들어 그 안에서 count 변수값을 메소드에 의해 증가, 감소 시키고 run 메소드에서 각각을 실행해 보고 값이 어떻게 변화하는지 살펴보도록 구현하였습니다.
그리고 이를 실행시키는 RaceConditionDemo를 하나 만들것인데요, psvm을 작성하면 public static void main을 만들어주는 shortcut이 나오는데 이를 생성해 줍니다.
public class RaceConditionDemo {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(counter, "Thread-1");
Thread t2 = new Thread(counter, "Thread-2");
Thread t3 = new Thread(counter, "Thread-3");
t1.start();
t2.start();
t3.start();
}
}
위 main 메소드를 실행시키 전에 먼저 예상하기로는 각 스레드에서 1과 0이 찍히기를 기대할 수 있습니다.
각 스레드에서는 초깃값 0에 대해서 한번 증가시키고 로그를 찍고 감소시키고 로그를 찍기 때문이죠.
그러나 실제로 실행을 해 보고 로그를 살펴보면
이와 같이 순서도 뒤죽박죽이고 나올 수 없는 2나 3과 같이 예상치 못한 숫자들이 나옵니다.
이를 통해 알 수 있는 사실은 싱글톤 객체에서 상태를 유지(stateful)하게 설계를 하게 되면 위와 같이 문제가 발생한다는 사실입니다.
해당 내용은 운영체제(OS) 과목에서 자세히 다루는 내용(Race Condition)으로 학교에서 배웠던 내용을 추후에 업로드 할 예정인데 학교에서 해당 내용을 배웠을 때도 굉장히 중요하게 다루었던 기억이 있습니다.
정리하자면, 싱글톤 객체이며 멀티 스레드(multi-thread) 환경에서 하나의 자원(resource)을 공유하게 되면 우리가 뜻하지 않던 race condition이 발생한다는 사실을 알 수 있습니다.
참고로race condition이란 여러 스레드가 동시에 하나의 자원에 접근하기 위해서 경쟁하는 상태를 말합니다.
해당 경우(race condition)는 thread safety하지 않다고 하는데, 위 경우에서 만약 thread safety 했다면 우리가 원하는 결과인 1 0 1 0 1 0 의 순서로 나왔어야 할 것입니다.
사실 이 부분은 동기화(synchronization)처리를 해 주면 쉽게 해결할 수 있습니다.
간단히 코드만 보여드리면,
public void run() {
synchronized (this) {
this.increment();
System.out.println("Value for Thread After increment " + Thread.currentThread().getName() + " " + this.getValue()); // 1
this.decrement();
System.out.println("Value for Thread at last " + Thread.currentThread().getName() + " " + this.getValue()); // 0
}
}
이처럼 synchronized 블럭으로 race condition이 일어날 부분을 묶어주면 됩니다.
그러고 나서 실행을 하면 아래와 같이 성공적으로 우리가 원하는 결과가 나오게 되는 것을 확인할 수 있습니다.
해당 부분에 대해서는 싱글톤 객체에 대해 상태를 유지하게 설계하면 race condition이 발생할 수 있음을 명심하도록 합시다. 이는 스프링에서 스프링 빈이라는 것이 디폴트가 싱글톤으로 관리하기 때문에 상태를 유지하게 설계하면 절대 안됩니다.