대부분의 시스템은 전체 기능 중에서 일부를 담당하는 다수의 컴포넌트로 구성되며, 각 컴포넌트들은 다른 컴포넌트와 상호작용하며 작업을 처리한다. 이런 관점에서 시스템을 바라보고 구현하는것을 '컴포넌트 기반 방법론'이라고 한다. 이러한 관점에서 어떠한 시스템을 만든다는 말은, 결국 시스템을 구성하는 컴포넌트들을 구현하고 이들이 서로에게 상호작용할 수 있도록 적절하게 연결해주는 작업을 일컫는 셈이다.
*여기서 '연결'이라는 행위는 궁극적으로 '참조의 확보'라고 할 수 있다. 예를 들어 A 인스턴스가 B 인스턴스의 참조를 갖고 있다면 A는 B의 메소드를 사용할 수 있다. 이 때 A와 B가 연결되었다고 말할 수 있다.
Spring 프레임워크는 각각의 컴포넌트를 구현할 때와 이들을 연결할 때 모두 강력한 기능을 지원한다. 개별 컴포넌트의 구현을 지원하는 프레임워크(C#의 닷넷, golang의 패키지 등)는 익숙하므로, 우리는 Spring프레임워크의 특징이라고 할 수 있는 '연결 작업'에 초점을 맞출 것이다. Spring 프레임워크는 컨테이너를 제공하고, 이 내부에 여러개의 컴포넌트들을 등록 및 생성하고 관리한다.
Spring이 제공하는 컨테이너를 [Spring 애플리케이션 컨텍스트]라고 하고, 이 내부에서 관리되는 컴포넌트들은 [빈(Bean)] 이라 한다. 컨테이너는 개발자가 원하는대로 쉽게 각각의 컴포넌트들을 연결할 수 있도록 지원한다. 물론 이것이 가능하기 위해서는 개발자가 Spring프레임워크에 빈을 등록하고, 각각의 빈들을 어떻게 연결해야 할지를 명시해주어야 한다. 이러한 '등록'과 '연결'작업을 구체적으로 어떻게 해야 하는지 알아보자.
*앞으로 편의상 "Spring 애플리케이션 컨텍스트"를 '컨테이너'라 부를 것이다.
*Spring 프레임워크를 처음 공부할때 흔히 만나게 되는 제어의 역전, 의존성 주입과 같은 전문용어는 지양한다. 이것들에 대해서는 이미 방대한 정보를 쉽게 찾을 수 있으므로, 나중에 필요하다면 찾아보자.
Spring에 빈을 등록하는 방법에는 여러가지가 있다. 가장 단순한 방법은 xml파일을 통해 직접적으로 빈을 등록하는 것인데, 최근에는 지양되는 분위기이므로 나중에 필요하면 찾아보도록 하자. 대부분의 경우에 빈은 [스테레오타입 애노테이션]을 사용하거나, [구성클래스]를 통해 등록한다.
참고적으로, Spring 프레임워크에서 빈은 싱글톤 방식으로 존재한다. 즉, 인스턴스가 하나라는 뜻이다. 단, 매번 새로운 인스턴스를 생성하도록 설정할수도 있다.
@Configuration
public class 구성클래스 {
@Bean
public 무기 a() {
return new 무기();
}
@Bean
public 군인 b() {
return new 군인();
}
}
클래스 레벨에 @Configuration 애노테이션을 추가하면 이 클래스는 Spring 프레임워크 입장에서 '구성 클래스'가 된다. 구성클래스들의 메소드에는 @Bean 애노테이션을 붙여주며, 각각의 메소드가 반환하는 인스턴스가 Spring 프레임워크에 빈으로 등록된다. 각각의 인스턴스가 실제로 만들어지는 시점은 그들이 최초로 사용되는 순간다.
위와 같이 별도의 구성클래스에 명시하지 않고, 무기와 군인 클래스 레벨에 @Component 애노테이션(혹은 다른 스테레오타입 애노테이션)을 추가함으로써 둘을 빈으로 등록할수도 있다. 아래와 같이 간단하게 말이다.
@Component
public class 무기 {}
@Component
public class 군인 {}
당연히, 두 방법을 모두 사용해서 빈을 등록할 수도 있다.
public class 무기 {
}
public class 군인 {
}
//무기와 군인은 구성클래스를 통해 등록
@Configuration
public class ConfigurationClass {
@Bean
public 무기 a() {
return new 무기();
}
@Bean
public 군인 b() {
return new 군인();
}
}
//몬스터는 스테레오타입 애노테이션을 통해 등록
@Component
public class 몬스터 {
}
위에서 보여지는 코드들에는 무기, 군인, 몬스터 빈을 실제로 사용하는 부분이 존재하지 않는다. 사실, Spring프레임워크는 이들이 최초로 사용되는 시점에 실제 인스턴스를 생성하고 이를 컨테이너에 등록한다. 위 로직에는 이들을 '사용'하는 부분이 없으며, 따라서 위의 코드만으로는 실제 빈이 생성되지 않는다.
위의 코드들은 단순히 빈들을 등록하는 방법에 대해서만 보인 것이다. 그러니까 위의 코드들은 단순히 '등록 명세서' 역할에 지나지 않는 것이다. 실제로 빈들을 등록해서 사용하는 전체 코드는 다음 절에서 확인할 것이다.
빈들의 연결을 명세할때에도 마찬가지로 두가지 방법이 있다. 구성클래스를 통해 연결을 명시하는 방법과, @Autowired 애노테이션을 통해 연결을 명시하는 방법을 사용할 수 있다. 당연히 두 방법을 혼용할 수도 있다.
@Configuration
public class ConfigurationClass {
@Bean
public 무기 a () {
return new 무기();
}
@Bean
public 군인 b () {
return new 군인(a());
}
}
위와 같이 구성클래스를 구성하면, Spring프레임워크는 군인 빈의 인스턴스가 생성될 때 자동으로 무기 빈의 인스턴스도 생성하고, 군인 빈의 생성자를 통해 무기 빈의 참조를 전달한다.
이어서 @Autowired 애노테이션을 통해 Spring 프레임워크에게 빈들의 연결을 명세 하는 방법을 알아보자. Spring 프레임워크가 하는 일은 자신의 컨테이너 안에 있는 빈들을 서로 연결해주는(참조를 확보해주는) 일이기 때문에, 당연히 연결되는 모든것은 빈이어야 한다. 아래는 TestController에 무기와 군인을 연결하는 것을 보여주는데, TestController도 같은 이유로 빈이어야 하기 때문에 @Component 애노테이션이 붙어있다.
@Component
public class TestController {
@Autowired
public 군인 sol;
@Autowired
private 무기 arr;
}
혹은 아래처럼 작업할수도 있다. 최근에는 아래와 같이 생성자를 통해 명시하는쪽이 선호된다.
@Component
public class TestController {
public 군인 sol;
private 무기 wep;
Public TestController(@Autowired 군인 s, @Autowired 무기 w) {
this.sol = s;
this.wep = w;
}
//Spring 4.3 이상부터는 생성자 인자의 @Autowired는 생략 가능하다.
//인자에 붙이지 않고, 그냥 생성자 위에 붙일수도 있다. 그래도 생략 가능하다.
}
이와 같이 작성하면, TestController 빈이 사용될 때 군인과 무기 빈의 참조가 TestController의 생성자에 전달된다. 이를 "군인과 무기가 TestController에 주입되었다"고 표현한다. 이는 Spring 프레임워크의 기반이 되는 의존성 주입 패턴을 기반으로 한 표현인데, 이에 대해 궁금하다면 따로 찾아보도록 하자. 여기서는 직관적으로 참조의 획득으로 이해하면 된다.
또한, 여기서도 빈들을 사용하는 코드는 존재하지 않는다. 때문에 실제 빈이 존재하지도 않으며, 위의 코드는 빈을 등록할 때 그랬던 것처럼 '명세서'역할에 지나지 않는다. 빈을 등록하는 코드를 '등록 명세서'라고 불렀으니, 위의 코드는 '연결 명세서'정도로 알아두면 되겠다. 연결 방식이 명세되어있기는 하지만, 실제로 일어난 일은 아직 아무것도 없다.
*명세서'는 공식적으로 통용되는 용어가 아니다
추가적으로, 개발자가 그냥 new TestController()를 수행했을때는 아무 일도 벌어지지 않는다는 점에 주의하자. 생성자 방식을 사용했다면 그나마 인자들도 직접 생성해 전달할 수 있겠지만, 그럴거면 Spring 프레임워크를 사용할 이유가 없다. 변수에 @Autowired를 사용하는 방식으로 작업했다면 직접 new를 통해 TestController를 생성할 경우, 당연히 이 객체의 두 변수에는 그냥 null이 할당된다.
같은 일을 하는데 두가지 이상의 방법이 존재하는것은 분명 바람직한 일이 아니다. 그런데 왜 Spring 프레임워크는 빈들을 등록하고 연결하는데 서로 다른 두가지 방법을(xml까지 하면 3가지) 제공할까? 하지만 이 경우는 그런 중복이 아니다. 사실, 대부분의 프로젝트에서는 두가지 방법이 혼용될 수 밖에 없다.
예를 들어, 우리는 컨트롤러 클래스에 @Controller 애노테이션을 추가하여 손쉽게 MVC의 컨트롤러를 빈으로써 등록할 수 있었다. 하지만 구성클래스를 통해서는 컨트롤러를 등록할 수가 없다. @Controller 애노테이션을 사용할 수 없기 때문이다. @Controller 애노테이션은 해당 클래스에 붙여야 한다.
반대로, 빈 등록/연결 명세서에 스테레오타입 애노테이션만 이용할 수 있다고 해도 애로사항이 발생한다. 저장소를 사용하는 시스템을 예로 들어보자. 이 시스템은 구동되는 환경에 따라서 메모리를 DB로 사용할 수도, 실제 DB를 사용할수도 있다고 가정하자.
이 경우에는 최소한 두개의 저장소 클래스가 존재하게 된다. 예를 들어 MemoryDB클래스와 SqlDB 클래스가 있다고 가정하자. 이 둘에게 둘 다 @Repository 애노테이션을 추가하게 된다면(이는 @Controller와 마찬가지로, 하나의 특화된 @Component 애노테이션이다) 항상 두개의 빈이 모두 만들어 질 수 밖에 없다.
더구나 서비스가 이를 주입받아 사용하려 할 때, 둘은 같은 인터페이스를 갖고 있기 때문에 정확히 어떤 빈을 사용해야 하는지 알 수가 없다. Spring 프레임워크가 어떤 저장소 빈을 서비스에 연결해야 하는지 알 방법이 없기 때문이다. 이 경우에는 구성클래스를 사용할 수 밖에 없다. 이 저장소 클래스를 주입받는 내용(즉, 연결하는 내용)을 구성클래스에 명세해 두면, 구성 클래스에서 저장소 클래스를 명확하게 명시할 수 있고, 필요한 경우 한줄 수정으로 쉽게 전환할 수도 있다.
이처럼, 애노테이션 기반 등록(공식적으로는 '컴포넌트 서치를 통한 자동 구성'이라고 한다)과 구성클래스 기반 등록은 그 용도에서 차이를 보이며, 둘 다 유의미하다. 둘 다 사용할 줄 알아야 한다는 뜻이다.
본 절에서는 Spring 프레임워크를 통해 시스템을 만들 때의 기본적인 패턴, 즉 컴포넌트와 컨테이너 구조에 대해 알아보았다. 그리고 컴포넌트(빈)들을 컨테이너에 등록하는 방법과, 컴포넌트들의 연결을 명세하는 방법에 대해서 공부했다. 하지만 이들 컴포넌트를 실제로 사용해보지는 않았다. 빈들이 최초 사용되는 시점에 실제 인스턴스화 되고 컨테이너에 등록되기 때문에, 지금으로써는 명세서만 존재하지 실제로 이뤄지는 작업은 아무것도 없는 셈이다.
따라서 다음 절에서는 실제로 이 빈들을 사용해볼 것이다. 등록이 명세되어있는 빈들이 사용되면 Spring 프레임워크는 이들의 실제 인스턴스를 생성한 다음, 연결이 명세되어있는 인스턴스들의 변수(혹은 생성자 인자)에 전달하여 해당 객체의 로직에서 해당 참조를 사용할 수 있게 해준다.
짤막 팁 (0) | 2021.05.25 |
---|---|
서블릿에서 스프링 MVC까지 - 2 (0) | 2021.05.21 |
서블릿에서 스프링 MVC까지 - 1 (0) | 2021.05.12 |
[Spring 해석] 2장. Spring MVC - 1절. 뷰와 컨트롤러의 상호작용 (0) | 2020.09.17 |
[Spring 해석] 1장. Spring이 시스템을 구성하는 방법 - 2절. 빈을 실체화 시키고 사용하기 (0) | 2020.09.16 |