[Spring] 스프링 부트 소개 및 서블릿 컨테이너

easyone·2024년 10월 2일
0

Spring

목록 보기
9/11

스프링 프레임워크 등장 배경

  • EJB가 자바 진영의 표준 기술로 잘 되어있는 기술이었지만, 굉장히 복잡하고 느리기도 해서 공부하기 어려운 기술이었음
  • 스프링의 등장: EJB 컨테이너를 대체할 수 있게 되었고, 단순하게 개발이 가능하여 현재 사실상 표준 기술이 되었음
    • MVC, DB 접근 기술 등 많은 기능을 제공하고 다양한 문제를 해결할 수 있음
    • 다양한 라이브러리들을 편리하게 사용할 수 있도록 통합함
    • 개발자의 생산성이 높아짐

스프링 프레임워크의 확대

  • 스프링 DI 컨테이너, AOP, 스프링 MVC, 트랜잭션, ORM, 캐시, 스케줄링, 테스트 지원 등 스프링 프레임워크가 제공하는 기능이 점점 증가하고 있다.
  • 다양한 오픈 소스의 등장으로 수많은 라이브러리를 함께 사용해야 한다.(버전도 호환되는 것을 사용해야함)
  • 프로젝트를 시작할 때 필요한 설정이 점점 늘어나고 복잡해져서 시작하는 것이 점점 어려워지게 된다.
  • 라이브러리를 스프링 컨테이너와 연결하려면 각각 스프링 빈을 등록해야 하는데 설정하는 과정도 복잡해지고, 시작할 때마다 반복해야 하기 때문에 번거로워진다. 이러한 설정을 자동으로 해주는 스프링 부트가 등장하게 된다.

스프링 부트

  • 스프링을 편리하게 사용할 수 있도록 지원해주며, 최근에는 기본으로 사용한다.
  • 단독으로 실행할 수 있는 스프링 애플리케이션을 spring boot starter 같은 것들을 통해 쉽게 생성할 수 있도록 해준다.

핵심 기능

  • WAS: 톰캣 같은 웹서버를 내장해서, 별도의 웹 서버를 설치하지 않아도 된다.
  • 라이브러리 관리: 쉬운 빌드 구성을 위해서 스타터 종속성을 제공한다.스프링에서 외부 라이브러리를 사용할 때 버전을 원래 직접 지정해줘야 하는데, 버전을 내부에서 검증하여 자동으로 관리해준다.
  • 자동 구성: 프로젝트 시작에 필요한 외부 라이브러리의 빈을 자동으로 등록해준다.
  • 외부 설정: DB,서버 등의 환경에 따라 달라져야 하는 외부 설정을 공통화해준다.
  • 프로덕션 준비: 모니터링을 위한 메트릭, 상태 확인 기능을 제공한다.

스프링 프레임워크와 스프링 부트

스프링 프레임워크를 사용하면, 스프링 부트가 제공하는 편의 기능이 너무 좋기 때문에 이제는 무조건 스프링 부트를 필수적으로 사용해야 한다. 하지만 스프링 부트는 스프링 프레임워크를 쉽게 사용할 수 있게 도와주는 도구일 뿐이고, 본질은 스프링 프레임워크다. 스프링 데이터, 세션, 시큐리티, 배치, 클라우드 등의 기능은 선택사항이다.

웹 서버와 스프링 부트 소개

내장 서버 vs 외장 서버

전통적인 방식

자바로 웹 애플리케이션 개발 시에는 먼저 서버에 톰캣 같은 웹 애플리케이션 서버를 설치 (WAS)
-> 그 안에서 동작하도록 서블릿 스텍에 맞춰서 코드를 작성하고 WAR라는 형식으로 빌드해서, war 파일을 생성한다.
-> 만들어진 war 파일을 WAS에 전달해서 배포하는 방식

이러한 방식은 인텔리제이, 이클립스 같은 개발환경에서 WAS와 연동하는 추가 설정이 들어가야 한다.

최근 방식

스프링 부트가 내장 톰캣을 포함하고 있다. 즉 애플리케이션 코드 안에 WAS가 내장되어 있고, 코드 작성 시 Jar로 빌드하고 원하는 위치에서 실행하기만 하면 별도 설정 없이 WAS가 실행된다. 메인 메서드만 실행하면 웹 애플리케이션을 실행할 수 있게 된다.

JAR vs WAR

JAR
자바는 여러 클래스,리소스를 묶어서 JAR라는 압축 파일을 만들 수 있다. JVM 위에서 직접 실행되거나 다른 곳에서 사용하는 라이브러리고 제공된다. 직접 실행하는 경우 메인 메서드가 필요하고, MANIFEST.MF 파일에 실행할 메인 메서드가 있는 클래스를 지정해두어야 한다.
WAR
WAR 파일은 웹 애플리케이션에 배포할 때 사용하는 파일이다. 스프링에서 파일을 빌드하고 생성된 WAR 파일의 압축을 풀어보면 구조가 다음과 같은 것을 알 수 있다.

  • WEB-INF: 라이브러리, 자바 클래스, 설정 정보가 들어가는 곳
    - classes: 실행 클래스 모음
    - lib: 라이브러리 모음
    -web.xml: 웹 서버 배치 설정 파일, 생략 가능
  • 그 외: 정적 리소스가 사용되는 영역

서블릿 컨테이너 초기화

ServletContainerIntializer 인터페이스 사용

서블릿은 ServletContainerIntializer라는 초기화 인터페이스를 통해 서블릿 컨테이너를 초기화하는 기능을 제공한다.

public interface ServletContainerIntializer {
	public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

ServletContext ctx: 서블릿 컨테이너로, 필터나 서블릿을 등록할 수 있다.

웹 애플리케이션을 실행하게되면, 서블릿 컨테이너 실행 시점에 MyContainerInitV1 메서드를 실행하게 된다.

프로그래밍 방식으로 직접 서블릿 등록

먼저 HelloServlet이라는 서블릿을 등록해준다.

public class HelloServlet  extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("HelloServlet.service");
        resp.getWriter().println("hello servlet!");
    }
}

서블릿 컨테이너는 애플리케이션 초기화라는 기능을 지원하는데, 먼저 AppInit이라는 인터페이스를 만들어준다.

public interface Applnit {
    void onStartUp(ServletContext servletContext);
}
public class AppInitServlet implements AppInit {
    @Override
    public void onStartUp(ServletContext servletContext) {
        System.out.println("AppInitServlet.onStartUp");

        // 순수 서블릿 코드 등록
        ServletRegistration.Dynamic helloServlet =
                 servletContext.addServlet("HelloServlet", new HelloServlet());

        helloServlet.addMapping("/hello-servlet");
    }
}

HelloServlet이라는 서블릿을 서블릿 컨테이너에 직접 등록하게 된다. /hello-servlet 요청을 보내면 서블릿이 실행된다. @WebServlet 애노테이션을 통해 서블릿을 등록할 수도 있지만, 프로그래밍 방식을 사용해서 등록할 수도 있다.

애노테이션 하나로 서블릿을 등록할 수 있다는 장점이 있지만, 하드코딩된 것처럼 동작하기 때문에 유연하게 변경하는 것이 어렵다는 단점이 있다. 프로그래밍 방식은 코딩을 더 많이 해야하지만 무한한 유연성을 제공한다. HTTP 요청 경로를 상황에 따라 위처럼 변경할 수 있고, 서블릿 자체도 if문으로 분기해서 등록하거나 빼는 것이 가능하다. 서블릿을 직접 생성하기 때문에 생성자에 필요한 정보를 넘길 수도 있다.

애플리케이션 초기화 실행 방식

@HandlesTypes(AppInit.class) // 애플리케이션 초기화 인터페이스 지정
public class MyContainerInitv2 implements ServletContainerInitializer {

    @Override
    public void onStartUp(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        System.out.println("MyContainerInitv2.onStartUp");
        System.out.println("MyContainerInitv2 c " + c);
        System.out.println("MyContainerInitv2 ctx = " + ctx);

        for (Class<?> appInitClass : c) {
            try{
                AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
                appInit.onStartUp(ctx);
                }catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

@HandlesTypes : 애플리케이션을 초기화하는 인터페이스를 지정한다. Set<Class<?>> c 에 AppInit의 구현체들이 모두 넘어오게 되는데, 이 코드에서는 AppInitServlet의 정보가 넘어오게 된다.
객체 인스턴스가 아닌 클래스가 넘어오기 때문에 객체를 생성해서 사용해야 한다.
appInit.onStartUp(ctx) : 애플리케이션 초기화 코드를 직접 실행하는 부분이고, 서블릿 컨테이너 정보가 담긴 ctx도 함께 전달한다.

애플리케이션 초기화까지 별도로 하게 되면 다음과 같은 장점이 있다.

  • 편리성: 서블릿 컨테이너 초기화 시 ServletContainerInitializer 인터페이스 구현한 코드를 만들고 추가로
    META-INF/services/jarkarta.servlet.ServletContainerInitializer 파일에 코드를 직접 지정해줘야 한다. 애플리케이션 초기화의 경우 AppInit 인터페이스를 만들어두면 필요할 때마다 구현체를 만들어서 사용하면 되기 때문에 편리하다.

  • 의존성: 애플리케이션 초기화에서, 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있기 때문에 서블릿 컨테이너에 대한 의존을 줄일 수 있다는 장점이 있다.

스프링 컨테이너 등록

앞의 서블릿 컨테이너 초기화와 애플리케이션 초기화를 활용해서 스프링 컨테이너를 등록할 수 있다. 과정은 다음와 같다.

  • 스프링 컨테이너 생성
  • 스프링 MVC 컨트롤러를 스프링 컨테이너에 빈으로 등록
  • 스프링 MVC를 사용할 때 필요한 디스패처 서블릿을 서블릿 컨테이너에 등록

먼저 컨트롤러를 생성해준다.

@RestController
public class HelloController {

    @GetMapping("/hello-spring")
    public String hello() {
        System.out.println("Hellocontroller.hello");
        return "hello spring";
    }
}

config 파일을 하나 생성해서 빈으로 등록해준다.

@Configuration
public class HelloConfig {
    @Bean
    public HelloController helloController() {
        return new HelloController();
    }
}
public class AppInitSpring implements AppInit{
    @Override
    public void onStartUp(ServletContext servletContext) {
        System.out.println("AppInitSpring.onStartUp");
        
         // 스프링 컨테이너 생성
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(HelloConfig.class);

        // MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcher = new DispatcherServlet(applicationContext);

        // 디스패처 서블릿을 서블릿 컨테이너에 등록
        ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", dispatcher);

        // /spring/* 요청이 디스패처 서블릿을 통하도록 설정
        servlet.addMapping("/spring/*");

    }
}

초기화 인터페이스인 AppInit 구현체를 하나 만들어주면 실행 시에 구현체도 실행되게 된다.

  • 스프링 컨테이너 생성 : AnnotationConfigWebApplicationContext가 스프링 컨테이너이고, appContext.register(HelloConfig.class); 에서 컨테이너에 스프링 설정을 추가한다.

  • 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결: dispatcher라는 디스패처 서블릿을 생성하고, 앞서 생성한 applicationContext라는 스프링 컨테이너를 전달하면 스프링 컨테이너가 디스패처 서블릿에 연결된다.

  • 디스패처 서블릿을 서블릿 컨테이너에 등록: addServlet을 통해 디스패처 서블릿을 서블릿 컨테이너에 등록하게 된다. 그러면 /spring/* 및 그 하위 요청들이 해당 서블릿을 통하게 된다.

스프링 MVC 서블릿 컨테이너 초기화 지원

스프링 MVC을 사용하면 서블릿 컨테이너 초기화 과정을 생략하고 애플리케이션 초기화 코드만 작성하면 된다.
스프링 MVC에서 지원하는 초기화 작업을 사용하려면 WebApplicationInitializer 인터페이스만 구현하면 된다.
spring-web 라이브러리에서 해당 작업을 구현하고 있는 것이다.

일반적으로는 스프링 컨테이너를 하나 만들고, 디스패처 서블릿도 하나만 생성하고 매핑 경로도 / 로 설정해서 하나의 디스패처 서블릿을 통해 모든 것을 처리하도록 한다.

profile
백엔드 개발자 지망 대학생

0개의 댓글