본문 바로가기

Java/spring

[Java/스프링] 스프링 컨테이너, 빈 (스프링의 싱글톤과 Java 싱글톤의 차이점)

들어가며

🗒️ IoC: Inversion of Control, 의존 관계 주입(Dependency Injection)이라고도 합니다.
어떤 객체가 사용하는 의존 객체를
직접 만들어 사용하는 게 아니라 주입 받아 사용하는 방법을 말합니다.
 


 

1. 스프링 IoC 컨테이너, 빈

1) 컨테이너(Container)

💡 스프링의 컨테이너는 "객체(빈)를 넣을 그릇"이라고 할 수 있습니다.
이 그릇은 애플리케이션의 요구사항에 맞게 생성된 객체(빈)들을 담고 관리합니다. 이러한 객체들은 XML 설정 파일이나 어노테이션을 통해 정의되며, 스프링 컨테이너에 의해 관리됩니다.

 
왜 IoC 컨테이너라고 부를까?

  • 전통적인 객체 생성 및 관리 방식과 다르게 객체의 생성, 초기화, 의존성 주입, 생명주기 관리 등을 개발자가 아닌 컨테이너가 담당하기 때문입니다. 이로 인해 객체 간 결합이 느슨해지고, 코드의 유연성과 재사용성이 높아집니다.
    • IoC
      • 전통적인 방식에서는 A, B, C, D 같은 구성 요소를 일체형으로 만들어 한 번에 조립하지만, 스프링에서는 이러한 요소들이 역순으로 만들어지고 결합됩니다. 예를 들어 D를 먼저 만들고, 그 다음에 C, B, A가 차례대로 생성되고 결합됩니다. 그렇게 역순으로 객체들이 만들어지고, 컨테이너가 이 과정을 제어하는 방식이 제어의 역전(IoC)입니다.
      • 객체의 생성, 생명 주기를 관리하는 책임이 어플리케이션 코드에서 스프링 컨테이너로 넘어간다는 것을 의미합니다.
        • 재사용성 향상: 결합도가 낮아진 객체들은 다른 컨테이너에서도 재사용하기 쉬워집니다. 특정 비즈니스 로직을 담고 있는 서비스 객체가 의존하는 DAO 객체를 다른 구현체로 교체하더라도, 서비스 객체를 그대로 사용할 수 있습니다.
    • DI
      • 스프링 IoC 컨테이너가 객체 간 의존성을 주입해 주는 방식입니다. 이 방법을 통해 객체들은 자신이 의존하는 객체를 직접 생성하거나 관리하지 않고, 외부에서 주입받게 됩니다.
        • 느슨한 결합: 객체 A가 객체 B를 직접 생성한다면 A와 B는 강하게 결합되어 있을 것입니다. 하지만 스프링 컨테이너가 B를 생성하고 A에 주입해준다면, A는 B가 구체적으로 무엇인지 몰라도 됩니다. 이렇게 하면 A와 B의 결합도가 낮아지고, B를 다른 구현체로 대체하기 쉽습니다.
        • 유연성 향상: 의존성 주입 덕분에, 클래스 A는 다양한 종류의 B를 사용할 수 있습니다.
          테스트 시에는 mock을 주입하고, 실제 운영에서는 실제 구현체를 주입할 수 있습니다. 이렇게 하면 코드를 수정하지 않고도, 다양한 상황에 맞게 객체의 동작을 조정할 수 있습니다.
      • 의존성 주입을 통해 객체 간 결합이 느슨해지고, 이는 객체들이 서로 독립적으로 동작할 수 있게 만들어 코드의 유지보수성을 높입니다.

 

예시로 생각해보자

  • 은행 어플리케이션을 예를 들어 스프링 IoC 컨테이너가 어떻게 사용되는지 생각해보겠습니다. 아래 예시는 계좌 이체(transfer) 기능을 제공하는 서비스 클래스를 다루겠습니다.
    • 요구사항
      • 은행 어플리케이션에서 계좌 이체를 처리하는 시스템을 구현해 주세요.
      • 이 시스템의 주요 클래스는 TransferService와 NotificationService입니다.
        • TransferService: 계좌 간 이체를 처리하는 클래스
        • NotificationService: 이체 완료 후 사용자에게 알림을 보내는 클래스
  • 전통적인 방식: 의존성을 직접 관리하는 방식
    • TransferService는 NotificationService를 직접 생성합니다. (new)
    • 이 경우, TransferService는 Nofication Service와 강하게 결합되어 있어, 나중에 NotificationService를 교체하거나 변경하기 어렵습니다.
public class TransferService {
    private NotificationService notificationService;

    public TransferService() {
        // TransferService가 NotificationService의 인스턴스를 직접 생성
        this.notificationService = new NotificationService();
    }

    public void transferFunds(Account fromAccount, Account toAccount, double amount) {
        // 계좌 이체 로직
        fromAccount.debit(amount);
        toAccount.credit(amount);
        
        // 이체 후 알림 전송
        notificationService.notifyTransfer(fromAccount, toAccount, amount);
    }
}

public class NotificationService {
    public void notifyTransfer(Account fromAccount, Account toAccount, double amount) {
        // 사용자에게 알림 전송 로직
        System.out.println("이체 금액: " + amount + ", 출금: " + fromAccount.getId() + " → " + toAccount.getId() + " 입금 완료.");
    }
}

 
 

  • DI: 스프링 IoC를 사용하는 방법
    • TransferService는 NotificationService에 대해 구체적인 구현을 알 필요가 없습니다. NotificationService 인스턴스는 스프링 컨테이너에서 주입되므로, TransferService는 다양한 NotificationService 구현체를 쉽게 교체할 수 있습니다.
      • 예를 들면 이메일로 알림을 보내는 EmailNotificationService로 교체하거나
        테스트 목적으로 알림을 보내지 않는 MockNotificationService로 대체할 수 있습니다.
        이는 모두 TransferService 코드를 수정하지 않고, 스프링 설정만 변경하면 가능합니다.
    • TransferService나 NotificationService는 결합도가 낮고 독립적이기 때문에 다른 환경에서도 쉽게 재사용할 수 있습니다.
// 1단계: 클래스 정의
public class TransferService {
    private NotificationService notificationService;

    // 의존성을 생성자 주입(Constructor)으로 받음
    public TransferService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void transferFunds(Account fromAccount, Account toAccount, double amount) {
        // 계좌 이체 로직
        fromAccount.debit(amount);
        toAccount.credit(amount);
        
        // 이체 후 알림 전송
        notificationService.notifyTransfer(fromAccount, toAccount, amount);
    }
}

public class NotificationService {
    public void notifyTransfer(Account fromAccount, Account toAccount, double amount) {
        // 사용자에게 알림 전송 로직
        System.out.println("이체 금액: " + amount + ", 출금: " + fromAccount.getId() + " → " + toAccount.getId() + " 입금 완료.");
    }
}

// 2단계: 스프링 설정 (어노테이션)
@Configuration
public class BankAppConfig {

    @Bean
    public NotificationService notificationService() {
        return new NotificationService();
    }

    @Bean
    public TransferService transferService() {
        return new TransferService(notificationService());
    }
}

// 3단계: 스프링 컨테이너 재사용
public class BankApplication {
    public static void main(String[] args) {
        // 스프링 컨테이너를 초기화
        ApplicationContext context = new AnnotationConfigApplicationContext(BankAppConfig.class);

        // TransferService 빈을 가져옴
        TransferService transferService = context.getBean(TransferService.class);

        // 계좌 이체 로직 실행
        Account fromAccount = new Account("12345");
        Account toAccount = new Account("67890");
        transferService.transferFunds(fromAccount, toAccount, 500.00);
    }
}

 


 

2) 빈(Bean)

💡 스프링 IoC 컨테이너에 의해 관리되는 객체입니다.

 

빈과 의존성 주입

  • 빈으로 등록된 객체만 의존성 주입(DI)를 받을 수 있습니다.
  • 빈의 스코프: 빈이 어플리케이션 내에서 어떻게 사용되는지 정의합니다.

 

빈의 스코프 종류

  • 싱글톤 스코프: 기본적으로 스프링 IoC 컨테이너는 빈을 싱글톤 스코프로 관리합니다. 즉, 하나의 빈 인스턴스만 생성되어 애플리케이션 전반에서 재사용됩니다.
    • 따라서 메모리 효율성과 성능 최적화에 유리합니다. 한 번 생성된 객체를 계속 사용하므로 불필요한 객체 생성 비용을 절약할 수 있습니다.
  • 프로토타입 스코프: 매번 빈을 요청할 때마다 새로운 인스턴스가 생성됩니다. 빈이 매번 새로운 상태로 필요할 때 사용됩니다.
    • 만약 특정 객체가 사용될 때마다 초기화가 필요하거나, 각 요청에서 독립된 상태를 유지해야 한다면 프로토타입 스코프를 사용할 수 있습니다. 하지만 매번 객체가 새로 생성되므로 메모리 사용량이 증가하고, 성능이 저하될 수 있습니다.

 

스프링의 싱글톤과 Java 싱글톤의 차이점

  • Java에서 싱글톤 패턴의 문제점
    • 싱글톤 패턴을 사용하여 인스턴스를 한 개로만 가져가면 메모리 비용이 절감되고 데이터를 공유할 수 있다는 장점도 있지만...
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어갑니다.
    • SRP(Single Responsibility Principle, 단일 책임 원칙) 위반:Primary Function과 하나의 인스턴스만 생성하는 두 가지 책임이 있습니다.
    • DIP(Dependency Inversion Principle, 의존관계 역전 원칙): 의존관계상 클라이언트가 구현체에 의존하면서 DIP를 위반하고
    • OCP(Open/Closed Principle 개방-폐쇄 원칙) 위반: 클라이언트가 구체 클래스에 의존해서 OCP를 위반할 가능성도 높습니다.(기능 추가 시 기존 코드 수정 필요)
구분Java의 싱글톤 패턴스프링의 싱글톤 스코프
생성 방식클래스 내에서 static 변수로 관리, 최초 호출 시 인스턴스 생성스프링 컨테이너가 관리, 컨테이너 초기화 시 인스턴스 생성
인스턴스 관리전역적으로 접근 가능컨테이너 내에서만 싱글톤 유지
결합도전역 상태로 인해 결합도 상승의존성 주입(DI)으로 결합도 낮음
테스트 용이성static으로 관리되기에 mock 객체를 만들어 테스트하기 어려움static이 아닌 컨테이너에서 관리
따라서 테스트에서 대체 빈(mock 객체) 사용 가능
상태 관리상태(변수값)를 가질 수 있음, 상태 관리 시 동시성 문제 발생 가능무상태(stateless)로 설계 권장, 상태 관리가 필요할 경우 다른 스코프(프로토타입) 사용 가능
안티 패턴 여부전역 상태 및 결합도 증가로 인해 안티 패턴으로 간주되기도 함객체를 전역으로 관리하지 않고, 컨테이너라는 별도 공간에서 관리하기에 코드 간 결합도가 낮아지고 유연성이 높아짐
안티 패턴이 아님
재사용성전역적으로 재사용 가능
(하지만 상태 관리 위험성 존재)
컨테이너 내에서 재사용 가능
(무상태로 설계 시 안전하게 재사용 가능)
생명주기 관리클래스 로딩 시 생성, 애플리케이션 종료시까지 유지됨스프링 컨테이너가 생명주기 관리, 필요 시 확장 가능

 
 


반응형