본문 바로가기

Spring/Spring

Spring boot Bean 생성 순서 정하는 방법, 생성되지 않은 Bean을 주입받으려고 하다가 실패했을 때 해결 방법, IoC 컨테이너를 공부해야하는 이유.. (※생명 주기 아님)

반응형

Spring boot에서 bean 등록 순서를 결정하는 방법

스프링/스프링부트에서 bean을 등록하는 방법은 여러가지가 있다.

스프링부트에서는 Bean을 등록할 때 자바코드(Annotation)로 등록하는 것을 권장한다.

따라서 스프링 부트를 사용하는 개발자들은 @Component, @Service, @Controller, @Repository, @Bean, @Configuration 등으로 Bean들을 등록하고 주입받아 사용하는게 일반적이다.

그런데 프로그램 개발중에 아무 생각없이 여러 개의 Bean들을 등록해놓고 어떤 Bean에서 @Autowired로 자연스럽게 주입받아서 '사용'하려다가 에러를 만났다.

바로 Bean에 '아직' 등록되지 않은 Bean을 클래스에서 사용하려고 했기 때문이다.

무슨 얘기인지 Spring을 기준으로 설명하겠다. (Spring boot랑은 조금 다름.)

A라는 클래스에서 @Component를 사용해서 bean이 자동적으로 등록되기를 원하고, B라는 클래스도 @Component를 사용해서 bean이 자동적으로 등록되기를 원하는 상황일 때,

B라는 클래스 내부에서는 A라는 클래스의 인스턴스(Bean)를 @Autowired로 주입받아서 해당 인스턴스의 method를 이용하려는 흐름이다.

사실 모든 bean들이 알아서 뜰때까지 무언가 작업을 하지 않으면 문제가 없을 수 있으나 bean이 생성되자마자 초기화같은 어떤 작업을 해버린다면 얘기가 달라진다.

참고로 Spring에서 xml을 이용해서 bean을 등록하게되면 자동적으로 위에서 아래로 bean들을 스캔하여 생성한다.

그런데 bean 생성 순서가 바뀌어야하는 상황이라면 어떨까?

아주 좋게도 스프링이 알아서 순서를 바꿔서 참조되는 bean(예시에서 A)을 먼저 생성하고 참조하고 있는 bean(예시에서 B)를 나중에 생성한다.

그래서 스프링에서는 문제가 되지 않는다.

근데 스프링부트에서는 Annotation을 이용해서 bean을 등록하게되면 웃기게도(?) 패키지에서 존재하는 순서대로(위에서 아래) 스캔하면서 bean을 생성한다.

따라서 괜히 알파벳순서에서 밀린 패키지, 클래스는 생성 순서를 맞춰주지 않는 문제가 생긴다.


문제 상황 재현

현재 상황을 설명하면,

패키지 구조가 왼쪽 상단과 같이 되어있고 BeanTest1,2,3을 생성하고 오른쪽 상단과 같은 코드를 만들었다.

코드 내용은 @Component를 통해 Bean으로 등록하고 생성자에서 hello()라는 메소드로 자신이 생성되었음을 console로 찍었다.

역시나 위에서 아래로 생성되는 것을 로그를 통해 알 수 있다.

그렇다면 문제의 상황을 만들기 위해서 어떻게 해야할까?

바로 BeanTest1 클래스에서 BeanTest3 bean을 주입받아 hello()메소드를 호출해 보는 방법으로 재현할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.beans;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
@Component
public class BeanTest1 {
    
    @Autowired
    BeanTest3 beanTest3;
    
    public  BeanTest1() {
        hello();
        beanTest3.hello();
    }
    public void hello() {
        System.out.println("hello Bean1");
    }
}
 
cs

결과는 위 그림과 같이 BeanCreationException을 발생시킨다. (hello Bean1은 찍히고 있음을 확인, hello Bean2,3는 안보임)

Bean3가 생성되지도 않았는데 주입받았을 것이라 생각하고 사용해버리니까 에러가 난 것이다.

이 문제를 해결하는 방법은 여러가지가 있다. 다 해볼 것이다.


Bean 순서 결정법 1

@DependsOn 애노테이션을 사용하자

결국은 스프링한테 "이 빈(Bean)은 어떤 X라는 빈을 참조하고 있어(의존하고 있어)" 라고 알려주는 것과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.beans;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class BeanTest3 {
    public BeanTest3() {
        System.out.println("Beantest3 생성");
    }
    
    @Bean("Bean3")
    public BeanTest3 create() {
        return this;
    }
    public void hello() {
        System.out.println("hello Bean3");
    }
}
 
cs

[BeanTest3.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo.beans;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
 
@Component
@DependsOn(value = {"Bean3"})
public class BeanTest1 {
    
    @Autowired
    BeanTest3 beanTest3;
    
    public  BeanTest1() { 
        //hello(); 이렇게하면 생성하면서 마찬가지로 주입받은 빈을 바로 사용하는 것과 같기 때문에 에러남
        System.out.println("BeanTest1 생성");
    } 
    
    public void hello() {
        beanTest3.hello();
        System.out.println("hello Bean1");
    }
}
 
cs

[BeanTest1.class]

위 코드에서 보다시피 클래스에 @DependsOn이라는 애노테이션을 쓰고 Bean3로 등록되는 bean에 의존하고 있다는 것을 알려주었다.

참고로 @Component("Bean3") 이런식으로 빈을 등록해봤을 때는 에러가 발생했다.

그래서 @Configuration에 @Bean으로 등록하는 방식으로 등록했더니 잘 참고해서 아래와 같은 결과를 얻을 수 있었다. (생성 순서, 엄밀히 말하면 이것으로 인증할 순 없음.)

* 요약

결과적으로 @DependsOn을 사용하면 의존한다는 사실을 스프링에게 직접 알려줄 수 있어서 문제는 해결된다.

하지만 이런식으로 코드를 여러 곳에 작성하면 다른 사람이 코드를 봤을 때 헷갈릴 수도 있다.

또한 여러 곳에 작성하다보면 무한루프(?)가 걸릴 수 있다. A->B->C->A 이런식으로 의존이 고리를 형성해버릴 수 있다.

그리고 @Component("Bean3") 이런식으로 코드 작성이 불가한게 단점이다. (사실 아닐 수 있음, 테스트에서는 안됨.)


Bean 순서 결정법 2

@PostConstruct 애노테이션을 사용하자

위의 애노테이션은 해당 컴포넌트가 완전히 생성된 후(주입된 후)에 한 번 실행해야할 일들을 코딩한 메소드에 붙이는 것이다.

즉, 해당 Bean이 완전히 생성된 후 무언가 작동하므로 NullPointerException이 일어나지 않는다.

물론 생성자에 붙이는 것은 여지없이 에러가 난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class BeanTest1 {
    
    @Autowired
    BeanTest3 beanTest3;
    
    public  BeanTest1() { 
        System.out.println("BeanTest1 생성");
    }  
    
    @PostConstruct
    public void hello() {
        beanTest3.hello();
        System.out.println("hello Bean1");
    }
}
 


[BeanTest1.class]

1
2
3
4
5
6
7
8
9
10
11
@Component
public class BeanTest3 {
    public BeanTest3() {
        System.out.println("Beantest3 생성");
    }
    
    public void hello() {
        System.out.println("hello Bean3");
    }
}
 
cs

[BeanTest3.class]

위와 같이 beanTest3.hello();가 빈이 완전히 생성된(@Autowired로 주입까지 완료된) 상태에서 실행되다보니 에러가 해결된다.

* 참고로 이 방법이 다른 빈들에게 의존성을 부여하지도 않고 깔끔한 코드가 되기 때문에 가장 적절한 방법이다.

Bean 순서 결정법 3

@Order 는 뭘까?

문제와는 약간 다르지만 특별한 상황에서 Bean 생성 순서를 결정할 수 있는? 방법이 @Order다.

간단하게 소개하면 같은 인터페이스를 구현하는 여러 Bean들이 어느 한 객체로 주입될 때 순서를 정할 수 있는 것이다.

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
public interface Person {
    public void eat();
}
 
=========================================
@Component
@Order(2)
public class Jeongpro implements Person {
    @Override
    public void eat() {
        System.out.println("jeongpro");
    }
}
=========================================
@Component
@Order(1)
public class Tistory implements Person {
    @Override
    public void eat() {
        System.out.println("tistory");
    }
}
=========================================
@Component
public class BeanTest1 {
    
    @Autowired
    List<Person> people;
    
    public  BeanTest1() { 
        System.out.println("BeanTest1 생성");
    }  
    
    @PostConstruct
    public void hello() {
        people.stream().forEach(x->x.eat());
    }
}
cs

Person이라는 인터페이스를 구현하고 있는 객체들이 BeanTest1에서 List<Person> people; 이라는 객체에 주입될 때 들어가는 순서를 정하는 것이다. @Order의 순서대로 제일앞에는 Tistory가 들어가고 다음에 Jeongpro가 들어간다.

[결과]


* 또 다른 방법으로 Bean이 주입되지 않았을 때를 고려해서 생성자에서 해당 bean을 파라미터로 받는 방법인데 마찬가지로 의존성을 단적으로 보여줘버리기 때문에 지양한다.

반응형
  • 가루 2018.09.22 18:10

    PostConstruct를 오해하고 있었네요. 어쩐지 어디다 붙여도 항상 시작 log의 맨마지막에 위치하더라니..
    깔끔하고 정확한 정리 감사합니다!

  • 익명 2018.09.30 19:42

    비밀댓글입니다

  • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2019.04.02 11:30 신고

    스프링 레퍼런스에서 권장하는 방법은 생성자 주입방법입니다.
    생성자 주입 방법은 주입받으려는 빈이 반드시 생성되는 것을 보장해주기 때문에 장점이 있습니다.
    하지만 빈들간의 순환 참조로 문제가 생기는 단점이 있습니다.
    순환 참조하지 않게 최대한 만드는 것이 좋겠지만 순환 참조가 일어나면 setter를 이용한 주입이나 멤버변수를 이용한 주입을 해주는 것으로 회피할 수 있습니다.

    • Favicon of https://coding-start.tistory.com BlogIcon 여성게 2019.04.03 23:40 신고

      제가 알기로는 @Autowired로 빈주입시 순환 참조가 일어나서 생성자 주입을 이용하는 걸로 알고 있는데... 제가 반대로 알고있는 건가요

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2019.04.04 11:56 신고

      @Service
      public class AService {
      @Autowired
      private BService bService;
      @PostConstruct
      private void test() {
      bService.hello();
      }
      public void hello() {
      System.out.println("AService hello");
      }
      }
      ////////////////////////////////////
      @Service
      public class BService {
      @Autowired
      private AService aService;
      @PostConstruct
      private void test() {
      aService.hello();
      }
      public void hello() {
      System.out.println("BService hello");
      }
      }

      위와같이 멤버 변수로 @Autowired 애노테이션을 사용해서 주입받으면 서로 참조하고 있지만 순환참조에러가 나지 않습니다. -> 정상적으로 AService, BService Bean이 등록이 됨.

      그러나 아래와 같이 스프링 레퍼런스에서 권장하는 생성자 주입 @Autowired를 쓰면 순환참조 에러가 납니다.
      @Service
      public class AService {
      private BService bService;

      @Autowired
      public AService(BService bService) {
      this.bService = bService;
      }
      @PostConstruct
      private void test() {
      bService.hello();
      }
      public void hello() {
      System.out.println("AService hello");
      }
      }

      * 다른 얘기로 스프링4.3? 이후부터는 생성자에 @Autowired를 쓰지 않아도 생성자의 파라미터 타입의 빈이 있으면 자동으로 주입해줍니다.
      심지어 Lombok을 이용하면 생성자를 쓰지 않아도 주입이 됩니다.

  • 익명 2022.03.30 17:06

    필드 주입과 생성자 주입 모두 순환참조가 발생할 수 있지만
    생성자 주입은 컴파일타임에 찾을 수 있고
    필드주입은 런타임에 발생하여 언제 오류를 발생할지 모를 위험을 가지고 있어
    생성자 주입을 권장하는 것으로 알고있습니다.
    위에 말씀하신 에러는 개발을 막는것이 아닌 스프링단에서 미리 검출해 주는것으로 받아들이는게 맞을 것 같습니다.

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2022.03.31 07:58 신고

      안녕하세요~ 댓글 감사합니다!

      제가 한 가지 의문이 드는 것은 필드 주입이 런타임에 순환 참조를 발생시켰다면 런타임(실제로 어떠한 요청이 와서 서로 참조되는 빈을 사용할 때)에 에러가 나야하는데 정상 동작했던 것으로 기억해서 다시 테스트해봐야겠다는 생각이 드네요...