본문 바로가기
Spring/Spring Story

[Spring] 생성자 주입 vs 필드 주입 (@Autowired)

by 잭피 2020. 9. 23.

 

안녕하세요~ 잭코딩입니다!

 

 

이번에는 스프링 프레임워크에서 의존성을 주입하는 방법을 살펴보고

어떤 방식으로 주입하는 게 좋은지 살펴볼까요?

우선 결론부터 말하자면 생성자 주입 (Constructor Injection) 방식을 권장합니다

그 이유는 아래에서 살펴볼게요!


스프링 프레임워크에서 의존성을 주입하는 방법은 3가지가 있습니다

1. 생성자 주입 (Constructor Injection)

2. 필드 주입 (Field Injection)

3. 수정자 주입 (Setter Injection)

 

예제를 통해 하나씩 살펴보겠습니다

JackCoding 빈에 Jack 빈을 주입하는 예제입니다

1. 생성자 주입 (Constructor Injection)

@Component
public class JackCoding {
    private final Jack jack;

    public JackCoding(Jack jack) {
        this.jack = jack;
    }
}

단일 생성자의 경우 @Autowired 어노테이션을 붙이지 않아도 되지만,

생성자가 2개 이상인 경우에는 생성자에 @Autowired 어노테이션을 붙여줘야 합니다

Lombok의 도움을 받아서 위의 코드를 조금 더 간단하게 사용할 수 있습니다

@Component
@RequiredArgsConstructor
public class JackCoding {
    private final Jack jack;
}

@RequiredArgsConstructor 어노테이션은 final이나 @NotNull을 사용한 필드에 대한 생성자를 자동으로 생성해줍니다

 

만약 같은 타입의 빈이 있을 경우는 어떻게 해야 할까요?

@Component
@RequiredArgsConstructor
public class JackCoding {

    @Qualifier("jack")
    private final Jack jack;
    
}

@Qualifier을 통해 구현체의 클래스명 or 지정한 빈의 이름으로 가져올 수 있습니다

또한, 생성자 주입은 다른 주입 방식과 달리 필드를 final 선언할 수 있습니다

 

2. 필드 주입 (Field Injection)

사실 이 방식이 가장 편리한 방법이고, 실무에서도 자주 보이는 주입 방법입니다

필드에 @Autowired 어노테이션을 붙여주면 의존성이 주입됩니다

@Component
public class JackCoding {

    @Autowired
    private Jack jack;
    
}

같은 타입의 빈이 있을 경우 @Qualifier나 @Resource를 통해 빈의 이름으로 가져와서 주입할 수 있습니다

생성자 주입과 달리 필드를 final로 정의할 수 없습니다

 

3. 수정자 주입 (Setter Injection)

@Component
public class JackCoding {
 
    private Jack jack;

    @Autowired
    public void setJack(Jack jack) {
        this.jack = jack;
    }
}

 이 방식도 필드를 final로 선언할 수 없습니다

 

 

생성자 주입이 아닌 다른 방식으로 주입하면 인텔리제이는 아래와 같은 경고를 주고 있습니다

Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.

스프링 팀에서는 생성자 주입 (Constructor Injection) 방식을 추천하고 있습니다

 

 

 

 

생성자 주입(Constructor Injection) 방식의 장점

왜 생성자 주입을 사용을 권할까요?

그 이유는 아래와 같습니다

 

1. 순환 참조 방지

2. final 선언이 가능

3. 테스트 코드 작성 용이

 

하나씩 살펴보겠습니다

1. 순환 참조 방지

객체의 의존성을 추가하다 보면 순환 참조 문제가 발생할 수 있습니다

순환 참조는 A -> B를 참조하면서, B -> A를 참조하는 경우 발생하는 문제입니다

예를 통해 보겠습니다

먼저 필드 주입을 통해 순환 참조를 해보겠습니다

Jack과 Coding의 빈을 하나씩 생성하고, 서로 필드 주입 (Field Injection)을 해줍니다

@Component
public class Jack {

    @Autowired
    private Coding coding;

    public void jack() {
        coding.coding();
    }
}
@Component
public class Coding {
    @Autowired
    private Jack jack;

    public void coding() {
        jack.jack();
    }
}

그리고 이 두 개를 빈을 JackCoding에 주입합니다

@Component
public class JackCoding {

    @Autowired
    private Jack jack;

    @Autowired
    private Coding coding;

    public void jackCoidng() {
        jack.jack();
        coding.coding();
    }
}

그리고 간단히 서버를 구동시키면, 잘 동작합니다

그러다 운영 중에 jackCoding() 메서드가 실행되면 순환참조로 인해 서버가 죽습니다

즉, 메소드 실행 시점 전까지 순환 참조가 있었더라도 알 수가 없습니다

 

이 순환 참조를 방지하기 위해서는 생성자 주입을 해야 합니다

위 예제를 생성자 주입으로 변경해보면 아래와 같습니다

@Component
@RequiredArgsConstructor
public class Jack {

    private final Coding coding;

    public void jack() {
        coding.coding();
    }
}

@Component
@RequiredArgsConstructor
public class Coding {

    private final Jack jack;

    public void coding() {
        jack.jack();
    }
}

@Component
@RequiredArgsConstructor
public class JackCoding {

    private final Jack jack;
    private final Coding coding;

    public void jackCoidng() {
        jack.jack();
        coding.coding();
    }
}

이제 서버를 구동시켜볼까요?

바로 에러가 발생합니다

즉, 서버 자체가 구동되지 않으므로 바로 순환 참조를 알 수 있고 방지할 수 있습니다

 

그러면 왜 이런 차이가 발생할까요?

바로 생성자 주입 방식은 필드 주입이나 수정자 주입과는 빈을 주입하는 순서가 다릅니다

 

필드 주입과 수정자 주입은 먼저 빈을 생성한 후, 주입하려는 빈을 찾아 주입한다

 

하지만, 생성자 주입은 먼저 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 만듭니다

그 후에 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출합니다

즉, 먼저 빈을 생성하지 않고 주입하려는 빈을 먼저 찾습니다

 

따라서 생성자 주입 방식을 사용하면 순환 참조가 실행하면서 문제가 됩니다 

객체 생성 시점에 빈을 주입하기 때문에

서로 참조하는 객체가 생성되지 않은 상태에서 그 빈을 참조하기 때문에 오류가 발생합니다

 

사전에 순환 참조를 방지하기 위해서는 생성자 주입을 사용하는 것이 좋습니다

 

2. final 선언이 가능

필드 주입과 수정자 주입은 필드를 final로 선언할 수 없습니다 

그 말은 나중에 변경될 수 있다는 뜻입니다

하지만, 생성자 주입의 경우는 필드를 final로 선언할 수 있습니다

즉, 런타임에 객체 불변성을 보장합니다

 

3. 테스트 코드 작성 용이

스프링 컨테이너 도움 없이 테스트 코드를 더 편리하게 작성할 수 있습니다

테스트하고자 하는 클래스에 필드 주입이나 수정자 주입으로 빈이 주입되어 있으면,

Mockito를 이용해 목킹한 후 테스트를 진행하여야 합니다

하지만, 생성자 주입의 경우는 단순히 원하는 객체를 생성한 후, 생성자에 넣어주면 됩니다

public class JackTest {
    @Test
    public void test() {
        Jack jack = new Jack();
        JackCoding jackCoding = new JackCoding(jack);
        jack.jack();
    }
}

 

이렇게 생성자 주입 장점 3가지를 정리해보았습니다

 

댓글