독립 실행형 서블릿 애플리케이션 살펴보기 (2)
들어가며
- 해당 포스팅은 토비의 스프링 부트 - 이해와 원리를 학습하며 정리한 글입니다.
- 토비님의 강의 내용과 제가 이해한 방식대로 작성된 부분이 혼합 되어 작성되어 있습니다.
1. 스프링 컨테이너 사용

-
스프링 컨테이너는 애플리케이션 로직이 담긴 평범한 자바 오브젝트, POJO와 구성 정보(Configuration Metadata를 런타임에 조합해서 동작하는 최종 애플리케이션을 만들어냅니다.
-
코드로 스프링 컨테이너를 만드는 간단한 방법은 컨테이너를 대표하는 ApplicationContext를 구현한 GenericApplicationContext를 이용합니다.

- 다음과 같은 구조를 띄도록 합니다.
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean(HelloController.class);
applicationContext.refresh();
-
GenericApplicationContext 를 통해 스프링 컨테이너에 등록할 빈 오브젝트 클래스 정보를 직접 등록할 수 있습니다.
-
hellocontroller 클래스를 등록하고, 이를 참고해서 컨테이너가 빈 오브젝트를 직접 생성합니다. (.registerBean())
-
이제 컨테이너에 필요한 정보가 등록되었다면, refresh() 를 통해 초기화 작업을 진행합니다.
HelloController helloController = applicationContext.getBean(HelloController.class);
-
이제 등록한 bean을 사용한다
-
ApplicationContext의 getBean() 메소드를 이용하여 컨테이너가 관리하는 bean 오브젝트를 가져옵니다.
-
bean의 타입(class, interface) 정보를 이용해서 해당 타입의 bean을 요청합니다.
코드
package tobyspring.helloboot;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class TobyspringApplication {
public static void main(String[] args) {
// 스프링 컨테이너 생성
GenericApplicationContext applicationContext = new GenericApplicationContext();
// HelloController를 등록하고, 초기화를 통해 적용한다
applicationContext.registerBean(HelloController.class);
applicationContext.refresh();
// servlet 컨테이너를 올린다
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
// frontcontroller 서블렛을 등록한다
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("frontcontroller", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 인증, 보안, 다국어, 등 공통 기능 처리
if (req.getRequestURI().equals("/hello") && req.getMethod().equals(HttpMethod.GET.name())) {
String name = req.getParameter("name");
HelloController helloController = applicationContext.getBean(HelloController.class);
String ret = helloController.hello(name);
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println(ret);
} else if (req.getRequestURI().equals("/user")) {
//
} else {
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
});
webServer.start();
}
}
2. 의존 오브젝트 추가
-
스프링 컨테이너는 싱글톤 패턴과 유사하게 애플리케이션이 동작하는 동안 딱 하나의 오브젝트만을 만들고 사용되게 만들어 줍니다.
-
이러한 점에서 스프링 컨테이너는 싱글톤 레지스트라고 합니다.
싱글톤?
-
싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스가 하나만 생성되도록 보장하는 패턴을 말합니다.
-
이 패턴은 전역 변수를 사용하지 않고 객체를 한 번만 생성하여 시스템 내의 여러 컴포넌트에서 공유할 수 있도록 합니다.
싱글톤 패턴의 특징
-
클래스의 인스턴스가 단 하나만 존재니다. 클래스로부터 오직 하나의 인스턴스만 생성되며, 이 인스턴스에 대한 전역 접근 지점을 제공합니다.
-
쉽게 접근 가능한 전역 인스턴스니다. 다른 클래스에서 쉽게 접근할 수 있으며, 항상 동일한 인스턴스를 사용합니다.
-
생성자가 private으로 설정되어, 외부에서 임의로 인스턴스를 생성할 수 없고, 클래스 자체가 인스턴스 생성을 책임집니다.
Spring Boot에서의 싱글톤 패턴
-
Spring Framework에서는 싱글톤 패턴이 매우 중요한 역할을 합니다.
-
Spring 컨테이너는 기본적으로 각각의 Bean을 싱글톤으로 관리하여, 애플리케이션 내에서 한 번 생성된 Bean은 계속해서 재사용 됩니다. 이는 리소스 사용을 최적화하고 성능을 향상시키는 데 도움을 줍니다.
Spring Boot에서 싱글톤 사용 예
-
Bean 정의와 생성 Spring Boot에서 Bean을 정의할 때, @Bean 어노테이션을 사용하거나 컴포넌트 스캔(Component Scan)을 통해 자동으로 Bean을 등록할 수 있습니다. 이러한 Bean들은 기본적으로 싱글톤으로 생성됩니다.
-
ApplicationContext의 역할 ApplicationContext는 Bean의 생성과 관리를 담당니다. 이 컨텍스트는 애플리케이션 실행 동안 생성된 모든 Bean을 캐시하고, 요청이 있을 때마다 동일한 인스턴스를 반환합니다.
-
싱글톤 보장 Spring은 내부적으로 싱글톤 패턴을 활용하여, Bean이 요청될 때마다 동일한 객체 인스턴스를 제공합니다. 만약 개발자가 프로토타입 스코프(Prototype Scope)를 명시적으로 지정하지 않는 한, 모든 Spring Bean은 싱글톤으로 관리 됩니다.
HelloController
package tobyspring.helloboot;
import java.util.Objects;
public class HelloController {
public String hello(String name) {
SimpleHelloService helloService = new SimpleHelloService();
return helloService.sayHello(Objects.requireNonNull(name));
}
}
HelloService
package tobyspring.helloboot;
public class SimpleHelloService {
String sayHello(String name) {
return "hello" + name;
}
}
-
HelloController가 기능을 의존해서 사용하는 SimpleHelloService라는 클래스를 작성합니다.
-
HelloController에서 해당 클래스의 오브젝트를 생성하여 사용합니다.
-
이렇게 하면 Controller가 직접 오브젝트를 생성하기 때문에 다른 Service 클래스를 사용하고 싶다면 코드를 직접 고쳐야 합니다. (의존성이 높다)
-
그래서 DI를 적용해야 합니다.
-
3. Dependency Injection
-
스프링 컨테이너는 DI(Dependency Injection) 컨테이너입니다.
-
스프링은 DI를 활용하여 만들어져 있고, 스프링을 이용하여 개발할때 DI를 손쉽게 적용할 수 있도록 지원합니다.

-
DI에는 두개의 오브젝트가 동적으로 의존관계를 가지는 것을 도와주는 제3의 존재가 필요한데, 이를 Assembler라고 합니다.
-
스프링 컨테이너는 DI를 가능하게 해주는 어셈블러로 동작합니다.
-
스프링 컨테이너 자체가 어셈블러입니다.

-
이제 SimpleHelloService도 빈으로 등록을 하고, 구현한 인터페이스 타입의 의존 오브젝트로 HelloController에 주입해서 사용되도록 합니다.
-
주입 방식은 컨트롤러의 파라미터를 이용합니다.
DI 적용

- 이제 service 클래스를 bean으로 등록하고 스프링 컨테이너가 어셈블러로서, DI를 통해 Service 클래스를 controller가 사용할 수 있도록 합니다.
HelloService
package tobyspring.helloboot;
public interface HelloService {
String sayHello(String name);
}
- 이제 service의 interface를 생성합니다.
SimpleHelloService
package tobyspring.helloboot;
public class SimpleHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "hello" + name;
}
}
- HelloService를 구현합니다.
HelloController
package tobyspring.helloboot;
import java.util.Objects;
public class HelloController {
private final HelloService helloService;
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
public String hello(String name) {
return helloService.sayHello(Objects.requireNonNull(name));
}
}
- 주입받은 의존 오브젝트는 노출할 필요가 없으니 private, 변경될 일도 없으니 final로 합니다.
Bean 등록
applicationContext.registerBean(SimpleHelloService.class);
-
이제 사용할 SimpleHelloService를 빈으로 등록합니다.
-
Interface를 등록하는 것이 아닙니다.
-
스프링 컨테이너는 bean 오브젝트 사이의 의존관계를 여러 방법을 통해서 자동으로 파악합니다.
-
만약 bean 클래스가 단일 생성자를 가지고 있다면, 생성자의 파리미터 타입의 빈 오브젝트가 있는지 확인하고, 있다면 이를 생성자 호출 시 주입해줍니다.
-
HelloController의 경우 단일 생성자를 가지고 있고, 생성자의 파라미터 타입의 빈 오브젝트(HelloService 타입의 SimpleHelloService가 빈으로 등록됨)가 있는지 확인이 되었으니, 이를 자동으로 생성자 호출시 주입해 줍니다.
-
명시적으로 의존 오브젝트를 주입하는 정보를 컨테이너에게 제공하려면, @Autowired와 같은 어노테이션을 지정할 수 있습니다.
4. DispatcherServlet
-
스프링에는 프론트 컨트롤러와 같은 역할을 담당하는 DispatcherServelt이 있습니다.
-
DispatcherServlet은 서블릿으로 등록되어서 동작하면서, 스프링 컨테이너를 이용해서 요청을 전달할 핸들러인 컨트롤러 오브젝트를 가져와 사용합니다.
-
DispatcherServlet이 사용하는 스프링 컨테이너는 GenericWebApplicationContext를 이용해서 작성합니다.
HelloController
package tobyspring.helloboot;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Objects;
@RequestMapping("/hello")
public class HelloController {
private final HelloService helloService;
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
@GetMapping
@ResponseBody
public String hello(String name) {
return helloService.sayHello(Objects.requireNonNull(name));
}
}
-
DispatcherServelt은 스프링 컨테이너에 등록된 bean 클래스에 있는 매핑 어노테이션 정보를 참고해서 웹 요청을 전달할 오브젝트와 메소드를 선정할 수 있습니다.
-
@RequestMapping @GetMapping 두가지 정보를 조합해서 매핑에 사용할 최종 정보를 생성합니다.
-
String 타입의 return의 경우는 이를 뷰로 해석하고 뷰 템플릿을 찾으려고 합니다.
- 그래서 @ResponseBody를 넣어줘야 합니다.
-
@RestController는 @ResponseBody를 포함하고 있기 때문에 메소드 레벨의 @ResponseBody를 넣지 않아도 적용된 것처럼 동작합니다.
5. Spring 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();
}
};
applicationContext.registerBean(SimpleHelloService.class);
applicationContext.registerBean(HelloController.class);
applicationContext.refresh();
6. 자바 코드 구성 정보 사용
-
registerBean으로 일일이 등록하는 것이 아닙니다.
-
팩토리 메소드를 통해 bean 메소드를 모두 생성 합니다.
-
@Bean 팩토리 메소드를 사용하면 자바 코드를 이용해서 구성 정보를 만들 수 있습니다.
-
자바 코드를 이용해서 빈 오브젝트를 직접 생성하고 초기화 하는 등의 작업을 명시적으로 작성합니다.
@Configuration
public class TobyspringApplication {
@Bean
public HelloController helloController(HelloService helloService) {
return new HelloController(helloService);
// 다음과 같은 코드는 controller에 있는 필드로 전달된다
// private final HelloService helloService
}
@Bean
public HelloService helloService() {
return new SimpleHelloService();
}
}
-
@Bean 메소드의 리턴 타입은 이 bean을 의존 오브젝트로 주입 받을 다른 빈에서 참조하는 타입(인터페이스)로 지정하는게 좋습니다.
-
@Bean 오브젝트를 생성할 때 주입할 의존 오브젝트는 @Bean 메소드의 파라미터로 정의하면 스프링 컨테이너가 이를 전달해 줍니다.
-
HelloService(인터페이스), 의존 오브젝트를 빈 메소드의 파라미터로 정의하였고 이를 통해 스프링 컨테이너가 이를 전달해 줍니다.
-
@Bean 메소드가 있는 클래스에서는 @Configuration 애노테이션을 붙여줘야 합니다.
public static void main(String[] args) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh();
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet",
new DispatcherServlet(this)
).addMapping("/*");
});
webServer.start();
}
};
applicationContext.register(TobyspringApplication.class);
applicationContext.refresh();
}
-
이제 자바 코드를 이용한 구성 정보를 사용하기 위해 AnnotationConfigWebApplicationContext클래스로 컨테이터를 만들어야 합니다.
-
onRefresh()를 @Override하여 서블릿 컨테이너를 생성하고 DispatcherServelt를 등록합니다.
7. @Component 스캔
-
조금 더 간결하게 bean을 등록하는 방법이 있습니다.
-
클래스에 일종의 레이블에 해당하는 annotation을 붙여주고, 이를 스캔해서 스프링 컨테이너의 빈으로 자동 등록해주는 방법을 사용합니다.
@Configuration
@ComponentScan
public class TobyspringApplication {
...
}
-
애플리케이션의 메인 클래스에는 @ComponentScan 애노테이션을 붙여줍니다.
-
@Component를 메타 애노테이션으로 가지고 있는 애노테이션도 사용할 수 있습니다.
- @Controller, @RestController, @Service 등이 있습니다.
@Service
public class SimpleHelloService implements HelloService {
...
}
@RestController
public class HelloController {
...
}
-
이렇게 스캔을 통해서 bean을 등록하는 것은 매우 편리합니다.
-
하지만 어떤 bean이 등록되는지 확인하려면 번거로울 수 있습니다.
-
만약에 bean이 많이 등록된다면 모두다 스캔해서 체크해봐야하는 단점이 있습니다.
-
그럼에도 불구하고 이렇게 스캔하는 것이 표준이고, 구조를 통해서 어렵지 않게 파악이 되기 때문에 사용됩니다.
-
-
메타 어노테이션은 어노테이션에 붙은 어노테이션입니다.
-
어노테이션을 정의할 때는 @Retention과 @Target을 지정합니다.
-
메타 애노테이션은 여러 단계로 중첩되기도 합니다.
-
@RestController의 경우 @Controller를 메타 애노테이션으로 가지고 있고, @Controller는 @Component를 메타 애노테이션으로 가지고 있습니다.
-
@RestController 는 @Component 애노테이션이 붙은 것과 같은 효과를 가집니다.
-
RestController Annotation
@Target(ElementType.TYPE) // 해당 어노테이션을 사용할 수 있는 Java 요소를 지정합니다 @Retention(RetentionPolicy.RUNTIME) //해당 어노테이션의 정보가 얼마나 오래 유지될지를 지정합니다 -> 런타임 내내 @Documented @Controller @ResponseBody public @interface RestController { /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) * @since 4.0.1 */ @AliasFor(annotation = Controller.class) String value() default ""; } -
8. Bean의 생명주기 메소드
-
톰캣 서블릿 서버팩토리와 DispatcherServlet도 빈으로 등록한 뒤 가져와서 사용할 수 있습니다.
-
@Bean 메소드에서 독립적으로 생성되게 하는 경우, DispatcherServlet이 필요로 하는 WebApplicationContext 타입 컨테이너 오브젝트는 스프링 컨테이너의 빈 생애주기 메소드를 이용해서 주입 받게 됩니다.
-
DispatcherServlet은 ApplicationContextAware라는 스프링 컨테이너를 setter 메소드로 주입해주는 메소드를 가진 인터페이스를 구현해놨고, 이런 생애주기 빈 메소드를 가진 빈이 등록되면 스프링은 자신을 직접 주입해줍니다.
-
bean 생애주기 메소드를 통해서 주입되는 오브젝트는 스프링 컨테이너가 스스로 빈으로 등록해서 빈으로 가져와 사용할 수도 있게 해줍니다.
springApplication
package tobyspring.helloboot;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
@ComponentScan
public class TobyspringApplication {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
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();
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
dispatcherServlet.setApplicationContext(this);
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet",
dispatcherServlet
).addMapping("/*");
});
webServer.start();
}
};
applicationContext.register(TobyspringApplication.class);
applicationContext.refresh();
}
}
9. SpringApplication
- MySpringApplication 클래스를 만들어 run() 메소드로 넣고, 메인 클래스를 파라미터로 받아서 사용하면 스프링 부트의 main()메소드가 있는 클래스와 유사한 코드가 됩니다.
TobySpringApplication
package tobyspring.helloboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
@ComponentScan
public class TobyspringApplication {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
public static void main(String[] args) {
MySpringApplication.run(TobyspringApplication.class, args);
}
}
MySpringApplication
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class MySpringApplication {
public static void run(Class<?> applicationClass, String... args) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh();
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
dispatcherServlet.setApplicationContext(this);
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet",
dispatcherServlet
).addMapping("/*");
});
webServer.start();
}
};
applicationContext.register(applicationClass);
applicationContext.refresh();
}
}
-
서블릿 컨테이너, dispatcherServlet를 생성하고, applicationClass(@bean 정보가 있는 class)를 등록합니다.
-
이를 보면, 스프링 부트의 main() 메소드가 있는 클래스와 유사한 코드가 만들어집니다.
boot.SpringApplication 적용
package tobyspring.helloboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
@ComponentScan
public class TobyspringApplication {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
public static void main(String[] args) {
SpringApplication.run(TobyspringApplication.class, args);
}
}
-
다른 점은 애노테이션이 2개 붙어있다는 점입니다.
-
서블릿 컨테이너와 DispatcherServlet을 만드는 @bean 메소드가 들어있다는 것입니다.
-
스프링 부트와 동일한 방식으로 코드를 만드려면 추가적인 작업이 필요합니다.
10. 마치며
-
독립 실행형 스프링 애플리케이션을 간단하게 구현하였습니다.
-
스프링 부트에서 제공하는 기능들이 어떻게 이루어져 있는지 이해할 수 있었습니다.
-
정말 간단해 보이던 SpringApplication.run()의 구조와 작동방식에 대해 이해할 수 있었습니다.
