들어가며: Spring Bean Scope의 중요성
Spring Framework는 엔터프라이즈 애플리케이션 개발에 있어 가장 널리 사용되는 프레임워크 중 하나입니다. Spring의 핵심 기능 중 하나인 Bean 관리 메커니즘은 객체 생성, 의존성 주입, 생명주기 관리를 담당하는데, 이때 Bean Scope는 Spring 컨테이너가 Bean을 어떤 방식으로 생성하고 관리할지를 결정하는 중요한 요소입니다.
Bean Scope를 제대로 이해하고 적절히 활용하면 애플리케이션의 성능, 메모리 사용량, 그리고 전반적인 아키텍처 설계에 큰 영향을 미칠 수 있습니다. 이 글에서는 Spring에서 가장 많이 사용되는 Bean Scope인 Singleton, Prototype, 그리고 웹 애플리케이션에서 특히 중요한 Request Scope에 대해 자세히 알아보겠습니다.
각 Scope의 특징, 사용 사례, 실제 코드 예제와 함께 실무에서 흔히 발생하는 문제점과 해결 방법까지 다룰 예정입니다. 제대로 된 Bean Scope 선택은 애플리케이션 성능과 안정성의 기반이 되므로, 이 글을 통해 Spring 개발자로서 한 단계 성장하시길 바랍니다.
Spring Bean Scope란 무엇인가?
Spring Bean Scope는 Bean 객체가 생성되고, 존재하고, 소멸되는 범위를 정의합니다. 다시 말해, Spring IoC 컨테이너가 특정 Bean 정의로부터 얼마나 많은 객체 인스턴스를 생성할 것인지, 그리고 그 인스턴스의 생명주기를 어떻게 관리할 것인지를 결정하는 설정입니다.
@Component
@Scope("singleton") // Bean Scope 지정
public class UserService {
// ...
}
Spring Framework는 다양한 Bean Scope를 제공하고 있으며, 각각은 특정 상황과 요구사항에 맞게 설계되었습니다:
- Singleton: 기본 Scope로, Spring IoC 컨테이너 당 하나의 객체 인스턴스만 생성
- Prototype: 요청할 때마다 새로운 인스턴스 생성
- Request: HTTP 요청마다 새로운 인스턴스 생성 (웹 애플리케이션)
- Session: HTTP 세션마다 하나의 인스턴스 생성 (웹 애플리케이션)
- Application: ServletContext 생명주기 동안 하나의 인스턴스 존재 (웹 애플리케이션)
- Websocket: WebSocket 생명주기 동안 하나의 인스턴스 존재 (웹 애플리케이션)
이번 글에서는 가장 많이 사용되는 Singleton, Prototype, Request Scope에 집중하여 알아보겠습니다.
Singleton Scope: Spring의 기본 Scope
Singleton Scope는 Spring Framework의 기본 Bean Scope입니다. 특별히 다른 Scope를 지정하지 않으면, 모든 Spring Bean은 Singleton으로 관리됩니다.
Singleton Scope의 특징
Singleton Scope의 가장 큰 특징은 Spring IoC 컨테이너당 오직 하나의 Bean 인스턴스만 생성되고 관리된다는 점입니다.
@Component // 별도의 Scope 지정이 없으면 기본값은 singleton
public class UserRepository {
public UserRepository() {
System.out.println("UserRepository 인스턴스 생성: " + this);
}
// ...
}
이 특징으로 인해 Singleton Bean은 다음과 같은 장점을 갖습니다:
- 메모리 효율성: 단일 인스턴스만 생성되므로 메모리 사용량이 감소합니다.
- 성능 향상: 객체 생성 비용이 한 번만 발생하므로 애플리케이션 시작 이후의 성능이 향상됩니다.
- 상태 공유: 애플리케이션 전체에서 같은 인스턴스를 사용하므로 상태를 쉽게 공유할 수 있습니다.
하지만 동시에 주의해야 할 점도 있습니다:
- 동시성 문제: 여러 스레드가 동시에 접근할 수 있으므로 Thread-safe하게 설계해야 합니다.
- 상태 관리 주의: 가변 상태(mutable state)를 가진 Bean은 예상치 못한 부작용을 일으킬 수 있습니다.
Singleton Bean 실무 예제
실무에서 Singleton Bean은 주로 서비스 레이어, 레포지토리 레이어, 그리고 유틸리티 클래스에 많이 사용됩니다.
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product findById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// 상태를 갖지 않는 메서드들...
}
위 예제의 ProductService는 상태를 갖지 않고 단순히 비즈니스 로직을 수행하기만 하므로 Singleton으로 관리하기에 적합합니다.
Singleton Bean 테스트하기
Singleton Bean의 동작을 확인하는 간단한 테스트 코드를 작성해보겠습니다:
@SpringBootApplication
public class SingletonScopeApplication implements CommandLineRunner {
@Autowired
private ApplicationContext context;
public static void main(String[] args) {
SpringApplication.run(SingletonScopeApplication.class, args);
}
@Override
public void run(String... args) {
// 같은 Bean을 여러 번 요청해도 동일한 인스턴스 반환
UserService service1 = context.getBean(UserService.class);
UserService service2 = context.getBean(UserService.class);
System.out.println("service1 == service2: " + (service1 == service2));
// 출력: service1 == service2: true
}
}
이 테스트 코드를 실행하면 service1과 service2가 동일한 인스턴스인 것을 확인할 수 있습니다.
Prototype Scope: 매번 새로운 인스턴스
Prototype Scope는 Singleton과 달리 Bean을 요청할 때마다 새로운 인스턴스를 생성합니다. 이는 상태를 가진 Bean이나 스레드 안전성이 보장되어야 하는 경우에 유용합니다.
Prototype Scope 설정 방법
Prototype Scope를 설정하는 방법은 다음과 같습니다:
// 방법 1: 어노테이션 사용
@Component
@Scope("prototype")
public class ShoppingCart {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
// ...
}
// 방법 2: Java 설정 클래스 사용
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public ShoppingCart shoppingCart() {
return new ShoppingCart();
}
}
// 방법 3: Constants 사용하기 (권장)
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart {
// ...
}
Prototype Scope의 특징
Prototype Scope Bean의 주요 특징은 다음과 같습니다:
- 독립적인 인스턴스: 매번 새로운 인스턴스가 생성되므로 각 인스턴스는 독립적인 상태를 가집니다.
- 상태 격리: 각 인스턴스가 독립적이므로 상태 간섭이 없습니다.
- 생명주기 관리 제한: Spring은 Prototype Bean의 생성만 관리하고, 이후의 생명주기는 관리하지 않습니다.
Prototype Bean 실무 예제
실무에서 Prototype Bean은 주로 상태를 가진 객체나 사용자별로 다른 데이터를 유지해야 하는 경우에 사용됩니다:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart {
private final List<Product> products = new ArrayList<>();
private final User user;
@Autowired
public ShoppingCart(User user) {
this.user = user;
System.out.println("ShoppingCart 생성됨: " + this + " for user: " + user.getUsername());
}
public void addProduct(Product product) {
products.add(product);
}
public List<Product> getProducts() {
return Collections.unmodifiableList(products);
}
public double calculateTotal() {
return products.stream()
.mapToDouble(Product::getPrice)
.sum();
}
}
위 예제의 ShoppingCart는 사용자별로 독립적인 장바구니를 유지해야 하므로 Prototype Scope로 선언했습니다.
Prototype Bean 테스트하기
Prototype Bean의 동작을 확인하는 테스트 코드입니다:
@SpringBootApplication
public class PrototypeScopeApplication implements CommandLineRunner {
@Autowired
private ApplicationContext context;
public static void main(String[] args) {
SpringApplication.run(PrototypeScopeApplication.class, args);
}
@Override
public void run(String... args) {
// 매번 다른 인스턴스 반환
ShoppingCart cart1 = context.getBean(ShoppingCart.class);
ShoppingCart cart2 = context.getBean(ShoppingCart.class);
System.out.println("cart1 == cart2: " + (cart1 == cart2));
// 출력: cart1 == cart2: false
cart1.addProduct(new Product("노트북", 1500000));
cart2.addProduct(new Product("스마트폰", 1000000));
System.out.println("cart1 제품 수: " + cart1.getProducts().size());
System.out.println("cart2 제품 수: " + cart2.getProducts().size());
// 각각 1개씩 출력됨 (독립적인 상태)
}
}
Request Scope: HTTP 요청별 인스턴스
Request Scope는 웹 애플리케이션에서 사용되는 Bean Scope로, HTTP 요청마다 새로운 인스턴스를 생성합니다. 하나의 HTTP 요청 내에서는 같은 Bean 인스턴스가 공유되지만, 다른 HTTP 요청에서는 다른 인스턴스가 사용됩니다.
Request Scope 설정 방법
Request Scope를 사용하려면 먼저 웹 관련 의존성이 필요합니다:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
그리고 다음과 같이 Request Scope를 설정할 수 있습니다:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserPreferences {
private String theme = "default";
private String language = "ko";
public UserPreferences() {
System.out.println("UserPreferences 생성됨: " + this);
}
// Getters and Setters
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
}
여기서 중요한 것은 proxyMode = ScopedProxyMode.TARGET_CLASS 설정입니다. 이 설정이 없으면 Singleton Bean이 Request Scope Bean을 주입받을 때 문제가 발생할 수 있습니다.
Request Scope의 특징
Request Scope Bean의 주요 특징은 다음과 같습니다:
- 요청별 인스턴스: 각 HTTP 요청마다 새로운 인스턴스가 생성됩니다.
- 요청 내 공유: 같은 HTTP 요청 내에서는 동일한 인스턴스가 공유됩니다.
- 요청 종료 시 소멸: HTTP 요청이 완료되면 Bean 인스턴스도 소멸됩니다.
- 프록시 사용: 일반적으로 프록시 모드와 함께 사용되어 Singleton Bean에 주입될 수 있습니다.
Request Scope 실무 예제
Request Scope는 주로 사용자의 요청별 데이터나 컨텍스트 정보를 저장할 때 유용합니다:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestLogger {
private final String requestId;
private final List<String> logMessages = new ArrayList<>();
private final LocalDateTime requestTime;
public RequestLogger() {
this.requestId = UUID.randomUUID().toString();
this.requestTime = LocalDateTime.now();
this.log("Request started");
}
public void log(String message) {
logMessages.add(String.format("[%s] %s",
LocalDateTime.now().format(DateTimeFormatter.ISO_TIME), message));
}
public List<String> getLogMessages() {
return Collections.unmodifiableList(logMessages);
}
public String getRequestId() {
return requestId;
}
public LocalDateTime getRequestTime() {
return requestTime;
}
@PreDestroy
public void onDestroy() {
this.log("Request completed");
System.out.println("RequestLogger 소멸: " + this);
System.out.println("Request ID: " + requestId + ", 로그 항목 수: " + logMessages.size());
}
}
위 예제의 RequestLogger는 각 HTTP 요청마다 독립적인 로그를 관리합니다.
Request Scope 사용 예제 (컨트롤러)
Request Scope Bean을 컨트롤러에서 사용하는 예제입니다:
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
private final RequestLogger requestLogger;
@Autowired
public UserController(UserService userService, RequestLogger requestLogger) {
this.userService = userService;
this.requestLogger = requestLogger;
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
requestLogger.log("요청 받음: /api/users/" + id);
User user = userService.findById(id);
requestLogger.log("사용자 조회 완료: " + user.getUsername());
return ResponseEntity.ok(user);
}
@GetMapping("/request-info")
public Map<String, Object> getRequestInfo() {
requestLogger.log("요청 정보 조회");
Map<String, Object> info = new HashMap<>();
info.put("requestId", requestLogger.getRequestId());
info.put("requestTime", requestLogger.getRequestTime());
info.put("logs", requestLogger.getLogMessages());
return info;
}
}
Request Scope의 주의사항
Request Scope를 사용할 때는 다음 사항에 주의해야 합니다:
- 프록시 모드 설정: Singleton Bean에서 Request Scope Bean을 주입받을 때는 반드시 프록시 모드를 설정해야 합니다.
- 비동기 처리: 비동기 메서드에서는 Request Scope가 예상대로 동작하지 않을 수 있으므로 주의가 필요합니다.
- 테스트 복잡성: Request Scope Bean을 테스트할 때는 HTTP 요청 컨텍스트를 모의(mock)해야 할 수 있습니다.
Bean Scope 간의 주입 문제와 해결책
다양한 Bean Scope를 사용할 때 발생할 수 있는 대표적인 문제 중 하나는 Scope가 더 넓은 Bean(예: Singleton)에 Scope가 더 좁은 Bean(예: Prototype, Request)을 주입할 때 발생합니다.
문제 상황
다음과 같이 Singleton Bean에 Prototype Bean을 주입하는 상황을 생각해봅시다:
@Service
public class UserManager {
// Prototype Bean 주입
@Autowired
private ShoppingCart shoppingCart; // Prototype Bean
public void addProduct(Product product) {
// 문제: 항상 같은 shoppingCart 인스턴스 사용
shoppingCart.addProduct(product);
}
}
이 경우, ShoppingCart가 Prototype Scope로 선언되었지만, UserManager가 생성될 때 한 번만 주입되므로 Prototype의 의도대로 동작하지 않습니다.
해결책 1: ObjectFactory 사용
@Service
public class UserManager {
@Autowired
private ObjectFactory<ShoppingCart> shoppingCartFactory;
public void addProduct(Product product) {
// 매번 새로운 ShoppingCart 인스턴스 얻기
ShoppingCart cart = shoppingCartFactory.getObject();
cart.addProduct(product);
}
}
해결책 2: ApplicationContext 사용
@Service
public class UserManager {
@Autowired
private ApplicationContext context;
public void addProduct(Product product) {
// 매번 새로운 ShoppingCart 인스턴스 얻기
ShoppingCart cart = context.getBean(ShoppingCart.class);
cart.addProduct(product);
}
}
해결책 3: 프록시 모드 사용 (Request Scope)
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserPreferences {
// ...
}
@Service
public class UserService {
@Autowired
private UserPreferences userPreferences; // 프록시 주입
public String getUserTheme() {
// 요청 시점에 실제 UserPreferences 인스턴스에 접근
return userPreferences.getTheme();
}
}
실무에서의 Bean Scope 선택 가이드
실무에서 어떤 Bean Scope를 선택해야 할지 판단하는 기준을 소개합니다.
Singleton을 선택해야 하는 경우
- 상태가 없거나 공유 상태만 있는 경우: 서비스, 레포지토리, 유틸리티 클래스
- 성능이 중요한 경우: 객체 생성 비용이 큰 경우
- 캐싱이 필요한 경우: 애플리케이션 전체에서 공유되는 캐시
예시:
@Service
public class ProductService {
// 상태 없음, 단순 비즈니스 로직만 수행
}
@Repository
public class ProductRepository {
// 데이터 액세스 로직만 수행
}
@Component
public class CacheManager {
// 애플리케이션 전체에서 공유되는 캐시
private final Map<String, Object> cache = new ConcurrentHashMap<>();
}
Prototype을 선택해야 하는 경우
- 상태를 가진 Bean이 필요한 경우: 사용자별 데이터, 세션별 데이터
- 스레드 안전성이 보장되어야 하는 경우: 멀티스레드 환경에서 각 스레드가 독립적인 인스턴스 필요
- 요청별로 다른 설정이 필요한 경우: 동적 설정 객체
예시:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class UserEditor {
private User user;
private List<String> changes = new ArrayList<>();
// 각 편집 세션마다 독립적인 상태 유지
}
Request Scope를 선택해야 하는 경우
- HTTP 요청별 데이터 필요: 사용자 요청 컨텍스트, 요청 로깅
- 요청 추적: 요청 ID, 요청 시작 시간 등 추적 정보
- 요청별 사용자 설정: 언어, 테마 등 요청별 설정
예시:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private final String requestId = UUID.randomUUID().toString();
private final Map<String, Object> attributes = new HashMap<>();
// 요청 추적 및 컨텍스트 정보 저장
}
Bean Scope 관련 고급 기능 및 팁
@Lookup 어노테이션 사용하기
@Lookup 어노테이션을 사용하면 Singleton Bean 내에서 Prototype Bean을 얻는 메서드를 쉽게 구현할 수 있습니다:
@Component
public abstract class UserProcessor {
public void processUser(User user) {
UserContext context = createUserContext();
context.setUser(user);
// 처리 로직...
}
@Lookup
protected abstract UserContext createUserContext(); // Spring이 구현 제공
}
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class UserContext {
private User user;
// getters and setters
}
@Lazy 어노테이션과 함께 사용하기
@Lazy 어노테이션을 사용하면 Bean 초기화 시점을 지연시킬 수 있습니다:
@Service
public class ExpensiveService {
@Autowired
@Lazy
private ExpensiveClient client; // 실제 사용 시점에 초기화
public void doSomething() {
// client 사용 시점에 초기화
client.execute();
}
}
커스텀 Scope 만들기
Spring은 커스텀 Scope를 정의할 수 있는 기능도 제공합니다:
@Component
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadScope =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadScope.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
Map<String, Object> scope = threadScope.get();
return scope.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 스레드 종료 시 호출될 콜백 등록
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
Scope 등록하기:
@Configuration
public class AppConfig implements ApplicationContextAware {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new ThreadScope());
return configurer;
}
}
성능 최적화를 위한 Bean Scope 활용
Bean Scope는 애플리케이션 성능에 직접적인 영향을 미칩니다. 다음은 성능 최적화를 위한 몇 가지 팁입니다:
Singleton 최적화
- 불변 객체 사용: Singleton Bean은 가능한 불변(immutable)으로 설계하여 스레드 안전성 확보
- 지연 초기화: 무거운 리소스는 @Lazy 어노테이션을 사용하여 필요 시점에 초기화
- 상태 격리: 상태가 필요한 경우 ThreadLocal 등을 사용하여 스레드별로 상태 격리
@Service
public class ConfigService {
private final Map<String, String> configs = new ConcurrentHashMap<>();
private final ThreadLocal<String> userContext = new ThreadLocal<>();
// 스레드 안전한 상태 관리
}
Prototype 최적화
- 캐싱 활용: 자주 사용되는 Prototype Bean은 캐시하여 생성 비용 절감
- ObjectFactory 활용: 필요한 시점에만 Prototype Bean 생성
@Service
public class ReportGenerator {
@Autowired
private ObjectFactory<ReportContext> contextFactory;
private final Map<String, ReportContext> contextCache = new HashMap<>();
public Report generateReport(String type, Map<String, Object> data) {
ReportContext context = contextCache.computeIfAbsent(type, t -> {
ReportContext ctx = contextFactory.getObject();
ctx.setType(t);
return ctx;
});
// 보고서 생성 로직...
}
}
Request Scope 최적화
- 필요한 경우만 사용: Request Scope는 HTTP 요청별로 인스턴스가 생성되므로 필요한 경우에만 사용
- 프록시 모드 최적화: ScopedProxyMode.INTERFACES를 사용하여 인터페이스 기반 프록시 사용 (가능한 경우)
- 비동기 처리 주의: 비동기 메서드에서는 RequestContextHolder를 활용하여 요청 컨텍스트 전파
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.INTERFACES)
public class RequestMetrics implements Metrics {
private final Map<String, Long> timings = new HashMap<>();
private final long startTime = System.currentTimeMillis();
@Override
public void recordTiming(String operation, long timeMs) {
timings.put(operation, timeMs);
}
@Override
public Map<String, Long> getTimings() {
return Collections.unmodifiableMap(timings);
}
@Override
public long getTotalTime() {
return System.currentTimeMillis() - startTime;
}
}
실제 사례로 보는 Bean Scope 문제와 해결책
실무에서 마주할 수 있는 Bean Scope 관련 문제와 해결책을 살펴보겠습니다.
사례 1: Singleton에서의 상태 공유 문제
문제: 여러 사용자가 동시에 접근할 때 데이터가 섞이는 문제
// 잘못된 구현
@Service
public class UserSessionManager {
private User currentUser; // 문제: 모든 요청이 이 상태를 공유함
public void setCurrentUser(User user) {
this.currentUser = user;
}
public User getCurrentUser() {
return currentUser;
}
}
해결책: ThreadLocal 사용 또는 Request Scope로 변경
// 해결책 1: ThreadLocal 사용
@Service
public class UserSessionManager {
private final ThreadLocal<User> currentUser = new ThreadLocal<>();
public void setCurrentUser(User user) {
this.currentUser.set(user);
}
public User getCurrentUser() {
return currentUser.get();
}
public void clear() {
currentUser.remove(); // 메모리 누수 방지
}
}
// 해결책 2: Request Scope 사용
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserContext {
private User currentUser;
// getters and setters
}
사례 2: Prototype Bean의 소멸 처리 문제
문제: Spring은 Prototype Bean의 생성만 관리하고 소멸은 관리하지 않아 메모리 누수 발생
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class LargeResourceHolder {
private final byte[] buffer = new byte[1024 * 1024]; // 1MB 버퍼
@PreDestroy
public void cleanup() {
System.out.println("리소스 정리"); // 호출되지 않음!
}
}
해결책: 커스텀 Bean 후처리기(BeanPostProcessor) 구현
@Component
public class PrototypeDestructionBeanPostProcessor implements BeanPostProcessor, DisposableBean {
private final Set<Object> prototypeBeans = ConcurrentHashMap.newKeySet();
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (BeanFactoryUtils.isPrototypeCurrentlyInCreation(
((ConfigurableApplicationContext) context).getBeanFactory(), beanName)) {
prototypeBeans.add(bean);
}
return bean;
}
@Override
public void destroy() {
prototypeBeans.forEach(bean -> {
if (bean instanceof DisposableBean) {
try {
((DisposableBean) bean).destroy();
} catch (Exception e) {
// 오류 처리
}
}
});
prototypeBeans.clear();
}
}
사례 3: Request Scope Bean의 비동기 메서드 문제
문제: 비동기 메서드에서 Request Scope Bean 사용 시 요청 컨텍스트가 손실됨
@RestController
public class AsyncController {
@Autowired
private RequestLogger logger; // Request Scope Bean
@GetMapping("/async")
public CompletableFuture<String> asyncProcess() {
logger.log("비동기 처리 시작");
return CompletableFuture.supplyAsync(() -> {
// 여기서 logger 사용 시 문제 발생
logger.log("처리 중"); // 오류 발생!
return "완료";
});
}
}
해결책: RequestContextHolder를 사용하여 컨텍스트 전파
@RestController
public class AsyncController {
@Autowired
private RequestLogger logger;
@GetMapping("/async")
public CompletableFuture<String> asyncProcess() {
logger.log("비동기 처리 시작");
// 현재 요청 저장
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return CompletableFuture.supplyAsync(() -> {
try {
// 비동기 스레드에 요청 컨텍스트 설정
RequestContextHolder.setRequestAttributes(requestAttributes, true);
logger.log("처리 중"); // 정상 동작
return "완료";
} finally {
// 컨텍스트 정리
RequestContextHolder.resetRequestAttributes();
}
});
}
}
Spring Boot 환경에서의 Bean Scope 최적화
Spring Boot는 자동 설정과 의존성 관리 기능을 통해 개발자가 더 쉽게 애플리케이션을 개발할 수 있게 해줍니다. Spring Boot 환경에서 Bean Scope를 더 효과적으로 활용하는 방법을 알아보겠습니다.
프로파일별 Bean Scope 설정
Spring Boot의 프로파일 기능을 활용하면 환경에 따라 다른 Bean Scope를 설정할 수 있습니다:
@Component
@Profile("production")
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class ProductionDataSource implements DataSource {
// 프로덕션 환경에서는 커넥션 풀링을 위해 싱글톤 사용
// ...
}
@Component
@Profile("development")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class DevelopmentDataSource implements DataSource {
// 개발 환경에서는 테스트 편의성을 위해 프로토타입 사용
// ...
}
조건부 Bean 등록
Spring Boot의 @ConditionalOn* 어노테이션을 활용한 조건부 Bean 등록:
@Configuration
public class RequestScopeConfig {
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
@ConditionalOnWebApplication
public RequestTracker requestTracker() {
return new RequestTracker();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
@ConditionalOnNotWebApplication
public RequestTracker standaloneRequestTracker() {
return new StandaloneRequestTracker();
}
}
액추에이터(Actuator)를 활용한 모니터링
Spring Boot Actuator를 활용하여 Bean Scope 별로 인스턴스 수를 모니터링할 수 있습니다:
@Component
public class BeanScopeMetrics {
private final Counter singletonBeanCount;
private final Counter prototypeBeanCount;
private final Counter requestBeanCount;
public BeanScopeMetrics(MeterRegistry registry) {
this.singletonBeanCount = registry.counter("beans.singleton.count");
this.prototypeBeanCount = registry.counter("beans.prototype.count");
this.requestBeanCount = registry.counter("beans.request.count");
}
public void incrementSingletonCount() {
singletonBeanCount.increment();
}
public void incrementPrototypeCount() {
prototypeBeanCount.increment();
}
public void incrementRequestCount() {
requestBeanCount.increment();
}
}
이를 위한 Bean 후처리기:
@Component
public class BeanScopeMetricsPostProcessor implements BeanPostProcessor {
private final BeanScopeMetrics metrics;
private final ApplicationContext context;
@Autowired
public BeanScopeMetricsPostProcessor(BeanScopeMetrics metrics, ApplicationContext context) {
this.metrics = metrics;
this.context = context;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
String scope = context.containsBean(beanName)
? context.findAnnotationOnBean(beanName, Scope.class).value()
: "";
if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(scope)) {
metrics.incrementSingletonCount();
} else if (ConfigurableBeanFactory.SCOPE_PROTOTYPE.equals(scope)) {
metrics.incrementPrototypeCount();
} else if (WebApplicationContext.SCOPE_REQUEST.equals(scope)) {
metrics.incrementRequestCount();
}
return bean;
}
}
마치며: 상황에 맞는 Bean Scope 선택하기
이 글에서는 Spring Framework에서 제공하는 다양한 Bean Scope, 특히 Singleton, Prototype, 그리고 Request Scope에 대해 자세히 살펴보았습니다. 각 Scope의 특징, 사용 사례, 그리고 실무에서 마주할 수 있는 문제와 해결책도 함께 알아보았습니다.
Bean Scope 선택은 애플리케이션의 성능, 메모리 사용량, 그리고 전체적인 아키텍처에 큰 영향을 미치는 중요한 결정입니다. 따라서 개발자는 각 상황에 가장 적합한 Scope를 선택할 수 있어야 합니다.
일반적으로 상태가 없거나 공유 상태만 있는 서비스 레이어, 레포지토리 레이어, 유틸리티 클래스는 Singleton Scope가 적합합니다. 반면, 상태를 가지고 있어 각 요청마다 독립적인 인스턴스가 필요한 경우에는 Prototype이나 Request Scope가 더 적합할 수 있습니다.
무엇보다 중요한 것은 각 Scope의 특성을 이해하고, 여러 Scope를 함께 사용할 때 발생할 수 있는 문제점을 인식하는 것입니다. 이 글이 Spring 개발자들에게 Bean Scope를 더 효과적으로 활용할 수 있는 길잡이가 되기를 바랍니다.
추가 자료 및 참고 문헌
더 깊이 있는 학습을 위한 참고 자료:
- Spring Framework 공식 문서 - Bean Scopes
- Spring Boot 공식 문서
- Baeldung - Spring Bean Scopes
- Spring in Action, 5th Edition - Craig Walls
- Expert Spring MVC and Web Flow - Seth Ladd, Darren Davison, Steven Devijver, Colin Yates
이 글이 여러분의 Spring 개발 여정에 도움이 되길 바랍니다.
질문이나 의견이 있으시면 댓글로 남겨주세요!
'Spring & Spring Boot 실무 가이드' 카테고리의 다른 글
REST API 예외 처리 패턴 – 글로벌 핸들러 vs 컨트롤러 별 처리 (0) | 2025.05.18 |
---|---|
코드 한 줄 안 바꾸고 Spring Boot 성능 3배 올리기: JVM 튜닝 실전 가이드 (1) | 2025.05.17 |
실전 코드로 배우는 Redis 캐싱 전략 - TTL, LRU, 캐시 무효화까지 (0) | 2025.05.12 |
Spring Boot에서 Excel 파일 업로드 & 다운로드 처리 – Apache POI 실전 가이드 (0) | 2025.05.10 |
[Java & Spring 실무] JPA Entity 간 N:1, 1:N 관계 설계 베스트 프랙티스 (0) | 2025.05.09 |