[Spring] Bean을 stateless하게 설계해야하는 이유

 

서론

최근에 인턴 면접관으로 참여할 일이 생겨 지원자들이 제출한 코드를 훑어보던 중 재밌는 코드를 발견했습니다.

@Component
public class SessionStore {

    // key : userId
    // value : session string
    private final HashMap<Long, String> memorySession = new HashMap<>();

    // CRUD method ...

}

 

바로 유저의 세션을 인 메모리 방식으로 관리하는 컴포넌트가 정의되어 있었습니다. 유저의 세션을 인 메모리 방식으로 구현한 것도 흥미로웠는데 무엇보다 싱글톤 빈에 정의한 멤버 변수에 이목이 끌렸습니다. 빈의 생명주기에 대해서 공부할 때 줄곧 싱글톤 빈에 상태를 저장하지 말라고 배웠으니 말이죠. 그렇다면 앞선 코드는 정상 동작했을까요? 만약 그렇지 않다면 어떤 문제가 있었을까요?

 

 

Bean을 stateless하게 설계해야하는 이유

앞선 코드는 두 가지 치명적인 문제점을 가지고 있는데, 첫 번째는 동기화 문제 두 번째는 데이터 정합성 문제입니다. 간단한 예제 코드와 시나리오로 두 문제를 설명해 보겠습니다.

 

동기화 문제

동기화 문제를 이야기하려면 멀티 쓰레딩에 대한 배경지식이 필요합니다. 싱글톤 패턴은 어플리케이션에서 특정 클래스의 인스턴스가 딱 하나만 생성되도록 하는 패턴입니다. 만약 멀티 쓰레드 환경이라면 싱글톤 객체가 여러 쓰레드에 의해서 동시에 접근되는 경우가 발생하고 공유 자원에 대한 경쟁 상태가 발생할 수 있습니다.

 

웹 어플리케이션 환경으로 해석하면 WAS의 여러 쓰레드가 싱글톤 빈에 접근하고 공유 자원에 대한 경쟁 상태가 발생한다고 말할 수 있습니다. 앞선 상황을 간단한 테스트 코드로 재현했습니다. 변수 map은 memorySession으로, WAS의 쓰레드는 테스트 코드의 쓰레드로 간주하면 이해하기 쉽습니다.

public class MapTest {

    Map<Integer, Integer> map = new HashMap<>();

    @Test
    void map_test() {
        CompletionException completionException = assertThrows(CompletionException.class, () -> {
            CompletableFuture[] futures = IntStream.range(0, 10)
                    .mapToObj(idx -> CompletableFuture.runAsync(() -> increase(idx)))
                    .toArray(CompletableFuture[]::new);

            CompletableFuture.allOf(futures).join();
        });

        assertTrue(completionException.getCause() instanceof ConcurrentModificationException);
    }

    private void increase(int key) {
        for (int i = 0; i < 10000; i++)
            map.compute(key, (k, v) -> v == null ? 1 : v + 1);
    }
}

앞선 코드는 매우 매우 높은 확률로 예외가 발생합니다. 자바에서는 여러 쓰레드가 동시에 한 컬렉션을 변경하려고 하면 ConcurrentModificationException이 발생하게 되어있습니다. 언어 차원에서 동시성 문제에 대해서 예외를 발생시키는 거죠. 이러한 동시성 문제를 방지하기 위해 concurrentHashMap 혹은 synchronized 키워드 사용을 권장하고 있습니다.

 

 

데이터 정합성 문제

데이터 정합성 문제는 여러 대의 WAS를 운영하는 경우 발생하게 됩니다. 예를 들어, 운영하는 서비스가 흥행에 성공해서 한 대의 WAS로는 트래픽을 감당할 수 없어서 다수의 WAS로 증설하는 경우, 임의의 한 WAS에서 인증을 마치면 해당 WAS는 세션 정보를 저장하고 있지만 다른 WAS는 유저의 세션 정보를 알 수 없습니다. 요청이 어떤 WAS로 전달될지 알 수 없으니 재수 없다면 매 요청마다 인증에서 문제가 발생할 수 있습니다. 이처럼 여러 대의 WAS를 운영하면서 동시에 싱글톤 빈에 상태를 저장한다면 데이터 정합성이 깨져도 문제가 없는 데이터를 유지해야 합니다.