session을 빈 클래스의 DI 필드로 사용하지 말자

Admin인지를 확인하는 Bean 클래스

@Slf4j
@Component
@RequiredArgsConstructor
public class AdminUtils {

    private final AdminService adminService;
    private final HttpSession session;

    public boolean isDefault() {
        User user = (User) session.getAttribute("user");
        log.debug("user={}", user);
        return user == null || !adminService.isAdmin(user.getId());
    }
}

단순히 이렇게 만들면, 컨트롤러에서 파라미터를 넣어줄 필요도 없고, 몹시 간단해보인다. 하지만, 이런 방식은 스프링의 추천 방식이 아니다.

세션은 요청마다 다르게 존재한다.

그렇다면, 싱글톤 빈에서 세션을 필드로 사용하면 세션이 공유될까?

세션이란, 요청마다 존재하는 것이다. 따라서, 해당 코드에서 user를 요청하고 있지만, Bean은 싱글톤으로 생성되므로, session 또한 공유되어 동시성 문제가 생길 수 있을 것만 같다. 하지만, 결과적으로 아무런 문제도 생기지 않는다. 왜일까?

Session 객체는 실제로 프록시 객체가 주입된다.

사실, 이렇게 DI 필드를 넣으면, Spring은 요청 스코프 객체(HttpSession)를 싱글톤 빈에 안전하게 주입하기 위해 프록시를 사용한다. 이는 요청마다 다른 세션을 사용하기 위함이다.

프록시 객체는 내부적으로 요청 스코프에 맞는 적절한 세션 객체를 반환하므로, 동시성 문제를 방지할 수 있다.

package com.weblibrary.domain.admin.controller;

import com.weblibrary.domain.admin.service.AdminService;
import com.weblibrary.domain.user.model.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class AdminUtils {

    private final AdminService adminService;
    private final HttpSession session;

    public boolean isDefault() {
        log.debug("adminService={}, hashCode={}", adminService, System.identityHashCode(adminService)); // 같은 객체, 같은 해시코드
        log.debug("session={}, id={}, hashCode={}", session, session.getId(), System.identityHashCode(session)); // 다른 id, 같은 해시코드

        User user = (User) session.getAttribute("user");
        log.debug("user={}", user);
        return user == null || !adminService.isAdmin(user.getId());
    }
}

session은 getId를 제공하는데, 이는 다른 session이라는 것을 의미한다. 그리고, 같은 해시코드인 것은, 같은 프록시 객체임을 의미한다.

그럼에도 쓰지 말아야 하는 이유

프로그램의 복잡도가 높아지고, 스프링의 설계에서 벗어나는 방법이기 때문이다. 멀티스레드 환경에서 문제가 발생할 가능성은 없지만, 요청 스코프 객체를 싱글톤 빈에 주입하는 방식은 Spring의 설계 원칙에서 벗어난다.

특히 복잡한 서비스 로직에서 세션 관리가 어려워질 수 있으므로, 명시적으로 세션 객체를 요청마다 전달하는 방식이 권장된다.

그러면 어떻게 사용할까?

HttpSession을 DI 필드로 주입받지 말고, isDefauit 메서드의 파라미터로 주입받으면 된다.

댓글

개발자  김철준

백엔드 개발자 김철준의 블로그입니다.

주요 프로젝트