들어가기 앞서
Spring에서 MVC 패턴으로 개발할 때, 흔히 Controller, Service, Repository 등을 만들곤 합니다.
그리고 이 그림 1처럼 클래스 명 위에 어노테이션 하나만 딱 달아두면 Main Application 클래스에서 Run 하면 알아서 서버가 돌아갑니다.
하지만, 자바에서 객체를 생성할 때 어떻게 하나요??
그림 2처럼 new 키워드를 사용해 객체를 생성하죠.
우리는 Spring으로 개발할 때, 따로 Main Application 클래스에서 객체를 생성하지 않았습니다.
하지만 Run을 하면 돌아가네요.
이걸 누가 해주는지에 대해 이 글에서 다뤄보겠습니다.
이 글은 공식 문서를 토대로 제 나름대로 정리한 글이라 충분히 틀린 점이 있음을 미리 안내드립니다.
Inversion of Control(IoC)
Spring Framework가 객체를 생성하는 방법에 대해 아시려면, 먼저 Inverison of Control(IoC)에 대해 알고 계셔야 합니다.
IoC는 한국말로 제어의 역전을 뜻합니다.
제어의 역전이 뭘까요??
간단한 예시를 들어볼게요.
class A {
private B b;
public A() {
this.b = new B(); // A가 B의 생성 및 관리를 담당
}
public void doSomething() {
b.action();
}
}
만약, A라는 클래스가 B의 클래스가 필요하다고 한다면, 생성자를 통해 다음과 같이 코드를 작성할 수 있습니다.
위 상황을 A와 B가 강하게 결합됐다고 말합니다.
왜 그러냐면, B 생성에 대한 책임이 존재하기 때문에, B 대한 변경이 생겼을 때, A 또한 수정해야 합니다.
이는 각 객체들을 관리해야 하는 FrameWork 입장에선 유지보수 및 작동에 큰 어려움이 생깁니다.
그러면 어떻게 A가 B를 의존하되 강한 결합성을 끊어낼 수 있을까요??
IoC가 그렇게 등장하였습니다.
의존성 주입(Dependency Inject, DI)
Spring Framework에선 강한 결합성을 느슨하게 하기 위해 의존성 주입(Dependency Inject, DI)을 사용합니다.
Spring 에선 의존성 주입에 3 가지를 사용한다고 공식문서에서 언급하고 있습니다.
- 생성자 주입 방식
- 팩토리 메서드의 인자 주입 방식
- 객체 인스턴스가 생성된 후 또는 팩토리 메서드에서 반환된 후 Setter 이용하는 방식
이에 대한 예시들을 보여드리겠습니다.
AppConfig에서 의존성 주입을 어떻게 하는 지만 보시면 됩니다.
(1) 생성자 주입 방식
IoC를 구현하는 방법 중 하나로, 객체가 필요한 의존성을 직접 생성하지 않고, 외부 컨테이너로부터 주입받는 방식입니다.
public class ConstructorInjection {
private final Dependency dependency;
// 생성자 주입
public ConstructorInjection(Dependency dependency) {
this.dependency = dependency;
}
public void execute() {
dependency.action();
}
}
@Configuration
public class AppConfig {
@Bean
public Dependency dependency() {
return new Dependency();
}
@Bean
public ConstructorInjection constructorInjection(Dependency dependency) {
return new ConstructorInjection(dependency);
}
}
아까와 다르게 생성자의 파라미터를 통해 클래스를 주입받는 모습을 보실 수 있습니다.
이를 통해 두 클래스의 강한 결합도를 낮출 수 있습니다.
(2) 팩토리 메서드의 인자 주입 방식
public class DependencyFactory {
public static Dependency createDependency(String type) {
if ("special".equals(type)) {
return new Dependency(); // 특별한 의존성 생성
}
return new Dependency();
}
}
public class FactoryInjection {
private final Dependency dependency;
public FactoryInjection(Dependency dependency) {
this.dependency = dependency;
}
public void execute() {
dependency.action();
}
}
@Configuration
public class AppConfig {
@Bean
public Dependency dependency() {
return DependencyFactory.createDependency("special");
}
@Bean
public FactoryInjection factoryInjection(Dependency dependency) {
return new FactoryInjection(dependency);
}
}
Factory 메서드를 거치기 때문에, 복잡한 객체 생성 로직을 캡슐화할 수 있습니다.
(3) 객체 인스턴스 생성 및 반환 이후 Setter 사용하는 방식
public class PropertyInjection {
private Dependency dependency;
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
public void execute() {
dependency.action();
}
}
@Configuration
public class AppConfig {
@Bean
public Dependency dependency() {
return new Dependency();
}
@Bean
public PropertyInjection propertyInjection() {
PropertyInjection injection = new PropertyInjection();
injection.setDependency(dependency());
return injection;
}
}
Setter를 사용하여 필요한 의존성만 부여할 수 있다는 장점이 있으나, 객체가 완전히 초기화되기 전에 호출될 가능성이 있습니다.
그러면 Spring에선 IoC를 어떻게 실현하는가?
Spring에서 제공되는 인터페이스, ApplicationContext
공식 문서에선 org.springframework.beans와 org.springframework.context 패키지는 Spring Framework의 IoC 컨테이너의 기반이 된다고 설명하고 있습니다.
이 중 BeanFactory는 객체에 관한 DI를 포함한 설정(Configuration)에 특화된 인터페이스를 제공하지만, ApplicationContext 인터페이스가 BeanFactory를 포함하고 있는 superset 이라고 표현하고 있습니다.
그러면 ApplicationContext는 BeanFactory에서 뭘 더 추가됐을까요??
- 스프링 AOP 기능과의 더 쉬운 통합
- 메시지 리소스 처리(여러 나라 문자에 사용)
- 이벤트 발행
- WebApplicationContext와 같은 웹 애플리케이션에 사용되는 애플리케이션 계층의 특정 컨텍스트
간단하게 ApplicationContext 가 DI에 필요한 인터페이스를 제공한다고 아시면 됩니다.
Bean??
스프링에서 애플리케이션의 핵심을 구성하고 Spring IoC Container에 의해 관리되는 객체
쉽게 말해 DI로 생성된 인스턴스를 말합니다.
BeanFactory는 이를 관리하는 기능을 가지고 있다고 생각하시면 됩니다.
Spring을 돌아가게 하는 핵심인 Spring IoC Container
Spring Framework에서 ApplicationContext 를 이용하여 결국 애플리케이션이 돌아가도록 객체를 주입해 주는 주체를 Spring IoC Container 라고 부릅니다.
하지만 Spring IoC Container 자체에서 어느 객체를 Bean으로 만들어야 하는지 모르고, Bean에 어떤 설정을 해야 하는지 판단을 못합니다.
그렇기 때문에, 개발자가 직접 Configuration Metadata를 설정해줘야 합니다.
Configuration Metadata는 어플리케이션 개발자 입장에선 어느 객체가 어떤 다른 객체의 의존성을 가지고 있는지에 대한 정보입니다.
객체를 만드는데 필요한 정보들이라고 생각하시면 됩니다.
비즈니스 객체와 Configuration Metadata를 Spring Container에서 합쳐 ApplicationContext가 생성이 되는 과정 속에 DI를 진행합니다.
최종적으로 실행 가능한 애플리케이션이 최종적으로 생성이 됩니다.
보다시피 Spring Container의 중요 기능은 Bean 들을 관리하는 것이기 때문에, Bean의 생명 주기를 관리하며 생성된 빈에게 추가적인 기능을 제공합니다.
Configuration Metadata를 설정하는 방법
Configuration Metadata를 설정하는 방식엔 XML, Groovy scripts 등의 외부 파일 이용과 자바로 설정이 있습니다.
(기존 Spring Framework에선 XML만 가능했으나, SpringBoot에 와선 자바 설정 방식이 생김)
(1) XML 설정 방식
<beans xmlns="http://www.springframework.org/schema/beans">
<bean id="dependency" class="com.example.Dependency" />
<bean id="constructorInjection" class="com.example.ConstructorInjection">
<constructor-arg ref="dependency" />
</bean>
</beans>
(2) 자바 설정 방식
@Configuration
public class AppConfig {
@Bean
public Dependency dependency() {
return new Dependency();
}
@Bean
public ConstructorInjection constructorInjection(Dependency dependency) {
return new ConstructorInjection(dependency);
}
}
최근엔 자바 설정 방식을 많이 사용하며
자바 설정 방식엔 또 크게 두 가지의 방식이 있습니다.
Configuration Metadata를 자바로 설정하는 방법
(1) 컴포넌트 내부에서 어노테이션을 이용하여 사용하는 방식
클래스와 메서드에 @Component, @Service, @Repository, @Controller, @Autowired, @Qualifier와 같은 어노테이션을 추가하여 구성합니다.
(2) 외부 클래스에서 어노테이션을 이용하는 방식
@Configuration 어노테이션이 달린 클래스에서 @Bean 어노테이션을 사용하여 명시적으로 빈을 정의하는 것으로 수동으로 작성하는 방식입니다.
두 방식 모두 자주 사용하니 알고 계시면 좋습니다.
출처
Container Overview :: Spring Framework
Spring - BeanFactory와 ApplicationContext의 차이점 - GeeksforGeeks