Java 17 그리고 Spring Boot 3.x

GyeongNam·2024년 3월 16일
1

프로젝트에서 자바 17과 Spring boot 3.x 버전을 사용했다.
이번 글을 통해 왜 사용했으며, 어떠한 점이 기존 jdk 8,11 과 Spring 2.x 와 다른지 설명하고자 한다.


JDK

자바 개발 키트(Java Development Kit). 썬 마이크로시스템즈에서 개발한 Java 환경에서 돌아가는 프로그램을 개발하는 데 필요한 툴을 모아놓은 소프트웨어 패키지이다. JRE(Java Runtime Environment)와 Java 바이트코드 컴파일러, Java 디버거 등을 포함하는 개발 도구로 이루어져 있다. IBM에서 자체적으로 변형한 IBM JDK와 오픈 소스 버전인 OpenJDK도 있다.

간단한 버전별 기능 추가 내용은 아래와 같다.

JDK 8

  • 람다 표현식: 함수형 프로그래밍 지원을 위해 람다 표현식이 추가되었다.
// 기존 방식
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, World!");
    }
};
// 람다 표현식 사용
Runnable runnable2 = () -> System.out.println("Hello, World!");
  • 스트림 API: 컬렉션을 처리하기 위한 새로운 스트림 API가 도입되었다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println);
  • 날짜 및 시간 API 개선: java.time 패키지를 통해 새로운 날짜 및 시간 API가 도입되었다.
LocalDate today = LocalDate.now();
System.out.println("Today's date: " + today);
  • 메서드 참조: 메서드를 가리키는 참조를 사용하여 코드를 간결하게 할 수 있다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println);
  • 병렬 스트림: 스트림 API의 병렬 처리 기능이 추가되었다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
                .mapToInt(Integer::intValue)
                .sum();
System.out.println("Sum of numbers: " + sum);
  • PermGen 제거: Permanent Generation 영역이 제거되고 Metaspace로 대체되었다.

JDK 11

  • HTTP 클라이언트: 기본적으로 제공되는 HTTP 클라이언트가 추가되었다.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpClientExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                                          .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
                                          .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println("Response body: " + response.body());
    }
}
  • 지속적인 GC: Z Garbage Collector와 Epsilon GC가 추가되었다.
// Z Garbage Collector를 사용하여 GC 로그를 출력하는 예시
-XX:+UseZGC
  • 지속적인 빌드: JDK의 빌드 프로세스가 Maven과 Gradle을 사용하여 구성되었다.

  • 지속적인 로그: JDK의 로깅 시스템이 갱신되었다.

  • 지속적인 파일 I/O: 새로운 파일 시스템 API가 추가되었다.

// 새로운 파일 시스템 API를 사용하여 파일을 읽는 예시
Path path = Path.of("example.txt");
List<String> lines = Files.readAllLines(path);
  • 로컬 변수 문법 개선: var 키워드를 사용하여 지역 변수의 형식을 추론할 수 있다.
// JDK 11 이상에서의 var 키워드 사용 예시
public class LocalVariableExample {
    public static void main(String[] args) {
        // 기존 방식
        String message1 = "Hello, World!";
        System.out.println(message1);
        
        // 로컬 변수 문법 개선을 통한 var 키워드 사용
        var message2 = "Hello, World!";
        System.out.println(message2);
        
        // 타입 추론에 의해 message2의 형식은 String으로 결정됨
    }
}

JDK 17

  • 패턴 매칭: instanceof 키워드를 통해 패턴 매칭이 도입되었다.
// instanceof 키워드를 통한 패턴 매칭 예시
Object obj = "Hello";
if (obj instanceof String str) {
    System.out.println("String length: " + str.length());
} else {
    System.out.println("Not a string");
}
  • Sealed 클래스: 클래스와 인터페이스를 봉인하여 상속과 구현을 제한할 수 있다.
// Character.java
public sealed class Character permits Hero, Monster {}

// Hero.java
// permits 로 선언된 class 들 (Link , Mario) 만이 Hero class 를 상속할 수 있다.
public sealed class Hero extends Character permits Link, Mario {}

// Monster.java
// non-sealed 로 선언된 Monster 는 어떤 class 든지 상 속 할 수 있다.
public non-sealed class Monster extends Character {}

// Hero 를 상속 받을 수 있는 클래스는 Link, Mario 뿐이다.
public final class Link extends Hero {}
public final class Maro extends Hero {}
// Link 와 Mario 는 final 로 선언된 class 들이기 때문에,
// 어떠한 class 도 이 둘을 상속 할 수 없다.
public class Troll extends Hero {} // 상속 불가능, 에러
public class Troll extends Link {} // 상속 불가능, 에러
  • Vector API: 벡터화 연산을 수행하는 데 사용할 수 있는 벡터 API가 추가되었다.
import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) {
        // Vector 생성
        Vector<String> vector = new Vector<>();

        // 요소 추가
        vector.add("Apple");
        vector.add("Banana");
        vector.add("Orange");

        // 요소 출력
        System.out.println("Vector: " + vector);

        // 요소 개수 확인
        System.out.println("Size: " + vector.size());

        // 요소 접근
        System.out.println("First element: " + vector.get(0));

        // 요소 삭제
        vector.remove(1);
        System.out.println("After removing Banana: " + vector);

        // 특정 요소의 인덱스 확인
        int index = vector.indexOf("Orange");
        System.out.println("Index of Orange: " + index);
    }
}
  • 노트 개선: 노트를 통해 JVM 런타임을 변경하지 않고도 JVM이나 JDK에 변화를 가할 수 있다.
  • 테스트를 위한 JDK 확장: JDK 17부터는 개발자가 테스트 목적으로 JDK를 확장하고 구성할 수 있다.

그래서?

우리는 Java를 사용할때 각 시장현황과 프로젝트에 알맞게 사용해야한다.
따라서 먼저 JDK를 담당하는 대표회사 oracle 사이트를 확인해 보자

https://www.oracle.com/java/technologies/java-se-support-roadmap.html

연장 종료일이 32년 1월인 11버전이 더 좋은거 아닌가? 라고 생각할 수 있지만 아직 17버전을 프리미어 종료 버전도 끝나지 않았다는 점을 기억하자.

당연히 오랬동안 지원하는 JDK를 선택하는게 좋을것이다.

다음은 사용량을 좀 확인해 보자

미국의 뉴 렐릭 이라는 웹 추적 및 분석 회사인데 2023년 1월 의 글이다.

https://newrelic.com/kr/resources/report/2023-state-of-the-java-ecosystem#toc--lts-java-14

Java 17 사용자 도입률 1년 만에 430% 증가

다음은 인텔리제이를 만든 회사. 젯브레인스 사이트를 방문하자.

https://www.jetbrains.com/ko-kr/lp/devecosystem-2023/

  • 2021, 2022

  • 2023

java 17의 사용량이 크게 증가하고 있다.
이건 개인적인 생각이지만 java 8 -> 11 -> 17 이 아니라
java 8 -> 17 로 건너뛰고 있는 것 같다는 생각이 들었다.

예를들어 코로나 펜데믹 이후 급격한 개발시장의 수요증가라던가.
java를 대표 프레임워크인 Spring의 버전 3.x 부터 java 17 버전이상부터 가능하다던가...

물론 국내시장은 조금 보수적이어서 해외시장을 1~2년 간격을 두고 따라가는 느낌이 없지 않아 있는데 일단 java 17이 많이 쓰일거란 것은 틀림 없다고 생각한다.

물론 해당 내용은 지극히 개인적인 견해이다.


그럼 Spring boot 3.x 를 쓴 이유는?

https://start.spring.io/

여기의 기본 세팅을 보아라

딱 보면 Spring boot 2.x 버전 대와 java 11을 선택하는 곳도 없다.

build.gradle 의

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.11'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.encore'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '11'
}

gradle 업데이트하라고 종료할때마다 붉은 글씨 봐야하고
파일 내용도 위에 내용으로 변경해줘야하는데 귀찮다.
나는 사용량이 늘어난 17과 spring boot 3.x를 써야겠다.


2.x 와 3.x 의 코드레벨에서의 차이는?

// Java EE에서 Jakarta EE로 패키지명 변경
// javax.servlet -> jakarta.servlet
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // Servlet 코드 작성
    }
}
  • GraalVM 기반의 Spring Native가 3년간의 실험을 마치고 공식 지원.
  • HTTP/RSocket Interface Client를 제공합니다,
  • Micrometer Observation API가 자동으로 구성되며, Observability 가 공식 지원.
  • HTTP API 에러 처리를 위한 RFC 7807 스펙을 지원.

RFC 7807 이전에는 일반적으로 각 API에서 고유한 오류 응답 형식

{
  "error": {
    "code": "INSUFFICIENT_CREDIT",
    "message": "Insufficient credit to perform this operation."
  }
}

RFC 7807

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "Insufficient Credit",
  "detail": "Your account does not have enough credit to perform this operation.",
  "status": 403
}
  • 보안상 이슈로 /api/hello 와 /api/hello/ 는 더 이상 일치하지 않는다.
  • Logback 및 Log4j2 날짜 및 시간의 기본값이 ISO-8601 표준을 따른다.
<!-- Logback configuration -->
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>application.log</file>
        <encoder>
            <!-- 날짜 및 시간 포맷을 ISO-8601에 맞춤 -->
            <pattern>%d{ISO8601} [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 나머지 설정 -->
</configuration>
<!-- Log4j2 configuration -->
<Configuration>
    <Appenders>
        <File name="FILE" fileName="application.log">
            <PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36} - %msg%n"/>
        </File>
    </Appenders>
    <!-- 나머지 설정 -->
</Configuration>

알았어 사용할 내용만 요약해

  • Spring Security 예시

  • Spring boot 2.x 까지

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final CorsFilter corsFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf().disable()

                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)

                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                .antMatchers("/test","/auth","/auth/login","/docs","/test","/health","/auth/check").permitAll()
                .antMatchers("/docs/**").permitAll()
                .antMatchers("/docs/index.html").permitAll()
                .anyRequest().authenticated();

    }
}
  • Spring boot 3.x
@Configuration
@EnableWebSecurity 
@EnableMethodSecurity // @enableglobalmethodsecurity(prePostEnabled = true)
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;
    private final LoginService loginService;
    private final LoginSuccessHandler loginSuccessHandler;
    private final LoginFailureHandler loginFailureHandler;
    private final CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    public SecurityConfig(
            JwtAuthFilter jwtAuthFilter,
            LoginService loginService,
            LoginSuccessHandler loginSuccessHandler,
            LoginFailureHandler loginFailureHandler,
            CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler,
            CustomAccessDeniedHandler customAccessDeniedHandler
    ) {
        this.jwtAuthFilter = jwtAuthFilter;
        this.loginService = loginService;
        this.loginSuccessHandler = loginSuccessHandler;
        this.loginFailureHandler = loginFailureHandler;
        this.customAuthenticationEntryPointHandler = customAuthenticationEntryPointHandler;
        this.customAccessDeniedHandler = customAccessDeniedHandler;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{ 
    // WebSecurityConfigurerAdapter 인터페이스 상속 x
        return httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .cors(cors -> cors.configurationSource(CorsConfig.corsConfigurationSource()))
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorizeRequest ->
                        authorizeRequest
                                .requestMatchers("/" ).permitAll()
                                .requestMatchers(SwaggerUrl).permitAll()
                                .requestMatchers(MemberApiUrl).permitAll()
                                .requestMatchers(LoginApiUrl).permitAll()
                                .requestMatchers(PostApiUrl).permitAll()
                                .requestMatchers(FileResource).permitAll()
                                .requestMatchers(ManagerApiUrl).hasAnyRole("MANAGER")
                                .anyRequest()
                                .authenticated()

                )

                .sessionManagement((sessionManagement) ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

                .oauth2Login((oauth2) -> oauth2
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(loginService)
                        )
                        .successHandler(loginSuccessHandler)
                        .failureHandler(loginFailureHandler)
                )

                .exceptionHandling( (exceptionHandling) -> {
                    exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPointHandler);
                    exceptionHandling.accessDeniedHandler(customAccessDeniedHandler);
                })

                .build();
    }

    private static final String[] SwaggerUrl = {
            "/api/vi/auth/**",
            "/swagger-ui/**",
            "/swagger-ui.html",
            "/v3/api-docs/**",
            "/v3/api-docs.yaml"
    };

    private static final String[] MemberApiUrl = {
            "/api/member/login",
            "/api/member/create",
            "/api/member/emailAuthentication",
            "/api/member/emailCheck",
            "/ws/**"
    };

    private static final String[] LoginApiUrl = {
            "/oauth2/**",
            "/login",
    };

    private static final String[] PostApiUrl = {
            "/api/post/list",
            "/PostDetail/**",
    };

    private static final String[] FileResource = {
            "/static/**",
            "/images/**",
            "api/file/images/*/image"
    };

    private static final String[] ManagerApiUrl = {
            "/manager/**",
    };
}
  • 2.x application.yml
spring:
  redis:
    host: localhost
    port: 6379
  • 3.x application.yml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    disable-swagger-default-url: true
  api-docs:
    version: openapi_3_0
  • 2.x Swagger
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api(){
        // http://localhost:8080/swagger-ui/#/
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.ant("/rest/**"))
                .build();
    }
}
  • 3.x Swagger
@Configuration
public class SwaggerConfig {
	// http://localhost:8080/swagger-ui/index.html
    @Bean
    public OpenAPI openAPI() {
        Info info = new Info()
                .title("Encore Space")
                .description("Encore Space API");

        return new OpenAPI()
                .components(new Components())
                .info(info);
    }
  • Spring Batch Changed (@EnableBatchProcessing)
// Sample with v4
@Configuration
@EnableBatchProcessing
public class MyJobConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Bean
    public Job myJob(Step step) {
        return this.jobBuilderFactory.get("myJob")
                .start(step)
                .build();
    }
}
// Sample with v5
@Configuration
@EnableBatchProcessing
public class MyJobConfig {

    @Bean
    public Job myJob(JobRepository jobRepository, Step step) {
        return new JobBuilder("myJob", jobRepository)
                .start(step)
                .build();
    }
}
// Sample with v4
@Configuration
@EnableBatchProcessing
public class MyStepConfig {

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Step myStep() {
        return this.stepBuilderFactory.get("myStep")
                .tasklet(..) // or .chunk()
                .build();
    }
}
// Sample with v5
@Configuration
@EnableBatchProcessing
public class MyStepConfig {

    @Bean
    public Tasklet myTasklet() {
       return new MyTasklet();
    }

    @Bean
    public Step myStep(JobRepository jobRepository, Tasklet myTasklet, PlatformTransactionManager transactionManager) {
        return new StepBuilder("myStep", jobRepository)
                .tasklet(myTasklet, transactionManager) // or .chunk(chunkSize, transactionManager)
                .build();
    }
}
  • Spring Cloud AWS 를 사용한다면 Spring Cloud AWS 3.x를 사용해야 한다.

  • Spring Cloud 를 사용할때는 꼭 현재 Spring boot 버전을 확인하고 잘 맞춰서 사용해야 한다. 현재는 3.3 버전대는 Spring Cloud를 사용하기 어렵다.

profile
503 Service Unavailable Error

0개의 댓글

관련 채용 정보