Spring Framework/토비의 스프링

Chapter 04. 독립 실행형 스프링 애플리케이션

계란💕 2023. 6. 24. 15:07


스프링 컨테이너 사용

  • 지난 시간까지는 독립 실행이 가능한 서블릿 애플리케이션을 만들었다. 
  • 그러면 독립 실행형 스프링 애플리케이션은 어떻게 만들 수 있을까?
  • 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로 바꾼다. 

GenericApplicationContext&nbsp; 오류

 

  • 이제  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 {
 ...
}

 

@RestController의 메타 애너테이션은 @Controller이다.

 


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

출처 - 인프런 토비의 스프링

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