본문 바로가기
Develop/Spring+JPA

[Spring] 생성자 주입 vs 필드 주입 vs 수정자 주입

by 연로그 2022. 4. 28.
반응형

의존성 주입; Dependency Injection

 

Spring에서 의존성을 주입하는 방법은 3가지가 있다.

  1. 생성자 주입; Constructor Injection
  2. 필드 주입; Field Injection
  3. 수정자 주입; Setter Injection

 

결론부터 말하자면 '생성자 주입'이 가장 좋다.

각 주입이 어느 차이점이 있는지 살펴보자.

 

 


생성자 주입; Constructor Injection

 

  • final 키워드 선언 가능
  • 테스트 코드 작성 용이
@Controller
public class HomeController {

    private final GameService gameService;

    // Spring 4.3 이전 버전이라면 @Autowired 필요
    public HomeController(GameService gameService) {
        this.gameService = gameService;
    }
}

 

🔻 테스트 코드 작성이 용이하다?

더보기

HomeController에 대한 테스트 코드를 작성해야 한다고 하자.

예제 코드같은 테스트용 클래스를 생성해서 주입시킬 수 있게 된다.

@Test
void controller_test() {
    HomeController homeController = new HomeController(new TestService());
	// ...
}

class TestService extends GameService{
    public TestService() {
        super();
    }
    
    // ...
}

 

 


필드 주입; Field Injection

 

  • final 키워드 선언 불가능
  • 코드 양이 줄어듦
  • 의존 관계 파악하기 힘듦
@Controller
public class HomeController {

    @Autowired
    private GameService gameService;
}

 

🔻 필드 주입도 순환 참조가 방지된다?

더보기

다른 블로그 글들을 보면 생성자 주입은 순환 참조 방지가 되어서 좋다, 필드 주입은 순환 참조 방지가 안 된다!는 식으로 설명이 많이 되어있다.

이 부분은 Spring Boot 2.6에서 패치되었다.

 

예를 들어 AClass는 BClass를 주입 받고, BClass는 AClass를 주입 받는다고 가정하자.

@Component
public class AClass {

    @Autowired
    private BClass bClass;
}
@Component
public class BClass {

    @Autowired
    private AClass aClass;
}

 

예전에는 위의 경우에 AClass 또는 BClass의 메서드를 호출하는 순간 순환 참조가 일어나며 서버가 죽었다고 한다.

SpringBoot 2.6 릴리즈 노트에 의하면 이에 대한 패치가 완료되었다고 한다.

Circular References Prohibited by Default

Circular references between beans are now prohibited by default. If your application fails to start due to a BeanCurrentlyInCreationException you are strongly encouraged to update your configuration to break the dependency cycle. If you are unable to do so, circular references can be allowed again by setting spring.main.allow-circular-references to true, or using the new setter methods on SpringApplication and SpringApplicationBuilder This will restore 2.5’s behaviour and automatically attempt to break the dependency cycle.

 

만약 서버를 구동시키려고 하면 아래와 같은 화면이 뜨며 서버 구동에 실패한다.

application을 실행해보니 cycle이 발생했다는 에러가 뜬다.

 

 


수정자 주입; Setter Injection

 

  • final 키워드 선언 불가능
  • setter 메서드에 @Autowired를 붙임
@Controller
public class HomeController {

    private GameService gameService;

    @Autowired
    public setService(GameService gameService) {
        this.gameService = gameService;
    }
}

 

 


❓ 의존성 주입 방법을 동시에 여러개 사용한다면?

 

만약 필드 주입과 동시에 setter 주입도 가능하게 한다면 어떻게 될까?

주입이야 당연히 되겠지만 Spring이 어느 것을 이용해서 주입시킬지 궁금해졌다.

(물론 이것은 좋지 않은 코드다. 불필요한 로직인데다 가독성도 떨어뜨린다.)

 

결론부터 말하자면 "생성자 -> 필드 -> 메서드" 순으로 호출된다.

 

✨브리✨가 찾아주신 StackOverflow 글을 요약해보겠다.

 

  1. 생성자를 통해 생성
  2. @Autowired 어노테이션이 달린 필드 주입
  3. @Autowired 어노테이션이 달린 메서드 호출

 

1의 경우는 객체에서 어떤 동작을 실행하기 위해 일단 객체를 생성해야 하니 당연한 일이다.

2, 3의 순서는 AutowiredAnnotationBeanPostProcessor의 소스 코드에서 확인할 수 있다.

 

// AutowiredAnnotationBeanPostProcessor 클래스
private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
    // ...
    ReflectionUtils.doWithLocalFields(targetClass, field -> {
        // ...
    });

    ReflectionUtils.doWithLocalMethods(targetClass, method -> {
        // ...
    });
    // ...
}

 

소스 코드 상으로 확인은 했으나 직접 sysout을 찍어가며 동작을 확인해보았다

 

🔻 생성자 주입 + 필드 주입 + 세터 주입

더보기
@Component
public class A {

    @Autowired
    private B b;

    public A() {
        System.out.println("empty constructor >>> " + this.b);
    }

    @Autowired
    public A(B b) {
        System.out.println("contructor >>> " + this.b);
        this.b = b;
        System.out.println("contructor >>> " + this.b);
    }

    @Autowired
    public void setB(B b) {
        System.out.println("setter >>> " + this.b);
        this.b = b;
        System.out.println("setter >>> " + this.b);
    }

    public void print() {
        b.print();
    }
}
생성자 주입 이용

 

🔻 필드 주입 + 세터 주입

더보기
@Component
public class A {

    @Autowired
    private B b;

    public A() {
        System.out.println("empty constructor >>> " + this.b);
    }

    @Autowired
    public void setB(B b) {
        System.out.println("setter >>> " + this.b);
        this.b = b;
        System.out.println("setter >>> " + this.b);
    }

    public void print() {
        b.print();
    }
}
필드 주입 이용

 

🔻 세터 주입

더보기
@Component
public class A {

    private B b;

    public A() {
        System.out.println("empty constructor >>> " + this.b);
    }

    @Autowired
    public void setB(B b) {
        System.out.println("setter >>> " + this.b);
        this.b = b;
        System.out.println("setter >>> " + this.b);
    }

    public void print() {
        b.print();
    }
}
세터 주입 이용

 

 


❓ 왜 생성자 주입이 가장 좋을까?

 

1. NullPointerException 방지

A와 B라는 클래스가 아래와 같이 존재한다고 가정하자.

@Component
public class A {

    @Autowired
    private B b;

    public void print() {
        b.print();
    }
}
@Component
public class B {
    public void print() {
        System.out.println("Hello World");
    }
}

 

new를 통해 A 클래스에 대한 객체를 만들고 메서드를 실행시켰다.

언뜻 보기에는 잘 돌아갈 것 같은데 NullPointerException이 발생한다.

@SpringBootTest
public class MyTest {

    @Test
    void 테스트() {
        A a = new A();
        a.print();
    }
}

호출 결과

 

위 코드에서는 A의 생성자를 호출할 때 Spring에서 관리하지 않는 객체 a를 생성했다.

Spring은 a 객체에 대해 알지 못하므로 B를 주입해야하는 것도 모른다. (null인 상태로 유지)

 

null인 객체 b의 메서드를 호출하려고 하니까 당연히 NullPointerException이 발생하게 된다.

생성자 주입을 사용했다면 생성과 동시에 B를 주입시켜주니 위 에러가 발생하지 않았을 것이다.

 

2. 객체의 불변성

생성자 주입의 경우에는 객체를 할당하려면 생성자밖에 방법이 없다.

하지만 setter의 경우에는 객체 생성 후에도 언제든지 의존성 주입이 가능하다.

의존 관계의 변경이 필요한 상황이 온다면 setter가 필요할 수도 있겠지만 변경이 필요없도록 설계하는 것이 좋다.


참고

반응형