개요

처음 웹을 배울 때 배운 것은 서블릿이였다. 서블릿은 싱글톤으로, 처음 생성되었을 때 init() 메소드가 실행되고 종료될 때는 destroy()메소드가 실행된다.
여러번 서블릿을 호출했을 때 init 메소드는 한 번만 실행되고 다음 요청부터는 service()메소드가 실행되는 것을 확인했다. 그 개념만 들었을 때는 조금 의아했다. 하나의 객체만 생성한다면 여러 클라이언트로부터 요청이 동시다발적으로 온다면 어떻게 처리가 되는거지?
하나의 요청에 대한 처리가 끝날 때까지 기다렸다가 다른 요청을 처리하는 건가?

싱글톤

싱글톤이란 여러개의 똑같은 인스턴스를 만들어서 사용하는 것이 아닌 하나의 인스턴스만 만들어서 이를 재사용하는 패턴을 말한다.
만약 무언가를 처리할 때 매번 인스턴스를 만들어서 사용한다면 이는 메모리적으로 낭비일 수 있다.
따라서 하나의 인스턴스를 만들어 사용하는 것이다.

서블릿, 스프링은 모두 객체를 하나만 만들고 재사용하는 싱글톤 패턴을 사용한다.

프로세스와 스레드

쓰레드보다 앞서 프로세스에 대해 알아보자.
프로세스란 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터, 메모리에 올라와 실행되고 있는 프로그램의 인스턴스를 말한다.

우리가 사용하는 프로그램을 생각해보자. 이는 윈도우로 따지면 .exe 확장자를 가지는 파일이고 맥으로 따지면 .dmg형식의 파일이다. 이처럼 컴퓨터에서 실행할 수 있는 파일을 말한다.

이는 자바, 파이썬과 같은 프로그래밍 언어로 이루어진 코드 뭉치이다.

프로세스는 이 프로그램을 실행시켜서 정적 -> 동적으로 상태가 변경되고 실행중인 프로그램을 말한다. 예를 들어 윈도우에서 작업을 하다가 프로그램이 멈추면 control + alt + delete 명령어를 통해 실행중인 프로그램을 확인하고 응답 없음으로 나타난 프로그램을 강제 종료시킨다.
여기에 나타난 목록이 바로 프로세스 목록이다.

정적 프로그램이 프로세스로 변환되기 위해서 운영체제는 자원을 할당해주어야한다.
그 후 프로그램 코드를 실행시켜 우리가 사용하는 프로그램을 사용할 수 있는 것이다.

스레드도 여기서 연관되는 개념이다.
보통 우리는 하나의 작업을 하며 다른 작업을 하는데에 익숙해져있다. 웹 브라우저를 켜고 하나의 탭에서는 유튜브 음악을 재생시키고 다른 탭에서는 파일을 다운받고 또 다른 탭에서는 블로그 글을 작성하는 등. 이 것이 가능한 이유가 바로 스레드 덕분이다.

스레드는 하나의 프로세스에서 동시에 진행되는 작업의 갈래를 말한다. 나뭇가지가 갈라져서 각기 자란 것처럼 동시 다발적이다.

프로세스는 내부에 4가지의 메모리 영역으로 구성되어있다.

  • 코드 영역 : 코드 뭉치가 기계어 형태로 저장되어있음
  • 데이터 영역 : 코드가 실행되면서 사용하는 변수나 데이터가 존재함
  • 스택 영역 : 함수의 호출과 관계있는 변수가 저장되는 영역
  • 힙 영역 : 동적으로 할당되는 데이터를 위해 존재

Java에서 애플리케이션을 실행시켰을 때도 마찬가지이다.
애플리케이션은 하나의 프로세스에 해당하고 여러 스레드가 동시에 작업을 수행할 수 있다.
클라이언트 3명이 서버에 요청을 동시에 보낸다면 이들은 각각 Thread 1,2,3와 같이 할당될 수 있다.

싱글톤과 스레드

image-15 image-16

@Service
public class StatelessService {
    public void process() {
        System.out.println("Processing request by " + Thread.currentThread().getName());
    }
}

@RestController
public class StatelessController {
    @Autowired
    private StatelessService statelessService;

    @GetMapping("/process")
    public String process() {
        statelessService.process();
        return "Request processed";
    }
}

우리가 자바 코드를 작성할 때 new 키워드를 통해 인스턴스를 생성하면 RDA의 힙 영역에 저장된다. 그리고 멀티 쓰레드 환경에서 주입 받은 statelessService와 같은 객체는 각 쓰레드의 stack영역에 객체의 참조로 저장된다.

여기서 또 궁금했던 것이 여러개의 쓰레드를 택배 물류 센터의 레일이라고 비유해보면 100개의 레일(100건의 동시 요청)을 한 사람(싱글톤 객체)이 처리하는 구조 아닌가 였다.

싱글톤과 멀티 쓰레드 환경에서 sample.doSomething()과 같이 메소드를 호출하는 코드를 만나면 JVM은 먼저 해당 클래스를 로드한다. 클래스가 로드되면 JVM은 메소드 영역에서 해당 클래스의 메타 데이터를 로드한다.
이 곳엔 클래스의 구조, 메소드, 바이트코드, 상수 풀 등의 정보가 포함된다. doSomething메소드의 바이트코드도 이 메소드 영역에 저장되어있다.
메소드 호출 시 JVM은 메소드 영역에서 Sample클래스의 doSomething메소드를 검색한다.
스택 프레임이 생성되어 로컬 변수와 메소드 상태를 저장하고 바이트코드를 해석하고 메소드를 실행한다.

메소드 실행이 완료되면 스택 프레임이 제거되고 결과가 호출자에게 반환된다.
스택 프레임이 제거되면 메소드 호출과 관련된 로컬 변수와 매개변수가 메모리에서 삭제된다.

이런 과정을 거친다. 즉, 객체는 힙에 존재하지만 코드를 실행시킬 때에는 메소드 영역을 참조하여 실행된다. 따라서 하나의 객체가 처리하지만 코드를 실행하는 정보는 메소드 영역을 참조한다는 것이다.

위에서 비유한 물류센터의 경우 라인 100개(100개의 동시 요청)를 한 사람(힙 영역의 싱글톤 객체)이 처리하는 것이 아닌 일처리 메뉴얼을 가진 로봇(메소드 영역의 정적인 정보)이 처리하는 것이라고 할 수 있다.

여기서 스프링의 빈을 설계하고 사용할 때의 주의점이 생긴다.
만약 빈 내부에 상태를 갖는 변수가 있다면 이 상태가 공유되기 때문에 문제가 생길 수 있다.
예를 들어 클래스 레벨에 전역 변수가 있다면 이는 힙 메모리에 저장된다. 따라서 이 값은 여러 스레드가 동시에 접근할 수 있고 동시성 이슈가 발생할 수 있는 것이다.

따라서 멀티 스레드 환경에서 스프링이 싱글톤을 사용하더라도 빈 내부에 상태를 갖도록 설계하지 않는다면 문제 없다는 결론이 나온다.