멀티쓰레드에서 서블릿 인스턴스를 재사용할 때 발생할 수 있는 문제점?

2 minute read

JVM 메모리는 어떻게 관리되며, 인스턴스를 재사용할 때 어떤 문제가 발생할 수 있을까?

자바 프로그래밍에서 클래스의 인스턴스를 생성할 때 비용이 발생한다. 인스턴스를 생성하고 더 이상 사용하지 않을 경우 가비지 콜렉션 과정을 통해 메모리에서 해제하는 과정 또한 비용이 발생한다. 따라서 인스턴스를 매번 생성할 필요가 없는 경우 매번 인스턴스를 생성하지 않는 것이 성능 측면에서 더 유리하다.

이 단계에서 개발자가 갖추어야 할 역량 중의 하나가 클라이언트 요청마다 매번 인스턴스를 생성해야 하는지, 생성하지 않고 이미 생성된 인스턴스를 재사용할 것인지를 판단해야 한다. 이에 대한 기준은 인스턴스가 상태 값을 유지해야 하는지에 따라 구분된다. 매 클라이언트마다 서로 다른 상태 값을 유지할 피요가 있는 경우에는 매 요청마다 인스턴스를 생성해야 한다. 하지만 Controller등은 매 요청마다 서로 다른 상태 값을 가지지 않기 때문에 매번 인스턴스를 생성하지 않고 인스턴스 하나를 생성한 후 재사용할 수 있다.

서블릿은 서블릿 컨테이너가 시작할 때 인스턴스 하나를 생성한 후 재사용한다. 이 같은 환경에서 소스코드 구현을 잘못하면 심각한 버그를 만들어 낼 수 있다. 이 버그는 매번 발생하는 버그가 아니라 여러명의 클라이언트가 동시에 같은 코드를 실행 하는 경우 발생할 수 있기 때문에 간헐적으로 발생한다. 따라서 버그의 원인을 찾기 힘들다. 하지만 버그에 따른 결과는 치명적일 수 있기 때문에 반드시 주의해야 한다.

JVM은 코드를 실행하기 위해 메모리를 스택과 힙 영역으로 나눠서 관리한다. 스택 영역은 각 메소드가 실행될 때 메소드의 인자. 로컬 변수 등을 관리하는 메모리 영역으로 각 스레드마다 서로 다른 스택 영역을 가진다. 힙 영역은 클래스의 인스턴스 상태 데이터를 관리하는 영역이다. 힙 영역은 스레드가 서로 공유할 수 있는 영역이다. 메모리의 구조는 대략 다음과 같다

connect

JVM은 각 메소드 별로 스택 프레임을 생성한다. ShowController의 execute() 메소드를 실행하면 execute() 메소드에 대한 스택 프레임의 로컬 변수 영역의 첫 번째 위치에 자기 자신에 대한 메모리 위치를 가리킨다. ShowController에 대한 인스턴스는 힙에 생성되어 있으며, ShowController는 필드에 Question과 List를 가지기 때문에 힙에 생성되어 있는 Question과 List 인스턴스를 가리키는 구조로 실행된다. 위와같은 구조에서 2개의 스레드가 ShowController의 execute() 메소드를 실행한 결과는 다음과 같다.

connect

첫 번째 쓰레드가 접근했을 때는 별다른 특이사항이 없다. 하지만 첫번째 스레드가 완료되지 않은 상태에서 두 번째 쓰레드 요청에 의해 execute() 메소드가 실행될 경우 발생한다. 이 때의 메모리 상태는 아래와 같다.

원래 JVM Stack은 각 쓰레드에 맞게 두개를 그려야 하지만 공간 상 하나로 대체 하였습니다.

connect

두 번째 쓰레드가 실행 되면서 ShowController가 가리키는 Question과 List가 1번이 아닌 2번으로 바뀌었다. 두 번째 쓰레드는 정상적인 실행이 되겠지만 이 때 `첫번째 쓰레드도 1번이 아닌 2번에 대한 응답을 받게` 된다.

이를 해결하기 위해서 Question과 List를 execute()의 로컬 변수로 구현함으로써 해결할 수 있다. 이 때 메모리 접근은 다음과 같이 변하게 된다.

connect

위와 같이 구현하면 ShowController가 Question과 List 인스턴스에 대한 참조를 가지지 않고 메소드의 스택프레임의 로컬 변수 영역에서 해당 인스턴스에 대한 참조를 가진다.

참고 : 포비의 자바 웹 프로그래밍 Next Step