아주 오랜만에 블로그에 포스팅을 한다. 거의 한달만인가? 이래저래 일도 있고 솔직히 조금 귀찮아서 포스팅을 안했는데.. 오늘 잠깐 시간이 되서 남긴다.
오늘 할 이야기는
@Configuration
과
@Bean
의 의존 관계 설정에 대해서 알아볼 예정이다.
Spring 3.0 이전에는 Spring 설정을 Xml로 많이(?) 아니 거의 대부분 했는데 Spring 3.0 부터 자바 소스 코드로 Spring 설정을 할 수 있게 되었다. 그런데도 불구하고 개발자들은 그렇게 많이 사용 안했던거 같다.(물론 필자 생각 내가 사용안해서 그랬던가..) 그리고 Spring 3.2 이전에는 추가로 디펜더시 받아야 하는 부분도 있어서 그랬던거 아닌가 싶기도 하다. 그나마 3.2 이후에는 디펜더시도 없고 그래서 전보다는 많이 자바로 설정하는 법을 사용한 것 같고 그 이후 빛을 본게 Spring boot 때문이지 않나 싶다. Spring boot는 거의 대부분이 자바 소스 코드로 설정을 많이 한다. 물론 Xml이 편하다면야 써도 되지만 굳이 그럴필요까지 있나 싶기도 하다. 하지만 굳이 꼭 이걸 사용해 저걸 사용해 하며 강제화 할 필요 없이 자기의 입맛대로 사용하면 되겠다. Xml 설정과 자바 소스코드 설정 둘다 장단점이 있으니 각자가 편한대로 사용해서 쓰면 되겠다. 참고로 필자는 좀 더 자바 소스 코드로 설정하는 법을 선호하는 편이긴 하다.
뭔가 두서가 길었군. 아무튼 한번 살펴보도록 하자.
의존 관계 설정
일단 의존관계를 대상이 될 2개의 클래스를 만들어보자.
//HelloPrint.class
public class HelloPrint {
public void print(String name) {
System.out.println(name);
}
}
//Hello.class
public class Hello {
private String name;
public void setName(String name) {
this.name = name;
}
public void print(HelloPrint helloPrint) {
helloPrint.print(name);
}
}
딱히 설명하지 않아도 아주 간단한 클래스 2개 이다.
Hello
클래스는
HelloPrint
클래스를 의존하고 있다. 물론 위의 코드들은 예제이므로 별 의미 없는 코드들이다. 단지 이름을 받아서 이름을 출력해 주는 그런 코드이다.
두개의 클래스를 어떻게 자바 코드로 설정하는지 일단 살펴보도록 하자.
@Bean 메서드 호출
가장 직관적이며 쉬운 방법이다.
@Configuration
public class HelloConfig {
@Bean
public Hello hello() {
Hello hello = new Hello();
hello.setName("wonwoo");
hello.print(helloPrint()); //DI
return hello;
}
@Bean
public HelloPrint helloPrint() {
return new HelloPrint();
}
}
딱 보면 무엇을 의미하는지 한눈에 보일 정도로 직관적이다. 눈으로만 봐도 직관적이지만 IDE에서 해당 메서드까지 바로 갈 수도 있어 더욱 편하다. 아주 쉽고 편리하지만 처음 본사람은 오해의 소지가 있을 수 있다. 만약 어디선가 동일하게 helloPrint()를 호출하면 어떻게 될까?
@Bean
public Hello hello1() {
Hello hello = new Hello();
hello.setName("kevin");
hello.print(helloPrint());
return hello;
}
어디선가 위와 같은 코드가 있을 경우
HelloPrint
는 두개가 생기는 걸까? 어? 스프링의 빈들은 기본이 싱글톤인데? 두개가 생기면 싱글톤이 아닌데? 일반적으로 java경우에는 두개 생기는게 맞다. 하지만 Spring은 조금 다르다.
@Configuration
어노테이션이 붙은 클래스는 조금 특별하게 동작해서 내부적으로 한개만 만들도록 한다. 한번 아래와 같이 사이에 끼어 넣어서 확인해보자.
@Bean
public Hello hello() {
Hello hello = new Hello();
hello.setName("wonwoo");
hello.print(helloPrint());
System.out.println(helloPrint() == helloPrint());
return hello;
}
위의 hello 메서드에
System.out.println(helloPrint() == helloPrint())
와 같이 추가해보자. 만약 true가 나온다면 필자말이 맞다는 것이고 false가 나오면 필자말이 틀리고
helloPrint
빈이 두개가 생긴다는 뜻이다.
@Test
public void helloConfigTest() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(HelloConfig.class);
Hello hello = context.getBean(Hello.class);
assertThat(hello).isNotNull();
}
위와 같이 테스트를 실행시켜보자. 우리가 원하는 대로 true가 나오고 테스트도 통과한다.
@Bean 오토 와이어링
이거 역시 간단하다. 위의 메서드 호출에서 오해의 소지가 있을 수 있던 부분도 해결된다.
@Configuration
public class HelloConfig {
@Bean
public Hello hello(HelloPrint helloPrint) {
Hello hello = new Hello();
hello.setName("wonwoo");
hello.print(helloPrint);
return hello;
}
@Bean
public HelloPrint helloPrint() {
return new HelloPrint();
}
}
이것의 최대 장점은 설정 파일이 외부에 있어도 가능하다는 점이다. 또한 여러개의 파라미터를 설정해도 상관없다. 물론 설정 파일이 다를 경우에는
HelloConfig
의 변수에
@Autowired
받아도 상관은 없다.
@Configuration
public class HelloConfig {
@Autowired
private HelloPrint helloPrint;
//...
}
각자가 편한 방법대로 입맛에 맞게 설정하면 된다. 필자의 경우에는 같은 설정 파일일 경우에는 메서드 호출 방식을 선호하고 다른 설정 파일일 경우에는 오토 와이어링를 선호한다. 다른 사람들이 볼땐 헷갈릴 수도 있는데 필자는 혼합해서 사용하는 것도 익숙해져서..
@Configuration 오토 와이어링 메서드 호출
이 설정방법은 필자도 잘 사용하지 않는 방법이지만 가능하기에 작성한다.
@Configuration
가 붙은 클래스도 동일하게 Spring 빈으로 등록된다. 그럼
@Configuration
붙은 클래스를 오토 와이어링해서 메서드를 호출해도 되지 않을까? 물론 된다.
@Configuration
public class Hello1Config {
@Bean
public HelloPrint helloPrint() {
return new HelloPrint();
}
}
@Configuration
public class HelloConfig {
@Bean
public Hello hello(Hello1Config hello1Config) {
Hello hello = new Hello();
hello.setName("wonwoo");
hello.print(hello1Config.helloPrint());
return hello;
}
}
설정 파일을 분리해봤다.
HelloConfig
의
HelloPrint
메서드를 삭제하고
Hello1Config
클래스로 이동 시켰다. 그리고
Hello1Config
을 DI 받고 난후에
helloPrint()
메서드를 호출 하였다. 이렇게 해도 잘 동작한다. 하지만 필자의 경우에도 이렇게 쓸일이 거의 아니 한번도 없었다. Hello라는 클래스에 Hello1Config에 해당하는 빈을 여러개 더 DI한다면 모를까 굳이 이렇게 쓸 필요는 없을 듯 하다. 그닥 추천하지는 않는 방식이다.
null인데 가능해?
위의 수정했던 코드를 다시 원상복귀시켜보자.
@Configuration
public class HelloConfig {
@Bean
public Hello hello(HelloPrint helloPrint) {
Hello hello = new Hello();
hello.setName("wonwoo");
hello.print(helloPrint);
return hello;
}
@Bean
public HelloPrint helloPrint() {
return new HelloPrint();
}
}
다시 위와 같은 코드가 있다고 가정해보자. 그런데 갑자기 Hi라는 설정을 추가할 일이 있어 Hi클래스를 만들었다. 이거 역시 의미 있는 코드는 아니다..
public class Hi {
private Hello hello;
public void setHello(Hello hello) {
this.hello = hello;
}
public Hello getHello() {
return hello;
}
}
위는 Hi는 Hello를 의존하고 있다. 그래서 아래와 같이 설정을 추가 하였다.
@Configuration
public class HelloConfig {
//... hello() , helloPrint()
@Bean
public Hi hi(HelloPrint helloPrint) {
Hi hi = new Hi();
hi.setHello(hello(helloPrint));
return hi;
}
}
hi는 기존에 있던 hello라는 빈에 의존하게 만들었다. 뭐 이정도라도 괜찮다. 근데 만약에 hi라는 빈에 여러개의 빈들을 의존하고 있고
hello
에도 여러개의 빈을 의존하고 있다면
hi
에는
hi
에 대한 의존성과
hello
에 대한 의존성을 두개다 갖고 있어야 한다.
@Bean
public Hi hi(SomeBean1 bean1, SomeBean2 bean2,, SomeBean3 bean3, ...
HelloPrint helloPrint, HelloPrint1 helloPrint1, HelloPrint2 helloPrint2 ...) {
Hi hi = new Hi();
hi.setHello(hello(helloPrint, helloPrint1, helloPrint2, ...));
hi.setBean1(bean1);
hi.setBean2(bean2);
hi.setBean3(bean3);
//...
return hi;
}
대충 위와 같은 형태의 빈을 설정할 수 있다. 뭐 위와 같이 해도 상관은 없지만 파라미터가 너무 길어 보기가 힘들다. 이럴경우에 조금 유용한 기능으로 hello 메서드를 호출할 때 null을 줘도 무방하다.
@Bean
public Hi hi(SomeBean1 bean1, SomeBean2 bean2,, SomeBean3 bean3, ...) {
Hi hi = new Hi();
hi.setHello(hello(null, null, null));
hi.setBean1(bean1);
hi.setBean2(bean2);
hi.setBean3(bean3);
//...
return hi;
}
아까 위에서 말했듯이
@Configuration
붙은 클래스는 조금 특별하게 동작한다. 그 방법을 이용한 것이다. 실제로 잘 테스트가 되는지 해보자.
@Bean
public Hi hi() {
Hi hi = new Hi();
hi.setHello(hello(null));
return hi;
}
위 코드를
HelloConfig
클래스에 넣고 테스트를 돌려보면 통과 한다. 하지만 이방법은 저렇게 파라마티가 많은 경우에만 필자도 사용하고 그 이외에는 어떠한 이유에도 사용하지는 않는다. 그닥 보기 싫은 코드이다.
자바 소스 코드로 Spring을 설정하는 방법은 이렇게 여러가지가 있다. 필자가 추천하는 방법은
@Bean 메서드 호출
,
@Bean 오토 와이어링
정도이다. 그리고 필요에 따라 다른 것들도 사용해도 무방하다.
오늘은 이렇게 @Configuration과 @Bean의 의존 관계 설정에 대해서 알아봤다. 봐야할 건 많은데 머리 속에는 다 들어가지 않고..