Spring Boot Framework 버전 업그레이드 과정

hyyyynjn·2023년 8월 2일
0

개발 기록

목록 보기
3/3

목차

  • new features of jdk 17 & spring boot 3
  • major spring projects

new features of jdk 17 & spring boot 3

📌supported java version: 17 ~ 19

JDK를 최소 17부터 19까지 지원함.

  • Java 11과 비교하여 GC 등 성능 개선
  • 문자열, 리스트 등 다양한 API 지원
  • 타입 추론 키워드 추가
  • switch 문 확장
  • record class 추가
  • sealed class 추가

text blocks

String textBlock = """
spring boot 3
is
awesome
!!
""";
  • text blocks """ some paragraph """ 삼중 따옴표로 멀티라인 문자열을 사용할 수 있음.

switch expressions

DayOfWeek day = DayOfWeek.FRIDAY;

// before jdk17
int numOfLettersOld;
if (day == MONDAY || day == FRIDAY || day == SUNDAY) {
    numOfLettersOld = 6;
} else if (day == TUESDAY) {
    numOfLettersOld = 7;
} else if (day == THURSDAY || day == SATURDAY) {
    numOfLettersOld = 8;
} else if (day == WEDNESDAY) {
    numOfLettersOld = 9;
}

// jdk 17 switch-case-construct
int numOfLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

nested if-else-operator를 사용하는 대신 switch-case-construct를 통해 깔끔한 변수 할당이 가능해짐.

pattern matching

// before jdk 17
if (vehicle instanceof Car) {
    return ((Car) vehicle).getNumberOfSeats();
} else if (vehicle instanceof Truck) {
    return ((Truck) vehicle).getLoadCapacity();
}

// jdk 17 pattern matching
if (vehicle instanceof Car car) {
    return car.getNumberOfSeats();
} else if (vehicle instanceof Truck truck) {
    return truck.getLoadCapacity();
}

// switch-case에서의 pattern matching
static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case Integer i -> i.doubleValue();
        case Float f -> f.doubleValue();
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

instaceof 나 switch-case문에서 데이터형과 함께 변수를 선언할 수 있음.

record

public record Person (String name, String address) {
    private static int a; // static field
    public Person { // 생성자 null 체크
        Objects.requirenonull(name);
    }
    
    public static Person empty() { // static 메소드
        return new Person("", "");
    }
}

// 주의
public record Person (@NotBlank String name, @NonNull String address) {}
  • immutable dto의 역할을 하는 data class인 record 을 사용할 수 있다.
    • 클래스에 바로 생성자 매개변수를 선언함.
    • record 내부에 static 필드만 선언할 수 있음.
  • record 생성자의 매개변수 유효성 검사를 Bean Validation으로 할 경우 제대로 동작하지 않은 이슈가 있으므로 주의해야 함

sealed class

https://www.baeldung.com/java-sealed-classes-interfaces

// sealed interfaces
public sealed interface Service permits Car, Truck {}
// sealed classes
public abstract sealed class Vehicle permits Car, Truck {}

// 확장에 닫혀있도록 final으로 선언
public final class Truck extends Vehicle implements Service {}
// 확장할 수 있도록 non-sealed로 선언
public non-sealed class Car extends Vehicle implements Service {}

sealed를 통해 class와 interface의 subtype를 정의(제한)할 수 있다.

  • Service 인터페이스의 구현체 클래스를 Car, Truck으로만 제한하기 위해 sealed로 봉인!
public sealed interface Vehicle permits Car, Truck {
    String getRegistrationNumber();
}

public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {
    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }
}

public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {
    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }
}

sealed class와 record와 함께 사용하는 예시

  • record는 암묵적으로 final 클래스이므로 final 키워드를 선언할 필요 없다

📌jakarta EE 9

Java EE에서 Jarkarta EE9으로 대체됨에 따라 package namespace도 javax.에서 jakarta.으로 변경되었다. 따라서 기존 import 코드 뿐만아니라 java/jakarta EE을 사용하는 third party library의 java to jakarta 마이그레이션 여부를 체크해야 한다.

  • import 코드 마이그레이션 : intellij IDEA ide에서 java to jakarta migration 기능을 제공

근데 왜 javax에서 jakarta로 네임스페이스까지 변경되는걸까?

Java EE(Enterprise Edition)은 과거 sun microsystems가 분산 어플리케이션 개발을 위해 내세운 산업 표준이였다.

  • ex) JDBC도 그중 하나. (Java EE 호환성 스펙에 맞는 JDBC 드라이버를 사용하면 큰 변경없이 다른 db로 교체할 수 있다.)

sun-oracle 합병 이후 Java EE는 비영리 단체 eclipse에 이관되었고 2019년도에 Java EE과 호환되는 Jakarta EE를 출시하게 되었다. java는 oracle이 상표권을 가지고 있었기 때문에 상표권 이슈로 api 네임스페이스까지 javax에서 jakarta로 변경하게 되었다.

삼성 sds의 jakarta 마이그레이션 과정

📌 클라우드 환경과 관련된 신규 기능

Spring Observability

https://spring.io/blog/2022/10/12/observability-with-spring-boot-3

Micrometer Observation API가 auto configuration을 통해 자동으로 구성되며, Observability의 공식 지원이 시작된다.
micrometer 및 micrometer tracing 기반의 spring observability가 도입되어, metric 정보를 기록하고 zipkin이나 telemetry를 통해 tracing할 수 있게 해준다.
spring boot 2.x에서 spring cloud sleuth을 통해 tracing이 가능했으나 3.x부터 sleuth 지원이 끊겨 micrometer로 마이그레이션이 필요하다.

Http Interface Client 추가

https://docs.spring.io/spring-framework/docs/6.0.0-RC1/reference/html/integration.html#rest-http-interface

서비스 인터페이스 선언만으로 Http Access가 가능한 Http Interface Client 가 추가되었다.
RestTemplate 또는 WebClient 와 같은 클래스로 직접 구현하지 않더라도, 인터페이스만 선언하면 API 호출이 가능해진다.

RestTemplate, WebClient

@SpringBootApplication
public class SampleApplication {

  public static void main(String[] args) {
    SpringApplication.run(SampleApplication.class, args);
  }

  @Bean
  ApplicationRunner init(ErApi erApi) {
    return args -> {
      // RestTemplate
      RestTemplate restTemplate = new RestTemplate();
      Map<String, Map<String, Double>> res = restTemplate.getForObject("https://open.er-api.com/v6/latest", Map.class);
      System.out.println(res.get("rates").get("KRW"));

      // WebClient
      WebClient client = WebClient.create("https://open.er-api.com");
      Map<String, Map<String, Double>> res2 = client.get().uri("/v6/latest").retrieve().bodyToMono(Map.class).block();
      System.out.println(res2.get("rates").get("KRW"));
    };
  }
}

HTTP interface

@SpringBootApplication
public class SampleApplication {

  public static void main(String[] args) {
    SpringApplication.run(SampleApplication.class, args);
  }

  @Bean
  ApplicationRunner init(ErApi erApi) {
    return args -> {
      // HTTP interface
      Map<String, Map<String, Double>> res3 = erApi.getLatest();
      System.out.println(res3.get("rates").get("KRW"));
    };
  }

  // url 마다 http interface bean 등록하는게 번거로울 수 있음. (추후에 jpa 같이 자동으로 설정해주는 라이브러리가 나올 수 있음)
  @Bean
  ErApi erApi() {
    WebClient client = WebClient.create("https://open.er-api.com");
    HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
        .builder(WebClientAdapter.forClient(client))
        .build();

    return httpServiceProxyFactory.createClient(ErApi.class);
  }

  interface ErApi {

    @GetExchange("/v6/latest")
    Map getLatest();
  }
}

major spring projects

spring framework 버전 업그레이드

  • 2.7.6 → 3.0.8 → 3.0.9

이에 따라 여러 spring project의 버전도 업데이트 됨

  • spring-batch-core
  • spring-security-core

spring-batch-core

spring frameworkspring-batch-core
2.7.64.3.7 (https://docs.spring.io/spring-batch/docs/4.3.7/reference/html/)
3.0.95.0.2 (https://docs.spring.io/spring-batch/docs/5.0.2/reference/html/)

4.3.7 vs 5.0.2

  1. job, step 생성 방식 차이
  2. multiple jobs 실행 기능 삭제
  3. job parameter의 전달 방식 변경 (command line)

job, step 생성 방식 차이

job 구성 차이

// 4.3.7 (https://docs.spring.io/spring-batch/docs/4.3.7/reference/html/job.html#configuringAJob)
@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}

// 5.0.2 (https://docs.spring.io/spring-batch/docs/5.0.2/reference/html/job.html#configuringAJob)
@Bean
public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}

버전별 job 구성 차이점

  • this.jobBuilderFactory.get("footballJob")
  • new JobBuilder("footballJob", jobRepository)

JobRepository이란

  • dataSource, transactionManager을 활용하여 spring-batch에서 사용되는 persisted domain 객체 (job, step 등)에 대한 CRUD를 수행
  • 즉, job을 만들기 위해서 필요한 존재

버전별 JobRepository의 configuration 구성방식

  • 4.3.7에서는 BatchConfigurer를 통해 구성
  • 5.0.2에서는 JavaConfiguration을 통해 구성

자세한 JobRepository 구성 방식 차이를 이해하기 위해서 버전별 spring batch configuration 차이를 알아야 함

spring batch configuration 차이

두 버전 모두 @EnableBatchProcessing 어노테이션을 달아주면 jobRepository을 포함한 spring batch 관련 bean을 등록해줌.

4.3.7

@EnableBatchProcessing 을 통해 등록되는 bean들

  • JobRepository: bean name "jobRepository"
  • JobLauncher: bean name "jobLauncher"
  • JobRegistry: bean name "jobRegistry"
  • !!PlatformTransactionManager: bean name "transactionManager"
  • !!JobBuilderFactory: bean name "jobBuilders"
  • !!StepBuilderFactory: bean name "stepBuilders"
@Bean
public BatchConfigurer batchConfigurer(DataSource dataSource) {
	return new DefaultBatchConfigurer(dataSource) {
		@Override
		public PlatformTransactionManager getTransactionManager() {
			return new MyTransactionManager();
		}
	};
}

위처럼 DefaultBatchConfigurer을 상속받아서 등록할 bean에 대한 customize이 가능했음.
(spring-batch-core 5부터 BatchConfigurer 클래스가 제거됨)

5.0.2

@EnableBatchProcessing 을 통해 등록되는 bean들

  • JobRepository: a bean named jobRepository
  • JobLauncher: a bean named jobLauncher
  • JobRegistry: a bean named jobRegistry
  • !!JobExplorer: a bean named jobExplorer
  • !!JobOperator: a bean named jobOperator

4버전과 비교했을 때 등록되는 bean 중 PlatformTransactionManager, JobBuilderFactory, StepBuilderFactory 이 사라짐.

BatchConfigurer 클래스가 삭제되면서 바뀐점

  1. dataSource와 transactionManager의 bean 등록 방식의 변화
  2. job, step 빈 등록 방식의 변화 (JobBuilderFactory, StepBuilderFactory 클래스가 deprecated)

dataSource와 transactionManager의 bean 등록 방식

@Configuration
@EnableBatchProcessing(dataSourceRef = "batchDataSource", transactionManagerRef = "batchTransactionManager") // <- just like this!
public class MyJobConfiguration {

	@Bean
	public DataSource batchDataSource() {
		return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
				.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
				.generateUniqueName(true).build();
	}

	@Bean
	public JdbcTransactionManager batchTransactionManager(DataSource dataSource) {
		return new JdbcTransactionManager(dataSource);
	}

	public Job job(JobRepository jobRepository) {
		return new JobBuilder("myJob", jobRepository)
				//define job flow as needed
				.build();
	}
}

dataSource와 transactionManager의 bean을 직접 등록하여 bean name을 @EnableBatchProcessing에 지정해주도록 변경됨.

job, step의 bean 등록 방식

JobBuilderFactory, StepBuilderFactory 클래스가 deprecated되고

JobRepositoryJobBuilder, StepBuilder로 job, step의 bean을 등록하도록 권장함.

// 4.3.7 (https://docs.spring.io/spring-batch/docs/4.3.7/reference/html/job.html#configuringAJob)
@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}

// 5.0.2 (https://docs.spring.io/spring-batch/docs/5.0.2/reference/html/job.html#configuringAJob)
@Bean
public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}

multiple jobs 실행 기능 삭제

Inline-image-2023-08-01 10.04.40.841.png

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#multiple-batch-jobs

4버전에는 spring.batch.job.names 프로퍼티에 job의 bean name을 , 으로 구분지어 전달하면 multiple job을 실행할 수 있었다.

5버전부터 spring.batch.job.names 대신 spring.batch.job.name 프로퍼티에 job bean name 하나만 지정하도록 변경되었다. 즉, multiple job 실행 기능이 삭제된것이다…!

왜 삭제되었을까?

  • JobLauncher 인터페이스를 사용하여 job을 프로그래밍적으로 실행하도록 장려하기 위함
  • job을 실행하기 위해 함께 전달되는 job parameter를 여러 job들이 공유하게 되어 발생하는 사이드 이펙트의 가능성을 없애기 위함.

commandline 에서 job parameter의 전달 방식 변경

# 4.3.7
<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay \
                                 +schedule.date(date)=2007/05/05 -vendor.id=123

# 5.0.2
<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay \
                                 schedule.date=2007-05-05,java.time.LocalDate,true \
                                 vendor.id=123,java.lang.Long,false
  • parameter(type)=valueparameter=value,type,identifying 형식으로 job parameter를 전달 (identifying은 해당 매개변수에 대한 식별여부를 나타냄)

JobParameterConverter

DefaultJobParameterConverter

spring batch 에서 전달 받은 파라미터(parameter=value,type,identifying)를 JobParameter 객체로 매핑하기 위해DefaultJobParametersConverter 을 기본으로 사용한다.

/**
	 * Decode a job parameter from a string.
	 * @param encodedJobParameter the encoded job parameter
	 * @return the decoded job parameter
	 */
	protected JobParameter<?> decode(String encodedJobParameter) {
		String parameterStringValue = parseValue(encodedJobParameter);
		Class<?> parameterType = parseType(encodedJobParameter);
		boolean parameterIdentifying = parseIdentifying(encodedJobParameter);
		try {
			Object typedValue = this.conversionService.convert(parameterStringValue, parameterType);
			return new JobParameter(typedValue, parameterType, parameterIdentifying);
		}
		catch (Exception e) {
			throw new JobParametersConversionException(
					"Unable to convert job parameter " + parameterStringValue + " to type " + parameterType, e);
		}
	}

	private String parseValue(String encodedJobParameter) {
		return StringUtils.commaDelimitedListToStringArray(encodedJobParameter)[0];
	}

	private Class<?> parseType(String encodedJobParameter) {
		String[] tokens = StringUtils.commaDelimitedListToStringArray(encodedJobParameter);
		if (tokens.length <= 1) {
			return String.class;
		}
		try {
			Class<?> type = Class.forName(tokens[1]);
			return type;
		}
		catch (ClassNotFoundException e) {
			throw new JobParametersConversionException("Unable to parse job parameter " + encodedJobParameter, e);
		}
	}

	private boolean parseIdentifying(String encodedJobParameter) {
		String[] tokens = StringUtils.commaDelimitedListToStringArray(encodedJobParameter);
		if (tokens.length <= 2) {
			return true;
		}
		return Boolean.valueOf(tokens[2]);
	}
/**
	 * Convert a comma delimited list (e.g., a row from a CSV file) into an
	 * array of strings.
	 * @param str the input {@code String} (potentially {@code null} or empty)
	 * @return an array of strings, or the empty array in case of empty input
	 */
	public static String[] commaDelimitedListToStringArray(@Nullable String str) {
		return delimitedListToStringArray(str, ",");
	}

DefaultJobParametersConverter#decode는 commandline에서 전달받은 파라미터를 JobParameter 객체로 디코딩한다. 이 때 파라미터의 메타데이터를 파싱하기 위해 StringUtils.commaDelimitedListToStringArray 메소드를 사용하는데, 구분자를 , 로 잡고 있다.

만약 parameter value에 , 가 포함되어 있다면?

parameter=value,type,identifying 형태로 파라미터를 전달할 경우 value, 가 포함되어있다면 JobParameter로 디코딩하는 과정에서 에러가 발생한다.

이를 해결하기 위해서 JobParameterConverter를 DefaultJobParametersConverter 대신 JsonJobParametersConverter 으로 설정해야 한다

JsonJobParameterConverter

JsonJobParameterConverter을 사용할 경우,
parameterName='{"value": "parameterValue", "type":"parameterType", "identifying": "booleanValue"}' 형태로 파라미터를 전달해야 한다.

@Override
	protected JobParameter decode(String encodedJobParameter) {
		try {
			JobParameterDefinition jobParameterDefinition = this.objectMapper.readValue(encodedJobParameter,
					JobParameterDefinition.class);
			Class<?> parameterType = String.class;
			if (jobParameterDefinition.type() != null) {
				parameterType = Class.forName(jobParameterDefinition.type());
			}
			boolean parameterIdentifying = true;
			if (jobParameterDefinition.identifying() != null && !jobParameterDefinition.identifying().isEmpty()) {
				parameterIdentifying = Boolean.valueOf(jobParameterDefinition.identifying());
			}
			Object parameterTypedValue = this.conversionService.convert(jobParameterDefinition.value(), parameterType);
			return new JobParameter(parameterTypedValue, parameterType, parameterIdentifying);
		}
		catch (JsonProcessingException | ClassNotFoundException e) {
			throw new JobParametersConversionException("Unable to decode job parameter " + encodedJobParameter, e);
		}
	}

	public record JobParameterDefinition(String value, String type, String identifying) {
	}

파라미터를 objectMapper을 통해 JobParameter로 디코딩한다.

  • etl-batch-job에서 이 방식으로 job parameter를 전달받는다.

commandline에서 job parameter의 전달 방식

parameterName='{"value": "parameterValue", "type":"parameterType", "identifying": "booleanValue"}'
- https://docs.spring.io/spring-batch/docs/current/reference/html/whatsnew.html#extended-notation

공식문서에 나온 방식처럼 JsonJobParametersConverter에 파라미터를 전달하면 제대로 동작하지 않는다. (https://github.com/spring-projects/spring-batch/issues/4299)

  • cli argument으로 파라미터를 전달할 때, "에 escape(\) 처리가 필요하며 파라미터의 value에서 json key와 value 사이에 공백문자가 있어선 안된다.
    • param1="{\"key\":\"value\"}"
  • spring-cloud-data-flow ui으로 파라미터를 전달할 때, 파라미터의 value에 ' 없이 json object를 전달하고 json key와 value 사이에 공백문자가 있어선 안된다
    • param1={"key":"value"}

spring-security-core

spring frameworkspring-security-core
2.7.65.7.5 (https://docs.spring.io/spring-security/reference/5.7/index.html)
3.0.96.0.5 (https://docs.spring.io/spring-security/reference/6.0/index.html)

5.7 → 5.8 (https://docs.spring.io/spring-security/reference/5.8/whats-new.html#_core)

  • remove된 클래스 및 메소드 0개
  • 성능 개선 및 deprecated 처리 관련 내용

5.8 → 6.0 (https://docs.spring.io/spring-security/reference/6.0/whats-new.html#_core)

  • remove된 클래스 및 메소드 16개
    • gh-11923 - Remove WebSecurityConfigurerAdapter. Instead, create a SecurityFilterChain bean.
    • gh-11939 - Remove deprecated antMatchersmvcMatchersregexMatchers helper methods from Java Configuration. Instead, use requestMatchers or HttpSecurity#securityMatchers.
  • 추가된 기능
    • gh-11446 - Add native image support for @PreAuthorize
    • gh-11737 - Add native image support for @PostAuthorize

spring security configuration 방식

**WebSecurityConfigurerAdapter가 삭제되므로서** spring-security configuration을 구성방식에 변화가 생겼다.

  • 5.7.0-M2 이전 : WebSecurityConfigurerAdapter 클래스를 상속받아 구성
  • 6.0 이후 : WebSecurityConfigurerAdapter 없이 구성

해당 내용은 https://www.baeldung.com/spring-deprecated-websecurityconfigureradapter 에 자세히 나와있다. 단순한 구성 방식 변경 내용뿐이라 다루지 않겠다.

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기