Home What's new in Spring Boot 2.6
Post
Cancel
Preview Image

What's new in Spring Boot 2.6

영상을 보고 대충 요약한 자료다.

필요한 것만… 재미있어 보이는 것 위주로 보고 테스트해봤다.

Deprecations from Spring Boot 2.4

https://docs.spring.io/spring-boot/docs/2.4.x/api/ 여기 들어가서 Deprecated API 를 체크하면 된다.

주로 사용하는건 그대로인 것 같다. (사실 대충 읽어봤다)

Circular References Prohibited by Default

https://start.spring.io/ 에서 프로젝트를 하나 만든다.

  • Spring Boot 2.5.10
  • Dependencies
    • Spring Web
    • Spring Boot Actuator

그리고 인텔리제이로 가서 서버를 실행해서 잘 뜨는지 본 뒤에 service 패키지를 만들고, PostService, CommentService를 아래와 같이 만든다.

1
2
3
4
5
6
7
8
package dev.bossm0n5t3r.blogger.service

import org.springframework.stereotype.Service

@Service
class PostService(
    private val commentService: CommentService
)
1
2
3
4
5
6
7
8
package dev.bossm0n5t3r.blogger.service

import org.springframework.stereotype.Service

@Service
class CommentService(
    private val postService: PostService
)

그리고 다시 서버 애플리케이션을 실행하면 아래와 같이 에러가 발생한다.

1.png

2.png

하지만 아래와 같이 PostServiceCommentService를 바꾸면 정상적으로(?) 실행된다.

1
2
3
4
5
6
7
8
9
10
package dev.bossm0n5t3r.blogger.service

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

@Service
class PostService {
    @Autowired
    private lateinit var commentService: CommentService
}
1
2
3
4
5
6
7
8
9
10
package dev.bossm0n5t3r.blogger.service

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

@Service
class CommentService {
    @Autowired
    private lateinit var postService: PostService
}

3.png

왜냐하면 field injection은 bean 들이 생성된 뒤 발생하기 때문이다.

그런데 이제 Spring Boot 2.6.4 버전으로 올린 뒤 다시 실행하면 짜잔!

4.png

아래 로그에서도 길게 아래와 같이 나온다.

1
2
3
4
5
6
7
8
9
...
2022-03-16 21:05:00.516  WARN 40126 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'commentService': Unsatisfied dependency expressed through field 'postService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'postService': Unsatisfied dependency expressed through field 'commentService'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'commentService': Requested bean is currently in creation: Is there an unresolvable circular reference?

...

Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
...

그런데 버전 업그레이드를 해서 이전 설정을 다 바꾸기 힘들다던가, 그대로 하고 싶다면, application.properties에 다음과 같이 추가해주면 된다.

1
spring.main.allow-circular-references=true

5.png

물론 순환참조는 지양하는게 맞는것 같다.

PathPattern Based Path Matching Strategy for Spring MVC

이제 다시 Spring Boot 2.5.10 으로 돌려서 다음 예제를 보겠다.

앞으로도 자주 변경사항을 비교하기 위해 버전을 왔다갔다 하겠다.

이제 새로 controller 패키지를 만들고, PostController를 아래와 같이 생성하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package dev.bossm0n5t3r.blogger.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
class PostController {
    @GetMapping("/posts/{name}/{name}")
    fun findPostByAuthor(@PathVariable name: String) {
        println("name: $name")
    }
}

Path에는 현재 name이 두 개 들어가 있는 상황이다.

이전에는 이것이 AntPathMatcher 를 통해 허용됐었다.

이전에는 Pattern을 match하기 위해서 AntPathMatcher 를 사용했지만, 2.6 버전부터는 PathPatternMatcher를 사용하게 된다.

위와 같이 사용하게 될 경우 기존(2.5.10)에서의 경우 상당히 관대했어서 두 파라미터의 이름이 같게 되는 경우를 신경쓰지 않았었다. 그래서 그냥 가장 마지막의 이름에 해당하는 것을 사용했다.

그래서 아래와 같이 테스트를 하게 되면 뒷 부분의 name이 출력되는 것을 알 수 있다.

6.png

7.png

이제 다시 2.6.4 로 바꾼뒤 다시 시도해보면 아래와 같이 에러가 발생하는 것을 볼 수 있다.

8.png

만약 다시 예전의 AntPathMatcher 을 사용하고 싶으면 위의 에러 내용처럼 application.properties 에 추가하면 된다.

1
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

9.png

정상적으로 실행된 것을 확인할 수 있다.

Actuator Env InfoContributor Disabled by Default

먼저 2.5.10 으로 돌아간다.

그리고 application.properties에 다음 라인을 추가한다.

1
2
3
4
5
6
7
8
9
spring.application.name=blogger
management.endpoints.web.exposure.include=*

# actuator env InfoContributor
info.app.name=${spring.application.name}
info.app.api.version=1.0.0
info.app.api.docs=https://www.spring.io
info.department.name=John Doe
info.department.email=john@do.e

그 다음 서버를 실행하고, http://localhost:8080/actuator/info 에 접속하면 아래 페이지와 같이 뜨게 된다.

10.png

이제 버전을 2.6으로 올리고 다시 접속해보면 아래와 같이 보이지 않게 된다.

11.png

물론 2.6에서도 보이게 처리하려면 아래 라인을 application.properties에 추가하면 다시 보이게 된다.

1
management.info.env.enabled=true

Application Startup

새로운 피쳐는 아니다. 일단 다시 2.5.10 으로 돌아가보자.

그리고 BloggerApplication.kt 파일을 아래와 같이 수정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package dev.bossm0n5t3r.blogger

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup
import org.springframework.boot.runApplication

@SpringBootApplication
class BloggerApplication

fun main(args: Array<String>) {
//    runApplication<BloggerApplication>(*args)
    val app = SpringApplication(BloggerApplication::class.java).apply {
        this.applicationStartup = BufferingApplicationStartup(10000)
    }
    app.run(*args)
}

그리고 서버를 실행한 뒤 http://localhost:8080/actuator 에 접속해보면 아래에 /startup endpoint를 확인할 수 있다.

12.png

http://localhost:8080/actuator/startup 에 접속해보면 아래와 같이 다른 metrics들을 확인할 수 있다.

(무수히 많다.)

13.png

맨 아래에 가면 spring.boot.application.running 이라는 이름이 보이는데,

14.png

2.6.4 에 가면 이름이 spring.boot.application.ready로 바뀐다.

15.png

그렇다고 한다.

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes#application-startup 설명에 따르면, lightRecorderApplicationStartup  or BufferingApplicationStartup 로부터 생성되는 endpoint의 이름이 바뀐 것으로 보인다.

Prometheus Version Property

Prometheus 버전 제어하는 속성이 prometheus-pushgateway.version 에서 prometheus-client.version 로 바뀌었다.

Records and @ConfigurationProperties**

예제에서는 자바의 Record를 활용하고 있다.

필자가 코틀린의 data class로 잘 적용되는지 확인해보려고 했지만, 2.6.4 에서는 되지 않았다.

따라서 자바의 Record 적용하는 방법을 통해서 기술했다.

먼저 2.5.10 으로 간 뒤, 다음 AuthorProperties 파일을 아래와 같이 추가해주자.

1
2
3
4
5
6
7
8
9
10
11
package dev.bossm0n5t3r.blogger.model

import org.springframework.boot.context.properties.ConfigurationProperties

@JvmRecord
@ConfigurationProperties("blogger.author")
data class AuthorProperties(
    val firstName: String,
    val lastName: String,
    val email: String
)

물론 BloggerApplication 에도 다음과 같이 @EnableConfigurationProperties 을 추가해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
import dev.bossm0n5t3r.blogger.model.AuthorProperties
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication

@SpringBootApplication
@EnableConfigurationProperties(AuthorProperties::class)
class BloggerApplication
...

이제 서버를 실행하면 아래와 같이 에러가 발생한다.

16.png

여기서 빌드된 파일인, /build/classes/kotlin/main/dev/bossm0n5t3r/blogger/model/AuthorProperties 를 보면, 아래와 같이 되어있다.

17.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package dev.bossm0n5t3r.blogger.model

@org.springframework.boot.context.properties.ConfigurationProperties public final data class AuthorProperties public constructor(firstName: kotlin.String, lastName: kotlin.String, email: kotlin.String) : java.lang.Record {
    public final val email: kotlin.String /* compiled code */

    public final val firstName: kotlin.String /* compiled code */

    public final val lastName: kotlin.String /* compiled code */

    public final operator fun component1(): kotlin.String { /* compiled code */ }

    public final operator fun component2(): kotlin.String { /* compiled code */ }

    public final operator fun component3(): kotlin.String { /* compiled code */ }
}

자세히 보면 getter 만 있고, setter가 없기에 발생한 문제다.

그래서 @ConstructorBinding 를 추가하면 해결이 된다.

18.png

이제 2.6.4에서는 @ConstructorBinding 없이도 잘 서버가 실행된다.

Record에서 값을 주입하는 방법은 @ConstructorBinding 밖에 없기 때문에 불필요하다고 판단했다고 한다.

주의 사항

아직 Spring Boot 2.6.4에서 코틀린의 data class는 제대로 되지 않는 것을 확인했다. 유의하기 바란다.

Kotlin 에서 자바의 Record 사용 방법은 https://kotlinlang.org/docs/jvm-records.html#declare-records-in-kotlin 를 참고하면 된다.

코틀린의 data class와 많이 유사한데, 실제로 사용할 때도 @JvmRecord 만 붙여주면 된다.

자세한 배경 지식은 https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure 를 참고하면 된다.

여기서는 2.6.4에서 진행한다.

먼저 SettingsController 를 아래와 같이 생성해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package dev.bossm0n5t3r.blogger.controller

import java.time.LocalDateTime
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpSession
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ResponseBody

@Controller
class SettingsController {
    companion object {
        @JvmStatic
        private val POSTS_PER_PAGE = "postsPerPage"
        @JvmStatic
        private val POSTS_PER_PAGE_DEFAULT = 5
    }

    @GetMapping
    @ResponseBody
    fun set(session: HttpSession, response: HttpServletResponse): Int {
        var value = session.getAttribute(POSTS_PER_PAGE) as Int?
        if (value == null) {
            value = POSTS_PER_PAGE_DEFAULT
            session.setAttribute(POSTS_PER_PAGE, value)
        }
        response.addCookie(Cookie("mycookie", LocalDateTime.now().toString()))
        return value
    }
}

그 다음 서버를 실행한 뒤에, http://localhost:8080/ 으로 들어가면 5 가 보일 것이고, 쿠키에 가면 아래와 같이 잘 설정된 것을 볼 수 있다.

19.png

여기서 JSESSIONIDSameSite 값이 비어있는 것을 보자.

이제 application.properties 에 가서 아래 라인을 추가한뒤 다시 서버를 실행해보자.

1
server.servlet.session.cookie.same-site=lax

그러면 아래 화면 처럼 JSESSIONIDSameSite 값이 들어온다.

20.png

하지만 아직 mycookie 에는 할당되지 않았다.

그래서 아래와 같이 BloggerApplication 에 수정해주면 추가된다.

1
2
3
4
5
6
7
8
9
10
...
@SpringBootApplication
@EnableConfigurationProperties(AuthorProperties::class)
class BloggerApplication {
    @Bean
    fun cookieSameSiteSupplier(): CookieSameSiteSupplier {
        return CookieSameSiteSupplier.ofStrict().whenHasName("mycookie")
    }
}
...

21.png

코드에서 설정한것처럼 Strict로 설정된 것을 볼 수 있다.

Java Runtime Information

2.6.4에서 아래 내용을 application.properties 에 추가해주자.

1
management.info.java.enabled=true

그 뒤 서버를 실행 한 뒤 http://localhost:8080/actuator/info 에 접속하면 아래와 같이 보인다.

22.png

Build Info Property Exclusions

Maven이나 Gradle로부터 생성되는 build-info.properties 의 특정 property를 제거할 수 있게 되었다.

Gradle유저는 아래와 같이 추가해주면 된다.

1
2
3
4
5
6
task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) {
		destinationDir project.buildDir
		properties {
				version = null
		}
}

Using WebTestClient for Testing Spring MVC

2.6.4 환경이다.

먼저 PostController에 아래 코드를 추가한 뒤, 테스트 코드도 추가해주자

1
2
@GetMapping("/posts")
fun findAllPosts() =listOf("Post 1", "Post 2", "Post 3")
1
2
3
4
5
6
7
8
9
10
11
package dev.bossm0n5t3r.blogger.controller

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

internal class PostControllerTest {

    @Test
    fun findAllPosts() {
    }
}

그리고 테스트코드에서 사용하기 위해 build.gradle.kts에 아래 코드를 추가해주자.

1
testImplementation("org.springframework.boot:spring-boot-starter-webflux")

그러면 아래와 같은 테스트 코드를 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package dev.bossm0n5t3r.blogger.controller

import org.hamcrest.Matchers
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.reactive.server.WebTestClient

@WebMvcTest(PostController::class)
internal class PostControllerTest {
    @Autowired
    private lateinit var webTestClient: WebTestClient

    @Test
    fun findAllPosts() {
        webTestClient
            .get()
            .uri("/posts")
            .exchange()
            .expectStatus().isOk
            .expectBody().jsonPath("$.size", Matchers.`is`(3))
    }
}

mock 환경에서 Spring MVC용 WebTestClient를 사용할 수 있게 되었다.

결론

재미있었다. 종종해야지~

repository에도 정리했다.

References

This post is licensed under CC BY 4.0 by the author.

[Kotlin] Make a simple 2-dimensional matrix

Clojure를 시작해보자