[인프런] 토비의 스프링 부트 - 이해와 원리

img.png

서론

스프링 책으로 유명한 토비님의 강의를 3회독 하고있는데 뭔가 기록을 안하고 로컬에만 넣어놓으니 휘발되는 느낌이라 다시 천천히 수강하며 강의를 나만의 언어로 이해해가며 정리했다.

스프링 부트 살펴보기

스프링 부트 소개

스프링 부트는 스프링을 기반으로 실무 환경에 사용 가능한 수준의 독립실행형 애플리케이션을 복잡한 고민 없이 작성할 수 있게 도와주는 여러 도구의 모음이다.

스프링 부트 핵심 목표 4가지

  • 빠르고 광범위한 영역의 스프링 개발 경험 제공
  • 강한 주장을 가지고 즉시 적용 가능한 기술 조합 제공하면서, 필요에 따라 원하는 방식으로 손쉽게 변형 가능
  • 프로젝트에서 필요로 하는 다양한 비기능적 기술(내장형 서버, 보안, metric, 상태 체크, 외부 설정 방식 등) 제공
  • 코드 생성이나 XML 설정을 필요로 하지 않음

Containerless 개발

컨테이너가 필요 없다가 아닌 "컨테이너를 신경 쓰지 않아도 된다" 라는게 핵심 의미.

“컨테이너 없는” 웹 애플리케이션 아키텍처란?

서블릿 컨테이너와 연결되어 스프링 컨테이너는 서클릿 뒤에 있다. 서블릿 컨테이너 내부의 웹 컴포넌트를 서블릿, 스프링 컨테이너 내부의 웹 컴포넌트를 빈이라 부른다.

자바의 표준 웹 기술을 쓰기위해 서블릿 컨테이너를 쓰는 것이다.

web.xml, war 빌드하고 배포하고 포트 설정 등등... 번거로운 일이 많다. 서블릿 컨테이너를 세팅안하고 추상화 시켜서 편하게 개발하게 해준다.

Sping의 main() 실행만해도 서블릿 컨테이너 관련세팅이 다 된다.

스프링 애플케이션 개발에 요구되는 서블릿 컨테이너의 설치, WAR 폴더 구조, web.xml, WAR 빌드, 컨테이너로 배치, 포트 설정, 클래스 로더, 로깅 등과 같은 필요하지만 애플리케이션 개발의 핵심이 아닌 단순 반복 작업을 제거해주는 개발 도구와 아키텍처를 지원한다.

설치된 컨테이너로 배포하지 않고 독립실행형(standalone) 자바 애플리케이션으로 동작된다.

Opinionated

내(Spring Boot)가 다 정해줄테니 당신은 비즈니스 로직에 충실하라

스프링 프레임워크의 설계 철학

  • 극닥적 유연함
  • 다양한 관점 수용
  • Not opinionated
  • 수 많은 선택지 모두 포용

하지만... 문제는 새로운 프로젝트를 시작할 때 세팅을 위해 고려할게 엄청 많다.

스프링 부트의 설계 철학

  • Opinionated - 자기 주장 강한, 독선적인
  • 일단 정해주는 대로 빨리 개발하라 고민은 나중에
  • 스프링을 잘 활용하는 뛰어난 방법 제공

스프링 부트가 결정해 주는 것들

  • 검증된 스프링 생태계 프로젝트
  • 표준 자바 기술
  • 오픈소스 기술의 종류와 의존관계
  • 사용 버전

각 기술을 스프링에 적용하는 방식(DI 구성)과 디폴트 설정값을 제공한다.

또한 유연한 확장을 제공해준다.

스프링 부트에 내장된 디폴트 구성을 커스터마이징하는 자연스럽고 유연한 방법을 제공한다. 스프링 부트가 스프링을 사용하는 방식을 이해한다면 언제든지 스프링 부트를 제거하고 원하는 방식으로 재구성할 수 있다.

그리고 스프링 부트처럼 기술과 구성을 간편하게 제공하는 나만의 모듈도 만들 수 있다.

스프링 부트를 이용한 개발의 오해와 한계

  • 애플리케이션 비즈니스 코드만 잘 작성하면 됨
  • 스프링 몰라도 개발 잘 할 수 음
  • 스프링 부트가 직접적으로 보여주지 않는 것은 몰라도 됨
  • 뭔가 기술적인 필요가 생길 때 검색으로 해결함

독립 실행형 서블릿 애플리케이션

서블릿 컨테이너 띄우기

지금 관심사는 서블릿 컨테이너를 직접 설치, 신경쓰지않고 어떻게 띄울것인가 하는 것이다.

서블릿(Servlet)이라는건 자바의 표준 기술이며 이 표준 기술을 구현한 컨테이너 제품들이 꽤 많다. 대표적인 것이 Tomcat.

TomcatServletWebServerFactory는 스프링 부트가 내장된 톰캣 서블릿 컨테이너를 코드로 쉽게 사용하게 해주려 만든 클래스다.

public class Application{
 
	public static void main(String[] args){
		ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
		WebServer webServer = serverFactory.getWebServer();
		webServer.start();
	}
	
}

이제 빈 서블릿 컨테이너를 띄울 수 있게된 상태다.

서블릿 등록

public class HellobootApplication {
 
        public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            servletContext.addServlet("hello", new HttpServlet() {
                @Override
                protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                    String name = req.getParameter("name");
 
                    resp.setStatus(HttpStatus.OK.value());
                    resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                    resp.getWriter().println("Hello " + name);
                }
            }).addMapping("/hello");
        });
        webServer.start();
    }
 
}

"hello" 서블릿을 하나 등록, /hello경로로 매핑, HttpServletRequest, HttpServletResponse 세팅이 완료되었다.

프론트 컨트롤러

지금까지의 코드를 보면 서블릿은 요청마다 직접 하나씩 매핑을 해야했다.

/hello라는 패스를 담당하는 서블릿을 따로 만들어 처리했는데 만약 다른 경로가 필요하다면 그에 매핑되는 서블릿을 또 따로 만들고 등록해 줘야한다.

그냥 그렇게 개발하면 될거 같지만 서블릿 코드의 "보일러 플레이트"가 많이 나오는 번거로움이 존재했다.

또한, 서블릿은 웹 요청과 웹 응답을 직접적으로 각 객체를 다뤄줘야되는 방식이기에 자연스럽지 않고, 역할이 섞여있는 형태가 된다.

이를 개선하기위해 Front Controller 가 등장했다.

모든 서블릿에 공통적으로 등장하는 코드를 중앙화된 객체에 모두 위임하고 제어하게 하자는 개념으로 나왔다.

이 프론트 컨트롤러를 내장하는 유명한 웹 프레임워크들이 등장하기 시작했다.

프론트 컨트롤러가 처리해주는 대표적인 공통작업은 인증, 보안, 다국어 처리, 모든 웹 요청에 공통으로 리턴해줘야 되는 내용 등의 작업이 있다.

프론트 컨트롤러 도입

public class HellobootApplication {
 
    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            servletContext.addServlet("hello", 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");
 
                        resp.setStatus(HttpStatus.OK.value());
                        resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                        resp.getWriter().println("Hello " + name);
                    }
                    else if (req.getRequestURI().equals("/user")) {
                        
                    }
                    else {
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }
                }
            }).addMapping("/*");
        });
        webServer.start();
    }
 
}

Hello 컨트롤러 매핑과 바인딩 - HelloController 이용하기

public class HelloController {
    public String hello(String name) {
        return "Hello " + name;
    }
}
 
public class HellobootApplication {
 
    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            HelloController helloController = new HelloController();
 
            servletContext.addServlet("hello", 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");
 
                        String ret = helloController.hello(name);
 
                        resp.setStatus(HttpStatus.OK.value());
                        resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                        resp.getWriter().println(ret);
                    }
                    else if (req.getRequestURI().equals("/user")) {
                        //
                    }
                    else {
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }
                }
            }).addMapping("/*");
        });
        webServer.start();
    }
 
}

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

스프링 컨테이너 사용(ApplicationContext)

지금까지는 독립 실행이 가능한 서블릿 애플리케이션을 만들어 봤다. 이제는 독립 실행 가능한 스프링 애플리케이션을 만들어 본다.

img_2.png

스프링 컨테이너는 애플리케이션 로직이 담긴 평범한 자바 오브젝트, 일명 POJO와 구성 정보(Configuration Metadata)를 런타임에 조합해서 동작하는 최종 애플케이션을 만들어낸다.

지금까지는 프론트 컨트롤러에서 직접 HelloController 객체를 생성하는 방식을 썻는데 이제 이건 스프링 컨테이너에서 관리하는 방식으로 바꿔본다.

스프링 컨테이너를 대표하는 대표적인 인터페이스가 있다. 바로 ApplicationContext.

애플리케이션을 구성하는 많은 정보, 컨테이너에 들어갈 빈, 리소스 접근 방법, 이벤트 전달, 구독 등

ApplicationContext 중에서 코드로 쉽게 만들 수 있도록 만들어진 구현체가 있는데 바로 GenericApplicationContext.

ApplicationContext에서 빈을 등록할때는 registerBean를 쓰는데 직접 객체를 생성하는 것보다 클래스의 메타정보를 넣어주는 방식으로 동작한다.

이렇게 세팅을 하고 가지고 있는 구성정보를 이용해서 컨테이너를 초기화 해야 빈이 생성되는데 이때 사용되는 메서드가 refresh다.

public class HellobootApplication {
 
    public static void main(String[] args) {
        GenericApplicationContext applicationContext = new GenericApplicationContext();
        applicationContext.registerBean(HelloController.class);
        applicationContext.refresh();
 
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            servletContext.addServlet("hello", 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 {
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }
                }
            }).addMapping("/*");
        });
        webServer.start();
    }
 
}

의존 오브젝트 추가

public class HelloController {
    public String hello(String name) {
        SimpleHelloService helloService = new SimpleHelloService();
 
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}
 
public class SimpleHelloService {
    String sayHello(String name) {
        return "Hello " + name;
    }
}

Dependency Injection

어떤 클래스가 바뀌면 다른 클래스가 영향을 받는다면 영향을 받는 클래스는 바뀐 클래스에 의존하고 있다고 할 수 있다.

인터페이스를 도입해서 구현 객체와의 의존성을 코드 레벨에서 끊어 냈다고 하더라도, 런타임에 어떤 인터페이스 구현체를 쓸 지 결정해야되는 문제가 있다.

이 문제를 해결하기 위한 제 3의 존재가 필요한데 이를 Assembler라 지칭한다. 객체를 가져다가 의존성을 주입해준다.

스프링에서는 AssemblerSpring Container 로 볼 수 있다.

주입 방식은 여러 방식이 있는데 생성자 활용, Factory Method 같은 걸로 빈을 만들도록 하면서 여기에 파라미터로 넘기기, Setter로 주입하기 등 이 있다.

여튼 Spring Container가 객체를 만들고 주입해주고 하는 작업들을 수행해주고 있는 것이다.

의존 오브젝트 DI 적용

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("hello", 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 {
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }
                }
            }).addMapping("/*");
        });
        webServer.start();
    }
 
}
 
public interface HelloService {
    String sayHello(String name);
}
 
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));
    }
}
 
public class SimpleHelloService implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

ApplicationContext에 빈 등록만 해주면 의존관계를 알아서 파악하고 주입을 해준다. 만약 하나의 인터페이스에 2개의 빈이 매핑되려한다면 예외를 발생시킨다.

DispatcherServlet 으로 전환

점점더 코드가 복잡해지는데 이번에 깔끔하게 해 본다.

현재는 Front Controller 라는 서블릿을 만들었지만 서블릿 컨테이너를 더이상 관리하거나 다루는 작업을 하지않도록 개발하고싶다는 니즈를 반영해보려한다.

Servlet Container-less로 가야되는데, 문제가 뭐냐면 이 애플리케이션의 로직과 강하게 연결되있는게 이 서블릿 코드 안에 있다는 것이다.

다음과 같은게 서블릿 코드 내부에 있다.

  • 매핑
    • 웹 요청을 가지고 이것을 처리해 줄 컨트롤로 메서드가 어떤 것인가 연결(매핑) 해주는 것
    • 이 로직이 if 로 하드코딩 되어있음
  • 바인딩(요청의 파라미터 URL의 쿼리 스트링 관련 처리 같은 것)
    • 파라미터를 추출
    • 파라미터를 특정 메서드로 전달
    • DTO에 타입 변환도 해주고 하는 것 등

이런 코드를 스프링이 제공하는 DispatcherServlet으로 대체가능하다. 코드가 꽤 많이 삭제되는 모습을 볼 수 있다.

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();
    }
}

애노테이션 매핑 정보 사용

@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));
    }
}

스프링 컨테이너로 통합

지금까지는 2부분으로 코드를 나눌 수 있다.

앞 부분은 Spring Container를 생성하고 Bean을 등록해서 초기화하는 작업을 해주는 Spring Container 작업 파트.

뒷 부분은 만들어진 Spring Container를 활용하면서 Servlet Container를 코드에서 생성하고 필요한 Front Controller 역할을 하는 DispatcherServlet을 등록하는 Servlet Container 초기화 코드로 구분될 수 있다.

이번 작업에서는 뒷 부분 작업을 스프링 컨테이너가 초기화되는 앞 부분에서 되도록 바꾸려한다.

왜? "스프링 부트"가 그렇게 하니까 따라해보는 것임.

템플릿 메소드 패턴을 활용한다.

public class HellobootApplication {
 
    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();
    }
}

자바코드 구성 정보 사용

이번에는 스프링 컨테이너가 사용하는 구성 정보 즉 우리가 만든 코드를 어떻게 객체로 만들어 컨테이너 내에 컴포넌트로 등록해 두고 스프링 컨테이너안에 들어있는 빈이라 불리는 객체가 또 다른 객체를 사용한다면 이 관계를 어떻게 맺어 줄 건가 어느 시점에 주입할 건가 등등 정보를 스프링 컨테이너에다 구성 정보로 우리가 제공해야한다.

여기선 Bean Factory Method를 이용해본다.

@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();
    }
}

@Comoponent 스캔

지금 까지는 클래스 정보를 레지스터 빈 메소드에 넘기거나 빈 팩토리 메서드를 만들어서 직접 빈 인스턴스를 생성하는 방법을 썻다.

그거보다 좀 더 간결하게 빈을 등록하는 방법이 있다. 여기선 그걸 알아보자.

@Component를 붙이면 @ComponentScan이 붙은 클래스 패키지 하위를 쫙 훓는 과정에서 빈으로 등록이 된다.

@ComponentScan방식이 편하긴한데 나중에 빈이 많아지면 애플리케이션 실행 시 어떤 것들이 등록되는가 이걸 찾아보기 번거로울 수 있다.

메타 애노테이션이란 애노테이션 위에 붙은 애노테이션이라는 의미.

@Controller, @Service이런거 드가보면 @Comonent가 메타 애노테이션으로 붙어있는거 확인 가능하다.

@RestController
public class HelloController {
    private final HelloService helloService;
 
    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }
 
    @GetMapping("/hello")
    public String hello(String name) {
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}
 
@Configuration
@ComponentScan
public class HellobootApplication {
 
    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();
    }
}
 
@Service
public class SimpleHelloService implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello " + name;
    }
}
 

Bean 생명주기 메소드⭐️

강의 후반 이해가 좀 안되네 ApplicationContextAware에 관해 다시 보기

Factory Method로 대체할건 대체해본다. 미래의 유연함을 위함.

@RestController
public class HelloController {
    private final HelloService helloService;
    private final ApplicationContext applicationContext;
 
    public HelloController(HelloService helloService, ApplicationContext applicationContext) {
        this.helloService = helloService;
        this.applicationContext = applicationContext;
 
        System.out.println(applicationContext);
    }
 
    @GetMapping("/hello")
    public String hello(String name) {
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}
 
@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();
    }
}

SpringBootApplication

mainrun을 동작할때 넘기는 클래스는 @Configuration, @Componentscan 과 Factory 메서드를 가지고 스프링 컨테이너에게 애플리케이션 구성을 어떻게 할 것인가를 알려주는 클래스여야 한다.

@RestController
public class HelloController {
    private final HelloService helloService;
    private final ApplicationContext applicationContext;
 
    public HelloController(HelloService helloService, ApplicationContext applicationContext) {
        this.helloService = helloService;
        this.applicationContext = applicationContext;
    }
 
    @GetMapping("/hello")
    public String hello(String name) {
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}
 
@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();
    }
}

음... 이름바꾸고 의존성 제거등 필없는거 제거한듯

@RestController
public class HelloController {
    private final HelloService helloService;
 
    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }
 
    @GetMapping("/hello")
    public String hello(String name) {
        return helloService.sayHello(Objects.requireNonNull(name));
    }
}
 
@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) {
        SpringApplication.run(HellobootApplication.class, args);
    }
}

DI와 테스트, 디자인 패턴 ⭐️

어떤 클래스를 테스트하는데 그 클래스가 의존하는 클래스의 동작에 문제가 있어서 내가 타겟으로 하는 클래스의 테스트가 실패하는 경우가 있을 수 있다.

이를 방지하기위해서 의존하는 클래스로 부터 고립을 시켜 테스트를 하면되는데, 이때 DI가 매우 유용하다.

DI 이용한 Decorator, Proxy 패턴

Decorator 패턴

기존 코드에 동적으로 책임을 추가할 때 쓰는 패턴.

객체 합성 구조로 확장이 가능하도록 설계되어 있고 DI를 적용해서 의존관계를 런타임에 주입할 수 있다면 의존 객체와 동일한 인터페이스를 구현한 확장기능(데코레이터)을 동적으로 추가 할 수 있다.

또한, 재귀적인 구조로 여러 개의 책임을 부여하는 것도 가능하다.

SCR-20251225-oeft.png

데코레이터는 자기가 구현하는 인터페이스 타입의 다른 객체를 의존한다. 추가 책임, 기능의 적용 중에 의존 객체를 호출한다.

SCR-20251225-ofrs.png

음... 기존 클래스 건드리지 않고 동적으로 새로운 기능을 추가해 줄 수 있게해주는구나.

데코레이터의 특징은 여러개를 갖다 붙여도 된다는 것.

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
 
@Service
@Primary
public class HelloDecorator implements HelloService {
    private final HelloService helloService;
 
    public HelloDecorator(HelloService helloService) {
        this.helloService = helloService;
    }
 
    @Override
    public String sayHello(String name) {
        return "*" + helloService.sayHello(name) + "*";
    }
}

이때 HelloService에 주입할 수 있는 구현체 후보가 2개가 있게된다. @Primary없이 스프링 애플리케이션 실행시 에러가 발생한다.

Proxy 패턴

프록시는 다른 객체의 대리자 혹은 플레이스 홀더 역할을 한다.

너무 생성비용이 큰 객체가 있는데 이 객체는 필요로 하는 시점에 온 디멘드로 만들면 된다. 즉, Lazy Loading이 필요할때 이 패턴을 쓸 수 있다.

프록시는 리모트 객체에 대한 로컬 접근이 가능하게 하거나, 필요가 있을 때만 대상 객체를 생성할 필요가 있을때 사용할 수 있다. 보안이나 접속 제어 등에 사용하기도 한다.

참고로 프록시 패턴의 프록시와 일반적인 용어 프록시, 자바의 다이나믹 프록시가 동일한건 아니다. SCR-20251225-ojbr.png

자동 구성 기반 애플리케이션⭐️

@AutoConfiguration

일단 메타 애너테이션에 관해 알아보자. 음 개인적 이해로는 애너테이션의 확장이라 바로보면 되는것 같다.

상속과는 다른데 모든 애너테이션이 메타 애너테이션으로 활용될수 있지 않다.(Target, Retension 지정 등)

음 그런데 결합되면 결국 상속?되는 느낌이긴하다.

메타 애너테이션을 여러개 써서 하나의 애너테이션화 시킨걸 합성(Composed) 애너테이션이라 부른다.

반복적으로 사용되는 애너테이션 조합을 하나의 애너테이션으로 합성해서 간결한 코드 작성이 가능해진다.

합성 애너테이션 적용

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
public @interface MySpringBootApplication {
}
 
@MySpringBootApplication
public class HellobootApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(HellobootApplication.class, args);
    }
 
}
 
@Configuration
public class Config {
    @Bean
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
 
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
}

팩토리 메서드를 기존위치에서 제거하고 따로 Config 클래스 두고 거기에 @Configuration 붙인다. @Configuration@Component가 메타로 붙어있다.

Bean Object와 역할과 구분

빈은 크게 애플리케이션 빈, 컨테이너 인프라스트럭처 빈으로 구분할 수 있다.

  • 애플리케이션 빈
    • HelloController
    • DataSourece
    • JpaEntityManagerFactory
    • JdbcTransactionManager
    • 여기서 또 구분이 가능함
    • 애플리케이션 로직 빈
      • HelloController
    • 애플리케이션 인프라스트럭처 빈
      • DataSourece
      • JpaEntityManagerFactory
      • JdbcTransactionManager
  • 컨테이너 인프라스트럭처 빈
    • ApplicationContext/BeanFactory
    • BeanPostProcessor
    • DefaultAdvisor, AutoProxyCreator
    • Environment

구성 정보 관점에서 나누자면

  • 사용자 구성 정보(ComponentScan)
    • HelloController
    • HelloDecorator
  • 자동 구성 정보(AutoConfiguration)
    • TomcatServletWebServerFactory
    • DispatcherServlet

인프라 빈 구성 정보의 분리 - 다음 구조로 코드가 이제 구성된다

img_3.png img_4.png

이제 좀 변화가 복잡하기에 관련 변화를 diff로 명확하고 쉽게 확인하기위해, 토비님의 깃헙링크를 걸도록 하겠다.

Github diff

동적인 자동 구성 정보 등록

현재는 @Import로 클래스가 하드코딩되있는데 이를 개선해보겠다.

ImportSelector라는걸 쓰는데 이건 다른 @Configuration이 붙은 클래스에 구성 정보 생성 작업이 모두 끝난 다음에 ImportSelector가 동작하도록 순서를 뒤로 지연하게 해준다함.

여튼, 우리가 알아야되는건 이 인터페이스를 구현한 클래스를 우리가 쓰면 Configuration 클래스들을 프로그램에서 동적으로 결정해서 가져올 수 있다는 뜻!

동적이라는 말은 어떤 Configuration을 가져올지 데이터베이스, 설정 파일 등을 이용해서 결정을 할 수 있다는 의미다.

Github diff: ImportSelector를 이용한 동적 구성 정보 등록

자동 구성 정보 파일 분리, 적용

MyAutoConfiguration 추가하고, *.imports파일로 설정 클래스들 임포트되게 구성되게 개선했음.

proxyBeanMethods = false로한 의미는 뭔가?

기본적으로 proxyBeanMethods = true인 클래스가 빈으로 등록될 때 이게 직접 빈으로 등록되는게 아니라 Proxy Object를 앞에 하나 두고 그것이 빈으로 등록된다.

class MyConfigProxy extends MyConfig {
	private Common common;
	
	@Override
	Common common() {
		if(this.common == null) this common = super.common();
		
		return this.common;
	}
}

img_5.png

Github diff