[알고쓰자] Spring - 02 : 업데이트 내역과 함께하는 변천사 알아보기

[알고쓰자] Spring - 02 : 업데이트 내역과 함께하는 변천사 알아보기

Intro

현재 많은 개발자 분들이 Spring Boot 2.x 혹은 3.x을 사용하고 계실거에요. 저희 팀은 Spring Boot 3.x, JDK 17 을 채택하여 사용하고 있죠. 전 Spring Boot 2.x 에서 3.x 로 넘어가는 시점에서 Spring Boot 에 입문했어요. 갑자기 Spring Framework 6 버전 업데이트 소식과 함께 Spring Boot 3 릴리즈 소식을 들었던 게 엊그제같네요. Spring Security 쪽 코드가 상당히 바뀌었고, 막 입문해서 공부하던 저에겐 양날검 같은 소식이었어요. 공부할 때 보던 코드들이 최신 버전에선 전혀 쓸 수 없었으니까요.

이제와서 생각해보면...그깟 설정 코드들이 뭐가 그렇게 중요했을까 싶어요.
중요한 건 Spring Security 아키텍처의 이해도였는데...
그 당시엔 코드 하나하나가 너무 생소했어서 뭔가 @Bean 으로 등록하는 필터체인이 새롭게 다가왔던 기억이 나네요.
이상 흔한 주니어 개발자의 회상이었습니다.

과거를 회상하며 커피를 한잔하던 도중 과연 내가 Spring의 변천사를 알고쓰고 있는 지 의문이 들었습니다. Spring Framework 6 부터 입문했다고 해도 할 말이 없는 상황이었는데 그 전의 Spring 은 어땠는 지를 제대로 알고 쓰지 않았단 생각이 들었습니다.

이번 주제는 Spring 업데이트 내역과 함께하는 변천사입니다. 재밌게 읽어주세요.

주요 업데이트 내역

변천사를 알아보기 전에 업데이트 내역을 한번 정리해보면 아래와 같아요.

  • 1.0 : 2004년 3월 : XML

  • 2.0 : 2006년 10월 : XML 편의기능 지원

  • 3.0 : 2009년 12월 : Java 코드를 통한 Config

  • 4.0 : 2013년 12월 : Java 8 지원

  • 5.0 : 2017년 9월 : Spring Framework 5.0, Spring Boot 2.0 출시, Reactive Programming 지원

  • 6.0 : 2022년 11월 : Spring Framework 6.0, Spring Boot 3.0 출시

아마 주로 쓰고 있을것이라 생각되는 Spring Boot 는 2010년에 나왔어요.

여기서 잠시! Spring Boot 가 뭔가요?
Spring 기반 애플리케이션 개발을 빠르게 시작할 수 있도록 도와주는 도구에요.
Spring Boot를 쓰게 된다면, 여러 고민을 할 필요 없이 일단 프로젝트를 셋업하고, 필요할 때 커스터마이징을 할 수 있다는 장점이 있어요.
Spring Framework 그 자체를 그대로 사용하려면 개발자가 직접 판단을 통해 프로젝트의 구성을 결정해야 해요.
예시로 JSON 처리와 관련 된 라이브러리를 선택할 때 Spring Framework 만 사용한다면 Jackson, GSON 등 여러가지 선택지 중에 하나를 고르고, 버전 호환성을 고민해야 해요.
또한 web.xml 설정, Web Application Server 설정 등 서비스를 배포하기 위해 복잡한 설정을 건드려야 해요.
Spring Boot 는 이런 고민을 덜어주죠. 미리 필요하다고 판단되는 여러 라이브러리들을 미리 정해주고 상호 호환되는 버전을 제공해줘요. 게다가 WAS 또한 내장되어있어서 단일 jar 파일로 배포할 수 있죠.

주요 패치내역은 위와 같지만, Spring Framework 가 가지는 핵심 원리들은 크게 변화하지 않았어요.

그럼 지금부터 버전 별로 핵심적인 키워드들을 한번 짚어 가볼게요.

1.0 ~ 2.0 : XML

초기의 Spring 은 XML 을 통해 프로젝트의 셋업을 진행했어요.

Spring 프로젝트를 처음 셋업하면 web.xml, root-context.xml, servlet-context.xml 이 세가지의 파일이 생성됩니다. Spring Boot 로 프로젝트를 셋업한다면, 사실 직접적으로 볼 일은 없겠지만 알고 넘어가는 게 좋겠죠? 각각의 역할을 정리하면 아래와 같아요.

  • web.xml

    • 애플리케이션의 배포 및 구성 정보를 정의합니다.

    • 주로 DispatcherServlet 을 설정하는 데 사용됩니다.

    •  <web-app xmlns="http://java.sun.com/xml/ns/javaee"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                                    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
                version="3.0">
      
           <!-- 1. 서블릿 정의 -->
           <servlet>
               <servlet-name>dispatcher</servlet-name>
               <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
               <load-on-startup>1</load-on-startup>
               <init-param>
                   <param-name>contextConfigLocation</param-name>
                   <param-value>/WEB-INF/spring/dispatcher-servlet.xml</param-value>
               </init-param>
           </servlet>
      
           <!-- 2. 서블릿 매핑 -->
           <servlet-mapping>
               <servlet-name>dispatcher</servlet-name>
               <url-pattern>/</url-pattern>
           </servlet-mapping>
      
           <!-- 3. 필터 정의 -->
           <filter>
               <filter-name>encodingFilter</filter-name>
               <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
               <init-param>
                   <param-name>encoding</param-name>
                   <param-value>UTF-8</param-value>
               </init-param>
               <init-param>
                   <param-name>forceEncoding</param-name>
                   <param-value>true</param-value>
               </init-param>
           </filter>
      
           <!-- 4. 필터 매핑 -->
           <filter-mapping>
               <filter-name>encodingFilter</filter-name>
               <url-pattern>/*</url-pattern>
           </filter-mapping>
      
           <!-- 5. 리스너 정의 -->
           <listener>
               <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
           </listener>
      
           <!-- 6. 컨텍스트 초기화 파라미터 -->
           <context-param>
               <param-name>contextConfigLocation</param-name>
               <param-value>/WEB-INF/spring/root-context.xml</param-value>
           </context-param>
      
           <!-- 7. 세션 타임아웃 설정 -->
           <session-config>
               <session-timeout>30</session-timeout>
           </session-config>
       </web-app>
      
  • root-context.xml

    • 애플리케이션 전반에 걸쳐 사용되는 공통 빈들을 정의하는 파일입니다.

    • DB Connection, Security, Service 계층 등을 정의합니다.

    • <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:context="http://www.springframework.org/schema/context"
             xmlns:tx="http://www.springframework.org/schema/tx"
             xsi:schemaLocation="
                 http://www.springframework.org/schema/beans
                 http://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context
                 http://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/tx
                 http://www.springframework.org/schema/tx/spring-tx.xsd">
      
          <!-- 1. Component Scan -->
          <context:component-scan base-package="com.example.service" />
      
          <!-- 2. DataSource 설정 -->
          <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
              <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
              <property name="url" value="jdbc:mysql://localhost:3306/mydb" />
              <property name="username" value="root" />
              <property name="password" value="password" />
          </bean>
      
          <!-- 3. Hibernate SessionFactory 설정 -->
          <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
              <property name="dataSource" ref="dataSource" />
              <property name="packagesToScan" value="com.example.entity" />
              <property name="hibernateProperties">
                  <props>
                      <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                      <prop key="hibernate.show_sql">true</prop>
                  </props>
              </property>
          </bean>
      
          <!-- 4. 트랜잭션 매니저 설정 -->
          <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
              <property name="sessionFactory" ref="sessionFactory" />
          </bean>
      
          <!-- 5. 트랜잭션 관리 활성화 -->
          <tx:annotation-driven transaction-manager="transactionManager" />
      
          <!-- 6. 서비스 빈 등록 -->
          <bean id="myService" class="com.example.service.MyServiceImpl">
              <property name="someDependency" ref="someDependencyBean" />
          </bean>
      
      </beans>
      
  • servlet-context.xml

    • 특정 서블릿과 관련된 설정을 관리하는 파일입니다.

    • 주로 ViewResolver, Controller 등의 웹 계층 설정을 정의합니다.

    • <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:context="http://www.springframework.org/schema/context"
             xmlns:mvc="http://www.springframework.org/schema/mvc"
             xsi:schemaLocation="
                 http://www.springframework.org/schema/beans
                 http://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context
                 http://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/mvc
                 http://www.springframework.org/schema/mvc/spring-mvc.xsd">
      
          <!-- 1. 컴포넌트 스캔 -->
          <context:component-scan base-package="com.example.controller" />
      
          <!-- 2. Spring MVC 활성화 -->
          <mvc:annotation-driven />
      
          <!-- 3. 뷰 리졸버 설정 -->
          <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
              <property name="prefix" value="/WEB-INF/views/" />
              <property name="suffix" value=".jsp" />
          </bean>
      
          <!-- 4. 정적 자원 핸들링 -->
          <mvc:resources mapping="/resources/**" location="/resources/" />
      
          <!-- 5. 메시지 소스 설정 (다국어 지원) -->
          <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
              <property name="basename" value="messages" />
          </bean>
      
          <!-- 6. 기본 핸들러 매핑 -->
          <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
      
          <!-- 7. 디폴트 서블릿 핸들러 활성화 (정적 파일 처리) -->
          <mvc:default-servlet-handler />
      
      </beans>
      

Servlet? 그게 뭐죠? DispatcherServlet 은 또 무엇이구요?
Java 기반 웹 애플리케이션에서 서버 측에서 동작하는 프로그램입니다.
주로 클라이언트의 HTTP 요청을 처리하고 HTTP 응답을 생성하는 역할을 해요.
Thymeleaf 와 같은 템플릿 엔진을 활용해서 웹페이지를 생성하거나, JSON 형태의 응답을 생성하는 데 사용됩니다.
DispatcherServlet 은 Spring 에서 클라이언트의 요청을 처리해서, 적절한 컨트롤러로 요청을 분배 (Dispatch) 하는 역할을 합니다.
이번 포스팅의 주제는 Spring 아키텍처에 대한 내용이 아니니 DispatcherServlet 에 대한 내용은 여기까지 하겠습니다.

잠깐 예시들을 보여드렸는데, 저 설정들이 익숙하신 분들도 계실거에요. Spring Framework 를 쓰기 전에 학부에서 JSP 공부할 때 저런 파일들을 하나하나 셋업했던 기억이 나네요.

3.0 : Java 기반 Configuration

Spring Framework 3.0 부터 Java 기반 Configuration 을 지원하기 시작했습니다.

Spring Framework 3.0 은 우리에게 익숙할 수 있는 버전이죠? 토비의 스프링이 3.1 버전 기준으로 작성 되어있죠.


@Configuration
public class AppConfig {
    private @Value("#{jdbcProperties.url}") String jdbcUrl;
    private @Value("#{jdbcProperties.username}") String username;
    private @Value("#{jdbcProperties.password}") String password;

    @Bean
    public FooService fooService() {
        return new FooServiceImpl(fooRepository());
    }

    @Bean
    public FooRepository fooRepository() {
        return new HibernateFooRepository(sessionFactory());
    }

    @Bean
    public SessionFactory sessionFactory() {
        // wire up a session factory
        AnnotationSessionFactoryBean asFactoryBean =
            new AnnotationSessionFactoryBean();
        asFactoryBean.setDataSource(dataSource());
        // additional config
        return asFactoryBean.getObject();
    }

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(jdbcUrl, username, password);
    }
}

아마 Spring Boot 로 입문했던 저 같은 사람들은 이렇게 Java 기반 Config 이 익숙할거에요. 저도 이게 훨씬 익숙하네요.

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver" />
@Bean
public AcceptHeaderLocaleResolver localeResolver() {
    return new AcceptHeaderLocaleResolver();
}

xml 을 통해 bean 을 등록하려면 Bean ID 와 class path 를 입력 해 주어야 했어요. Java 기반 Config 은 훨씬 직관적으로 Bean 컴포넌트를 명세하고 등록할 수 있죠.

@Bean@Component 의 차이는 개발자가 얼마나 개입하느냐의 차이에요.
@Bean 은 RestTemplate, DataSource 등 개발자가 개입하기 어려운 외부 라이브러리 Bean 을 생성할 때 사용해요.
@Component 는 개발자가 코딩 한 컴포넌트를 Bean 으로 등록할 때 사용하죠.

@Component
public class ExampleComponent () {
  //TODO
}

4.0 : Java 8

Java 8 버전은 제가 학부때 처음 JDK 를 설치했을 때 사용했 던 버전이에요. 생각보다 Java 8 이 릴리즈 된 게 오래 전의 일은 아니에요. 2014년 릴리즈니까 이제 10년이 지났죠.

2030 년 까지 지원되는 현재 많은 유저들이 아직도 사용중인 Java 버전이에요. 사견으로는 Java 8에서도 상당히 많은 변화가 있었고, 그 당시에 Java 8 마이그레이션 했던 개발자들이 상당히 고생해서 그런 것 아닐까 하는 생각도 듭니다. (두번은 하기싫어요!!! 💦 )

Java 8 에서 볼 수 있는 가장 큰 변화점들에 대해 설명드리고 넘어갈게요.

class Main {
    public static void main(String[] args) {
        List<Integer> numList = Arrays.asList(0,1,2);
        numList.forEach(x -> System.out.println(x));
    }
}

먼저 Lambda 식이에요. 이전에 익명함수라고 불리던 기능이 있었죠. Java 8 부터는 이 익명함수를 Lambda 표현식으로 작성할 수 있어요.

public interface IExample {
    int getMax(int x, int y);
}

//Java 8  이전의 익명함수
public static void main(String[] args) {
    IExample iExample = new IExample() {
        @Override
        public int getMax(int x, int y) {
            return (x > y) ? x : y;
        }
    };
    System.out.println("결과는 " + iExample.getMax(10,20) + "입니다.");
}

//Java 8 부터의 익명함수
public static void main(String[] args) {
    IExample iExample = (x, y) ->x > y ? x : y;
    System.out.println("결과는 " + iExample.getMax(10,20) + "입니다.");
}

다음으로 Optional 클래스 기능이에요.
Null 값이 될 수 있는 객체를 Optional 객체에 담을 수 있어요. Null 값을 직접적으로 건드리는 경우 Java 에서는 NullPointerException 이 발생할 수 있지만, 언어의 특성 상 이 NPE를 컴파일 단계에서 피할 수 있는 방법이 당시엔 없었어요. Optional 객체를 사용하면, Nullable 한 객체에 대한 접근을 보호함과 동시에 Stream API 와 연계한 로직을 처리할 수 있어요.

class Main {
    public static void main(String[] args) throws Exception {
       Car car = new Car();

       // 빈 생성자로 생성 된 객체의 값에 직접 접근하면 NPE가 발생할 수 있습니다.
       System.out.println(car.getPerson().getName())

       // Optional 을 연계하면 NPE 가 발생하지 않습니다.
       System.out.println(Optional.of(car)
               .map(Car::getPerson)
               .map(Person::getName)
               .orElse("사용자 없음"));
    }
}

class Person {
    private String name;
    //getter, setter
}

class Car {
    private String name;
    private Person person;
   //getter, setter
}

다음으로 Stream API 이에요.
Stream API 는 컬렉션 데이터를 처리하기 위해 사용되는 Java 8 에서 추가 된 신규 기능이에요. 데이터의 연속적인 흐름을 표현한다는 의미에서 Stream 이라는 이름이 생겼다고 하네요. for Loop 반복문을 통한 데이터 처리보다 간결하고 가독성이 높은 코드를 작성할 수 있어요. 또한 데이터를 가공하는 데 쓰이는 다양한 기능들을 제공해요.

Lambda 식과는 일맥상통 한다고 볼 수 있는 기능인데, Stream API 는 컬렉션 데이터에 대핸 Stream 을 생성시켜서 연산을 처리하는데, 이 연산 처리 과정에서 Lambda 식을 연동시킬 수 있어요.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> result = names.stream()
                          .filter(name -> name.startsWith("A"))
                          .collect(Collectors.toList());

filter, map, sorted, distinct 등의 중간 연산 (Intermediate Operators) 사이에 들어가는 로직은 Lambda 식으로 작성할 수 있어요. 또한 Stream의 마지막 최종 연산 collect, forEach, reduce, count 등 (Terminal Operations) 을 통해 우리가 원하는 결과로 값을 변환시킬 수 있죠.

또한 Stream API 는 병렬처리를 지원해요. pararellStream 을 생성해서 병렬적으로 데이터를 처리할 수 있죠. 내부적으로 Fork/Join 프레임워크를 사용하여 여러 쓰레드에서 데이터를 분할하고 처리한 후 결과를 결합시켜줘요.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> result = names.parallelStream()
                           .filter(name -> name.startsWith("A"))
                           .collect(Collectors.toList());

다음으로 LocalDateTime 이에요.
이 기능에 대해선 설명을 짧게 정리하겠습니다. 불변 시간정보를 가지고 있는 객체고 기존의 DateTime, Date 를 대체하는 기능이죠.

마지막으로 interface default, static 메소드에요.
인터페이스 자체는 구현부가 없죠. 하지만 Java 8 부터는 default, static 지시어로 된 메소드를 구현할 수 있어요.

차이점이 있다면 default 는 재정의가 가능하고, static 은 재정의가 불가능해요.

public interface Calculator {

    default int sumDefault(int x, int y) {
        return x + y;
    }

    static int sumStatic(int x, int y) {
        return x + y;
    }
}

여기까지 정리 해 보니 왜 자꾸 Java8 에서 안 넘어 가시려는 지 알것 같네요. 이거 어느세월에 마이그레이션 하셨데요...

5.0 : Reactive Programming

Spring Framework 5.0 이 출시 되었습니다. Spring Boot 는 2.0 이 출시 된 시점이죠.

이 시점부터 Spring 은 Reactive Programming 을 지원하기 시작했죠. 찬호 대리님이 지난 호에 Webflux 에 대한 글을 작성하셨죠.

Reactive Programming 을 지원하기 전 Spring 은 전형적인 MVC 패턴으로 요청을 처리했습니다. 추후 Spring 의 구조에 대해 포스팅을 할 때 더 자세히 다루겠지만, Spring 은 Model 의 데이터를 View 에 출력하고 Controller 를 통해 View 와 Model 사이의 요청을 관리해요.

여기까지만 봤을 땐 당연한 것 아닌가...라는 생각이 드시겠지만, 이 패턴은 대규모의 요청이 들어왔을 때 시스템이 부하를 주는 구조라는 단점이 존재해요.

Spring MVC 에서는 매 요청마다 스레드를 할당 시켜요. 요청에 대한 응답이 나오기 전까지 스레드는 유지되죠. 이를 블로킹 방식이라고 해요. 요청이 늘어날 수록 할당 되는 스레드도 많아지겠죠. 언젠가 이 스레드가 고갈되게 된다면 더이상 요청을 정상적으로 처리할 수 없겠죠.

Reactive Programming 을 사용하려면 Spring Webflux 를 사용하실 수 있어요. Webflux 는 비동기 논블로킹 방식을 사용하는데, 요청이 들어오면 즉시 처리할 수 없다면 이벤트루프에 등록하고, 이후 작업이 완료되면 이벤트를 발생시켜 처리하는 방식이에요. 해당 방식은 스레드를 적게 사용하고, 많은 요청을 효과적으로 처리할 수 있죠.

2017년 시점에서 기억을 되돌아보면 저는 군대 (해군병 639기 입니다. 해군이시라면 함께 소주와 함께 군대 얘기를....크흡) 에 있긴 했지만, 이미 대부분의 사람들의 손에는 스마트폰이 쥐어 진 상태였죠. 꼭 최신 플래그쉽이 아니더라도 그 당시엔 저가형 스마트폰도 상당히 많았던 기억이 나요. 이미 그 시점에서도 유튜브나 SNS는 핫 했죠. 인터넷 서비스의 접근성이 낮아지는 만큼 트래픽은 늘어났고, Spring 생태계에도 이러한 대용량 트래픽을 적은 비용으로 해결할 수 있는 방법들에 대한 요구가 늘어났었다고 해요.

Spring Webflux 와 MVC의 코드를 한번 비교해볼게요. /hello/{name} 경로에 대한 요청을 받으면, Service 레이어에서 "Hello, {name}!" 이라는 데이터를 반환하는 예시에요. 편의상 이번엔 코틀린으로 작성 해 보았습니다.

// Spring MVC Controller
@RestController
@RequestMapping("/mvc")
class HelloController(private val helloService: HelloService) {

    @GetMapping("/hello/{name}")
    fun sayHello(@PathVariable name: String): ResponseEntity<String> {
        val message = helloService.getGreeting(name)
        return ResponseEntity.ok(message)
    }
}

// Spring MVC Service
@Service
class HelloService {

    fun getGreeting(name: String): String {
        // 비즈니스 로직 처리
        return "Hello, $name!"
    }
}

우리가 알고있는 전형적인 코드죠. 요청이 들어오는 순간 스레드가 할당되어 코드들을 실행해요.

// Spring WebFlux Controller
@RestController
@RequestMapping("/webflux")
class HelloReactiveController(private val helloService: HelloReactiveService) {

    @GetMapping("/hello/{name}")
    fun sayHello(@PathVariable name: String): Mono<String> {
        return helloService.getGreeting(name)
    }
}

// Spring WebFlux Service
@Service
class HelloReactiveService {

    fun getGreeting(name: String): Mono<String> {
        // 비즈니스 로직 처리
        val message = "Hello, $name!"
        return Mono.just(message)
    }
}

요청이 들어오면 비동기적으로 sayHello 메소드가 실행되고, Mono 가 완료될 때 까지 스레드를 점유하지 않아요. MVC에서는 커피를 반드시 그자리에서 받아야 했다면, Webflux 에서는 커피를 주문하고 기다릴 수 있는거죠.

Webflux에서는 Mono, Flux 이 두가지를 요청의 최종 리턴타입으로 가질 수 있어요. Mono 는 최대 하나, Flux는 0개 이상의 요소를 리턴하는 경우에 사용하죠.

fun findUserById(id: String): Mono<User> {
    return Mono.just(User(id, "Alice"))
}

fun findAllUsers(): Flux<User> {
    return Flux.just(
        User("1", "Alice"),
        User("2", "Bob"),
        User("3", "Charlie")
    )
}

6.0 : 그리고 지금

2022년 Spring Framework 6.0 이 출시되었죠. Spring Boot 또한 3.0 이 출시되었습니다. 지금 저희 팀에선 3.1.X를 사용하고 있죠.

SpringFramework 6.0 이상부터 최소 JDK 17 이상이 필요합니다. 또한 Java EE 를 사용하던 많은 라이브러리들이 Jakarta EE 로 변환되었습니다.
특히 JPA 와 관련 된 코드들은 Java EE 가 아닌 Jakarta EE 로 마이그레이션 해야 했어요.

기존의 Java EE 는 왜 Jakarta EE 로 대체되었을까요?
Oracle 이 2017 년 Jave EE 8 릴리즈를 마지막으로 이클립스 재단에 Java EE 프로젝트를 이관했기 때문이에요.
Java EE 는 1999년 썬 마이크로시스템즈가 발표한 소프트웨어 개발 표준 플랫폼이에요.
Oracle 이 썬 마이크로시스템즈를 인수하면서 Java EE 프로젝트의 주도권을 가지고 있었으나, Spring Framework 등의 오픈소스 소프트웨어가 발전하면서 상업적 목적으로의 Java EE 프로젝트 개발에 메리트가 줄었기 때문이에요.
이클립스 재단에 Java EE 가 넘어간 이후로부턴 Jakarta EE 라는 이름으로 불리고 있어요.
Java EE 라는 이름의 상표권은 아직 Oracle 에 있기 때문이에요.

또한 XML 은 SpringFramework 6.0 이후부턴 가능하면 사라지는 추세로 변경 된다고 해요. 또한 사소한 변경으로는 HttpMethod가 Enum 이 아니라 Class 로 변경된다는 등이 있죠.

눈에 띄는 변화로는 AOT (Ahead Of Time) 엔진을 지원한다는 점인데, 기존의 JVM 방식에서는 프로그램이 올라갈 때 바이트 코드를 컴파일 해요.

하지만 Spring AOT는 스프링 애플리케이션의 코드와 의존성을 분석하여 필요한 정보를 미리 생성된 메타데이터로 저장하고, 이후 GraalVM을 사용하여 네이티브 이미지로 컴파일할 수 있게 하죠. 이를 통해 최종적으로 생성된 애플리케이션은 JVM 없이도 실행 가능한 바이너리 파일로 만들 수 있어요.

주로 spring-aot-maven-plugin이나 spring-aot-gradle-plugin을 사용하여 AOT 빌드 프로세스를 지원해요.

JVM 을 통해 실행하는 Spring 에 비해 Spring AOT 를 사용하면 아래와 같은 이점을 가질 수 있어요요.

  • 애플리케이션 실행시간 단축

    • AOT로 생성 된 네이티브 이미지는 JIT 컴파일이 불필요해요. 이 점은 서버리스 환경 (AWS Lambda) 에서 유리할 수 있어요.
  • 낮은 메모리 사용량

    • AOT로 생성 된 네이티브 이미지는 메모리를 덜 사용해요. 필요하지 않은 메타데이터를 줄이고, 가비지 컬렉션 부담을 줄여 애플리케이션이 보다 적은 메모리로 실행될 수 있어요.
  • 경량화 및 배포 용이성

    • JVM이 필요 없는 네이티브 이미지이기 때문에 배포 파일 크기가 줄어들며, 경량 컨테이너 이미지 작성이 가능해요.
    • 이는 클라우드네이티브 애플리케이션이나 컨테이너화된 애플리케이션에서 이점을 제공해요.
  • 성능 최적화

    • GraalVM의 AOT 방식은 JIT보다 성능이 더 나은 경우가 많으며, 특히 애플리케이션이 일정한 성능을 지속적으로 요구할 때 유용하죠.

그 외에 변동사항으로는 jakarte.servlet 은 Tomcat 11, Jetty11 을 사용하고, RPC 지원이 만료됩니다.
jakarta.persistence 는 Hibernate ORM 6버전 기준으로 변경되었구요. 일부 Java EE API (EJB, JCA, JAX-WS) 는 지원이 불가능 할 수 있다 정도네요.

요약하면 SpringFramework 6.0 부터는 클라우드 네이티브 환경에 대응 한 버전이라고 할 수 있어요.

Outro

지금까지 간략하게 SpringFramework 의 변천사에 대해 알아보았어요. 한번의 포스팅으로는 Spring 의 모든 변천사를 다룰 순 없겠지만, 큰 핵심 맥락들은 알아볼 수 있었어요.

프레임워크가 시간이 지날 수록 현재의 웹 서비스 환경에 좀 더 최적화 되어가는 과정을 돌아볼 수 있었어요. 실제로 가면 갈 수록 클라우드 환경에서 실행되는 서비스가 늘어나고 있잖아요? 상황에 따라 Scale UP & Out 되거나 빠른 배포를 지원하는 배포 파이프라인을 구축할 수 있는 클라우드 환경은 서비스의 개발 속도를 빠르게 하고, 서비스의 유지보수 비용을 줄여주죠.

글을 쓰기 전 까지는 사실 SpringFramework 6.0 이상을 워낙 당연하다시피 쓰고 있어서 얼마나 해당 버전이 클라우드 환경에 최적화 되어 있는 지 잘 와닫 지 않았어요. Docker 와 같은 컨테이너 환경을 사용할 수 있을 땐 별 생각 없이 이미지 빌드 기능을 사용했었죠. 프레임워크의 개발 방향성이 기능 그 자체도 있지만, 환경에 맞춰서 변해간다는 생각을 많이 했어요. Java 버전이 올라가니까, XML 파일이 잘 안쓰이니까 프레임워크도 마찬가지로 사람들의 니즈에 맞게 변해가는 모습을 다시 되돌아보며 사람들의 니즈를 계속 관찰하는 개발자가 되어야 겠단 생각이 들었어요.

다음 포스팅의 주제는 Spring 의 구조 편입니다. 이렇게 업데이트 내역이 길고 곧 7.0 이 나오는 Spring 이라고 해도 핵심 구조가 크게 변하지는 않았어요. 과연 우리는 Spring 의 구조를 잘 알고 쓰고 있을까요?

다음 포스팅도 꽤나 고생할 것 같네요....다음에 다시 봽겠습니다.