이 글은 제목만 보면 bean에 대한 설명할 것 같지만, 사실 bean에 대한 설명보다는 bean 존재의 의의인 IoC에서의 역할에 대한 설명에 집중한다.
IoC가 뭔지, 그리고 이게 어떻게 구현되는지에 대해서는 예전 글에서 이론적으로 어느정도 설명했는데, 그게 Spring에서 bean을 기반으로 어떻게 이루어지는지 확인해보도록 하자.
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application.
그러면 이게 Spring에서 어떻게 작용되...는지 알기 전에 몇몇 Domain class들을 한번 지정해보자.
public class Company {
private Address address;
public Company(Address address) {
this.address = address;
}
// getter, setter and other properties
}
public class Address {
private String street;
private int number;
public Address(String street, int number) {
this.street = street;
this.number = number;
}
// getters and setters
}
Companry
가 Address
에 dependency를 가진다는 것이다.IoC를 바로 들어가기 전에, 전통적으로 저 의존관계를 해결하는지 알아보도록 하자.
...단순하다. Address
객체를 만든다음에 Company
객체에 주입하는 것이다. 예를 들면 생성자의 parameter로 준다든가.
Address address = new Address("High Street", 1000);
Company company = new Company(address);
하지만 object class들이 많아지면 이런 식의 수동적인 주입이 점점 번거로워지고 관리하기도 힘들다.
게다가 object를 몇개 만들건지, scope는 어느정도로 할지도 전부 다를 수 있다. 특정 목적의 요청이 올 때마다 object를 만든다든가, 아니면 애플리케이션 시작 때 하나만 만들고 모두가 이를 공유하든가. 이걸 위와 같은 방식으로 수동으로 관리하는것도 매우 힘들다.
이 번거로움을 IoC 기법으로 해결하는 것이 가능하다. IoC container이라는 것을 만들고, 거기서 object의 의존관계를 다 관리해주는 것이다. 다만 그 container이 진짜 똑똑해가지고 모든걸 판단하는 것은 아니고, 프레임워크들을 제공하면 그 프레임워크를 개발자가 환경해서 이런저런 환경설정 메타데이터를 구성해주는 것이다. 결국 개발자가 관여하지만, 위와 같은 방식보다는 편하다.
Spring에서 이런 환경 설정을 하는 방식은 여러가지가 있고, 사실 이전의 생성자 기반 DI에서 그 예시를 이미 한번 봤다.
환경 설정을 담당하는 class가 누군지를 @Configuration
으로 설정하고, 추가로 만들어야 할 bean을 스캔하라는 것을 @ComponentScan
으로 지정, @Bean
으로 bean을 만드는 method를 지정, @Component
로 bean으로 만들어야 하는 class를 지정하는 등의 방식으로 말이다.
이런 프레임워크들을 사용해서 IoC container이 만들어야하는, 그리고 관리해야하는 object(bean)이 뭔지 전부 지정이 가능하다.
@Component
public class Company {
// this body is the same as before
}
@Configuration
@ComponentScan(basePackageClasses = Company.class)
public class Config {
@Bean
public Address getAddress() {
return new Address("High Street", 1000);
}
}
이 부분을 다뤄본적이 없다. documentation을 보면 annotation이 붙은 component를 찾을 package를 지정하는데 쓰인다고 한다. 그러면 원래 @ComponentScan
에 package를 전달하던 것이랑 별 차이가 없는데... 왜 package가 아닌 class를 전달하는 것일까?
이유는 전달한 class가 속한 package를 탐색하는데 사용되기 때문이다. 그런데 그냥 package를 지정을 하지, 왜 class를 전달하는 형태로 우회하는 방식이 존재하는걸까?
documentation을 보면 basePackages
의 type-safe 버전이라고 한다. basePackage
는 이전에 봤던 것처럼 어떤 package를 탐색할지 지정하는데 사용된다. 여기서 type-safe가 무엇인가?
프로그래밍 언어 이론이나 컴파일러에 관해 조금 공부했으면 많이 들어봤을 것이다. 바로 컴파일 시에 type 오류가 발견되면 runtime으로 넘어가지 않고 바로 오류를 내뱉는 것이다.
다만 언어마다 오류를 내뱉는 수준이 다르다. 유명한 언어중 이 type-safety에 관해서 가장 애매한 언어가 C다. 예를들어 C는 implicit하게 어떤 struct pointer이 다른 struct pointer로 변하는걸 허용하지 않는다. 근데 explicit하게 cast하면 된다(?). 그리고 pointer/배열과 정수 덧셈도 된다(??). 그리고 char pointer, 즉 string이 들어가야하는데 정수가 들어가도 상관이 없다.(???) 그래서 '보통은' weak type-safe라고 한다. (이것도 사람마다 주장하는게 다르다.)
또 Haskell의 경우에도 type-safety가 어느정도냐 왈가왈부가 많은 언어 중 하나다.
여기서 이 개념을 깊이 들어가는건 좀 애매하고 (궁금하면 이 두 글을 읽어보자. 1, 2) 의의를 살펴보면 type-safe 수준이 높을수록 컴파일 시간에 프로그램에 있는 잠재적 오류를 탐지하기 때문에 런타임에서 사단이 나는것을 방지해준다는 이점이 있다는 것이다.
basePackage
를 사용하면 package 전달한거 이름이 오타가 있거나 패키지 구조가 이전과 많이 달라져서 문제가 있어도 '런타임'에서야 그 오류가 발견이 된다. 하지만 basePackageClasses
를 사용할 경우 이게 컴파일하는 도중에 발견이 된다.
이거랑 더불어 소소한 장점으로 IDE로 리팩토링 할 때 유용하다는 것이 있다. 보통 IDE가 class reference는 전부 추적하고 변동이 있을 때 이를 자동으로 업데이트하는 기능이 있기도 하다. 그래서 앞의 문제가 생길 때 이를 즉각 추적하는 것이 가능하다. 하지만 basePackage
형태로 문자열로 주어지면 이를 IDE가 제대로 파악하기 힘들다. 역시나 런타임에서야 그 문제를 발견하는게 가능하다.
AnnotationConfigApplicationContext
의 instance를 생성해야 한다. 밑을 main에 넣으면 된다.ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Company company = context.getBean("company", Company.class);
assertEquals("High Street", company.getAddress().getStreet());
assertEquals(1000, company.getAddress().getNumber());