세상에 나쁜 코드는 없다

스프링 컨테이너에 빈을 등록하는 방법 본문

웹개발/백엔드

스프링 컨테이너에 빈을 등록하는 방법

Beomseok Seo 2023. 9. 14. 12:38

📖

개요

저번 글인 ‘의존주입 Dependency Injection’에서 어떻게 의존성을 관리하여 프로그램을 유연하게 만드는지 살펴봤습니다. 저번 장의 예제인 LogService는 스프링의 도움을 받지 않고 만들었습니다.

public static void main(String[] args) {
	SpringApplication.run(DiApplication.class, args);
	LogService logService = new LogService(Injector.getLogDecorator());
	logService.log("Hello World!");	
}

스프링의 도움을 받지 않았기 때문에 우리는 애플리케이션에서 필요한 객체를 직접 생성해주었으며, 그 과정에서 필요로 하는 객체를 판단하여 생성자에 주입해주었습니다. 이 과정은 스프링 컨테이너에 의해서 수행되는 부분으로, 이후 글에서는 어떻게 스프링의 기능을 사용하여 이러한 작업들을 위임할 수 있는지 알아보겠습니다.

Spring Bean & Spring Container

Spring Bean

스프링 빈은 스프링 컨테이너에 의해 관리되는 인스턴스를 의미합니다.

스프링 컨테이너는 스프링 빈을 만들어 객체의 생성과 소멸과 같은 라이프사이클을 관리하고 객체지향의 여러 원리를 쉽게 적용시킬 수 있게 합니다.

스프링 빈은 다음과 같은 특징을 갖습니다.

  1. 여러 Scope 를 지원한다

여기서 Scope란 bean object가 생성되고 관리되며 소멸되는 방식이자 사이클을 의미합니다. 지원되는 Scope로는 Singleton, Prototype, Request, Session 등이 있습니다.

스프링은 서버 환경에서 사용되기 위해 만들어진 기술입니다. 서버에서 다수의 클라이언트의 요청을 처리할 때마다 관련된 객체들을 생성하고 삭제하는 작업을 한다면 부하를 적절히 처리하지 못할지도 모릅니다. 따라서 스프링의 기술은 컴포넌트 오브젝트를 시스템 내에 하나만 유지하여 효율적으로 동작할 수 있도록 합니다.

스프링 빈으로 등록되는 객체는 싱글톤 패턴에 대한 어떤 코드도 작성되지 않지만, 스프링 컨테이너에 의하여 마치 싱글톤처럼 사용됩니다. 스프링 빈이 싱글톤으로 관리되는 것을 알고있는 것은 웹개발을 할때 알고있어야할 중요한 요소 중 하나입니다.

  1. 이름을 갖는다.

스프링 빈은 이름과 오브젝트의 쌍을 이루어 스프링 컨테이너에 저장됩니다. 기본값으로 등록하면 관례에 의하여 클래스명의 제일 앞글자를 소문자로 한 이름을 갖게 됩니다.

이름 - 오브젝트 쌍 관계이기 때문에 한 클래스의 여러 인스턴스가 서로 다른 이름을 통해 빈으로 등록될 수 있습니다.

  1. 의존관계 주입을 받을 수 있다

우리가 직접 작성했던 Injector에 의해 수행되는 의존성 주입이 스프링 컨테이너에 의해 수행됩니다. 스프링 빈은 자신을 필요로 하는 곳에 주입되기도 하고, 필요로 하는 빈을 주입받기도 합니다.

Spring Container

스프링 컨테이너(=Spring DI Container, Spring IoC Container)는 스프링 빈의 생명 주기를 관리하며, 생성된 스프링 빈들에게 추가적인 기능을 제공하는 역할을 합니다.

날것의 객체지향 프로그래밍에서는 객체의 생성/사용/소멸을 직접 관리해줘야하지만, 스프링을 이용하면 스프링 컨테이너가 이 역할을 대신 해줍니다.

또한 스프링 컨테이너는 런타임 과정에서 객체간 의존관계에 대한 설정을 해줍니다.

스프링 프레임워크에서는 ApplicationContext라는 인터페이스를 구현한 여러 스프링 컨테이너를 제공합니다. 우리가 제공하는 설정 값에 따라 스프링은 런타임에 컨테이너를 만들고 스프링 빈을 관리하게 됩니다.

스프링 컨테이너가 빈을 등록하기 위해 필요로 하는 정보

스프링 컨테이너가 빈을 등록하기 위해서는 2가지 정보가 필요합니다.

첫번째 정보는 웹 컴포넌트 클래스입니다. 클래스의 인스턴스가 빈으로 등록되는 것이기 때문에 클래스의 정보가 필요한 것은 너무나도 당연합니다.

두번째 정보는 웹 컴포넌트 객체를 어떻게 빈으로 등록할지에 대한 정보를 담고있는 Configuration Metadata입니다.

Configuration Metadata에는 어떤 클래스를 빈으로 등록할 것인지, 어떤 Scope로 등록할 것인지, 어떤 이름으로 등록할 것인지, 어떤 의존관계를 갖게 할 것인지 등이 포함되어있는 데이터 집합입니다. 이 데이터 집합은 XML 파일, 자바 클래스, 애노테이션 등의 형태를 가질 수 있습니다.

스프링 컨테이너는 위 두 정보를 조합하여 스프링 빈을 만들어 냅니다.

아래에서 다룰 두가지 방식

  1. @Configuration 애노테이션빈 팩토리 메소드
  1. @ComponentScan@Component 애노테이션

은 스프링 컨테이너에 클래스와 Configuration Meatadata를 제공하여 웹 컴포넌트 객체를 어떻게 빈으로 등록할지에 대해 알려주는 힌트와 같습니다.

@Configuration 과 빈 팩토리 메소드를 통한 빈 생성

@Configuration 애노테이션과 빈 팩토리 메서드를 통해 빈을 등록하는 방법에 대해서 알아보겠습니다.

하위 디렉터리인 config을 만들고 AppConfig.java를 작성합니다.

@Configuration
public class AppConfig {
	@Bean
	public LogDecorator logDecorator() {
	  return new ComplexLogDecorator();
	}
	
	@Bean
	public LogService logService() {
	  return new LogService(logDecorator());
	}
}

스프링 컨테이너는 @Configuration이 붙은 클래스를 빈 설정 정보가 있는 클래스로 인지하고, 이 클래스(AppConfig)를 빈으로 등록한 뒤 내부의 빈 팩토리 메서드를 읽어 빈들을 생성합니다.

빈 팩토리 메서드는 클래스와 Configuration Metadata를 스프링 컨테이너에 제공합니다. logDecorator() 메서드는 “ComplexLogDecorator 인스턴스를 logDecorator라는 이름으로 스프링 컨테이너에 싱글톤으로 등록해줘” 라는 의미를 갖고 있습니다. 물론 이러한 설정 정보는 메서드의 이름, @Bean 애노테이션의 엘리먼트 작성등으로 변경할 수 있습니다. 아무것도 작성하지 않은 경우 관습에 의한 기본값들이 설정됩니다.

logService()는 LogService 인스턴스를 스프링 빈으로 등록하기 위한 메서드입니다. 저번 글에서 LogService에서 의존주입 개체에 대한 의존을 없애기 위해 생성자를 통해 LogDecorator를 주입받기로 했기 때문에, 여기서 LogService를 생성할 때 파라미터에 필요로하는 것을 주입해주어야 합니다. 이 곳에 위에서 작성한 logDecorator()를 작성해줌으로써, 스프링 빈으로 관리되는 ComplexLogDecorator가 주입됩니다.

이런 의존주입 방식은 직관적으로 봤을 때 싱글톤으로 관리되지 못할 것 같아 보입니다. 만약 다른 Service 빈에서 logDecorator를 주입받는다면 그곳에서 logDecorator()가 또 호출될 것이고, 그렇다면 new 연산자를 통해 새로운 인스턴스가 생성될 것이기 때문입니다. 스프링은 이러한 문제를 프록시를 통해 해결합니다. 스프링의 이 프록시 패턴으로 어떻게 빈들을 싱글톤으로 관리하는지에 대해서는 다음에 다뤄보겠습니다.

ApplicationRunner를 통한 빈 등록 확인

스프링 컨테이너에 빈이 정상적으로 등록됐는지 확인하는 방법은 여러가지가 있지만, 그 중 학습목적으로 적합해 보이는 ApplicationRunner를 사용하여 콘솔에서 시각적으로 등록 여부를 확인해보겠습니다.

ApplicationRunner는 스프링에서 제공하는 인터페이스로 run이라는 하나의 메서드를 가지고 있으며, 스프링은 스프링의 초기화가 완료된 이후 ApplicationRunner를 구현한 클래스의 run 메서드를 일괄적으로 수행시킵니다. 이 때 ApplicationRunner의 구현체는 꼭 스프링 빈으로 등록되어있어야 합니다.

public class MyApplicationRunner implements ApplicationRunner {

	private final LogService logService;
	
	public MyApplicationRunner(LogService logService) {
	  this.logService = logService;
	}

	@Override
	public void run(ApplicationArguments args) throws Exception {
	  logService.log("Hello World!");
	}
}

MyApplicationRunner를 스프링 빈으로 등록한다면 LogService 빈을 주입받고 스프링의 초기화가 끝난 이후 logService.log()를 수행할 것입니다.

@Configuration
public class AppConfig {
	...
	
	@Bean
	public ApplicationRunner myApplicationRunner() {
		return new MyApplicationRunner(logService());
	}
}

AppConfig에 MyApplicationRunner를 빈으로 등록하는 빈 팩토리 메서드를 생성하고, 파라미터로 주입될 빈을 넘겨주었습니다.

기존에 남아있는 main 메서드에 작성한 내용들을 삭제해주고 프로그램을 실행시키면 다음과 같은 결과가 나옵니다. 로그가 찍히고 ComplexLogDecorator 가 잘 작동된 것을 보면 스프링 빈으로 잘 등록된 것으로 유추해볼 수 있습니다.

ApplicationContext 주입받기

@Configuration 애노테이션이 붙은 클래스는 스프링이 읽어들여 스프링 빈으로 관리하게 됩니다. 그렇기 때문에 AppConfig 역시 스프링에 의해 라이프사이클이 관리되며, 빈을 주입받을 수 있는 개체가 됩니다.

ApplicationContext는 스프링 컨테이너의 구현체로 스프링 빈은 아니지만, 특별하게 관리되어 스프링 빈에 주입될 수 있습니다. 저희는 ApplicationRunner에서 ApplicationContext를 사용하기 위해서 먼저 AppConfig에 ApplicationContext를 주입받은 후, 주입받은 것을 ApplicationRunner로 넘겨주겠습니다.

@Configuration
public class AppConfig {

	private final ApplicationContext ac;
	
	public AppConfig(ApplicationContext ac) {
	  this.ac = ac;
	}
	...
	@Bean
	public ApplicationRunner myApplicationRunner() {
		return new MyApplicationRunner(ac);
	}
}

AppConfig 빈이 생성되는 과정에서 ApplicationContext를 주입받을 수 있게 생성자에 파라미터를 놓아주고 주입된 값을 멤버변수에 저장해놓습니다. 스프링 컨테이너는 AppConfig를 빈으로 등록하는 과정에서 ApplicationContext를 주입합니다. 이후 ApplicationRunner를 등록하는 과정에 ApplicationContext가 주입될 수 있도록 합니다. MyApplicationRunner는 다음과 같이 수정되어야 합니다.

public class MyApplicationRunner implements ApplicationRunner {

	private final ApplicationContext ac;
	
	public MyApplicationRunner(ApplicationContext ac) {
		this.ac = ac;
	}
	
	@Override
	public void run(ApplicationArguments args) throws Exception {
		LogService logService = ac.getBean("logService", LogService.class);
		logService.log("Hello World");
	
		Arrays.stream(ac.getBeanDefinitionNames()).forEach(System.out::println);
	}
}

MyApplicationRunner는 ApplicationContext를 주입받은 후, 컨테이너에서 LogService 빈을 꺼내 log()를 호출합니다. 맨 아래 문장은 스프링 컨테이너에 있는 모든 빈의 이름을 출력하는 메서드입니다. 수행 결과는 다음과 같습니다.

스프링의 초기화가 끝나고 스프링 컨테이너를 주입받은 ApplicationRunner가 수행한 내용들입니다. 컨테이너에서 LogService빈을 성공적으로 가져와 log()를 호출한 내용이 보이며, 이후 내용들은 컨테이너에 저장되어있는 빈들의 이름들입니다. 우리가 등록했던 logDecorator, logService, myApplicationRunner가 빈으로 잘 등록되어있음을 확인할 수 있습니다.

정리

@Configuration 클래스와 빈 팩토리 메서드로 빈을 등록하는 방법에 대해 알아봤습니다. 또한 이것이 성공적으로 등록됐는지 확인하기 위해서 MyApplicationRunner를 추가적으로 등록하고, 이것이 다른 빈들을 주입받아 동작하게 만들었습니다.

이러한 빈 등록 방법은 직관적이지만 매번 빈 컴포넌트를 생성할 때마다 빈 팩토리 메서드를 생성해줘야 한다는 단점이 있습니다. 대규모 웹애플리케이션에서 수많은 빈들이 만들어질텐데 이것을 다 Configuration 클래스에서 관리하기란 여간 버거운 일이 아닙니다. 이러한 문제를 해결하기 위해 애노테이션 기반으로 Configuration Metadata를 제공하는 방법이 존재합니다.

사실 애노테이션 기반의 빈 등록은 이미 위 예제에 존재하는 내용입니다. @Configuration 애노테이션을 부착한 AppConfig은 우리가 별도로 빈 팩토리 메서드를 작성해주지 않았음에도 스프링 컨테이너가 이를 읽어서 빈으로 등록했습니다. 또한 빈을 생성하는 과정에서 ApplicationContext를 주입받기도 했습니다. 이러한 등록은@ComponentScan과 @Component 애노테이션을 통해 수행되는데, 아래에서는 이 방법으로 빈을 등록하는 것에 대해 알아보겠습니다.

@ComponentScan, @Component로 빈 생성

@ComponentScan을 통한 빈 생성은 비교적 그 방법이 간단합니다. 빈으로 등록하고자 하는 클래스에 @Component 애노테이션을 작성해준다면 스프링 컨테이너는 해당 클래스를 읽어내어 빈으로 등록합니다.

새로운 빈 등록방법을 사용해보기 위해 기존에 작성했던 AppConfig.java는 삭제하고 진행합니다.

LogService, ComplexLogDecorator, MyApplicationRunner의 클래스 위에 @Component 애노테이션을 작성해줍니다.

@Component
public class MyApplicationRunner implements ApplicationRunner {

	private final ApplicationContext ac;
	
	public MyApplicationRunner(ApplicationContext ac) {
    this.ac = ac;
	}
	
	@Override
	public void run(ApplicationArguments args) throws Exception {
    LogService logService = ac.getBean("logService", LogService.class);
    logService.log("Hello World");
	
		Arrays.stream(ac.getBeanDefinitionNames()).forEach(System.out::println);
	}
}

이후 프로그램을 구동해보면 기존과 똑같은 결과가 나오는 것을 확인할 수 있습니다. 이는 @Component 애노테이션이 부착된 클래스를 스프링 컨테이너가 읽어들여 빈으로 등록했기 때문입니다. 이 방식에서도 Configuration Metadata를 커스텀하여 제공할 수 있으며, 제공하지 않는다면 기본값으로 전달됩니다.

사실 @Component 애노테이션을 통해 빈을 등록하려면 @ComponentScan 애노테이션이 붙은 빈 설정 클래스가 존재해야합니다. 하지만 지금 예제에서는 어떠한 부분에서도 @ComponentScan 애노테이션을 사용하지 않았는데, 이는 이미 @SpringBootApplication 애노테이션에서 이를 제공하고 있기 때문입니다.

@SpringBootApplication
public class DiApplication {

	public static void main(String[] args) {
		SpringApplication.run(DiApplication.class, args);
	}
}
@SpringBoortApplication.java

@SpringBootApplication은 여러 애노테이션의 합성 애노테이션이며, @Configuration 애노테이션과 @ComponentScan 애노테이션을 포함하고 있습니다.

@ComponentScan 애노테이션의 basePackage 속성을 통해 스캔 대상 패키지를 지정할 수 있으며, 위와같이 속성이 제공되지 않는다면 기본값인 해당 애노테이션이 부착되어있는 클래스의 패키지로 지정됩니다. 이 경우 DiApplication이 존재하는 패키지와 그 하위 패키지에 있는 모든 클래스 중 @Component가 작성되어있는 클래스들을 확인하여 빈으로 등록하게 됩니다.

빈으로 등록하는 과정에서 생성자가 유일하다면, 해당 생성자의 파라미터로 정해진 규칙에 의해 빈을 주입하게 됩니다. MyApplicationRunner의 경우 생성자에 ApplicationContext를 받고 있으므로, 스프링 컨테이너는 컴포넌트 스캔에 의해 빈을 등록하는 과정에서 그것을 주입해줍니다. LogService도 마찬가지로 LogDecorator를 주입받게 되며, 이때 LogDecorator를 구현한 빈은 ComplexLogDecorator 하나밖에 없으므로 ComplexLogDecorator가 자동으로 주입됩니다. 만약 SimpelLogDecorator도 빈으로 등록되어있다면 어떤 빈을 주입해야할지 모호하므로 추가적인 설정이 필요할 것입니다.

합성 애노테이션

앞의 예제에서 AppConfig에 대하여 별도로 빈 팩토리 메서드를 작성해주지 않았음에도 빈으로 등록된 이유는 @Configuration 애노테이션이 @Component 애노테이션을 포함하고 있는 합성 애노테이션이기 때문입니다.

스프링은 이와 같이 다양한 합성 애노테이션들을 제공하여 기능을 확장하는데 사용합니다. @Controller, @RestController, @Service, @Repository 등은 모두 @Component를 포함하는 합성 애노테이션입니다.

컨트롤러는 라우팅 힌트를 제공할 의무가 있는 웹 컴포넌트입니다. 컨트롤러 클래스에 @Controller 애노테이션을 작성하면, 스프링 컨테이너는 이 애노테이션이 @Component 애노테이션의 합성 애노테이션이기 때문에 이를 스프링 빈으로 등록할 것이고, 더하여 @Controller 애노테이션임을 체크하여 이 클래스가 라우팅 정보를 갖고 있다고 판단하여 추가적인 로직을 수행할 것입니다.

@RestController 는 @Controller 과 @ResponseBody 의 합성 애노테이션입니다. ResponseBody 애노테이션은 컨트롤러의 반환값이 그대로 Http Body에 담겨질 것을 설정해주는 애노테이션입니다. @RestController 애노테이션을 통해 자주 사용되는 두개의 애노테이션을 하나로 합쳐 코드를 줄이고 의미를 갖는 애노테이션을 사용할 수 있습니다.

@Service 와 @Repository는 각각의 스프링 빈이 어떤 웹 계층에 있는지 의미를 부여해줍니다. 따라서 처음보는 코드더라도 애노테이션을 봤을때 이 스프링 빈이 어떤 역할을 할지 대강 짐작할 수 있게 해줍니다.

마무리

public static void main(String[] args) {
	SpringApplication.run(DiApplication.class, args);
	
	//LogService logService = new LogService(Injector.getLogDecorator());
	LogService logService = ac.getBean(LogService.class);
	logService.log("Hello World!");	
}

스프링 컨테이너에게 클래스와 Configuration Metadata 정보를 제공하여 빈을 등록하는 두 방법에 대해 알아봤습니다. 위에서 다룬 두 방법을 예제의 맨 처음단계에서 했던 객체의 생성과 의존관계 주입 등의 과정을 스프링 컨테이너에게 위임할 수 있습니다. 이를 통해 스프링 컨테이너가 제공하는 강력한 객체지향의 원리를 제공받아 프로그램에 유연성을 제공할 수 있고 빠르고 효과적으로 개발할 수 있는 계기가 됩니다.

빈을 생성할 때 대부분 애노테이션 기반으로 생성하지만 그 과정에서 세밀한 설정 값 조정이 필요한 경우 빈 팩토리 메서드를 직접 작성하기도 합니다. 따라서 두 방법은 모두 익혀놓아야할 필요가 있습니다.


Uploaded by N2T