Spring의 Method Injection

2025. 6. 20. 15:03Spring Framework/Spring IoC

일반적인 어플리케이션 시나리오에서, Spring 컨테이너에 등록된 대부분의 Bean은 싱글톤이다. 싱글톤 Bean이 다른 싱글톤 Bean과 협업하거나, 프로토타입(Non-singleton) Bean이 또 다른 프로토타입 Bean과 협업하는 경우, 보통은 한 Bean을 다른 Bean의 프로퍼티로 정의하여 의존성을 처리한다.

 

🔨 Method Injection란?

Method Injection은 싱글톤 빈이 프로토타입 빈을 참조할때, 새로운 인스턴스를 주입받아야 하는 경우 발생하는 문제를 해결하는 주입방법이다.

 

왜 필요할까

Spring의 대부분의 빈은 디폴트로 싱글톤으로 관리된다. 하지만 때때로 싱글톤 빈이 프로토타입 빈을 사용해야 하는 상황이 발생할 수 있다.

상황 문제
매번 새로운 인스턴스가 필요할 때 생성자/필드 주입은 항상 같은 객체 주입됨
선택적 의존성 필요할 때 생성자에 넣으면 너무 복잡해짐
런타임에 객체를 바꿔야 할 때 생성자 주입은 고정적

 

 

💥 문제 상황 발생 💥

싱글톤 빈이 프로토타입 빈을 사용해야 한다고 가정

싱글톤 빈은 한 번만 생성되므로, 프로토타입 빈도 한번만 주임됨

 

config 패키지

@Configuration
@ComponentScan(basePackages = "dependency.methodinjection.lookupexample.service")
public class AppConfig {}

 

service 패키지 

@Component
@Scope("prototype")
public class Task {
	
    public void execute() {
        System.out.println("Executing task");
    }
}
@Component
public class TaskService {

    private final Task task;

    public TaskService(Task task) {
        this.task = task;
    }

    public void run() {
        task.execute();
    }
}

 

Main 클래스

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = 
                new AnnotationConfigApplicationContext(AppConfig.class);

        TaskService taskService = context.getBean(TaskService.class);
        // 싱글톤 빈은 한번만 생성되므로 PrototypeBean도 한 번만 주입됨
        // → 새로운 인스턴스를 받을 수 없음
     
                           // @Lookup 안쓸시
        taskService.run(); // Task@1e1a0406
                           // TaskService@3cebbb30
        taskService.run(); // Task@1e1a0406
                           // TaskService@3cebbb30
        taskService.run(); // Task@1e1a0406
                           // TaskService@3cebbb30
    }
}

 

실행시

Executing task // Task@1e1a0406
               // TaskService@3cebbb30
Executing task // Task@1e1a0406
               // TaskService@3cebbb30
Executing task // Task@1e1a0406
               // TaskService@3cebbb30

→ run()을 여러 번 호출해도 항상 같은 객체 사용됨

→ 매번 새로운 객체 사용과 불일치(문제 발생!!)

 

🛠️ 해결 방법 1 : ApplicationContext(컨테이너)를 활용한 Bean 조회(❌ 비추천!! ❌)

Spring 컨테이너를 직접 사용하여 필요할 때마다 새로운 빈을 가져오는 방식

@Component
public class TaskService {

    private ApplicationContext context;

    @Autowired
    public TaskService(ApplicationContext context) {
        this.context = context;
    }

    public void run() {
        Task task = context.getBean(Task.class); // 매번 새 Bean 요청
        task.execute();
    }
}

 

실행시

Executing task // Task@36916eb0
               // TaskService@7bab3f1a
Executing task // Task@437da279
               // TaskService@7bab3f1a
Executing task // Task@23c30a20
               // TaskService@7bab3f1a

 

  • ⚠️ DI 원칙 위배 (IOC 컨테이너를 직접 참조)
  • ❌ Spring 프레임워크에 종속적(비즈니스 로직이 Spring API에 의존하게 됨)
  • ❌ 테스트 및 유지보수 어려움
  • ✔️ 구현은 간단해서 빠른 해결이 필요할 땐 임시로 사용 가능

 

🛠️ 해결 방법 2 : Lookup Method Injection (@Lookup 사용)

Spring 컨테이너가 특정 메서드를 오버라이드하여, 새로운 프로토타입 빈을 반환하도록 한다.

@Component
public class TaskService {

    public void run() {
        Task task = new Task(); // 매번 새로운 Task 인스턴스 생성
        task.execute();
    }

    @Lookup
    protected Task getTask() {
        // Spring이 런타임에 이 메서드를 오버라이드하여 새로운 Task 인스턴스를 반환함
        return null;		
    }
}

 

실행시

Executing task // Task@36916eb0  TaskService$$SpringCGLIB$$0@21282ed8 프록시 클래스
Executing task // Task@7bab3f1a
Executing task // Task@437da279

 

💡 @Lookup을 쓰는 이유

프로토타입 빈의 의미를 살리기 위해 @Autowired로는 싱글톤처럼 동작해버림
매번 새로운 인스턴스를 얻기 위해 @Lookup 메서드를 호출할 때마다 새로운 빈 반환
런타임 메서드 오버라이딩 제공 Spring이 프록시를 만들어 해당 메서드를 오버라이딩함
코드 의존성 최소화 ApplicationContext나 ObjectProvider 없이 깔끔한 방식