Web/Spring 2019. 8. 17. 19:31

 

스프링에서 빈을 생성할 때, 기본 전략은 모든 빈이 싱글톤으로 생성된다. 즉, 어디에서든지 빈을 주입받는 다면 동일한 빈을 주입받는 것을 보장한다. 하지만 필요에 따라서 빈 주입마다 새로운 빈을 생성해야할 필요가 있을 경우도 있다. 이럴 경우에는 빈 생성시 Scope를 prototype으로 주면 빈 주입마다 새로운 인스턴스가 생성되는 것을 보장한다. 하지만 프로토타입 빈을 사용할 경우 주의해야 할 상황이 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class ABean {
    
    @Autowired
    private BBean b;
 
    public void bMethod() {
        b.print();
    }
    
}
 
@Component
@Scope("prototype")
public class BBean {
    
    public void print() {
        System.out.println("BBean !");
    }
}
cs

 

이런식으로 사용한다면 어떻게 될까? 이것은 사실 프로토타입을 쓰나마나이다. 싱글톤으로 생성된 A빈에 프로토타입 B빈을 주입해봤자 A빈은 더이상 생성되지 않기 때문에 항상 동일한 B빈을 사용하게 되는 것이다.

 

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
@Component
public class ABean {
    
    @Autowired
    private BBean b;
    
    public void print() {
        System.out.println(b.hashCode());
    }
    
}
 
@Component
@Scope("prototype")
public class BBean {
    
}
 
@Component
public class CBean {
    
    @Autowired
    private ABean a;
    
    public void print() {
        a.print();
    }
}
 
@Component
public class DBean {
    
    @Autowired
    private ABean a;
    
    public void print() {
        a.print();
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired private CBean c;
    @Autowired private DBean d;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        c.print();
        d.print();
    }
    
}
cs

 

위 코드를 실행시켜보자. 프로토타입 빈으로 등록된 B빈이지만 항상 어디서든 동일한 해시코드를 반환한다. 즉, 프로토타입빈을 사용하기 위해서는 아래와 같이 사용하자.

 

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
@Service
public class BeanUtil implements ApplicationContextAware {
 
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
 
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
 
@Component
public class ABean {
    
    public void print() {
        BBean b = BeanUtil.getBean(BBean.class);
        System.out.println(b.hashCode());
    }
    
}
 
cs

 

ApplicationContext 객체에서 직접 빈을 가져와서 메소드 내부에서 사용하도록 하자. 이제는 매번 다른 B빈의 해시코드를 반환할 것이다.

 

2019/02/25 - [Web/Spring] - Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!

 

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때! @Autuwired,@Inject 등의 어노테이션으로 의존주입을 하기 위해서는 해당 객체가 빈으로 등록되어 있어야만 가능하다. 사실..

coding-start.tistory.com

 

posted by 여성게
:
Web/Spring 2019. 8. 17. 18:47

 

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

 

<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 체크를 의존성을 사용하는 모든 코드에 구현해야한다.

 

posted by 여성게
:
Web/Spring 2019. 2. 25. 15:57

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!



@Autuwired,@Inject 등의 어노테이션으로 의존주입을 하기 위해서는 해당 객체가 빈으로 등록되어 있어야만 가능하다.

사실 이런 상황은 웹프로그래밍에서는 거의 없겠지만... 빈으로 등록되지 않은 객체에 빈으로 등록된 객체를 의존주입해야할 상황이 있을 수도 있다.

 그럴때 사용할수 있는 하나의 UtilClass 이다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class BeanUtils implements ApplicationContextAware {
 
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // TODO Auto-generated method stub
        context = applicationContext;
    }
 
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
cs


ApplicationContextAware를 구현한 BeanUtils 클래스를 하나 만들었다. 그리고 setApplicationContext() 메소드로 ApplicationContext를 

주입받고 있는 상황이다. 그리고 static으로 선언된 getBean 메소드를 이용하여 빈주입을 원하는 어딘가에서


BeanUtils.getBean()를 호출하여 빈을 주입받을 수 있다.!

posted by 여성게
:
Web/Spring 2018. 3. 13. 16:07

스프링에서 어노테이션을 이용한 DI 방법



1.@Autowired,@Resource,@Qualfier 어노테이션


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
59
60
61
62
63
64
65
package com.web.nuri;
 
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
 
import javax.annotation.Resource;
 
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException;
 
import com.web.nuri.user.UserDTO;
import com.web.nuri.user.UserService;
 
//JUnit 확장기능들(import문은 수동으로 넣어줘야한다)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml"//테스트용 applicationContext.xml kodictest/kodictest
public class UserDAOTest {
    @Resource(name="userServiceImpl")
    UserService userService;
    @Autowired
    PlatformTransactionManager transactionManager;
    
    //트랜잭션 경계설정 코드
    @Test(expected=DataAccessException.class)
    public void insertUserTest() {
        //트랜잭션의 시작을 알린다.(트랜잭션을 얻어오는 시점이 시작)
        TransactionStatus status=this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        UserDTO user1=new UserDTO();
        UserDTO user2=new UserDTO();
        UserDTO user3=new UserDTO();
        try {
            user1.setId("test@test.com");
            user1.setPw("1111");
            user1.setNickName("tester");
            user2.setId("test1@test.com");
            user2.setPw("1111");
            user2.setNickName("tester1");
            user3.setId("test@test.com");
            user3.setPw("1111");
            user3.setNickName("tester2");
        
            userService.insertUser(user1);
            userService.insertUser(user2);
            userService.insertUser(user3);
            this.transactionManager.commit(status);
        }catch (RuntimeException e) {
            // TODO: handle exception
            this.transactionManager.rollback(status);
            throw e;
        }
        //UserDTO user4=userService.getUser(user1);
        //assertThat(user2.getId(),is(user1.getId()));
        //userService.deleteUser(user);
    }
}
 
cs

위에서 보듯 어노테이션을 이용한 DI방법은 여러가지가 있지만 대표적으로 2가지를 설명하자면 @Autowired , @Resource 를 이용한 DI 주입 방법을 들수있다. @Autowired는 해당 변수의 인터페이스 타입으로 등록된 빈들에서 찾아서 DI를 주입해주는 방법이다. 만약 UserService 라는 인터페이스를 구현한 클래스가 2개 이상 빈으로 등록되어있다면 이름의 충돌이 발생해 제대로 DI가 되지 않는 경우가 있다.(에러가 뜬다. 하지만 에러를 발생하지 않게 하려면 @Autowired(require=false) 로주면 에러는 발생하지 않는다.) 변수의 타입으로 판별 할수 없다면 변수명을 이용하여 DI를 한다고 하지만 그 마저 마땅치 않으면 예외가 발생 할 수가 있다. 그래서 사용하는 어노테이션이 @Resource 어노테이션이다. @Resource(name="~") name이라는 속성안에 등록된 빈의 이름을 지정한다면 같은 타입의 인터페이스를 구현한 클래스들이 여러개 등록되어 있더라도 정확히 이름으로 골라서 등록이 가능하다. 하지만 만약 클래스를 @Service,@Component 등의 어노테이션으로 빈등록을 했다면? 방법이 있다. 예를 들어 UserServiceImpl 라는 UserService 인터페이스 구현클래스를 어노테이션으로 빈등록을 했다면 그 빈은 자동으로 userServiceImpl 라는 이름으로 빈등록이 된다. (앞글자를 소문자로) 그래서 내부적으로 지정된 userServiceImpl 라는 이름을 @Resource의 name 속성에 넣어주면 된다. @Autowired로 빈을 결정 지을 수 없을때, 밑에 @Qualifier("~")를 붙여서 특정빈을 선택한다.




2. DI 이전에 필요한 스프링설정


위의 어노테이션들을 사용하여 DI를 주입하기 위해서는 각각 어노테이션을 인식하기 위한 스프링 설정이 필요하다.


1
2
<!-- Root Context: defines shared resources visible to all other web components -->
    <context:component-scan base-package="com.web.nuri"></context:component-scan>
cs

posted by 여성게
: