μš΄μ˜μ€‘μΈ μ„œλΉ„μŠ€ μŠ€ν”„λ§λΆ€νŠΈ 3 μ—…κ·Έλ ˆμ΄λ“œ

2024. 2. 19. 21:17ㆍBackend/🌿 Spring

λͺ©μ°¨

  • 0. JDK
  • 1. javax -> jakarta
  • 2. querydsl
  • 3. spring security

 

 

ν˜„μž¬ Tidify  iOS 앱을 운영 쀑이닀. μ„œλ²„λŠ” μŠ€ν”„λ§λΆ€νŠΈ 2.7.8 버전을 μ‚¬μš© μ€‘μ΄μ—ˆλŠ”λ° μ—°νœ΄κΈ°κ°„ λ™μ•ˆ 버전업을 μ§„ν–‰ν–ˆλ‹€.

버전업을 μ§„ν–‰ν•˜λ©΄μ„œ λ‹€μ–‘ν•œ λ²½(?)에 λΆ€λ”ͺν˜”λŠ”λ° κ·Έ 과정을 ν•˜λ‚˜ν•˜λ‚˜ 풀어보겠닀. 

 

μŠ€ν”„λ§λΆ€νŠΈ 3 μ—λŠ” λ‹€μ–‘ν•œ λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μΆ”κ°€λμ§€λ§Œ λ‹€μŒμ˜ 큰 νŠΉμ§•μ΄ μžˆλ‹€.

  • Java17κ³Ό Java19 지원 (Java 21 도 μΆ”κ°€)
  • GraalVM 지원
  • Spring framework 6.0 기반

 

0. jdk

μŠ€ν”„λ§λΆ€νŠΈ 3은 java17 을 기반으둜 μž‘μ„±λκΈ° λ•Œλ¬Έμ— 17 미만의 JDKλŠ” μ§€μ›ν•˜μ§€ μ•ŠλŠ”λ‹€. 

기쑴에도 jdk λŠ” java 17 을 μ‚¬μš©ν•˜κ³  μžˆμ—ˆκΈ°μ— λ³„λ„λ‘œ jdk μ—…λ°μ΄νŠΈλŠ” ν•˜μ§€ μ•Šμ•˜λ‹€.

 

1. javax -> jakarta

μŠ€ν”„λ§λΆ€νŠΈ 3 λΆ€ν„΄ JavaEE κ°€ JakartaEE 둜 λ³€κ²½λ˜λ©΄μ„œ javax.* 둜 λͺ…λͺ…λœ νŒ¨ν‚€μ§€λ₯Ό λͺ¨λ‘ jakarta.* 둜 μˆ˜μ •ν•΄μ•Ό ν•œλ‹€.

  • Java Servlet(javax.servlet) -> Jakarta Servlet(jakarta.servlet)
  • Java Message Servie (javax.jms) -> Jakarta Messaging (jakart.jms)
  • JPA:Java Persistence API (javax.persistence) -> Jakarta Persistence(jakarta.persistence)
  • JTA:Java Transaction API (javax.transaction) -> Jakarta Transaction(jakarta.transaction)
  • Java Mail (javax.mail) -> Jakarta Mail (jakarta.mail)

ν•„μžλŠ” IntelliJ IDE λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμ–΄ κ°„λ‹¨ν•˜κ²Œ cmd + shifth + R λͺ…λ Ήμ–΄λ‘œ javax λ₯Ό λͺ¨λ‘ jakarta 둜 μˆ˜μ •ν–ˆλ‹€.

 

ν•„μžμ˜ ν”„λ‘œμ νŠΈκ°€ μ†Œκ·œλͺ¨λΌ μ΄λŸ°μ‹μœΌλ‘œ javax νŒ¨ν‚€μ§€λ₯Ό -> jakarta νŒ¨ν‚€μ§€λ‘œ μˆ˜μ •ν–ˆλŠ”λ° λŒ€κ·œλͺ¨ ν”„λ‘œμ νŠΈμ—μ„œλ„ 이런 μ‹μœΌλ‘œ νŒ¨ν‚€μ§€λ₯Ό μˆ˜μ •ν•΄λ„ 될지 λͺ¨λ₯΄κ² λ‹€. 

 

2. querydsl

μš°μ„  build.gradle μ—μ„œ querydsl ν”ŒλŸ¬κ·ΈμΈμ„ μ œκ±°ν•œλ‹€. 이 뢀뢄을 μ œκ±°ν•˜μ§€ μ•Šμ•„μ„œ 컴파일 ν•  λ•Œ μ§€μ†μ μœΌλ‘œ μ—λŸ¬κ°€ λ°œμƒν–ˆλ‹€.

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

    // querydsl 주석 처리
    // id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
	...
}

 

dependencies λΈ”λŸ­μ—λŠ” jakarta νŒ¨ν‚€μ§€μ˜ μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•œλ‹€.

dependencies {
	...
    // querydsl κΈ°μ‘΄ λ””νŽœλ˜μ‹œ
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
    
    // μΆ”κ°€λœ λ””νŽœλ˜μ‹œ
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    
}

 

dependencies κΉŒμ§€ μΆ”κ°€ν•œ ν›„μ—λŠ” build.gradle 파일 ν•˜λ‹¨μ— μ•„λž˜μ˜ λ‚΄μš©μ„ μΆ”κ°€ν•œλ‹€.

ν•„μžλŠ” build/generated 경둜 ν•˜μœ„μ— querydsl λΉŒλ“œ νŒŒμΌμ„ μƒμ„±ν•˜λ„λ‘ μ„€μ •ν–ˆλ‹€. 이 뢀뢄은 개개인 λ§ˆλ‹€ 차이가 μžˆμ–΄ κ°œλ³„ ν”„λ‘œμ νŠΈ 섀정에 맞게 λ³€κ²½ν•΄μ•Όν•œλ‹€.

 

κ°œμ • 버전

// 버전업
def querydslDir = "build/generated/querydsl"
sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.generatedSourceOutputDirectory = file(querydslDir)
}

clean.doLast {
    file(querydslDir).deleteDir()
}

sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}

 

 

κΈ°μ‘΄ 버전

// κΈ°μ‘΄
def querydslDir = "build/generated/querydsl"
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

 

3. spring security

μŠ€ν”„λ§λΆ€νŠΈ3.0 λΆ€ν„΄ μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° 6.0.0 μ΄μƒμ˜ 버전이 μ μš©λœλ‹€. κΈ°μ‘΄ 2.7.3 이후 λΆ€ν„° μ‹œνλ¦¬ν‹° μ„€μ •μ˜ deprecated 된 λ‚΄μš©μ΄ μΆ”κ°€λ˜μ—ˆκ³  3.0 λΆ€ν„°λŠ” μ•„μ˜ˆ μ‚­μ œλœ 섀정도 μžˆλ‹€. 

버전업을 ν•˜κ³  λŠλ‚€μ μ€ μ‹œνλ¦¬ν‹°μ—μ„œ λžŒλ‹€ DSL 을 보닀 적극적으둜 λ„μž…ν•œ 것이닀. κ³΅μ‹λ¬Έμ„œμ—μ„œ μ–ΈκΈ‰ν•˜λ“― μŠ€ν”„λ§μ‹œνλ¦¬ν‹° 5.2 버전뢀터 λžŒλ‹€ DSL 을 μ‚¬μš©ν•  수 있고 HttpSecurity μ„€μ •μ‹œ 이λ₯Ό μ μš©ν•  수 μžˆλ‹€.

λžŒλ‹€ DSL 이 적용된 μ£Όμš”ν•œ 이유λ₯Ό μš”μ•½ν•˜μžλ©΄ 1) 리턴 νƒ€μž…μ΄ λͺ…ν™•ν•˜μ§€ μ•Šλ‹€λŠ” 점과 2) 버전 κ°„ μ½”λ“œ 일관성 톡일을 μœ„ν•΄μ„œ μˆ˜μ •λλ‹€κ³  ν•œλ‹€.

 

https://docs.spring.io/spring-security/reference/migration-7/configuration.html

 

μ•„λž˜ μˆ˜μ •λœ μ½”λ“œλ₯Ό 보자. Boot 3.x μ΄μ „μ—λŠ” antMatchers(...), mvcMatchers(...)  μ„ μ–Έν•΄μ„œ νŠΉμ • λ¦¬μ†ŒμŠ€μ— λŒ€ν•œ μ ‘κ·ΌκΆŒν•œμ„ λΆ€μ—¬ν–ˆμ§€λ§Œ, κ°œμ • λ²„μ „μ—μ„œλŠ” authorizeRequests() μ—μ„œ λžŒλ‹€λ‘œ url request 의 κΆŒν•œμ„ μ„€μ •ν•œλ‹€.

λ˜ν•œ Boot 3.x μ΄ν›„ λΆ€ν„°λŠ” antMatchers(), mvcMatchers() κ°€ 없어지고 requestMatchers() λ₯Ό μ‚¬μš©ν•œλ‹€.

// κΈ°μ‘΄
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.httpBasic()
        .and()
        .authorizeRequests()
        .antMatchers("/oauth2/login").permitAll()
        .and()
        .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}

// 버전업
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable);
    http.authorizeHttpRequests(authorize ->
            authorize.requestMatchers(new MvcRequestMatcher(introspector, "/**")).permitAll()
                .requestMatchers(new MvcRequestMatcher(introspector, "/oauth2/login")).permitAll()
                .anyRequest()
                .authenticated())
        .httpBasic(Customizer.withDefaults());
    return http.build();
}

 

λ˜ν•œ κΈ°μ‘΄ λ²„μ „μ—μ„œλŠ” WebSecurityConfig μ—μ„œ WebSecurityConfigurerAdapter λ₯Ό 상속받아 μ‚¬μš©ν–ˆμ§€λ§Œ μŠ€ν”„링 μ‹œνλ¦¬ν‹° 5.7λΆ€ν„° WebSecurityConfigurerAdapterκ°€ Deprecated 됨에 따라 μ˜€λ²„λΌμ΄λ”©ν•΄μ„œ μ‚¬μš©ν•œ configure() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•  수 μ—†κ²Œ 됐닀.   

λŒ€μ‹  SecurityFilterChain Bean 을 등둝해 μ„€μ •ν•  수 μžˆλ‹€. 

 

JwtSecurityConfig

SecurityConfigurerAdapter λ₯Ό filterChain 에 직접 μΆ”κ°€ν•  수 μžˆλ‹€. 이λ₯Ό μœ„ν•΄ SecurityConfigureAdapter λ₯Ό μƒμ†λ°›λŠ” JwtSecurityConfig 클래슀λ₯Ό μƒμ„±ν•˜κ³  이λ₯Ό filterChain 에 μΆ”κ°€ν–ˆλ‹€.

ν˜„μž¬ ν”„λ‘œμ νŠΈλŠ” jwt λ₯Ό μ΄μš©ν•œ λ‘œκ·ΈμΈμ„ κ΅¬ν˜„ν–ˆκΈ°μ— JwtSecurityConfig 클래슀λ₯Ό λ§Œλ“€μ–΄ SecurityConfigurerAdapter λ₯Ό μƒμ†λ°›μ•˜λ‹€.

 

@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final JwtTokenProvider tokenProvider;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        JwtAuthenticationFilter authFilter = new JwtAuthenticationFilter(tokenProvider);
        builder.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

JwtAuthenticationFilter

ν˜„μž¬ ν”„λ‘œμ νŠΈλŠ” OncePerRequestFilter λ₯Ό μƒμ†λ°›λŠ” Jwt 인증 ν•„ν„°λ₯Ό μ‚¬μš©μ€‘μ΄κΈ°μ— 이λ₯Ό configure method λ‚΄λΆ€μ—μ„œ 직접 ν˜ΈμΆœν•˜μ—¬ filter 에 μΆ”κ°€ν–ˆλ‹€. JwtTokenProvider λŠ” jwt accessToken, refreshToken 을 λ°œκΈ‰ν•˜λŠ” μš©λ„μ˜ ν΄λž˜μŠ€λ‹€.

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

        String accessToken = resolveAccessToken(request);
        ... (λ‚΄λΆ€κ΅¬ν˜„)
        filterChain.doFilter(request, response);
    }
}

 

 

이제 JwtSecurityConfig 클래슀λ₯Ό filterChain 내뢀에 μΆ”κ°€ν•˜κΈ°λ§Œ ν•˜λ©΄ λœλ‹€. 

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable);
    http.authorizeHttpRequests(authorize ->
            authorize.requestMatchers(new MvcRequestMatcher(introspector, "/**")).permitAll()
                .requestMatchers(new MvcRequestMatcher(introspector, "/oauth2/login")).permitAll()
                .anyRequest()
                .authenticated())
        .httpBasic(Customizer.withDefaults())
        .apply(new JwtSecurityConfig(jwtTokenProvider)); // JwtSecurityConfig μΆ”κ°€

    return http.build();
}

 

λ„μ‹ν™”ν•˜λ©΄ μ•„λž˜μ™€ 같은 λͺ¨μŠ΅μœΌλ‘œ ν΄λž˜μŠ€κ°„ μ˜μ‘΄κ΄€κ³„κ°€ ν˜•μ„±λœλ‹€.

 

 

WebSecurityConfig μ½”λ“œ 비ꡐ 정리

이전 버전 (μŠ€ν”„λ§λΆ€νŠΈ v2.7.8)

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .antMatchers("/api")
            .antMatchers("/oauth2/login")
            .antMatchers("/root");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.httpBasic()
            .and()
            .authorizeRequests()
            .antMatchers("/oauth2/login").permitAll()
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

 

 

μ—…κ·Έλ ˆμ΄λ“œ 버전 (μŠ€ν”„λ§λΆ€νŠΈ v3.0.2)

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

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


    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().
            requestMatchers(new AntPathRequestMatcher("/api"))
            .requestMatchers(new AntPathRequestMatcher( "/oauth2/login"))
            .requestMatchers(new AntPathRequestMatcher( "/root"));
    }

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests(authorize ->
                authorize.requestMatchers(new MvcRequestMatcher(introspector, "/**")).permitAll()
                    .requestMatchers(new MvcRequestMatcher(introspector, "/oauth2/login")).permitAll()
                    .anyRequest()
                    .authenticated())
            .httpBasic(Customizer.withDefaults())
            .apply(new JwtSecurityConfig(jwtTokenProvider));
        return http.build();
    }

}

 

 

마무리

ν˜„μž¬ ν‹°λ””νŒŒμ΄ μ•±μ˜ μ„œλ²„λŠ” ν•„μˆ˜μ μœΌλ‘œ μŠ€ν”„λ§λΆ€νŠΈ 3.0이 ν•„μš”ν•œ 상황은 μ•„λ‹ˆμ—ˆμ§€λ§Œ, 버전 μ—…κ·Έλ ˆμ΄λ“œλ₯Ό μ§„ν–‰ν•˜λ©΄μ„œ κ·Έκ°„ λͺ¨ν˜Έν•˜κ²Œ μ•Œμ•˜λ˜ JavaEE 와 JakartaEE 에 λŒ€ν•œ λ‚΄μš©μ΄λ‚˜ spring security 에 κ΄€ν•œ 섀정을 μžμ„Ένžˆ μ•Œμ•„λ³Ό 수 μžˆμ—ˆλ‹€. λ‹€μŒ ν¬μŠ€νŒ…μ—μ„  μŠ€ν”„λ§λΆ€νŠΈ3 의 graalVm에 κ΄€ν•œ λ‚΄μš©λ„ μΆ”κ°€μ μœΌλ‘œ 곡뢀해봐야겠닀.