Spring에서 Bean이 관리되는 방법

들어가며 

  Spring에서 Bean은 Spring IoC(Inversion of Control) 컨테이너에 의해 관리된다.

이러한 Bean들은 일반적으로 Application의 서비스 계층, 데이터베이스 연결, 메시지 처리 등의 기능을 수행하는데

 

Spring에서 개발자가 정의한 대부분의 Bean들은 싱글톤(Singleton) 패턴으로 관리된다.

IoC 컨테이너는 하나의 인스턴스만 생성하고, Application의 모든 부분에서 해당 인스턴스를 공유하는 것이다.

 

물론 Bean의 Scope 설정에 바꾸어주면 여러개의 인스턴스를 생성하여 관리할 수도 있긴하다. 

 

- Singleton으로 관리되는 Case

1. @Service 어노테이션

보통 @Serice 어노테이션이 붙은 클래스는 어떠한 클라이언트 요청에도 독립적으로 처리할 수 있도록 Stateless하게 설계하기 때문에 멀티스레드 환경에서도 스레드에 안전하게 작동하도록 한다. 따라서 필요한 데이터는 메소드의 매개변수로 사용하고 인스턴스 변수에 데이터를 저장하지 않는데 이러한 특징들이 서비스 계층이 상태를 가지지 않도록 하는 목적이다.

 

2. @Repository 어노테이션

 @Repository 어노테이션이 붙은 클래스는 DAO 클래스와 마찬가지로 데이터 베이스와의 상호작용을 담당하는 역할을 하필요한 데이터는 요청마다 파라미터로 받아 처리하기 때문에 무상태(stateless) 객체라고하며 이러한 특성 때문에 여러 스레드에서 동시에 안전하게 사용할 수 있으며 이를 위해 싱글톤 패턴으로 관리된다.

 

3. @Controller 어노테이션

컨트롤러는 HTTP 요청을 처리하는 메소드를 포함하지만, 이 메소드들은 보통 상태 정보를 저장하지 않으므로 멀티 스레드 환경에서 안전하게 사용하기 위해 싱글톤 패턴으로 관리된다.

 

4. Configuration 클래스, @Configuration 

 Spring 설정 정보를 내포하고있는 클래스들은 보통 어플리케이션 전체에서 공유되어야 하기때문에 싱글톤 패턴으로 관리된다.

 

5. Filter와 Interceptor

Spring에서는 필터와 인터셉터도 마찬가지로 어플리케이션 전체에서 공유되어야하기 때문에 싱글톤 패턴으로 관리된다.

 

- Singleton으로 관리하지 않는 Case

 보통 이러한 경우는 요청마다 새로운 객체가 필요한 경우로 상태를 유지하는(stateful) 경우에 발생한다.

예를 들면 세션관리, 트랜잭션 관리, 비동기 작업, 임시 데이터 저장 등을 해야하는 상황이 대표적이다.

 

  1. 세션관리 

@Scope("prototype")을 사용하면, 매번 새로운 인스턴스가 생성된다. 이는 요청할 때마다 새로운 객체가 필요한 경우, 즉

상태를 가지고 있어야하는 경우에 발생하는데 다음과 같은 상황이 있을 수 있다.

 

 @Scope("session")을 사용하면 HTTP 세션 단위로 Bean을 관리할 수 있다. 이렇게 하면 각 사용자 세션마다 별도의 Bean 인스턴스가 생성되므로, 서로 다른 사용자 간에 데이터가 공유되지 않는다.

 

쇼핑몰 사이트에서 사용자의 장바구니를 관리해야하는 상황일 경우  @Scope("session")으로 장바구니 기능을 구현하면 각 HTTP 세션마다 별도의 인스턴스가 생성되므로, 서버는 자동으로 각 사용자의 장바구니를 추적하고 관리할 수 있게하는데 편리하다.

 

< @Scope("session") 예제 코드 >

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {

}

 

또 다른 예시로 각각의 HTTP 요청에 대해 특정 사용자가 어떤 작업을 수행하는지 로깅하고 싶은 상황이 있을 수 있다.

이 때, @Scope("request")는 HTTP 요청마다 새로운 빈 인스턴스를 생성하고, 그 요청의 생명 주기 동안만 그 인스턴스를 유지하므로 해당 기능을 구현하는데 편리하다.

 

< @Scope("request") 예제 코드 >

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestLog {

    private List<String> messages = new ArrayList<>();

    public void addMessage(String message) {
        this.messages.add(message);
    }

    public List<String> getMessages() {
        return messages;
    }
}

 

  2. 트랜잭션관리

 Java에서는 synchronized 라는 키워드를 통해서 멀티스레드 환경에서 동시성 문제를 방지하기 위한 기능을 제공하지만 데이터 베이스와 같은 외부 자원을 다루는 경우에는 synchronized만으로는 충분하지 않다. 

 

이런 경우 보통 데이터베이스의 트랜잭션 관리 기능을 활용하는데 Spring에서는

@Transactional 어노테이션을 사용하면 편리하게 기능을 구현할 수 있다.

 

< @Transactional 예제코드 >

@Service
public class BankAccountService {

    @Autowired
    private BankAccountRepository bankAccountRepository;

    @Transactional
    public void withdraw(Long accountId, BigDecimal amount) {
        BankAccount account = bankAccountRepository.findById(accountId).orElseThrow(() -> new IllegalArgumentException("Invalid account id."));
        account.setBalance(account.getBalance().subtract(amount));
        bankAccountRepository.save(account);
    }

    @Transactional
    public void deposit(Long accountId, BigDecimal amount) {
        BankAccount account = bankAccountRepository.findById(accountId).orElseThrow(() -> new IllegalArgumentException("Invalid account id."));
        account.setBalance(account.getBalance().add(amount));
        bankAccountRepository.save(account);
    }
}

 

다만 이러한 방식은 아래와 같은 주의점이 존재한다.

 

1. 상태유지(stateful) : 세션 스코프 빈은 상태를 유지하므로, 동시성 문제와 메모리 문제를 염두해 두어야한다.

동기화(synchronization) 없이 상태 변경을 시도하면 "경쟁상태 (race condition)"와 같은 동시성 문제가 발생할 수 있는데 

경쟁상태 (race condition) 문제란 두개 이상의 연산이 순서나 타이밍에 따라서 다른 결과를 초래하는 상황을 말한다.

 

예를 들어, 어느 한 A고객의 계좌에 잔액이 1,000만원이 있다고 가정해보자.

A고객이 200만원을 출금하려고하며 거의 동시에 B고객이 A고객의 계좌에 300만원을 입금하려고 하는 상황이다.

 

1. A의 출금 트랜잭션은 잔액을 확인하여 1,000만원이 있음을 확인

2. 그 후 B의 입금 트랜잭션이 실행되어 잔액에 300만원을 추가하여 이 시점에서 실제 잔액은 1,300만원이다.

3. 그런데 A의 출금 트랜잭션이 처음 확인했던 1,000만원에서 200만원을 차감하여 최종적인 잔액을 계산

4. 따라서 최종적인 계좌 잔액이 800만원으로 설정되는 문제가 발생

 

원래대로라면 1,100만원이 잔액으로 설정되어야 정상이지만 이러한 경쟁상태(race condition) 문제가 발생할 수 있다.

 

 

2. 분산 환경  : MSA 아키텍처 형태로 구성된 서버라면 각각의 서비스들은 별도의 프로세스로 실행되고 각자 자신만의 데이터베이스를 가질 수도 있다. 이런 구조에서는 사용자의 정보를 여러 서버에서 공유할 수 있는 방법을 찾아야 한다.

( 보편적인 방법으로는 Redis에 세션 정보를 저장하고 공유하는 것이다. )