스택과 힙 메모리

jeonye·2022년 2월 5일
0

소프트웨어 개념

목록 보기
1/2

자바 웹 프로그래밍 Next Step(박재성 지음) 책을 공부하던 중 애플리케이션과 메모리의 내용이 이해하기 쉽게 기재되어 있어 반복적으로 보기 위해 책의 내용을 발췌하였다.
발췌 범위는 p312~318의 내용이다.

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

Model 객체는 클라이언트마다 서로 다른 상태 값을 가진다. 이와같이 매 클라이언트마다 서로 다른 상태 값을 유지할 필요가 있는 경우에는 매 요청마다 인스턴스를 생성해야한다.
하지만, JdbcTemplate, 모든 DAO, Controller는 매 요청마다 서로 다른 상태 값을 가지지 않기 때문에 매번 인스턴스를 생성하지 않고 인스턴스 하나를 생성한 후 재사용할 수 있다.

서블릿은 서블릿 컨테이너가 시작할 때 인스턴스 하나를 생성한 후 재사용한다. 그런데 서블릿 컨테이너는 멀티스레드 환경에서 동작한다.
즉, 멀테스레드 환경에서 여러명의 사용자가 인스턴스 하나를 재사용하고 있다.
이 같은 환경에서 소스코드 구현을 잘못하면 심각한 버그를 만들어 낼 수 있다. 이 버그는 매번 발생하는 버그가 아니라 여러명의 클라이언트가 동시에 같은 코드를 실행하는 경우 발생할 수 있기 때문에 간헐적으로 발생한다.

public class ShowController extends AbstractController {
    private QuestionDao questionDao = new QuestionDao();
    private AnswerDao answerDao = new AnswerDao();
    private Question question;
    private List<Answer> answers;

    @Override
    public ModelAndView execute(HttpServletRequest req, HttpServletResponse response) throws Exception {
        Long questionId = Long.parseLong(req.getParameter("questionId"));

        question = questionDao.findById(questionId);
        answers = answerDao.findAllByQuestionId(questionId);

        ModelAndView mav = jspView("/qna/show.jsp");
        mav.addObject("question", question);
        mav.addObject("answers", answers);
        return mav;
    }
}

위 소스코드에서 문제가 발생할 수 있는 부분은 ShowController의 필드로 구현되어 있는 부분이다. ShowController는 인스턴스 하나를 여러 개의 스레드가 공유하고 있다.

이 코드가 메모리에서 어떻게 동작하는지 알아보자.

먼저 JVM은 코드를 실행하기 위해 메모리를 스택과 힙 영역으로 나눠서 관리한다.

  • 스택 영역 : 각 메소드가 실행될 때 메소드의 인자, 로컬 변수 등을 관리하는 메모리 영역으로, 각 스레드마다 서로 다른 스택 영역을 가진다.
  • 힙 영역 : 클랙스의 인스턴스 상태 데이터를 관리하는 영역으로, 스레드가 서로 공유할 수 있는 영역이다.

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

첫 번째 스레드(사용자)가 접근했을 때는 별다른 특이사항이 없다. 문제는 첫 번째 스레드가 실행되는 과정, 즉, execute() 메소드가 완료되지 않은 상태에서 두 번째 스레드 요청에 의해 execute() 메소드가 실행될 경우 발생한다. 이 때의 메모리 상태는 다음과 같다.

두 번째 스레드가 실행되면서 ShowController가 가리키는 Question과 List가 1번이 아닌 2번으로 바뀌었다. 두 번째 스레드는 정상적으로 2번 글에 대한 질문과 답변을 응답으로 받는다. 문제는 첫 번째 스레드에서 발생한다. 첫 번째 스레드는 1번 글을 보기 위한 요청을 보냈다. 그런데 두 번째 스레드가 execute() 메소드를 실행하면서 ShowController가 가리키는 질문과 답변은 1번 글이 아니라 2번 글로 바뀐 상태이다. 따라서 첫 번째 글에 대한 응답은 1번 글이 아니라 2번 질문과 답변을 응답으로 받게 된다.

이 버그의 경우 사용자1이 새로고침을 하면 1번 글을 정상적으로 볼 수 있다. 그리 큰 버그가 아닐 수 있다.
하지만 계좌이체를 하는 경우, 상품 주문을 하는 경우 등과 같이 돈과 관련되어 있는 애플리케이션을 구현할 때 이 같은 버그가 발생한다면 회사와 사용자에게 막대한 피해가 발생할 수 있다.

따라서 멀티 스레드에서 웹 애플리케이션을 개발할 때 자신이 구현하고 있는 코드가 어떻게 동작하는지를 정확히 이해하고 프로그래밍하는 것은 안전한 애플리케이션을 개발하는데 있어 정말 중요하다.

앞에서 발생한 문제는 Question과 List를 ShowController의 필드가 아닌 execute() 메소드의 로컬 변수로 구현함으로써 해결할 수 있다.

public class ShowController extends AbstractController {
    private QuestionDao questionDao = new QuestionDao();
    private AnswerDao answerDao = new AnswerDao();

    @Override
    public ModelAndView execute(HttpServletRequest req, HttpServletResponse response) throws Exception {
        Long questionId = Long.parseLong(req.getParameter("questionId"));

        Question question = questionDao.findById(questionId);
        List<Answer> answers = answerDao.findAllByQuestionId(questionId);

        ModelAndView mav = jspView("/qna/show.jsp");
        mav.addObject("question", question);
        mav.addObject("answers", answers);
        return mav;
    }
}

위와 같이 로컬 변수로 구현했을 때의 메모리 상태 변화는 다음과 같다.

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

이 때 2개의 스레드가 동시에 접속했을 때의 상태 변화는 다음과 같다.

위 그림을 보면 2개의 스레드가 참조하는 ShowController 인스턴스는 같다. 하지만 힙 메모리에 생성되어 있는 Question과 List는 서로 다른 인스턴스를 참조하고 있다. 따라서 ShowController의 execute() 메소드가 동시에 실행되어도 서로 다른 인스턴스를 참조하고 있기 때문에 서로 간의 영향을 미치는 일이 발생하지 않는다.

애플리케이션이 메모리에서 어떻게 실행되는지 확인하기 힘들지만, 이클립스와 같은 통합 개발 도구의 디버깅 기능을 활용하면 문제가 발생하는 과정을 재현해볼 수 있다.

0개의 댓글