Spring

[스프링부트] 빈 컨테이너 생성

hongyb 2024. 4. 8. 01:28

Bean Container 생성 후 Bean을 등록하는 과정에 대해서 알아보자.

 

Bean Contianer

이전에는 서블릿 컨테이너를 생성하고 서블릿을 등록하는 것까지 배웠다. 또 다른 컨테이너인 빈 컨테이너를 생성하고 등록하는 과정에 대해서 알아보겠다.

 

DI

실습을 하기 앞서 http 요청을 처리할 클래스를 2개 생성할 것이다. 

 

첫 번째 클래스는 HelloController이다. "/hello" url의 요청을 처리할 클래스이다. 코드는 다음과 같이 작성한다.

public class HelloController {
    private final HelloService helloService = new HelloService();

    public String hello(String name) {
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}

 

HelloService 코드는 다음과 같다.

public class SimpleHelloService {
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

 

위에 코드를 보면 알 수 있듯이 HelloController가 SimpleHelloService에 의존하고 있다.

위의 예제 코드가 작동하는데 문제가 발생하는 것은 아니지만 클래스간 연관 관계가 생기면 결합도가 높아지는 문제점이 발생한다. 따라서 HelloController는 SimpleHelloService에 의존하게 되는 문제가 발생한다. 이를 해결할 수 있는 디자인 패턴이 Dependency Injection이다. 의존성 주입은 객체 간의 결합도를 낮추는 디자인 패턴으로, 클래스나 객체가 직접 필요로 하는 의존 객체를 직접 생성하거나 관리하지 않고, 외부에서 주입받는 것을 의미한다. 이를 통해 의존 객체의 생성 및 관리를 외부에서 담당함으로써 결합도를 낮추고, 코드의 유연성을 높일 수 있다.

 

HelloService라는 interface를 만든 후 HelloController가 HelloService(인터페이스)에만 의존하도록 설계할 것이다.

public interface HelloService {
    String sayHello(String name);
}

 

또한 SimpleHelloService와 HelloController를 다음과 같이 변경할 것이다.

@RestController
public class HelloController {
    private final HelloService helloService;

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    @GetMapping("/hello")
    @ResponseBody
    public String hello(String name) {
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}
@Service
public class SimpleHelloService implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

@RestController 어노테이션과 @Service 어노테이션에서 @Component가 있기 때문에 각각의 클래스를 빈 컨테이너에 등록한다. 이후 HelloController의 생성자에서 인터페이스인 helloService에 오브젝트를 주입해서 사용할 수 있도록 한다. 

 

클래스 구조는 다음과 같이 변한다.

 

 

Dispatchet Servlet 생성 및 스프링 컨테이너 생성

이전에 등록하고 사용했던 servlet을 사용하지 않고 DispatcherServlet을 사용할 것이다. 또한 스프링 컨테이너(Bean Container)를 만들 것이다. 코드는 다음과 같다.

GenericWebApplicationContext applicationContext = new GenericWebApplicationContext() { 
	@Override
	protected void onRefresh() { 
    	super.onRefresh();
	
    	ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory(); 
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
    	servletContext.addServlet("dispatcherServlet", new DispatcherServlet(this))
		.addMapping("/*");
		});
	webServer.start(); 
    }
};

 

빈 컨테이너는 생성했지만 클래스를 빈 컨테이너에 등록하지 못했기 때문에 정상적으로 요청을 처리하지 못한다. 이를 위해 2가지 방법으로 클래스를 빈 컨테이너에 등록해야 한다.

 

Bean Container 등록

 

첫 번째는 @Bean 어노테이션을 사용해서 Config 클래스에 등록하는 방법이다. 예제 코드는 다음과 같다.

@Configuration
public class HellobootApplication {
	@Bean
	public HelloController helloController(HelloService helloService) {
	return new HelloController(helloService); 
    }

	@Bean
	public HelloService helloService() {
	return new SimpleHelloService(); 
    }
    
    ...

 

@Bean 메소드가 있는 클래스에는 @Configuration  애노테이션을 붙여줘야 한다.

 

다른 방법은 @Component 스캔 방법이다. 등록 대상이 될 클래스에 @Component라고 붙여주면 자동으로 빈 컨테이너에 등록이 된다. 이후 메인 클래스에 @ComponentScan 애노테이션을 붙여줘야 한다. 앞서 말했듯이 @Service와 @RestController에 들어가 보면 @Component가 붙여져 있다. 따라서 HelloController와 SimpleHelloService 클래스는 빈 컨테이너에 등록하지 않아도 된다.

 

Spring Application으로 변환

ServletWebServerFactory와 DispatcherServlet을 직접 생성하는 것이 아닌 Bean 컨테이너에 등록한 후 getBean으로 객체의 인스턴스 값을 가져오도록 변환할 것이다. 코드가 다음과 같이 변한다.

@Configuration
@ComponentScan
public class HellobootApplication {
	@Bean
	public ServletWebServerFactory serverFactory() {
		return new TomcatServletWebServerFactory();
	}

	@Bean
	public DispatcherServlet dispatcherServlet() {
		return new DispatcherServlet();
	}

	public static void main(String[] args) {
		AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
			@Override
			protected void onRefresh() {
				super.onRefresh();

				//servlet container 생성
				//TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
				ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
				DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
				dispatcherServlet.setApplicationContext(this);

				WebServer webServer = serverFactory.getWebServer(servletContext -> {
					servletContext.addServlet("dispatcherServlet",
							//new DispatcherServlet(this)
							dispatcherServlet
					).addMapping("/*");
				});
				webServer.start();
			}
		};
		applicationContext.register(HelloBootApplication.class);
		applicationContext.refresh();
	}
	
	
}

 

main안에 있는 로직을 따로 class를 생성하여 구분한다. class의 이름은 MySpringApplication으로 만든 후 Class 타입과, String[] 타입을 받는 static 메서드를 생성해 준다. static 메서드의 이름은 run이다.

public class MySpringApplication {
    public static void run(Class<?> applicationClass, String... args) {
        //bean container 생성
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
            @Override
            protected void onRefresh() {
                super.onRefresh();

                //servlet container 생성
                //TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
                ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
                DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
                dispatcherServlet.setApplicationContext(this);

                WebServer webServer = serverFactory.getWebServer(servletContext -> {
                    servletContext.addServlet("dispatcherServlet",
                            //new DispatcherServlet(this)
                            dispatcherServlet
                    ).addMapping("/*");
                });
                webServer.start();
            }
        };
        applicationContext.register(applicationClass);
        applicationContext.refresh();
    }
}

 

그렇다면 HelloBootApplication 코드는 다음과 같이 변한다.

@Configuration
@ComponentScan
public class HellobootApplication {
	@Bean
	public ServletWebServerFactory serverFactory() {
		return new TomcatServletWebServerFactory();
	}

	@Bean
	public DispatcherServlet dispatcherServlet() {
		return new DispatcherServlet();
	}

	public static void main(String[] args) {
		MySpringApplication.run(HellobootApplication.class, args);
	}
}

 

main메서드를 살펴보면 우리가 익숙하게 봤던 메서드와 똑같은 것을 볼 수 있다. 바로 SpringApplication.run() 메서드이다. 이전까지 미지의 알지 못했던 SpringApplication.run() 메서드가 어떤 식으로 작동하는지 이해할 수 있게 되었다. 복잡하고 어려워 보였던 Spring Boot도 결국 Servlet container와 Bean Container를 Java 코드로 구현하고 작동하는 것이다. 어렵게 생각할 필요가 없던 것이었다.