Spring - Field vs Constructor vs Setter Injection 그리고 순환참조(Circular Reference)

2019. 8. 17. 18:47Web/Spring

 

오늘 포스팅할 내용은 필드,생성자,세터 의존주입에 대한 내용이다. 우리가 보통 생각하는 의존주입은 무엇인가? 혹은 우리가 평소에 사용하는 의존주입의 방식은 무엇인가? 한번 생각해보고 각각에 대한 내용을 다루어보자.

 

<Field Injection>

 

1
2
3
4
5
6
7
8
9
10
11
@Component
public class ABean {
    
    @Autowired
    private BBean b;
    
    public void bMethod() {
        b.print();
    }
    
}
cs

 

보통 위와 같이 필드에 의존주입할 빈을 선언하고 @Autowired를 붙여 빈 주입을 한다.

 

<Constructor Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ABean {
    
    private BBean b;
    
    public ABean(BBean b) {
        this.b=b;
    }
    
    public void bMethod() {
        b.print();
    }
    
}
cs

 

생성자를 위한 빈 주입은 위와 같이 생성자의 매개변수로 의존 주입할 빈을 매개변수로 넣어준다. 스프링 4.3 버전 이후로는 생성자 의존주입에 @Autowired를 넣을 필요는 없다.

 

<Setter Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class ABean {
    
    private BBean b;
    
    @Autowired
    public void setB(BBean b) {
        this.b = b;
    }
 
    public void bMethod() {
        b.print();
    }
    
}
cs

 

세터를 이용한 빈주입이다. 의존 주입할 빈 객체에 대한 Setter를 만들어주고 @Autowired를 붙여준다.

 

지금까지 3개의 의존주입 방법을 다루어보았다. 아직은 똑같은 결과물을 내는 다른 방법이라는 것만 느껴진다. 그렇다면 특정 상황을 연출해보자. 바로 Circular Reference(순환참조)인 경우이다. 3가지 의존주입 방법을 모두 활용하여 순환참조 상황을 재연해보자.

 

순환참조

-A빈이 있고 B빈이 있는데, 각각 서로가 서로를 참조하고 있는 상황에서 발생한다. 이러한 상황에서 A빈이 메모리에 올라가기 전에 B빈이 A빈을 의존주입하는 상황이나 혹은 그 반대의 경우 문제가 발생한다.

 

<Field Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Component
public class ABean {
    
    @Autowired
    private BBean b;
 
    public void bMethod() {
        b.print();
    }
    
    public void print() {
        System.out.println("ABean !");
    }
    
}
 
@Component
public class BBean {
    
    @Autowired
    private ABean a;
    
    public void aMethod() {
        a.print();
    }
    
    public void print() {
        System.out.println("BBean !");
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired
    private ABean a;
    @Autowired
    private BBean b;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        a.bMethod();
        b.aMethod();
    }
    
}
 
cs

 

위는 순환참조 상황을 필드 의존주입으로 재연해본 것이다. 과연 결과를 어떻게 나올 것인가?

 

<Constructor Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
public class ABean {
    
    private BBean b;
    
    public ABean(BBean b) {
        this.b=b;
    }
 
    public void bMethod() {
        b.print();
    }
    
    public void print() {
        System.out.println("ABean !");
    }
    
}
 
@Component
public class BBean {
    
    private ABean a;
    
    public BBean(ABean a) {
        this.a=a;
    }
    
    public void aMethod() {
        a.print();
    }
    
    public void print() {
        System.out.println("BBean !");
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired
    private ABean a;
    @Autowired
    private BBean b;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        a.bMethod();
        b.aMethod();
    }
    
}
cs

 

순환참조 상황을 생성자 의존주입으로 재연해보았다. 이 또한 어떠한 결과가 발생할까?

 

<Setter Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Component
public class ABean {
    
    private BBean b;
    
    @Autowired
    public void setB(BBean b) {
        this.b = b;
    }
 
    public void bMethod() {
        b.print();
    }
    
    public void print() {
        System.out.println("ABean !");
    }
    
}
 
@Component
public class BBean {
    
    private ABean a;
    
    @Autowired
    public void setA(ABean a) {
        this.a = a;
    }
 
    public void aMethod() {
        a.print();
    }
    
    public void print() {
        System.out.println("BBean !");
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired
    private ABean a;
    @Autowired
    private BBean b;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        a.bMethod();
        b.aMethod();
    }
    
}
cs

 

마지막으로 세터 의존주입을 이용하여 순환참조 상황을 재연하였다. 3가지 상황에서의 각각의 결과는 어떻게 될까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
=>Field Injection
BBean !
ABean !
 
=>Constructor Injection
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
The dependencies of some of the beans in the application context form a cycle:
 
   circularReferenceApplication (field private com.example.demo.ABean com.example.demo.CircularReferenceApplication.a)
┌─────┐
|  ABean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/ABean.class]
↑     ↓
|  BBean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/BBean.class]
└─────┘
 
=>Setter Injection
BBean !
ABean !
cs

 

결과는 위와 같다. 어떻게 된것일까? 다 같은 결과를 내는 의존주입인데, 단 하나 생성자 의존주입에서는 애플리케이션이 기동되지 못하고 순환참조 관련 예외가 발생하였다. 이유는 3가지의 객체의 라이프사이클을 떠올려보자. 필드,세터 의존주입은 필드&세터 메소드를 이용하여  의존주입을 하게 된다. 그렇다면 전제가 무엇일까? 바로 해당 객체가 메모리에 적재된 후에 빈을 주입하게 되는 것이다. 그렇다면 생성자 의존주입은 어떨까? 생성자 의존주입은 객체를 생성자로 생성하는 시점에 필요한 빈들을 의존주입한다. 즉, 객체를 생성하는 동시에 빈을 주입하는 것 그리고 객체를 이미 생성한 이후에 빈을 주입하는 것의 차이가 되는 것이다. 위에서 순환참조에 대해 간단히 설명을 하였다. 다시 한번 떠올려보자. 

 

1
2
3
4
5
┌─────┐
|  ABean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/ABean.class]
↑     ↓
|  BBean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/BBean.class]
└─────┘
cs

 

위와 같은 예외가 발생한 이유는 A는 B를 필요로하고 B도 A를 필요로 할때 발생하는 문제인데, 단순 서로를 참조하기 때문의 문제가 아니라 서로 참조하는 객체가 생성되지도 않았는데, 그 빈을 참조하기 때문에 발생하는 예외인 것이다. 즉, 순환참조는 당연히 생성자 의존주입에서만 문제가 될 수 밖에 없는 것이다. 생성자 의존주입은 빈 주입을 객체 생성시점에 주입하기 때문이다.

 

여기서 생각해 볼 것이 있다. 그러면 순환참조를 피하기 위해 필드 혹은 세터 의존주입을 사용해야 되는 것인가? 답은 아니다. 순환참조를 유발하는 객체 설계 자체가 잘못 설계된 객체임을 생각해볼 수 있다. 순환참조를 필드,세터 의존주입으로 피하는 것은 단순히 잘못 설계된 객체를 억지로 문제를 회피하여 사용하는 것은 아닌가 생각해볼 필요가 있다는 것이다.

 

객체지향 설계에서 객체의 의존에 순환관계가 있다면 잘못 설계된 객체인지 살펴볼 필요가 있다. 아니다. 왠만하면 리팩토링하자 !

 

<Field Injection vs Constructor Injection>

 

1.단일 책임의 원칙 위반 
의존성을 주입하기가 쉽다. @Autowired 선언 아래 3개든 10개든 막 추가할 수 있으니 말이다. 여기서 Constructor Injection을 사용하면 다른 Injection 타입에 비해 위기감 같은 걸 느끼게 해준다. Constructor의 파라미터가 많아짐과 동시에 하나의 클래스가 많은 책임을 떠안는다는 걸 알게된다. 이때 이러한 징조들이 리팩토링을 해야한다는 신호가 될 수 있다. 

 

2.의존성이 숨는다.
DI(Dependency Injection) 컨테이너를 사용한다는 것은 클래스가 자신의 의존성만 책임진다는게 아니다. 제공된 의존성 또한 책임진다. 그래서 클래스가 어떤 의존성을 책임지지 않을 때, 메서드나 생성자를 통해(Setter나 Contructor) 확실히 커뮤니케이션이 되어야한다. 하지만 Field Injection은 숨은 의존성만 제공해준다.

 

3.DI 컨테이너의 결합성과 테스트 용이성
DI 프레임워크의 핵심 아이디어는 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다. 즉, 필요한 의존성을 전달하면 독립적으로 인스턴스화 할 수 있는 단순 POJO여야한다. DI 컨테이너 없이도 유닛테스트에서 인스턴스화 시킬 수 있고, 각각 나누어서 테스트도 할 수 있다. 컨테이너의 결합성이 없다면 관리하거나 관리하지 않는 클래스를 사용할 수 있고, 심지어 다른 DI 컨테이너로 전환할 수 있다. 
하지만, Field Injection을 사용하면 필요한 의존성을 가진 클래스를 곧바로 인스턴스화 시킬 수 없다.

4.불변성(Immutability)
Constructor Injection과 다르게 Field Injection은 final을 선언할 수 없다. 그래서 객체가 변할 수 있다.

5.순환 의존성
Constructor Injection에서 순환 의존성을 가질 경우 BeanCurrentlyCreationExeption을 발생시킴으로써 순환 의존성을 알 수 있다.

 

<Setter Injection vs Construct Injection>

 

1.Setter Injection

Setter Injection은 선택적인 의존성을 사용할 때 유용하다. 상황에 따라 의존성 주입이 가능하다. 스프링 3.x 다큐멘테이션에서는 Setter Injection을 추천했었다.


2.Constructor Injection
Constructor Injection은 필수적인 의존성 주입에 유용하다. 게다가 final을 선언할 수 있으므로 객체가 불변하도록 할 수 있다. 또한 위에서 언급했듯이 순환 의존성도 알 수 있다. 그로인해 나쁜 디자인 패턴인지 아닌지 판단할 수 있다. 
스프링 4.3버전부터는 클래스를 완벽하게 DI 프레임워크로부터 분리할 수 있다. 단일 생성자에 한해 @Autowired를 붙이지 않아도 된다.(완전 편한데?!) 이러한 장점들 때문에 스프링 4.x 다큐멘테이션에서는 더이상 Setter Injection이 아닌 Constructor Injection을 권장한다. 굳이 Setter Injection을 사용한다면, 합리적인 디폴트를 부여할 수 있고 선택적인(optional) 의존성을 사용할 때만 사용해야한다고 말한다. 그렇지 않으면 not-null 체크를 의존성을 사용하는 모든 코드에 구현해야한다.