스프링 빈의 스코프 및 다양한 인스턴스화 전략

1. 스프링 빈 스코프 이해하기

스프링 컨테이너 내에서 빈 인스턴스가 어떻게 생성되고 관리되는지를 정의하는 중요한 개념 중 하나는 '스코프(Scope)'입니다. 빈의 스코프를 설정함으로써, 애플리케이션의 특정 요청이나 세션마다 새로운 인스턴스를 생성할지, 아니면 애플리케이션 전체에 걸쳐 단일 인스턴스를 유지할지 결정할 수 있습니다.

1.1. 싱글턴 (Singleton) 스코프

싱글턴 스코프는 스프링에서 가장 기본적인 빈 스코프이자 기본값입니다. 컨테이너가 시작될 때 한 번만 인스턴스가 생성되며, 이후 해당 빈에 대한 모든 요청은 동일한 인스턴스를 반환합니다. 이는 애플리케이션 전반에 걸쳐 공유되는 서비스나 데이터 접근 객체(DAO)에 적합합니다.

예시 클래스:

package com.example.spring;

public class SimpleService {
    public SimpleService() {
        System.out.println("SimpleService의 기본 생성자 실행됨");
    }

    public String getMessage() {
        return "서비스 메시지입니다.";
    }
}

스프링 XML 설정 (bean-scopes.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- scope를 지정하지 않으면 기본값인 singleton으로 설정됩니다. -->
    <bean id="mySimpleService" class="com.example.spring.SimpleService"/>

</beans>

테스트 코드:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertSame;

public class BeanScopeTests {
    @Test
    void testSingletonScope() {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-scopes.xml");

        System.out.println("컨테이너에서 첫 번째 SimpleService 빈 요청:");
        SimpleService serviceA = context.getBean("mySimpleService", SimpleService.class);
        System.out.println("컨테이너에서 두 번째 SimpleService 빈 요청:");
        SimpleService serviceB = context.getBean("mySimpleService", SimpleService.class);
        System.out.println("컨테이너에서 세 번째 SimpleService 빈 요청:");
        SimpleService serviceC = context.getBean("mySimpleService", SimpleService.class);

        System.out.println("Service A: " + serviceA);
        System.out.println("Service B: " + serviceB);
        System.out.println("Service C: " + serviceC);

        // 모든 인스턴스가 동일한 객체인지 확인
        assertSame(serviceA, serviceB);
        assertSame(serviceB, serviceC);
    }
}

실행 결과:

SimpleService의 기본 생성자 실행됨
컨테이너에서 첫 번째 SimpleService 빈 요청:
컨테이너에서 두 번째 SimpleService 빈 요청:
컨테이너에서 세 번째 SimpleService 빈 요청:
Service A: com.example.spring.SimpleService@...
Service B: com.example.spring.SimpleService@...
Service C: com.example.spring.SimpleService@...

생성자가 한 번만 호출되고, 모든 객체 참조가 동일한 메모리 주소를 가리키는 것을 확인할 수 있습니다.

1.2. 프로토타입 (Prototype) 스코프

프로토타입 스코프는 빈을 요청할 때마다 새로운 인스턴스를 생성합니다. 이는 각 사용자에 대해 독립적인 상태를 가져야 하는 빈에 적합합니다. 스프링 컨테이너는 프로토타입 빈의 생성까지만 관여하며, 이후 생명주기는 애플리케이션이 관리합니다.

스프링 XML 설정 (bean-scopes.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- scope="prototype"을 지정하여 요청 시마다 새로운 인스턴스 생성 -->
    <bean id="myPrototypeService" class="com.example.spring.SimpleService" scope="prototype"/>

</beans>

테스트 코드 (testPrototypeScope 메서드 추가):

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotSame;

public class BeanScopeTests {
    // ... (testSingletonScope 메서드 생략)

    @Test
    void testPrototypeScope() {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-scopes.xml");

        System.out.println("컨테이너에서 첫 번째 SimpleService 빈 요청 (Prototype):");
        SimpleService protoServiceA = context.getBean("myPrototypeService", SimpleService.class);
        System.out.println("컨테이너에서 두 번째 SimpleService 빈 요청 (Prototype):");
        SimpleService protoServiceB = context.getBean("myPrototypeService", SimpleService.class);
        System.out.println("컨테이너에서 세 번째 SimpleService 빈 요청 (Prototype):");
        SimpleService protoServiceC = context.getBean("myPrototypeService", SimpleService.class);

        System.out.println("Prototype Service A: " + protoServiceA);
        System.out.println("Prototype Service B: " + protoServiceB);
        System.out.println("Prototype Service C: " + protoServiceC);

        // 모든 인스턴스가 서로 다른 객체인지 확인
        assertNotSame(protoServiceA, protoServiceB);
        assertNotSame(protoServiceB, protoServiceC);
    }
}

실행 결과:

SimpleService의 기본 생성자 실행됨
SimpleService의 기본 생성자 실행됨
SimpleService의 기본 생성자 실행됨
Prototype Service A: com.example.spring.SimpleService@...
Prototype Service B: com.example.spring.SimpleService@...
Prototype Service C: com.example.spring.SimpleService@...

각각의 getBean() 호출마다 생성자가 실행되고 새로운 객체 인스턴스가 생성되는 것을 확인할 수 있습니다.

1.3. 다른 빈 스코프

스프링은 웹 애플리케이션 환경을 위한 추가적인 스코프를 제공합니다.

  • request: HTTP 요청마다 새로운 빈 인스턴스가 생성됩니다. 요청이 끝나면 빈도 소멸됩니다. (웹 환경 전용)
  • session: HTTP 세션마다 새로운 빈 인스턴스가 생성됩니다. 세션이 만료되면 빈도 소멸됩니다. (웹 환경 전용)
  • application: 웹 애플리케이션의 ServletContext 생명주기 동안 단일 빈 인스턴스가 유지됩니다. (웹 환경 전용)
  • websocket: 단일 WebSocket 세션 생명주기 동안 단일 빈 인스턴스가 유지됩니다. (웹 환경 전용)
  • global session: 포틀릿(Portlet) 기반 애플리케이션에서 사용되며, 하나의 전역 HTTP 세션에 대해 단일 인스턴스가 생성됩니다. 서블릿 기반 웹 애플리케이션에서는 session 스코프와 동일하게 동작합니다.
  • custom: 개발자가 직접 스코프를 정의하고 등록하여 사용할 수 있습니다.

2. 스프링 빈 인스턴스화의 다양한 방식

스프링 컨테이너는 빈 객체를 생성하기 위해 여러 가지 인스턴스화 전략을 지원합니다. 이는 다양한 시나리오와 복잡한 객체 생성 로직에 유연하게 대응하기 위함입니다.

2.1. 기본 빈 클래스 (제품 클래스 예시)

다음은 다양한 빈 인스턴스화 방식에서 사용될 일반적인 클래스입니다.

package com.example.spring;

public class Product {
    private String name;

    public Product() {
        System.out.println("Product의 기본 생성자 실행됨");
        this.name = "기본 제품";
    }

    public Product(String name) {
        System.out.println("Product의 이름 있는 생성자 실행됨");
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Product{name='" + name + "'}";
    }
}

2.2. 생성자 기반 인스턴스화 (Constructor-based Instantiation)

가장 일반적인 방식입니다. 스프링은 빈 정의에 지정된 클래스의 생성자를 호출하여 인스턴스를 생성합니다. 기본 생성자(인자가 없는)가 가장 흔하게 사용됩니다.

XML 설정 (bean-creation-methods.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 1. 기본 생성자를 통해 인스턴스화 -->
    <bean id="defaultProduct" class="com.example.spring.Product"/>

    <!-- 2. 매개변수 있는 생성자를 통해 인스턴스화 -->
    <bean id="namedProduct" class="com.example.spring.Product">
        <constructor-arg value="맞춤형 제품"/>
    </bean>

</beans>

2.3. 정적 팩토리 메서드 (Static Factory Method)

정적 팩토리 메서드를 사용하여 빈을 인스턴스화할 수 있습니다. 이는 복잡한 초기화 로직을 캡슐화하거나, 이미 존재하는 팩토리 클래스에서 객체를 가져올 때 유용합니다.

팩토리 클래스:

package com.example.spring;

public class ProductStaticFactory {
    public static Product createStandardProduct() {
        System.out.println("ProductStaticFactory의 createStandardProduct() 실행됨");
        return new Product("표준 제품");
    }
}

XML 설정 (bean-creation-methods.xml):

<!-- 3. 정적 팩토리 메서드를 통해 인스턴스화 -->
<bean id="staticFactoryProduct" class="com.example.spring.ProductStaticFactory" factory-method="createStandardProduct"/>

2.4. 인스턴스 팩토리 메서드 (Instance Factory Method)

팩토리 빈의 인스턴스 메서드를 호출하여 빈을 생성하는 방식입니다. 이 경우 팩토리 빈 자체는 스프링 컨테이너에 의해 관리되어야 합니다.

팩토리 클래스:

package com.example.spring;

public class ProductInstanceFactory {
    public Product createCustomProduct() {
        System.out.println("ProductInstanceFactory의 createCustomProduct() 실행됨");
        return new Product("주문형 제품");
    }
}

XML 설정 (bean-creation-methods.xml):

<!-- 4. 인스턴스 팩토리 메서드를 통해 인스턴스화 -->
<!-- 먼저 팩토리 빈을 정의합니다. -->
<bean id="productFactory" class="com.example.spring.ProductInstanceFactory"/>
<!-- 그리고 이 팩토리 빈의 메서드를 사용하여 제품 빈을 생성합니다. -->
<bean id="instanceFactoryProduct" factory-bean="productFactory" factory-method="createCustomProduct"/>

2.5. FactoryBean 인터페이스 구현

org.springframework.beans.factory.FactoryBean 인터페이스를 구현하는 특별한 빈입니다. 이 빈은 다른 빈을 생성하고 관리하는 '팩토리' 역할을 합니다. FactoryBean은 복잡한 객체 생성 로직을 캡슐화하거나, 특정 타입의 객체를 주입하기 위한 어댑터 역할을 할 때 유용합니다.

팩토리 빈 구현:

package com.example.spring;

import org.springframework.beans.factory.FactoryBean;

public class CustomProductFactoryBean implements FactoryBean<Product> {
    private String productType;

    // 스프링에 의해 주입될 속성
    public void setProductType(String productType) {
        this.productType = productType;
    }

    @Override
    public Product getObject() throws Exception {
        System.out.println("CustomProductFactoryBean의 getObject() 실행됨");
        // 복잡한 로직을 통해 Product 객체 생성
        if ("premium".equalsIgnoreCase(productType)) {
            return new Product("프리미엄 제품");
        } else {
            return new Product("일반 제품");
        }
    }

    @Override
    public Class<?> getObjectType() {
        return Product.class;
    }

    // 기본적으로 true (싱글턴)를 반환하며, 필요에 따라 오버라이드 가능
    @Override
    public boolean isSingleton() {
        return true;
    }
}

XML 설정 (bean-creation-methods.xml):

<!-- 5. FactoryBean 인터페이스 구현을 통한 인스턴스화 -->
<bean id="factoryBeanProduct" class="com.example.spring.CustomProductFactoryBean">
    <property name="productType" value="premium"/>
</bean>

모든 인스턴스화 방식 테스트 코드:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.junit.jupiter.api.Test;

public class BeanCreationMethodsTests {
    @Test
    void testAllBeanCreationMethods() {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-creation-methods.xml");

        // 1. 기본 생성자 방식
        Product p1 = context.getBean("defaultProduct", Product.class);
        System.out.println("Default Product: " + p1);

        // 2. 매개변수 있는 생성자 방식
        Product p2 = context.getBean("namedProduct", Product.class);
        System.out.println("Named Product: " + p2);

        // 3. 정적 팩토리 메서드 방식
        Product p3 = context.getBean("staticFactoryProduct", Product.class);
        System.out.println("Static Factory Product: " + p3);

        // 4. 인스턴스 팩토리 메서드 방식
        Product p4 = context.getBean("instanceFactoryProduct", Product.class);
        System.out.println("Instance Factory Product: " + p4);

        // 5. FactoryBean 방식
        Product p5 = context.getBean("factoryBeanProduct", Product.class);
        System.out.println("FactoryBean Product: " + p5);
    }
}

실행 결과 예시:

Product의 기본 생성자 실행됨
Product의 이름 있는 생성자 실행됨
ProductStaticFactory의 createStandardProduct() 실행됨
Product의 이름 있는 생성자 실행됨
ProductInstanceFactory의 createCustomProduct() 실행됨
Product의 이름 있는 생성자 실행됨
CustomProductFactoryBean의 getObject() 실행됨
Product의 이름 있는 생성자 실행됨
Default Product: Product{name='기본 제품'}
Named Product: Product{name='맞춤형 제품'}
Static Factory Product: Product{name='표준 제품'}
Instance Factory Product: Product: Product{name='주문형 제품'}
FactoryBean Product: Product: Product{name='프리미엄 제품'}

2.6. FactoryBean 활용 예시: 복잡한 객체 주입

FactoryBean은 특히 DateLocalDateTime과 같이 복잡한 형식을 파싱하여 객체를 생성하고 주입해야 할 때 유용합니다. 여기서는 LocalDateTime 객체를 특정 문자열 형식으로 받아 파싱하여 주입하는 예시를 살펴보겠습니다.

대상 빈 클래스 (주문 정보):

package com.example.spring;

import java.time.LocalDateTime;

public class OrderDetails {
    private String orderId;
    private LocalDateTime orderDateTime;

    public String getOrderId() { return orderId; }
    public void setOrderId(String orderId) { this.orderId = orderId; }

    public LocalDateTime getOrderDateTime() { return orderDateTime; }
    public void setOrderDateTime(LocalDateTime orderDateTime) { this.orderDateTime = orderDateTime; }

    @Override
    public String toString() {
        return "OrderDetails{" +
               "orderId='" + orderId + '\'' +
               ", orderDateTime=" + orderDateTime +
               '}';
    }
}

LocalDateTime을 생성하는 FactoryBean:

package com.example.spring;

import org.springframework.beans.factory.FactoryBean;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class LocalDateTimeParserFactoryBean implements FactoryBean<LocalDateTime> {
    private String dateTimeString; // XML에서 설정될 날짜/시간 문자열
    private String formatPattern = "yyyy-MM-dd HH:mm:ss"; // 기본 형식

    public void setDateTimeString(String dateTimeString) {
        this.dateTimeString = dateTimeString;
    }

    public void setFormatPattern(String formatPattern) {
        this.formatPattern = formatPattern;
    }

    @Override
    public LocalDateTime getObject() throws Exception {
        System.out.println("LocalDateTimeParserFactoryBean의 getObject() 실행됨");
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern);
        return LocalDateTime.parse(dateTimeString, formatter);
    }

    @Override
    public Class<?> getObjectType() {
        return LocalDateTime.class;
    }

    @Override
    public boolean isSingleton() {
        return true; // 이 FactoryBean은 싱글턴으로 LocalDateTime 인스턴스를 생성
    }
}

XML 설정 (bean-creation-methods.xml에 추가):

<!-- LocalDateTime을 파싱하여 생성하는 FactoryBean -->
<bean id="orderDateTimeFactoryBean" class="com.example.spring.LocalDateTimeParserFactoryBean">
    <property name="dateTimeString" value="2023-11-01 10:00:00"/>
    <!-- <property name="formatPattern" value="yyyy-MM-dd HH:mm:ss"/> -->
</bean>

<!-- OrderDetails 빈에 LocalDateTime 주입 -->
<bean id="customerOrder" class="com.example.spring.OrderDetails">
    <property name="orderId" value="ORD-2023-007"/>
    <property name="orderDateTime" ref="orderDateTimeFactoryBean"/>
</bean>

테스트 코드 (testFactoryBeanLocalDateTime 메서드 추가):

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.junit.jupiter.api.Test;

public class BeanCreationMethodsTests {
    // ... (testAllBeanCreationMethods 메서드 생략)

    @Test
    void testFactoryBeanLocalDateTime() {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-creation-methods.xml");
        OrderDetails myOrder = context.getBean("customerOrder", OrderDetails.class);
        System.out.println("Customer Order Details: " + myOrder);
    }
}

실행 결과 예시:

LocalDateTimeParserFactoryBean의 getObject() 실행됨
Customer Order Details: OrderDetails{orderId='ORD-2023-007', orderDateTime=2023-11-01T10:00}

FactoryBean을 통해 문자열을 LocalDateTime 객체로 변환하여 OrderDetails 빈에 성공적으로 주입된 것을 확인할 수 있습니다.

3. BeanFactoryFactoryBean의 차이점

스프링 컨테이너를 학습할 때 자주 혼동될 수 있는 두 가지 개념이 바로 BeanFactoryFactoryBean입니다. 이름은 유사하지만 그 역할은 명확히 다릅니다.

3.1. BeanFactory

BeanFactory는 스프링 IoC 컨테이너의 핵심 인터페이스이자 가장 기본적인 컨테이너입니다. '빈 팩토리'로 번역되며, 이름 그대로 빈 객체를 생성하고 관리하는 '공장' 역할을 수행합니다. ApplicationContextBeanFactory를 확장한 것으로, 더 많은 엔터프라이즈급 기능을 제공합니다.

  • 역할: 빈의 정의를 읽고, 빈 인스턴스를 생성하며, 빈 간의 의존성을 주입하고, 빈의 생명주기를 관리하는 스프링 IoC 컨테이너의 최상위 인터페이스입니다.
  • 관계: 스프링 컨테이너 자체를 의미하며, 모든 빈을 관리하는 주체입니다.

3.2. FactoryBean

FactoryBean은 스프링 컨테이너 내에서 특별한 종류의 빈입니다. 일반적인 빈과는 다르게, FactoryBean 자신은 또 다른 객체를 생성하는 '팩토리' 역할을 하는 빈입니다. 즉, FactoryBean은 스프링 컨테이너가 관리하는 "다른 빈을 만들어주는 빈"입니다.

  • 역할: 복잡한 초기화 로직이 필요하거나, 특정 인터페이스를 구현하는 객체를 동적으로 생성해야 할 때 사용됩니다. FactoryBeangetObject() 메서드가 실제 반환될 빈 객체를 생성합니다.
  • 관계: BeanFactory(컨테이너)에 의해 관리되는 하나의 빈이며, 이 FactoryBean은 다시 다른 특정 빈을 생성합니다. 컨테이너가 FactoryBean을 통해 얻는 객체는 FactoryBean 자신이 아니라 FactoryBeangetObject() 메서드가 반환하는 객체입니다.

요약하자면, BeanFactory는 모든 빈을 관리하는 스프링의 컨테이너이며, FactoryBean은 그 BeanFactory가 관리하는 특별한 빈 중 하나로, 자체적으로 다른 빈을 생성하는 로직을 가지고 있습니다.

태그: Spring Framework Spring Bean Scope Bean Instantiation FactoryBean BeanFactory

6월 22일 03:31에 게시됨