 |
|
난이도 : 중급 Brian Goetz, Senior Staff Engineer, Sun Microsystems
옮긴이: 오국환 dwkorea@kr.ibm.com
2008 년 11 월 11 일 HttpSession은 서블릿 프레임워크가 제공하는 세션의 상태 관리 메커니즘입니다. 이것 덕분에 상태 의존형(stateful) 애플리케이션을 쉽게 작성할 수 있는 반면, 이를 자칫 오용하기도 쉽습니다. 많은 웹 애플리케이션에서 HttpSession을 충분한 조율 없이 사용하여 (자바빈 클래스처럼) 수정 가능(mutable) 데이터를 관리하므로, 잠재적으로 동시성 위험(concurrency hazards)을 안고 있습니다.
자바(Java™) 생태계에는 많은 웹 프레임워크가 있는데, 모두 직간접적으로 서블릿 하부구조(infrastructure)를 바탕으로 한다. 서블릿 API는 일련의 유용한 기능을 제공하는데, 이 기능 중에는 HttpSession과 ServletContext 메커니즘을 통한 상태 관리도 포함한다. 이 메커니즘으로 애플리케이션은 다수의 사용자 요청에 걸쳐 지속되는 상태를 관리할 수 있다. 그러나 웹 애플리케이션에서 상태를 공유하는 데는 몇 가지 미묘한 (또한 대체로 문서에 없는) 규칙이 얽혀 있다. 여기에 많은 애플리케이션이 무심코 걸려 넘어진다. 결과적으로 수많은 상태 공유 웹 애플리케이션에는 미묘하고 심각한 결함이 있다.
영역별 컨테이너
서블릿 규격에서 ServletContext, HttpSession, HttpRequest 객체를 영역별 컨테이너(scoped containers)라고 부른다. 각각은 getAttribute(), setAttribute() 메서드를 가지는데, 이로써 애플리케이션에서 필요한 데이터를 저장한다. 이것들 간의 차이는 영역별 컨테이너의 수명이다. HttpRequest에 저장된 데이터의 수명은 해당 요청의 수명이며, HttpSession에 저장된 데이터의 수명은 사용자와 애플리케이션 간 세션의 수명이고, ServletContext의 수명은 애플리케이션의 수명이다.
HTTP에는 상태라는 개념이 없으므로, 영역별 컨테이너는 상태 의존형 웹 애플리케이션 작성에 매우 요긴하다. 서블릿 컨테이너는 애플리케이션 상태와 데이터의 생명 주기를 관리하는 책임이 있다. 규격에서는 이러한 주제를 거의 다루지 않지만, 세션이나 애플리케이션 영역별 컨테이너는 일정 수준으로 스레드 안전성(thread-safety)을 보장해야 한다. getAttribute(), setAttribute() 메서드는 다른 스레드에서 어느 때든 부를 수 있기 때문이다(규격에서는 스레드 안전성을 구현에 직접 강제하지는 않으나, 제공하려는 서비스의 본질상 그 점을 효과적으로 고려할 필요가 있다).
웹 애플리케이션에서 영역별 컨테이너를 사용하면 잠재적으로 중요한 이점이 더 있다. 즉, 컨테이너는 애플리케이션 상태의 복제나 오동작을 애플리케이션 자체와 별개로 관리할 수 있다.
세션
세션(session)은 특정 사용자와 웹 애플리케이션 간에 이뤄지는 일련의 요청-응답 교환이다. 사용자는 웹 사이트에서 인증 결과, 장바구니의 내용물, 이전 요청에서 웹 폼에 입력한 정보 등을 기억해 주기를 바란다. 그러나 기본 HTTP에는 상태라는 개념이 없다. 즉, 기본 HTTP만으로는 한 요청에서 필요한 정보는 그 요청에 모두 포함하여야 한다. 따라서, 단일 요청-응답 주기 이상으로 사용자와 적절히 상호 작용하려면, 세션 상태를 어딘가에서 관리해야 한다. 서블릿 프레임워크에서는 각 요청마다 해당 세션과 연계할 수 있다. 서블릿 프레임워크는 HttpSession 인터페이스를 제공하며, 이 인터페이스는 세션과 관련된 (키, 값) 데이터 아이템의 저장소 역할을 한다. Listing 1은 HttpSession에 장바구니 데이터를 저장하는 전형적인 서블릿 코드를 보여 준다.
Listing 1. 장바구니 정보를 저장하기 위한 HttpSession의 사용 예
HttpSession session = request.getSession(true);
ShoppingCart cart = (ShoppingCart)session.getAttribute("shoppingCart");
if (cart == null) {
cart = new ShoppingCart(...);
session.setAttribute("shoppingCart");
}
doSomethingWith(cart);
|
Listing 1은 전형적인 서블릿 사용 예다. 애플리케이션은 객체가 이미 세션에 있는지 확인하고, 없으면 그 세션의 다음 요청에서 사용할 수 있도록 새로운 객체를 생성한다. (JSP, JSF, SpringMVC 등) 서블릿 기반 웹 프레임워크는 세션 영역에 해당하는 데이터를 다루는 상세한 작업을 내부로 숨기지만, 본질적으로는 이러한 작업을 수행한다. 불행히도, Listing 1의 사용 예도 부정확한 측면이 있다.
스레드에 대한 고려
서블릿 컨테이너가 HTTP 요청을 받으면, 서블릿 컨테이너가 관리하는 스레드의 컨텍스트에서 HttpRequest/HttpResponse 객체를 생성하여 서블릿의 service() 메서드에 전달한다. 서블릿은 응답을 작성할 책임이 있다. 즉, 서블릿은 해당 요청이 마칠 때까지 줄곧 스레드의 제어를 관장한다. 그 요청에 대한 처리가 끝났을 때에야 비로소 해당 스레드는 일꾼 스레드의 집합소로 돌아온다. 서블릿 컨테이너는 스레드와 세션 간에 상관 관계를 염두에 두지 않는다. 같은 세션에 다음 요청이 오면 현재 요청과는 다른 스레드에서 그 요청을 처리할 수도 있다. 사실상 같은 세션에서 동시다발적으로 여러 요청이 오면, 충분히 이런 상황이 발생할 수 있다. (사용자가 특정 페이지와 상호 작용하는 중에도 프레임이나 Ajax 기술이 서버에서 데이터를 받아 가는 웹 애플리케이션에서 이런 경우는 생길 수 있다.) 이 경우, 동일 사용자로부터 서로 다른 스레드를 통해 동시다발적으로 여러 요청을 받는다.
대개 웹 애플리케이션 개발자는 이처럼 스레드에 대한 고려를 하지 않는다. 상태 개념이 없는 HTTP의 본질상, 한 응답은 해당 요청에 저장된 데이터나 이미 동시성을 제어하는 (데이터베이스 같은) 저장소에 저장된 데이터만을 다루는 함수의 값으로 생각한다(여기서 말하는 요청이란 다른 동시적인 요청과 어떠한 정보도 공유하지 않는 걸 말한다). 그러나 일단 웹 애플리케이션이 HttpSession이나 ServletContext 같은 공유 컨테이너에 데이터를 저장하면, 그 애플리케이션은 동시성을 요구하는 애플리케이션이 되고 개발자는 애플리케이션 내에서 스레드 안전성을 고려해야 한다.
주로 코드를 설명하려고 스레드 안전성이라는 용어를 쓰지만, 사실상 이 용어는 데이터에 관한 것이다. 다수의 스레드가 수정 가능 데이터에 접근하려 할 때, 특별히 스레드 안전성은 이러한 접근을 적절히 제어한다는 의미다. 서블릿 애플리케이션은 수정 가능 데이터를 공유하지 않으며 따라서 동기화가 추가로 필요하지 않다는 사실에 근거하여 대체로 스레드 안전하다. 그러나 공유 상태가 웹 애플리케이션에 필요한 상황이 여럿 있을 수 있다. HttpSession과 ServletContext 같은 영역별 컨테이너가 아니라도, HttpServlet 객체의 정적 필드나 인스턴스 필드에 접근하는 것도 이러한 상황에 해당한다. 일단 웹 애플리케이션이 여러 요청에 걸쳐 데이터를 공유하려면, 애플리케이션 개발자는 공유 데이터의 위치를 확인하고 이 공유 데이터에 접근하는 스레드 간에 충분한 조율(동기화)이 이뤄지는지 살펴, 스레드 위험을 회피해야 한다.
웹 애플리케이션의 스레드 위험
웹 애플리케이션이 HttpSession에 장바구니 같은 수정 가능 세션 데이터를 저장하려 할 때, 두 요청이 동시에 그 장바구니에 접근하려 할 수 있다. 이 경우 여러 오류 시나리오가 전개될 수 있다. 예상 가능한 시나리오는 다음과 같다.
- 단위 작업 오류(atomicity failure): 한 스레드가 여러 데이터 아이템을 갱신하는 중에 다른 스레드가 불일치 상태의 데이터를 읽으려 하는 경우를 말한다.
- 읽기 스레드와 쓰기 스레드 간의 가시성 오류(visibility failure): 한 스레드가 장바구니 내용을 수정하여, 다른 스레드에서 철 지난 또는 불일치 상태의 장바구니 내용물을 읽는 상황을 말한다.
단위 작업 오류
Listing 2는 게임 애플리케이션에서 최고 점수를 쓰고 읽는 메서드를 (잘못) 구현한 예다. 본 예에서는 PlayerScore 객체를 써서 최고 점수를 나타낸다. 이는 name과
score 속성을 가진 평범한 자바빈 클래스이며, 애플리케이션 영역의 ServletContext에 저장된다. (애플리케이션 시작 시점에 최고 점수 초기값을 ServletContext의 highScore 속성으로 세팅해 두면, getAttribute() 호출에서 문제가 발생할 일은 없다.)
Listing 2. 영역별 컨테이너에 관련 아이템을 저장하기 위한 잘못된 접근법
public PlayerScore getHighScore() {
ServletContext ctx = getServletConfig().getServletContext();
PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
PlayerScore result = new PlayerScore();
result.setName(hs.getName());
result.setScore(hs.getScore());
return result;
}
public void updateHighScore(PlayerScore newScore) {
ServletContext ctx = getServletConfig().getServletContext();
PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
if (newScore.getScore() > hs.getScore()) {
hs.setName(newScore.getName());
hs.setScore(newScore.getScore());
}
}
|
Listing 2의 코드를 보면 깨진 부분이 여러 군데 있다. 본 접근법에서는 ServletContext에 플레이어 중 최고 득점자의 이름과 점수를 수정 가능 객체에 저장한다. 최고 득점이 바뀔 경우, 본 객체에서 이름과 점수 또한 갱신된다.
현재의 최고 득점자가 Bob이며 점수는 1000이라고 가정해 보자. 그런데, Joe가 1100의 점수로 Bob을 눌렀다. Joe의 점수로 교체되려는 찰나에 다른 플레이어가 최고점을 요청한다. getHighScore() 메서드는 서블릿 컨텍스트에서 PlayerScore 객체를 꺼낼 것이다. 운 나쁜 시점이라면 Bob의 이름과 Joe의 점수를 읽을 가능성도 있다. 실제로는 일어나지 않은 일이지만, 마치 Bob이 1100점을 기록한 것으로 비춰질 수 있다. (무료 게임 사이트에서는 이런 정도의 오류는 눈감아 줄 법하지만, "점수"가 아닌 "은행 계좌"라고 생각하면 끔찍한 상황이 될 수도 있다.) 이것이 단위 작업 오류다. 서로 한 단위라고 가정하는 두 작업(예를 들면, 이름/점수를 가져 오는 작업과 이름/점수를 갱신하는 작업)이 사실상 한 단위로 수행되지 않는 것이다. 여러 스레드 중 하나가 어정쩡한 상태에서 공유 데이터를 읽는 것이다.
나아가, 점수 갱신 로직이 확인 후 행동(check-then-act) 패턴을 따르기 때문에, 두 스레드가 서로 최고점을 갱신하려고 "경주"하여 예측 불가능한 결과를 초래할 수도 있다. 현재 최고점이 1000인데, 두 플레이어가 거의 동시에 1100과 1200의 최고점을 경신했다고 가정해 보자. 운 나쁜 시점에, 둘이 모두 "현재 최고점보다 더 높은 점수를 획득했다"는 테스트를 통과할 수 있다. 둘은 최고점을 갱신하는 코드에 진입할 것이다. 이 경우도 시점에 따라, 일관성 없거나(한 플레이어의 이름과 다른 플레이어의 점수) 또는 잘못된(1200점 획득한 플레이어의 기록을 1100점 획득한 플레이어가 덮어씀) 결과를 얻는다.
가시성 오류
단위 작업 오류보다 더 미묘한 것이 가시성 오류(visibility failures)다. 동기화하지 않고 한 스레드가 한 변수에 쓰고 다른 스레드는 같은 변수를 읽는다면, 읽는 스레드에서는 유효하지 않고(stale) 철 지난 데이터를 읽을 수 있다. 더 나쁘게는, 읽기 스레드가 변수 x의 최신 데이터를 읽고 (변수 y가 x에 앞서 쓰였다면) y의 철 지난 데이터를 읽을 수도 있다. 가시성 오류는 예측 불가능하면서도 자주 발생하지도 않아 미묘하다. 드물게 오류를 유발하여, 디버깅하기 어렵다. 가시성 오류는 (공유 변수에 접근할 때 적절히 동기화하지 않아서) 데이터 경주(data race)로 인해 발생한다. 데이터 경주가 일어나는 프로그램은 그 동작을 신뢰할 만큼 예측할 수 없으므로, 어떤 식으로든 깨지게 마련이다.
자바 메모리 모델(Java Memory Model, JMM)은 변수를 읽는 스레드에서 다른 스레드의 쓰기 결과를 볼 수 있도록 보장하는 조건을 정의한다(JMM을 모두 설명하는 것은 이 글의 범위를 벗어난다. 참고자료를 참조하라). JMM은 선발(先發, happens-before)이라고 하는, 프로그램 작업 순서를 정의한다. 공통의 락으로 동기화하거나 공통의 활성 변수에 접근하는 경우에 여러 스레드에 걸쳐 프로그램 작업 순서를 정의하는 것이 선발 열거(happens-before ordering)다. 선발 열거를 배제한 자바 플랫폼에서는 다양한 경우에서 한 스레드의 쓰기 결과를 다른 스레드의 읽기에서 보는 시점이 지연되거나 그 순서가 바뀐다.
Listing 2의 코드는 단위 작업 오류는 물론 가시성 오류도 함께 갖고 있다. updateHighScore() 메서드는 ServletContext로부터 HighScore 객체를 꺼내고 그 객체의 상태를 수정한다. 이렇게 하는 의도는 수정된 결과를 getHighScore()를 호출하는 다른 스레드에서도 보게 하는 것이다. 그러나 updateHighScore()에서 이름과 득점 속성을 쓰는 작업과 다른 스레드에서 getHighScore()를 호출하여 이 속성을 읽는 작업 간에 선발 열거가 없다면, 그저 운 좋게 정확한 값을 스레드에서 읽어 주기를 바라는 셈이다.
가능한 해법
서블릿 규격에는 서블릿 컨테이너가 반드시 지원하도록 선발 보장을 충분히 기술하지 않는다. 이 규격에서 내릴 수밖에 없는 결론은 공유 영역별 컨테이너(HttpSession이나 ServletContext)에 속성을 넣는 작업이 다른 스레드에서 같은 속성을 읽기 전에 일어나야 한다는 것이다. (이러한 결론을 내리는 추론에 대해서는 JCiP 4.5.1 참고. 모든 규격이 말하기를, "여러 요청 스레드를 처리하는 다수의 서블릿은 동시에 하나의 세션 객체에 활성 상태로 접근할 수도 있다. 적절히 세션 자원으로의 접근을 동기화하는 것은 개발자의 몫이다.")
세팅 전에 쓰기(set-after-write) 기법
영역별 세션 컨테이너에 저장된 수정 가능 데이터를 갱신할 때 그 데이터를 수정하면 다시 setAttribute()를 호출해야 한다는 것은 경험상 가장 좋은 방법으로 널리 알려져 있다. Listing 3은 이 기술을 써서 updateHighScore()를 다시 작성한 예제다. (이 기술의 동기 중 하나는 컨테이너에 값이 바뀌었다고 힌트를 주는 것인데, 이로써 세션이나 애플리케이션 상태를 분산된 웹 애플리케이션의 여러 인스턴스에 걸쳐 다시 동기화할 수 있다.)
Listing 3. 세팅 전에 쓰기 기법을 사용하여 서블릿 컨테이너에 값이 바뀐 힌트를 주는 것
public void updateHighScore(PlayerScore newScore) {
ServletContext ctx = getServletConfig().getServletContext();
PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
if (newScore.getScore() > hs.getScore()) {
hs.setName(newScore.getName());
hs.setScore(newScore.getScore());
ctx.setAttribute("highScore", hs);
}
}
|
불행히도, 이 기술은 애플리케이션 클러스터에서 세션과 애플리케이션 상태를 효율적으로 복제하는 문제에는 도움이 되지만, 이 글의 예에서 보는 기본적인 스레드 안전성 문제를 해결하기에는 충분하지 않다. 이는 (다른 플레이어가 updateHighScore())에서 갱신된 값을 절대 볼 수 없으므로) 가시성 문제를 줄이는 데는 충분하지만, 여러 잠재적인 단위 작업 문제를 다루기에는 충분하지 않다.
동기화에 묻어가기(Piggybacking on synchronization)
선발 열거가 추이적(transitive)인 특성으로 인해, 세팅 전에 쓰기 기법은 가시성 문제도 제거할 수 있다. updateHighScore()의 setAttribute() 호출과 getHighScore()의 getAttribute() 호출 간에는 선발 연결 관계가 있다. HighScore 상태의 갱신은 setAttribute()보다 앞서 발생하고, setAttribute()는 getAttribute()의 리턴보다 앞서 발생하며, getAttribute()의 리턴은 getHighScore()의 호출자가 상태를 사용하는 것보다 앞서 발생한다. 이러한 추이적 효과의 결론은 getHighScore()의 호출자가 보는 값이 setAttribute()를 가장 최근에 호출한 것만큼은 적어도 최신이라는 점이다. 이러한 기법을 동기화에 묻어가기라고 부른다. getHighScore()와 updateHighScore() 메서드는 최소한의 가시성을 보장하기 위해 getAttribute()와 setAttribute() 내에서 동기화 지식을 사용할 수 있기 때문이다. 그러나 본 문서의 예만 두고 보더라도, 이 방법 역시 아직 충분한 것은 아니다.
수정 불능(immutability)에 의존하기
스레드 안전한 애플리케이션을 생성하기 유용한 기법은 가급적 수정 불능 데이터에 의존하는 것이다. Listing 4는 HighScore를 수정 불능(immutable) 방식으로 재작성한 예다. 이 경우 호출자가 존재하지도 않는 플레이어와 득점이 짝을 이루는 것을 보게 되는 단위 작업 오류를 비롯하여, getHighScore() 호출자가 updateHighScore() 호출로 갱신된 가장 최근 값을 보지 못하는 가시성 오류 역시 발생하지 않는다.
Listing 4. 대부분의 단위 작업 및 가시성 함정을 피하는 방법으로, 수정 불능 HighScore 객체 사용
Public class HighScore {
public final String name;
public final int score;
public HighScore(String name, int score) {
this.name = name;
this.score = score;
}
}
public PlayerScore getHighScore() {
ServletContext ctx = getServletConfig().getServletContext();
return (PlayerScore) ctx.getAttribute("highScore");
}
public void updateHighScore(PlayerScore newScore) {
ServletContext ctx = getServletConfig().getServletContext();
PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
if (newScore.score > hs.score)
ctx.setAttribute("highScore", newScore);
}
|
Listing 4의 코드는 잠재적인 오류 상황을 상당수 피한다. setAttribute()와 getAttribute()에서의 동기화에 묻어가기는 가시성을 보장한다. 단지 수정 불능 데이터 아이템 하나만 저장된다는 사실만으로 getHighScore() 호출자가 이름과 득점의 짝이 서로 불일치 갱신된 것을 읽는 단위 작업 오류도 피할 수 있다.
영역별 컨테이너에 수정 불능 객체를 넣으면, 대부분의 단위 작업 및 가시성 오류를 피하게 된다. 영역별 컨테이너에 효율적 수정 불능(effectively
immutable) 객체를 넣는 것이 또한 안전하기도 하다. 효율적 수정 불능 객체란 본질상 수정 가능이지만 일단 배포하면 그 후로는 절대 수정하지 않는 객체를 말한다. 자바빈의 경우, 일단 HttpSession에 해당 객체를 넣었다면 세터(setter)를 절대 호출하지 않아야 한다.
세션 내 요청이 HttpSession에 위치한 데이터는 접근하는 것 외에도, 컨테이너가 상태 복제 등의 작업을 하는 경우 컨테이너 자체가 그 데이터에 접근할 수 있다.
모든 HttpSession이나
ServletContext에 위치한 데이터는 스레드 안전하거나 효율적 수정 불능이어야 한다.
단위 상태 변경에 영향 주기
Listing 4의 코드는 여전히 한 가지 문제를 가진다. updateHighScore()의 확인 후 행동은 최고점을 갱신하려는 두 스레드 사이에 잠재적인 경주를 여전히 허용한다. 운 나쁜 타이밍에는 갱신 정보를 소실할 수도 있다. 경우에 따라, 두 스레드가 모두 서로 "이전 값보다 더 큰 최고점이다"라는 시험을 동시에 통과하여, 양쪽 다 setAttribute()를 호출한다. 타이밍에 따라서는, 두 점수 간에 더 높은 점수가 이긴다는 보장도 없는 것이다. 이 마지막 함정까지 막으려면, 간섭으로부터의 자유를 보장하면서도, 단위 갱신할 수 있는 수단도 필요하다. 이러한 목적 달성을 위한 접근법이 여럿 있다.
updateHighScore()에 동기화를 추가하여 일반적인 갱신 절차에서 발생하는 확인 후 행동을 다른 갱신과 동시에 실행하지 않는다는 점을 보장하려는 것이 Listing 5의 코드다. 모든 수정 로직에서 updateHighScore()가 사용하는 동일한 락을 요구한다면, 이러한 접근법이 적절하다.
Listing 5. 마지막 단일 작업 함정을 막기 위한 동기화 사용
public void updateHighScore(PlayerScore newScore) {
ServletContext ctx = getServletConfig().getServletContext();
PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
synchronized (lock) {
if (newScore.score > hs.score)
ctx.setAttribute("highScore", newScore);
}
}
|
Listing 5의 기법이 무난하지만, 더 나은 기법도 있다. java.util.concurrent 패키지의 AtomicReference 클래스를 써 보라. 이 클래스는 compareAndSet() 호출을 통한 단위별 조건 갱신을 지원하려고 디자인한 것이다. Listing 6은 AtomicReference를 사용하여 본 예제에서 단일 작업이 필요한 마지막 한 요소까지도 모두 복원하는 방법을 보여 준다. 고득점 갱신 방법에 있어 여러 전제를 우연히 위반하기란 더욱 어려우므로, 이러한 접근법은 Listing 5의 코드보다 권장할 만하다.
Listing 6. AtomicReference를 사용하여 최후의 단일 작업 함정 막기
public PlayerScore getHighScore() {
ServletContext ctx = getServletConfig().getServletContext();
AtomicReference<PlayerScore> holder
= (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
return holder.get();
}
public void updateHighScore(PlayerScore newScore) {
ServletContext ctx = getServletConfig().getServletContext();
AtomicReference<PlayerScore> holder
= (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
while (true) {
HighScore old = holder.get();
if (old.score >= newScore.score)
break;
else if (holder.compareAndSet(old, newScore))
break;
}
}
|
동기화나 java.util.concurrent의 단위 작업 클래스를 통하여 영역별 컨테이너에 담긴 수정 가능 객체의 상태 전환을 단위 작업으로 구성해야 한다.
HttpSession에 접근을 직렬화(serialization)하기
지금까지 살펴 본 예에서는 애플리케이션 내에서의 ServletContext 내 데이터에 접근할 때 발생할 수 있는 여러 위험을 피하려고 노력했다. 여러 요청에서 ServletContext로 접근할 수 있으므로, ServletContext 접근에는 조심스러운 조율이 필요하다는 것은 명백하다. 그러나 많은 상태 의존형 웹 애플리케이션에서 세션 영역별 컨테이너인 HttpSession에 더 심하게 의존한다. 동일한 세션에서 어떻게 동시에 여러 요청이 발생할 수 있는지 명확하지 않을 수도 있다. 무엇보다도 한 세션은 특정 사용자와 브라우저 세션에 묶여 있고, 사용자는 한 번에 여러 페이지를 요청할 것으로 보이지도 않는다. 그러나 Ajax 애플리케이션과 같은 프로그램에 의해 요청을 생성하는 여러 애플리케이션이라면 한 세션에서 여러 요청이 중첩될 수도 있다.
단일 세션에서의 여러 요청이 필시 중첩될 수 있다. 그리고 이런 능력은 불행한 것이다. 한 세션에서의 여러 요청을 쉽게 직렬화할 수 있다면, HttpSession의 공유 객체에 접근하더라도 여기 기술한 거의 모든 함정이 별 이슈가 되지 않을 것이다. 직렬화하면 단위 작업 오류를 방지한다. HttpSession에서 동기화에 묻어가기를 암묵적으로 활용하면 가시성 오류도 방지한다. 특정 세션에 묶인 여러 요청을 직렬화하더라도 부득불 처리량에 심각한 영향을 끼칠 여지는 낮다. 한 세션에 여러 요청이 중첩되는 일이 거의 드물기도 하고, 한 세션에 다수의 요청이 중첩되는 일은 극히 드물기 때문이다.
불행하게도 서블릿 규격에 "동일 세션에 대한 요청은 반드시 직렬화하라"는 옵션은 없다. 그러나 SpringMVC 프레임워크는 이러한 옵션을 설정할 수 있는 방편을 제공한다. 게다가, 다른 프레임워크에서도 이런 접근법을 쉽게 구현할 수 있다. SpringMVC 컨트롤러의 기반 클래스인 AbstractController는 synchronizeOnSession이라는 boolean 변수를 제공한다. 이 변수값이 세팅되면, 한 세션에서는 한번에 한 요청만 실행하도록 락을 사용한다.
HttpSession에서 요청을 직렬화하면 다수의 동시성 함정이 사라진다. 이는 스윙(Swing) 애플리케이션에서 객체를 EDT(Event Dispatch Thread)를 묶어 두어 동기화 필요성을 줄이는 방식과 비슷하다.
요약
많은 상태 의존형 웹 애플리케이션이 HttpSession과 ServletContext 같은 영역별 컨테이너에 저장된 수정 가능 데이터에 적절한 조율 없이 접근함으로써, 심각한 동시성 취약점을 안고 있다. 대개 getAttribute()와 setAttribute() 메서드에 내재된 동기화만으로 충분하다고 쉽게 가정하지만, 이는 명백한 실수다. 속성이 수정 불능 또는 효율적 수정 불능이거나 스레드 안전하거나 컨테이너에 접근하는 요청이 직렬화되어 있는 등 특정 조건하에서만 이러한 가정이 사실이다. 일반적으로 영역별 컨테이너에 넣는 모든 것이 효율적 수정 불능이거나 스레드 안전해야 한다. 서블릿 규격이 제공하는 영역별 컨테이너 메커니즘은 내부 동기화를 제공하지 않는 수정 가능 객체를 관리할 의도가 전혀 없었다. 가장 큰 골칫덩이는 통상적인 자바빈 클래스를 HttpSession에 저장하는 것이다. 이 글에서 소개한 기법은 자바빈을 세션에 저장한 후에는 절대 수정하지 않을 때만 그 동작을 보장한다.
참고자료 교육
-
자바 이론과 실습(Brian Goetz, 한국 developerWorks): 전체 연재를 읽어보라.
-
Java
Concurrency in Practice(Brian Goetz, Addison-Wesley Professional, 2006년 5월): 4.5.1절에서 선발이 공유 영역별 컨테이너의 동작을 보장한다는 결론의 분석을 다룬다. Section 3.5.4에서는 효율적 수정 불능의 개념을 다룬다. 16장에서는 자바 메모리 모델과 선발(happens-before) 열거의 상세한 내용을 다룬다.
-
서블릿 2.5 규격:
HttpSession과 ServletContext 메커니즘을 정의한다.
-
스프링 프레임워크: SpringMVC에 대해 더 배워보라.
-
웹 티어의 상태 복제: 서블릿 컨테이너에 의한 상태 복제를 설명한다.
-
이들 또는 기타 기술적인 주제를 다루는 책을 기술 서점에서 찾아보라.
-
developerWorks 자바 기술 존: 자바 프로그래밍의 여러 관점에 대한 수백 가지 자료
토론
필자소개  | 
|  | Brian Goetz는 지난 20년간 전문 소프트웨어 개발자로 일했다. 현재는 썬 마이크로시스템즈의 책임 엔지니어로 있으며, 다수의 JCP Expert Group에서 일하고 있다. 그의 책 Java Concurrency In Practice는 2006년 5월에 Addison-Wesley에서 발간되었다. 업계의 유명한 출판물로, Brian이 출간하였거나 발행할 저서도 읽어 보라. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|