스프링 컨테이너 사용
지난 시간까지는 독립 실행이 가능한 서블릿 애플리케이션을 만들었다.
그러면 독립 실행형 스프링 애플리케이션은 어떻게 만들 수 있을까?
1) POJO(Plain Old Java Object): 비지니스 로직을 담은 Java 오브젝트 (상속 X, )
2) 구성 정보를 담은 Configuration 메타 데이터
에러가 나지 않는한 서블릿 컨테이너가 기본적으로 Http response 200을 세팅해서 넣어준다. (생략 가능)
Ex)
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)
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) 인터페이스 추출 결과
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));
}
}
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()을 이용하면 빈 정보를 가져올 수 있다.
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() 를 오버라이드할 필요 없다.
DispatcherServlet은 웹 환경에서 쓰도록 만들어진 GenericWebApplicationContext가 필요하다.
GenericApplicationContext => GenericWeb ApplicationContext로 바꾼다.
GenericApplicationContext 오류
이제 dispatcherServlet에게 applicationContext를 넘긴다.
dispatcherServlet이 매핑하다가 작업을 위임할 오브젝트를 찾아야하는데 그 때 사용할 서블릿이 applicationContext가 된다.
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)
@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 는 생략하면 안 된다.
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
@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의 메타 애너테이션이다.
애너테이션 생성
애플리케이션이 실행되는동안 유지된다.
어느 계층에서 어느 역할을 하는 애너테이션인지 표현할 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface MyComponent {
}
그리고 컨트롤러 위에 다음과 같이 붙여준다.
그러면 컨트롤러 위에 컴포넌트를 붙인 것처럼 똑같이 컴포넌트로 등록해준다.
@MyComponent
@RequestMapping("/hello")
public class HelloController {
...
}
@RestController의 메타 애너테이션은 @Controller이다.
Bean의 생명 주기 메서드
독립 실행형 애플리케이션 Object 생성 방법 - 애
1) TomcatServletWebServerFactory
2) DispatchServlet
다음과 같이 @Bean을 통해 두 개의 빈을 등록한다.
this.getBean(ServletWebServerFactory.class)
ServletWebServerFactory 라는 빈은 없지만 ServletWebServerFactory 타입의 빈은 딱 하나뿐이므로 그걸 가져온다.
애플리케이션 컨텍스트를 주입하지 않았다.
스프링 컨테이너가 DispatcherServlet은 애플리케이션 컨텍스트가 필요하겠다고 판단하여 알아서 주입해준 것이다.
@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을 팩토리메서드에서 생성자 없이 인스턴스를 생성해서 반환하더라도 문제없이 동작한다.
@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() 메서드는 서블릿 컨테이너를 생성하는 빈이므로 반드시 필요하다.
@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);
}
}
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
title
출처 - 인프런 토비의 스프링
https://www.inflearn.com/course/%ED%86%A0%EB%B9%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9D%B4%ED%95%B4%EC%99%80%EC%9B%90%EB%A6%AC/dashboard