스프링 컨테이너 사용
- 지난 시간까지는 독립 실행이 가능한 서블릿 애플리케이션을 만들었다.
- 그러면 독립 실행형 스프링 애플리케이션은 어떻게 만들 수 있을까?
- 1) POJO(Plain Old Java Object): 비지니스 로직을 담은 Java 오브젝트 (상속 X, )
- 2) 구성 정보를 담은 Configuration 메타 데이터
- 에러가 나지 않는한 서블릿 컨테이너가 기본적으로 Http response 200을 세팅해서 넣어준다. (생략 가능)
Ex)
<hide/>
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean(HelloController.class);
applicationContext.refresh();
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
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 result = helloController.hello(name);
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().print(result);
}
else if(req.getRequestURI().equals("/user")){
}else {
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
});
webServer.start();
- GenericApplicationContext: 코드에 의해 손쉽게 만들 수 있는 애플리케이션 컨텍스트
- registerBean() : 빈 등록한다.
- refresh(): 컨테이너 초기화
- 이로써 스프링 컨테이너를 만들었다.
의존 오브젝트 추가
- 앞에서 코드를 이용해서 스프링 컨테이너를 생성했다. 이 방법은 프론트 컨트롤러가 new 키워드를 이용해서 직접 오브젝트를 만들어 쓰는 방법과 어떻게 다를까?
- 스프링 컨테이너는 나중에도 적용할 수 있도록 기본 구조를 짜놨다는 것이 중요한다.
- 스프링 컨테이너는 어떤 오브젝트를 만들 때, 딱 하나만 만들어서 재사용한다. => "싱글톤 패턴"
- 웹을 통해 들어온 유저의 요청 사항을 검증하고 비지니스 로직을 담당하는 다른 오브젝트에게 요청을 보내서 결과를 돌려 받고 클라이언트에 어떻게 돌려줄 것인가.
- SimpleHelloService 라는 빈을 만든다. => 컨트롤러는 책임이 줄어든다.
Ex)
<hide/>
public class HelloController {
public String hello(String name) {
SimpleHelloService service = new SimpleHelloService();
return service.sayHello(Objects.requireNonNull(name));
}
}
- name 이 넘어오지 않으면 예외처리를 한다.
- Objects.requireNonNull(): 파라미터로 들어온 데이터가 null인 경우 예외 처리하고 아닌 경우는 파라미터를 반환한다.
- 기존의 컨트롤러 클래스에서 애너테이션을 제거하고 서비스 로직을 서비스 클래스에 위임한다.
Dependency Injection
- SimpleHelloService 의 로직이 바뀌면 컨트롤러는 영향을 받는다.
- 변경이 일어날 때마다 의존 관계에 있킄 클래스들 또한 수정이 필요하다. 이런 문제를 해결하기 위한 소프트웨어 원칙이 의존 관계 주입이다.
- Assembler(어셈블러): 디펜던시 인젝션을 위한 제3 의 존재
- 어셈블러가 바로 스프링 컨테이너이다.
- 우리가 메타 정보를 주면 이를 가지고 싱글톤 오브젝트를 만드는데 이 오브젝트를 주입하는 작업을 어셈블러가 담당한다.
- 생성자 주입: Controller 클래스를 만들 때 생성자 파라미터로 Service를 넣어준다. 대표적이고 쉬운 방법
- 팩토리 메서드로 빈을 만들어서 property를 만들어서 setter를 만든다?
의존 오브젝트 DI 적용
- 위와 같이 인터페이스를 구현하는 방식으로 변경이 되고 override 애너테이션이 붙는다. 새로운 인터페이스 HelloService가 생성된다.
- Service라는 인스턴스를 만들 필요 없이 어셈블러인 스프링 컨테이너가 컨트롤러의 클래스의 오브젝트를 만들 때, 생성자 파라미터로 주입할 수 있도록 변경한다.
Note) 인터페이스 추출 결과
- HelloService
public interface HelloService {
String sayHello(String name);
}
- SimpleHelloService: HelloService를 구현하는 방식으로 변경된다.
public class SimpleHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello! " + name;
}
}
- HelloController
- 기존에는 컨트롤러가 Service 인스턴스를 직접 만드는 방식이었지만
- 어셈블러인 스프링 컨테이너가 HelloController 오브젝트를 만들 때, 생성자 파라미터로 주입할 수 있도록 바꿀 예정이다.
public class HelloController {
public String hello(String name) {
SimpleHelloService service = new SimpleHelloService();
return service.sayHello(Objects.requireNonNull(name));
}
}
- 생성자 주입
<hide/>
public class HelloController {
private final HelloService helloService;
public elloController(HelloService service) {
this.helloService = service;
}
public String hello(String name) {
return helloService.sayHello(Objects.requireNonNull(name));
}
}
- Application 코드를 다음과 같이 바꾼다.
- 스프링 구성 정보를 만들 때는 반드시 클래스가 필요하며 인터페이스로는 만들 수 없다.
- registerBean()을 통해 빈을 생성하고 나면 getBean()을 이용하면 빈 정보를 가져올 수 있다.
<hide/>
public class HellobootApplication {
public static void main(String[] args) {
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean(HelloController.class);
applicationContext.registerBean(SimpleHelloService.class);
applicationContext.refresh();
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
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 result = helloController.hello(name);
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().print(result);
}
else if(req.getRequestURI().equals("/user")){
}else {
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
});
webServer.start();
}
}
DispatcherServlet으로 전환
- DispatcherServlet: 스프링이 처음 나올 때부터 많은 기능을 수행하는 Servlet 클래스가 들어있다. 프론트 컨트롤러의 많은 기능을 수행한다.
- 서블릿 컨테이너를 관리하지 않고 싶다. 자동으로 관리 되게끔 하려면?
- DispatcherServlet:을 이용하면 기존의 service() 를 오버라이드할 필요 없다.
- 다른 Servlet을 집어 넣는다.
- DispatcherServlet은 웹 환경에서 쓰도록 만들어진 GenericWebApplicationContext가 필요하다.
- GenericApplicationContext => GenericWebApplicationContext로 바꾼다.
- 이제 dispatcherServlet에게 applicationContext를 넘긴다.
- dispatcherServlet이 매핑하다가 작업을 위임할 오브젝트를 찾아야하는데 그 때 사용할 서블릿이 applicationContext가 된다.
<hide/>
public class HellobootApplication {
public static void main(String[] args) {
GenericWebApplicationContext applicationContext = new GenericWebApplicationContext();
applicationContext.registerBean(HelloController.class);
applicationContext.registerBean(SimpleHelloService.class);
applicationContext.refresh();
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", new DispatcherServlet(applicationContext) {
}).addMapping("/*");
});
webServer.start();
}
}
Note) 실행 결과 - 에러 정상
- dispatcherServlet에게 어떤 오브젝트가 웹 요청을 가지고 왔는지 요청 정보를 넘겨줘야하는데 이와 관련한 정보를 주지 않았기 때문에 당연히 오류가 발생한다.
- 과거에는 매핑 정보(url, bean)를 모두 xml 파일에 넣어줘야했다.
- 현재는 요청을 처리할 컨트롤러에 직접 매핑 정보를 넣는 방법을 사용한다.
애너테이션 매핑 정보 사용
- @GetMapping 는 @RequestMapping(value = "", method = "RequestMethod.GET")와 같다.
- 클래스 위에 @RequestMapping()을 붙이면 안에 있는 path에 이어서 method에 붙은 path를 이어붙인다.
- 컨트롤러 메서드의 반환형이 String일 경우, 컨트롤러가 처리하는 방식이 있다. 반환값과 이름이 같은 View 파일을 찾는다.
- 그런데 지금, "Hello momo!"라는 View 가 존재하는 게 아니라 응답 값으로 보여주고 싶은 상황이다.
- 이럴 때에는 컨트롤러의 메서드 앞에 @ResponseBody를 붙이면 해결된다.
- @RestController: 애너테이션 앞에 "Rest" 를 붙어 있으면 각 메서드에 @ResponseBody를 적용해준다.
Ex)
<hide/>
@RequestMapping("/hello")
public class HelloController {
private final HelloService helloService;
public HelloController(HelloService service) {
this.helloService = service;
}
@GetMapping
@ResponseBody
public String hello(String name) {
return helloService.sayHello(Objects.requireNonNull(name));
}
}
스프링 컨테이너로 통합
- application.refresh(): 스프링 컨테이너의 초기화 작업이 이뤄진다.
- 템플릿 메서드 패턴을 이용하면 상속을 통한 기능 확장이 가능하다.
- onRefresh() 를 오버라이드한다.
- 컨테이너를 초기화하는 코드를 onRefresh() 안에 넣어준다.
- super 클래스를 호출하는 코드 super.onRefresh는 생략하면 안 된다.
<hide/>
public static void main(String[] args) {
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(HelloController.class);
applicationContext.registerBean(SimpleHelloService.class);
applicationContext.refresh();
}
자바코드 구성 정보 구성
- 클래스 앞에 @Configuration를 붙여서 안에 빈 팩토리 메서드가 있다는 것을 스프링 컨테이너에게 알려준다.
- 구성 정보도 함께 넣어준다.
- 기존에 쓰던 GenericWebApplicationContext => AnnotationConfigWebApplicationContext
<hide/>
@Configuration
public class HellobootApplication {
@Bean
public HelloController helloController(HelloService helloService){
return new HelloController(helloService);
}
@Bean
public HelloService helloService(){
return new SimpleHelloService();
}
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(HellobootApplication.class);
applicationContext.refresh();
}
}
@Component 스캔
- 클래스 위에 @Component 를 붙이면 컴포넌트 스캐너가 해당 클래스를 빈으로 등록한다.
- @ComponentScan: ApplicationContext 클래스 위에 애너테이션을 붙이면 해당 클래스의 패키지부터 시작해서 하위패키지를 하나씩 확인하면서 컴포넌트를 등록한다.
- 새로운 빈을 추가하는 경우, 구성 정보를 매번 등록할 필요 없이 @Component 만 붙이면 된다.
- 메타 애너테이션: 애너테이션 위에 붙은 애너테이션을 말한다.
- ex)
- @Controller: @Component가 @Controller의 메타 애너테이션이다.
- @RestController: @Controller가 @RestController의 메타 애너테이션이다.
- 애너테이션 생성
- 애플리케이션이 실행되는동안 유지된다.
- 어느 계층에서 어느 역할을 하는 애너테이션인지 표현할 수 있다.
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface MyComponent {
}
- 그리고 컨트롤러 위에 다음과 같이 붙여준다.
- 그러면 컨트롤러 위에 컴포넌트를 붙인 것처럼 똑같이 컴포넌트로 등록해준다.
<hide/>
@MyComponent
@RequestMapping("/hello")
public class HelloController {
...
}
Bean의 생명 주기 메서드
- 독립 실행형 애플리케이션 Object 생성 방법 - 애
- 1) TomcatServletWebServerFactory
- 2) DispatchServlet
- 다음과 같이 @Bean을 통해 두 개의 빈을 등록한다.
- this.getBean(ServletWebServerFactory.class)
- ServletWebServerFactory 라는 빈은 없지만 ServletWebServerFactory 타입의 빈은 딱 하나뿐이므로 그걸 가져온다.
- 애플리케이션 컨텍스트를 주입하지 않았다.
- 스프링 컨테이너가 DispatcherServlet은 애플리케이션 컨텍스트가 필요하겠다고 판단하여 알아서 주입해준 것이다.
<hide/>
@Configuration
@ComponentScan
public class HellobootApplication {
@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);
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", dispatcherServlet)
.addMapping("/*");
});
webServer.start();
}
};
applicationContext.register(HellobootApplication.class);
applicationContext.refresh();
}
}
- 컨트롤러
- 스프링 컨테이너가 초기화되는 시점에 오버라이드한 setApplicationContext()가 실행된다.
- 서버를 띄우기만 해도 메서드가 실행된다.
- DispatcherServlet을 팩토리메서드에서 생성자 없이 인스턴스를 생성해서 반환하더라도 문제없이 동작한다.
<hide/>
@RestController
public class HelloController {
private final HelloService helloService;
private final ApplicationContext applicationContext;
public HelloController(HelloService service,ApplicationContext applicationContext) {
this.helloService = service;
this.applicationContext = applicationContext;
System.out.println(applicationContext);
}
@GetMapping("/hello")
@ResponseBody
public String hello(String name) {
return helloService.sayHello(Objects.requireNonNull(name));
}
}
Note) 실행 결과
SpringBoot Application
- 기존에 HelloBootAppluication 클래스의 applicationContext.register(HellobootApplication.class) 를 바꾸려고 한다.
- 원래 짰던 코드를 다음과 같이 바꿔주면
- main 메서드가 있는 클래스가 달라지더라도 해당 클래스만 파라미터로 넘겨주면 되기 때문에 다른 메인이 되는 클래스에서도 main()을 재사용할 수 있다.
- 새로운 클래스 MySpringApplication을 만들고 run() 메서드를 이동한다.
- HellobootApplication
- 그러면 상단에 있는 빈 메서드를 생략해도 될까?
- servletWebServerFactory() 메서드는 서블릿 컨테이너를 생성하는 빈이므로 반드시 필요하다.
<hide/>
@Configuration
@ComponentScan
public class HellobootApplication {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
public static void main(String[] args) {
MySpringApplication.run(HellobootApplication.class, args);
}
}
- MySpringApplication
<hide/>
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);
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", dispatcherServlet)
.addMapping("/*");
});
webServer.start();
}
};
applicationContext.register(applicationClass);
applicationContext.refresh();
}
}
title
- content
- content
title
- content
- content
출처 - 인프런 토비의 스프링
'Spring Framework > 토비의 스프링' 카테고리의 다른 글
Chapter 06. 메타 애너테이션과 합성 애너테이션 (2) | 2023.07.14 |
---|---|
Chapter 05. DI와 테스트, 디자인 패턴 (0) | 2023.07.04 |
Chapter 03. 독립 실행형 서블릿 애플리케이션 (0) | 2023.06.20 |
Chapter 02. 스프링 부트 시작하기 (0) | 2023.06.18 |
Chapter 01. 스프링 부트 살펴보기 (0) | 2023.06.17 |