Spring

Spring "Field Injection"? or "Constructor Injection"?

RyanGomdoriPooh 2020. 6. 16. 22:45

이야기에 앞서서...


Spring Framework에서 제공하는 @Autowired나 @Qualifier를 이용해서 객체를 찾아서 DI(Dependency Injection)해왔습니다.

 

물론 Java에서도 @Inject, @Resource를 제공해주어서 DI(Dependency Injection)를 할 수 있었습니다.

 

우선, DI는 IoC(Inversion of Control)의 핵심 원리를 구현하는 개념이라고 할 수 있습니다.

 

간단하게 IoC(Inversion of Control)란?

: IoC의 핵심은 기존의 Programing code 안에 들어가 있던 객체의 생성/관리를 Spring Container에게 위임하여 객체의 생명주기를 관리하게 하는 것입니다. 관리하는 객체의 단위를 Bean이라고 명명합니다.

 

IoC를 구현하기 위해서는 DI를 통해서 Bean을 생성하는 시점에 필요한 객체를 주입해주어야 합니다.

 

결국, 우리가 이야기하는 핵심은 "언제 어떻게 Bean을 생성하는 시점에 필요한 객체를 주입해야 하는가?"가 됩니다.

 

Field Injection인 @Autowired의 이야기부터 시작하겠습니다.

 

IDE에서 보여주는 경고

최근에 리팩터링을 진행하면서, 코드에서 발견한 부분이 있었습니다. @Autowired에 대한 노란색 밑줄 경고죠.

 

해당하는 경고 문구는 다음과 같습니다.

Injection 경고 문구

경고 문구는 간략하게 다음과 같습니다.

  • Field injection을 추천하지 않는다.
  • Spring Team은 너의 Bean들에게 Constructor 기반의 DI를 항상 사용해라.
  • 필수적인 의존성들을 위한 assertions를 사용해라.

위 문구는 한 문장으로 "Constructor based injection을 써라. 안 쓰면 DI를 보장 못할 수 있어."라고 표현하면 맞겠네요.

 

뭔가 문제가 있다고 생각하니 경고를 보내고 있다고만 일단 생각하면 됩니다.

 

그러면 이제 "어떤 문제가 있는 것인가?"를 찾아보면 되겠네요.

 

 

Field Injection은 어떤 문제를 가지고 있는가?


우리는 Java를 사용하면서, 가장 많이 들었던 말이 있습니다. OOP(Object Oriented Programming)입니다.

 

OOP는 낮은 "결합성", 높은 "응집성" 구현하기 위한 개념이죠.

 

결국, "Inheritance", "Polymolphism", "Encapsulation", "Abstraction"는 낮은 "결합성", 높은 "응집성" 구현하기 위한 개념입니다.

 

IoC의 DI를 통해서 우리는 낮은 "결합성"을 구현할 수 있습니다.

 

여기까지는 "Setter Injection", "Constructor Injection", "Field Injection" 모두 아무런 문제를 발견할 수 없었습니다.

  • Code를 통한 Injection : "Setter Injection", "Constructor Injection"
  • Annotation을 통한 Injection : "Field Injection"

이제 문제를 찾아보겠습니다.

 

다음은 "Setter Injection"에 대한 코드입니다. (코드를 간단하게 표현하기 위해서 DTO는 사용하지 않겠습니다.)

@Service
public class UserService {
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUser(long userId) {
        return userRepository.findById(userId);
    }
}
@Controller
public class UserController {
	
    @GetMapping("/users/{id}")
    public User getUserInfo(@PathVariable long id) {
    	UserService userService = new UserService();
        userService.setUserRepository(new UserRepository());
        
        return userService.getUser(id);
    }
}

여기까지는 정상적으로 new UserRepository()를 setter에 전달해주었기 때문에 문제가 없습니다.

 

그런데 항상 우리 개발자는 실수를 할 수도 있죠. 바로 다음과 같은 코드입니다.

@Controller
public class UserController {
	
    @GetMapping("/users/{id}")
    public User getUserInfo(@PathVariable long id) {
    	UserService userService = new UserService();
        // userService.setUserRepository(new UserRepository());
        
        return userService.getUser(id);
    }
}

new UserRepository()를 setter로 전달해주는 것을 까먹은 것입니다.

 

문제는 CompileTime에 이 실수를 잡아주지 않습니다.

 

UserService는 생성이 되고, 결국 RunTime에 userService.getUser(id);가 호출되는 시점에 NullPointException()이 발생하게 됩니다.

 

Field Injection도 Setter Injection과 마찬가지로 Spring Container가 주입해야할 new UserRepository()를 주입하지 않아도  UserService가 생성되는 문제가 있습니다.

결국 Setter Injection처럼 userService.getUser(id);가 호출되는 시점에 NullPointException()이 발생하게 됩니다.

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;//<- null

    public User getUser(long userId) {
        return userRepository.findById(userId);
    }
}
@Controller
public class UserController {
	
    @GetMapping("/users/{id}")
    public User getUserInfo(@PathVariable long id) {
    	UserService userService = new UserService();
        
        return userService.getUser(id);
    }
}

차이점은 주입을 setter로 프로그래머가 직접 하는가, 아니면 @Autowired로 Spring Container가 해주는가입니다.

 

여기까지 Setter Injection / Field Injection의 단점에 대해서 알게 되었습니다.

  • CompileTime에 문제가 발견되지 않고, RunTime에 Method를 호출하게 되면, NPE가 발생하는 것

 

Constructor Injection은 어떤 장점이 있을까?


Service Layer의 Business Logic이 복잡해지면 복잡해질수록, 우리는 주입된 Bean들이 엄청 늘어나는 것을 볼 수 있습니다.

 

Bean들이 늘어나면서, 서로 다른 Service에서 거미줄처럼 주입되어있게 되는데,

 

Annotation "@Autowired"을 통한 Field Injection은 CompileTime에 순환 참조를 확인할 수 없습니다.

 

이유는 다음과 같습니다.

  1. Setter Injection / Field Injection은 일단 모든 Bean을 생성하여 Bean Factory에 등록합니다.
  2. Bean이 모두 생성되어 Bean Factory에 등록되고나면, 주입할 Bean들을 가져와서 주입합니다.
  3. 결국 순환 참조를 알아챌 타이밍을 이 시점에는 놓쳐버리게 되는 겁니다.
  4. 이제 순환 참조가 만들 지옥은 Runtime에 Business Logic에서 발생하게 되겠습니다.

결국에 나도 모르는 사이에 Runtime 순환 참조가 발생할 수 있는 가능성이 생겨버릴 수 있는 것입니다.

 

Field Injection을 통한 순환 참조 발생 문제 발생을 설명하겠습니다.

  1. 순환 참조된 상태의 Bean이 2개가 생성된다.
  2. 순환 참조된 상태의 Bean A와 Bean B가 Business Logic에 서로 Method를 호출하는 Method A를 만든다.
  3. Method A가 Runtime에 호출이 된다.
  4. 서로 호출되면서 Recusive 방식으로 Stack에 호출된 Method가 쌓인다.
  5. 결국 Stack의 Memory Size가 넘어가면서, java.lang.StackOverflowError가 발생합니다.

코드는 다음과 같습니다.

@Service
public class AService {

    @Autowired
    private BService bService;

    public void aMethod() {
        bService.bMethod();
    }
}

 

@Service
public class BService {

    @Autowired
    private AService aService;

    public void bMethod() {
        aService.aMethod();
    }
}
aService.aMethod(); //가 호출되는 순간 StackOverflowError가 발생됩니다.

 

그래서 우리는 애초에 Bean이 주입되는 시점에 순환 참조의 가능성을 막아야 합니다.

 

이 시점에 IDE에서 Constructor Injection을 추천하는 이유가 나옵니다.

 

결론부터 이야기를 하면,

 

Spring Container가 Constructor Injection을 사용하면 Application 실행 시점에 Bean이 순환 참조되었다는 것을 다음처럼 BeanCreationException으로 알려줍니다.

 

알려줄 수 있는 이유는 다음과 같습니다.

  1. Setter Injection / Field Injection과 다르게 Constructor Injection은 생성자로 빈을 주입하게 됩니다.
  2. 생성자로 빈을 주입한다는 이야기는 자바에서 객체가 생성이 되려면, 주입될 Bean의 소재지를 찾아서 넣어줘야한다는 것입니다.
  3. 결국 생성 시점에 모든 Bean이 정상적인 상태가 아니면 에러를 발생시킬 수 있다는 것입니다.
  4. 자바의 객체 생성 원리가 Bean의 순환 참조 가능성을 제거해줄 수 있습니다.

Constructor Injection을 통한 순환 참조 예시입니다.

@Service
public class AService {

    private final BService bService;
    
    public AService(BService bService) {
        this.bService = bService;
    }

    public void aMethod() {
        bService.bMethod();
    }
}
@Service
public class BService {

    private final AService aService;
    
    public BService(AService aService) {
        this.aService = aService;
    }

    public void bMethod() {
        aService.aMethod();
    }
}
***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  AService defined in file [AService.class]
↑     ↓
|  BService defined in file [BService.class]
└─────┘


2020-06-16 22:12:55.080  WARN 13272 --- [           main] o.s.boot.SpringApplication               : Unable to close ApplicationContext

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'applicationAvailability' defined in class path resource [org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.class]: BeanPostProcessor before instantiation of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'metaDataSourceAdvisor': Cannot resolve reference to bean 'methodSecurityMetadataSource' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration': Initialization of bean failed; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry' available
...

 

그리고 한 가지 더 Constructor Injection을 이용하면, 위 코드에서 보셨겠지만 final 키워드를 사용할 수 있습니다.

 

final은 상수를 만드는 키워드 입니다. 한 번 주입된 객체를 다른 객체로 변경을 불가능합니다.

 

흔히 "Immutable Object"라 이야기 많이 합니다. Spring Container가 아니면 Tread Safe 이야기도 나올 수 있는 부분이죠.

 

물론 primitive가 아니기 때문에 객체 내용물의 상태는 변화할 수 있습니다. 하지만 객체 자체를 수정하는 것은 불가능합니다.

 

그래서 Bean 주입을 위해서 쓰이기에는 최적입니다.

 

 

결론


Field Injection은 다음과 같은 문제점이 있습니다.

  • 주입되지 않은 Bean으로 인한 NullPointException 발생 가능성
  • Runtime에 순환 참조로 인한 StackOverflowError 발생 가능성

그래서 사전에 위 문제점을 해결이 가능한 Construction Injection을 사용하는 것입니다.

 

장점은 다음과 같습니다.

  • Bean이 주입되지 않는 경우, Compile Time에 알려주어서 NullPointException을 예방할 수 있습니다.
  • Bean이 순환 참조될 경우를 Spring Container가 미리 알려주기 때문에 순환참조 가능성을 없앨 수 있습니다.
  • final로 "Immutable Object"를 만들어버리기 때문에 Application이 재가동이 되기 전까지는 불변하는 Bean을 사용할 수 있습니다.

최근에는 Lombok의 @RequiredArgsConstructor를 이용해서 생성자 주입을 하기도 합니다만,

DDD를 공부하는 저는 개인적으로 Lombok을 좋아하지 않아서 사용하진 않았습니다.