자프링(자바 + 스프링) 외길 12년차 서버 개발자가 코프링(코틀린 + 스프링)을 만난 후 코틀린의 특징과 스프링의 코틀린 지원을 알아가며 코프링 월드에서 살아남은 이야기…
코드 저장소: https://github.com/arawn/kotlin-support-in-spring
4. | 코틀린의 철학
Kotlin in Action
Dmitry Jemerov and Svetlana Isakova
https://www.manning.com/books/kotlin-in-action
!
코틀린은 자바와의 상호운용성에 초점을 맞춘 실용적이고 간결하며
안전한 언어이다.
5. | 코틸린의 철학 : 간결성
/* 데이터 보관을 목적으로 사용하는 클래스가 필요할 때는 class 앞에 data 를 붙여 정의한다
* 프로퍼티에 대한 getter(), setter(), equals(), hashCode(), toString(),
* copy(), componentN() 메소드를 컴파일 시점에 자동으로 생성한다
*/
data class Person(
val id: UUID,
val firstname: String,
val lastname: String,
val address: Address
)
// 표준 라이브러리의 풍부한 API와 고차 함수의 도움을 받아 간결하게 목적을 달성할 수 있다
val persons = repository.findByLastname("Matthews")
val filteredPersons = persons.filter { it.address.city == "Seoul" }
// 단일표현 함수는 등호로 함수 정의와 바디를 구분하여 짧게 표현할 수 있다
fun double(x: Int): Int = x * 2
val beDoubled = double(2)
6. | 코틸린의 철학 : 안전성
// 널null이 될 수 없는 값을 추적하며, NullPointException 발생을 방지한다
val nullable: String? = null // 널이 될 수 있음
val nonNullable: String = "" // 널이 될 수 없음
// 타입 검사와 캐스트가 한 연산자에 의해 이뤄지며, ClassCastException 발생을 방지한다
val value = loadValue()
if (value is String) {
// 문자열 타입이 제공하는 메서드를 사용 할 수 있음
value.uppercase(Locale.getDefault())
}
// break 문이 없어도 되며, 열거형 같은 특별한 타입과 함께 쓰면 모든 값이 평가되었는지 확인한다
val scoreRange = when(CreditScore.EXCELLENT) {
CreditScore.BAD -> 300..629
CreditScore.FAIR -> 630..689
CreditScore.GOOD -> 690..719
CreditScore.EXCELLENT -> 720..850
}
enum class CreditScore {
BAD,FAIR,GOOD,EXCELLENT
}
14. | 다 놀았니? 이제 할 일을 하자
fun mapItem(item: NewsItem<*>): NewsItemDto {
if (item is NewsItem.NewTopic) {
return NewsItemDto(item.content.title, item.content.author)
} else if (item is NewsItem.NewPost) {
return NewsItemDto(item.content.text, item.content.author)
} else {
throw IllegalArgumentException("This item cannot be converted")
}
}
15. | 다 놀았니? 이제 할 일을 하자 : 문statement과 식expression
https://kotlinlang.org/docs/control-flow.html
!
fun mapItem(item: NewsItem<*>): NewsItemDto {
return if (item is NewsItem.NewTopic) {
NewsItemDto(item.content.title, item.content.author)
} else if (item is NewsItem.NewPost) {
NewsItemDto(item.content.text, item.content.author)
} else {
throw IllegalArgumentException("This item cannot be converted")
}
}
fun mapItem(item: NewsItem<*>): NewsItemDto {
if (item is NewsItem.NewTopic) {
return NewsItemDto(item.content.title, item.content.author)
} else if (item is NewsItem.NewPost) {
return NewsItemDto(item.content.text, item.content.author)
} else {
throw IllegalArgumentException("This item cannot be converted")
}
}
16. | 다 놀았니? 이제 할 일을 하자 : 문statement과 식expression
https://kotlinlang.org/docs/control-flow.html
!
fun mapItem(item: NewsItem<*>) = if (item is NewsItem.NewTopic) {
NewsItemDto(item.content.title, item.content.author.username)
} else if (item is NewsItem.NewPost) {
NewsItemDto(item.content.text, item.content.author)
} else {
throw IllegalArgumentException("This item cannot be converted")
}
fun mapItem(item: NewsItem<*>): NewsItemDto {
if (item is NewsItem.NewTopic) {
return NewsItemDto(item.content.title, item.content.author)
} else if (item is NewsItem.NewPost) {
return NewsItemDto(item.content.text, item.content.author)
} else {
throw IllegalArgumentException("This item cannot be converted")
}
}
17. | 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
fun mapItem(item: NewsItem<*>) = when (item) {
is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.username)
is NewsItem.NewPost -> NewsItemDto(item.c...t.text, item.c...t.author)
else -> throw IllegalArgumentException("This item cannot be converted")
}
fun mapItem(item: NewsItem<*>): NewsItemDto {
if (item is NewsItem.NewTopic) {
return NewsItemDto(item.content.title, item.content.author)
} else if (item is NewsItem.NewPost) {
return NewsItemDto(item.content.text, item.content.author)
} else {
throw IllegalArgumentException("This item cannot be converted")
}
}
18. | 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
fun mapItem(item: NewsItem<*>) = when (item) {
is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.username)
is NewsItem.NewPost -> NewsItemDto(item.c...t.text, item.c...t.author)
else -> throw IllegalArgumentException("This item cannot be converted")
}
abstract class NewsItem<out C> {
val type: String
get() = javaClass.simpleName
abstract val content: C
data class NewTopic(...) : NewsItem<TopicDetails>()
data class NewPost(...) : NewsItem<Post>()
}
19. | 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
abstract class NewsItem<out C> {
val type: String
get() = javaClass.simpleName
abstract val content: C
data class NewTopic(...) : NewsItem<TopicDetails>()
data class NewPost(...) : NewsItem<Post>()
data class NewLike(...) : NewsItem<Like>()
}
fun mapItem(item: NewsItem<*>) = when (item) {
is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.username)
is NewsItem.NewPost -> NewsItemDto(item.c...t.text, item.c...t.author)
else -> throw IllegalArgumentException("This item cannot be converted")
}
20. | 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
sealed class NewsItem<out C> {
val type: String
get() = javaClass.simpleName
abstract val content: C
data class NewTopic(...) : NewsItem<TopicDetails>()
data class NewPost(...) : NewsItem<Post>()
}
fun mapItem(item: NewsItem<*>) = when (item) {
is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.username)
is NewsItem.NewPost -> NewsItemDto(item.c...t.text, item.c...t.author)
else -> throw IllegalArgumentException("This item cannot be converted")
}
https://kotlinlang.org/docs/sealed-classes.html
!
21. | 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
sealed class NewsItem<out C> {
val type: String
get() = javaClass.simpleName
abstract val content: C
data class NewTopic(...) : NewsItem<TopicDetails>()
data class NewPost(...) : NewsItem<Post>()
}
fun mapItem(item: NewsItem<*>) = when (item) {
is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.username)
is NewsItem.NewPost -> NewsItemDto(item.c...t.text, item.c...t.author)
}
https://kotlinlang.org/docs/sealed-classes.html
!
22. | 오버하지마
class Post(
val id: Long?,
val text: String,
val author: AggregateReference<User, Long>,
val topic: AggregateReference<Topic, Long>,
val createdAt: Date,
val updatedAt: Date
) {
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>, createdAt: Date
) : this(null, text, author, topic, createdAt, createdAt)
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>
) : this(text, author, topic, Date())
}
23. | 오버하지마
class Post(
val id: Long?,
val text: String,
val author: AggregateReference<User, Long>,
val topic: AggregateReference<Topic, Long>,
val createdAt: Date,
val updatedAt: Date
) {
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>, createdAt: Date
) : this(null, text, author, topic, createdAt, createdAt)
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>
) : this(text, author, topic, Date())
}
Post(null, "", Ref.to(authorId), Ref.to(topicId), Date(), Date())
Post("", Ref.to(authorId), Ref.to(topicId), Date())
Post("", Ref.to(authorId), Ref.to(topicId))
24. | 오버하지마 : 이름 붙인 인자Named arguments
https://kotlinlang.org/docs/functions.html#named-arguments
!
Post(
id = null,
text = "...",
author = AggregateReference.to(authorId),
topic = AggregateReference.to(topicId),
createdAt = Date(),
updatedAt = Date()
)
Post(
text = "...",
author = AggregateReference.to(authorId),
topic = AggregateReference.to(topicId),
createdAt = Date()
)
Post(
text = "...",
author = AggregateReference.to(authorId),
topic = AggregateReference.to(topicId)
)
25. | 오버하지마 : 기본 인자Default arguments
https://kotlinlang.org/docs/functions.html#default-arguments
!
class Post(
val id: Long? = null,
val text: String,
val author: AggregateReference<User, Long>,
val topic: AggregateReference<Topic, Long>,
val createdAt: Date = Date(),
val updatedAt: Date = Date()
) {
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>, createdAt: Date
) : this(null, text, author, topic, createdAt, createdAt)
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>
) : this(text, author, topic, Date())
}
26. | 오버하지마 : 기본 인자Default arguments
https://kotlinlang.org/docs/functions.html#default-arguments
!
class Post(
val id: Long? = null,
val text: String,
val author: AggregateReference<User, Long>,
val topic: AggregateReference<Topic, Long>,
val createdAt: Date = Date(),
val updatedAt: Date = Date()
) {
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>, createdAt: Date
) : this(null, text, author, topic, createdAt, createdAt)
constructor(
text: String, author: AggregateReference<User, Long>,
topic: AggregateReference<Topic, Long>
) : this(text, author, topic, Date())
}
27. | 오버하지마 : 기본 인자Default arguments
https://kotlinlang.org/docs/functions.html#default-arguments
!
class Post(
val id: Long? = null,
val text: String,
val author: AggregateReference<User, Long>,
val topic: AggregateReference<Topic, Long>,
val createdAt: Date = Date(),
val updatedAt: Date = Date()
)
Post(
text = "...",
author = AggregateReference.to(authorId),
topic = AggregateReference.to(topicId)
)
28. | 너의 이름은
class TopicDetails private constructor(
val id: UUID,
val title: String,
val author: User,
val createdAt: Date,
val updatedAt: Date
) {
companion object {
fun of(topic: Topic, authorMapper: AuthorMapper): TopicDetails {
return TopicDetails(
id = topic.id,
title = topic.title,
author = authorMapper.map(topic.author),
createdAt = topic.createdAt,
updatedAt = topic.updatedAt
)
}
}
}
interface AuthorMapper {
fun map(ref: AggregateReference<User, Long>): User
}
29. | 너의 이름은
class TopicDetails private constructor(
val id: UUID,
val title: String,
val author: User,
val createdAt: Date,
val updatedAt: Date
) {
companion object {
fun of(topic: Topic, authorMapper: AuthorMapper): TopicDetails {
return TopicDetails(
id = topic.id,
title = topic.title,
author = authorMapper.map(topic.author),
createdAt = topic.createdAt,
updatedAt = topic.updatedAt
)
}
}
}
interface AuthorMapper {
fun map(ref: AggregateReference<User, Long>): User
}
30. | 너의 이름은 : 익명anonymous classes
class ForumQueryService(
val executor: ThreadPoolTaskExecutor,
val userQueryRepository: UserQueryRepository,
val topicQueryRepository: TopicQueryRepository,
val postQueryRepository: PostQueryRepository
) : ForumNewsPublisher, ForumReader {
override fun loadPosts(topicId: UUID): Posts {
val topic = topicQueryRepository.findById(topicId)
val posts = postQueryRepository.findByTopic(topic)
return Posts(
TopicDetails.of(
topic,
object : AuthorMapper {
override fun map(ref: AggregateReference<User, Long>): User {
return userQueryRepository.findById(ref.id!!)
}
}
),
posts
)
}
}
31. | 너의 이름은 : 함수형 인터페이스Functional interfaces
https://kotlinlang.org/docs/fun-interfaces.html
!
class ForumQueryService(
val executor: ThreadPoolTaskExecutor,
val userQueryRepository: UserQueryRepository,
val topicQueryRepository: TopicQueryRepository,
val postQueryRepository: PostQueryRepository
) : ForumNewsPublisher, ForumReader {
override fun loadPosts(topicId: UUID): Posts {
val topic = topicQueryRepository.findById(topicId)
val posts = postQueryRepository.findByTopic(topic)
return Posts(
TopicDetails.of(
topic,
{ ref ->
userQueryRepository.findById(ref.id!!)
}
),
posts
)
}
}
fun interface AuthorMapper {
fun map(ref: AggregateReference<User, Long>): User
}
32. | 너의 이름은 : 함수 타입Function types
https://kotlinlang.org/docs/lambdas.html#function-types
!
class TopicDetails private constructor(
val id: UUID,
val title: String,
val author: User,
val createdAt: Date,
val updatedAt: Date
) {
companion object {
fun of(
topic: Topic,
authorMapper: (ref: AggregateReference<User, Long>) -> User
): TopicDetails {
return TopicDetails(
id = topic.id,
title = topic.title,
author = authorMapper.map(topic.author),
createdAt = topic.createdAt,
updatedAt = topic.updatedAt
)
}
}
}
fun interface AuthorMapper {
fun map(ref: AggregateReference<User, Long>): User
}
33. | 품행제로
import org.springframework.data.domain.Persistable
import org.springframework.data.jdbc.core.mapping.AggregateReference
import java.util.*
class Topic(
private val _id: UUID,
val title: String,
val author: AggregateReference<User, Long>): Persistable<UUID> {
override fun getId(): UUID {
return _id;
}
companion object {
fun create(title: String, author: User): Topic {
val createdAt = Date()
val topic = Topic(
UUID.randomUUID(),title,AggregateReference.to(author.requiredId())
)
topic._isNew = true
return topic
}
}
}
43. | 뭣이 중헌디이 : 할 일만 간결하게 with Kotlin support
https://docs.spring.io/spring-framework/docs/5.3.x/kdoc-api/spring-jdbc/org.springframework.jdbc.core/index.html
!
class SpringJdbcPostQueryRepository(
val namedParameterJdbcOperations: NamedParameterJdbcOperations
) : PostQueryRepository {
val jdbcOperations: JdbcOperations
get() = namedParameterJdbcOperations.jdbcOperations
override fun findByTopic(topic: Topic): Array<Post> {
return jdbcOperations.query(SQL_findByTopic, topic.id) {
rs, rowNum ->
Post(
rs.getLong("ID"),
rs.getString("TEXT"),
AggregateReference.to(rs.getLong("AUTHOR")),
AggregateReference.to(UUID.fromString(rs.getString("TOPIC"))),
rs.getDate("CREATED_AT"),
rs.getDate("UPDATED_AT")
)
}.toTypedArray()
}
companion object {
const val SQL_findByTopic = "..."
}
}
44. | 너니까 너답게
@WebMvcTest(ForumController::class)
internal class ForumControllerTests(@Autowired val mockMvc: MockMvc) {
@MockBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
given(forumReader.loadTopics()).willReturn(topics)
mockMvc.perform(
get("/forum/topics").accept(MediaType.TEXT_HTML)
).andExpect(
status().isOk
).andExpect(
view().name("forum/topics")
).andExpect(
model().attributeExists("topics")
)
}
}
45. | 너니까 너답게 : 생성자 주입 with Kotlin support
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#testing
!
@WebMvcTest(ForumController::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
internal class ForumControllerTests(val mockMvc: MockMvc) {
@MockBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
given(forumReader.loadTopics()).willReturn(topics)
mockMvc.perform(
get("/forum/topics").accept(MediaType.TEXT_HTML)
).andExpect(
status().isOk
).andExpect(
view().name("forum/topics")
).andExpect(
model().attributeExists("topics")
)
}
}
46. | 너니까 너답게 : MockMvc Test
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#mockmvc-dsl
!
@WebMvcTest(ForumController::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
internal class ForumControllerTests(val mockMvc: MockMvc) {
@MockBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
given(forumReader.loadTopics()).willReturn(topics)
mockMvc.perform(
get("/forum/topics").accept(MediaType.TEXT_HTML)
).andExpect(
status().isOk
).andExpect(
view().name("forum/topics")
).andExpect(
model().attributeExists("topics")
)
}
}
47. | 너니까 너답게 : MockMvc DSL
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#mockmvc-dsl
!
@WebMvcTest(ForumController::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
internal class ForumControllerTests(val mockMvc: MockMvc) {
@MockBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
given(forumReader.loadTopics()).willReturn(topics)
mockMvc.get("/forum/topics") {
accept = MediaType.TEXT_HTML
}.andExpect {
status { isOk() }
view { name("forum/topics") }
model { attributeExists("topics") }
}
}
}
48. |
@WebMvcTest(ForumController::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
internal class ForumControllerTests(val mockMvc: MockMvc) {
@MockBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
given(forumReader.loadTopics()).willReturn(topics)
mockMvc.get("/forum/topics") {
accept = MediaType.TEXT_HTML
}.andExpect {
status { isOk() }
view { name("forum/topics") }
model { attributeExists("topics") }
}
}
}
너니까 너답게 : MockMvc DSL
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#mockmvc-dsl
!
영역 특화 언어(DSL; Domain-Specific Language)
✔ 코드의 가독성과 유지 보수성을 좋게 유지할 수 있다
✔ 코드 자동완성 기능을 누릴 수 있다
✔ 컴파일 시점에 문법 오류를 알 수 있다
49. | 너니까 너답게 : Mockito Test
https://github.com/Ninja-Squad/springmockk
!
@WebMvcTest(ForumController::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
internal class ForumControllerTests(val mockMvc: MockMvc) {
@MockBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
given(forumReader.loadTopics()).willReturn(topics)
mockMvc.get("/forum/topics") {
accept = MediaType.TEXT_HTML
}.andExpect {
status { isOk() }
view { name("forum/topics") }
model { attributeExists("topics") }
}
}
}
50. | 너니까 너답게 : MockK DSL
https://github.com/Ninja-Squad/springmockk
!
@WebMvcTest(ForumController::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
internal class ForumControllerTests(val mockMvc: MockMvc) {
@MockkBean
private lateinit var forumReader: ForumReader
@Test
fun `주제 목록 화면 접근시`() {
val topics = arrayOf(Topic.create("test"))
every { forumReader.loadTopics() } returns topics
mockMvc.get("/forum/topics") {
accept = MediaType.TEXT_HTML
}.andExpect {
status { isOk() }
view { name("forum/topics") }
model { attributeExists("topics") }
}
}
}
52. | 헤어짐을 아는 그대에게
@RestController
@RequestMapping("/notification")
class NotificationController(val forumNewsPublisher: ForumNewsPublisher) {
@GetMapping("/subscribe/news")
fun subscribeNews(): Mono<NewsDto> {
return forumNewsPublisher.subscribeReactive(
Duration.ofSeconds(5)
).flatMap { news ->
Mono.just(
NewsDto.of(news)
)
}
}
}
53. | 헤어짐을 아는 그대에게 : 비동기 요청 처리
@RestController
@RequestMapping("/notification")
class NotificationController(val forumNewsPublisher: ForumNewsPublisher) {
@GetMapping("/subscribe/news")
fun subscribeNews(): Mono<NewsDto> {
return forumNewsPublisher.subscribeReactive(
Duration.ofSeconds(5)
).flatMap { news ->
Mono.just(
NewsDto.of(news)
)
}
}
}
스프링 3.2 이상부터
지원되는 비동기 요청 처리
스프링 5부터는
리액티브 타입(Mono, Flux)도 지원
54. | 헤어짐을 아는 그대에게 : 코루틴으로 비동기 처리 with Kotlin support
@RestController
@RequestMapping("/notification")
class NotificationController(val forumNewsPublisher: ForumNewsPublisher) {
@GetMapping("/subscribe/news")
suspend fun subscribeNews(): NewsDto {
return NewsDto.of(
forumNewsPublisher.subscribe(Duration.ofSeconds(5))
)
}
}
55. | 번개같이 비동기 프로그래밍 훑어보기
GithubInfo getGithubInfo(String username, String accessToken)
깃헙 사용자 정보외 조직, 저자소 정보를 조회하기
사용자 정보로 조직 정보 조회
사용자 정보 조회
사용자 정보로 저장소 정보 조회
결과
56. | 번개같이 비동기 프로그래밍 훑어보기 : 자바 8+ with CompletableFuture
CompletableFuture<GithubInfo> getGithubInfoAsync(String username, String accessToken) {
var operations = new GithubOperations(accessToken);
return operations.fetchUserAsync(username).thenCompose(user -> {
var organizationsFuture = operations.fetchOrganizationsAsync(user);
var repositoriesFuture = operations.fetchRepositoriesAsync(user);
return organizationsFuture.thenCombine(
repositoriesFuture,
(organizations, repositories) -> new GithubInfo(user, organizations, repositories)
);
});
}
class GithubOperations {
public CompletableFuture<User> fetchUserAsync(String username) {..}
public CompletableFuture<List<Organization>> fetchOrganizationsAsync(User user) {..}
public CompletableFuture<List<Repository>> fetchRepositoriesAsync(User user) {..}
}
57. | 번개같이 비동기 프로그래밍 훑어보기 : 리액티브 라이브러리 with Reactor
class GithubOperations {
public Mono<User> fetchUserReactive(String username) {..}
public Mono<List<Organization>> fetchOrganizationsReactive(User user) {..}
public Mono<List<Repository>> fetchRepositoriesReactive(User user) {..}
}
Mono<GithubInfo> getGithubInfoReactive(String username, String accessToken) {
var operations = new GithubOperations(accessToken);
return operations.fetchUserReactive(username).flatMap(user -> {
var organizationsFuture = operations.fetchOrganizationsReactive(user);
var repositoriesFuture = operations.fetchRepositoriesReactive(user);
return organizationsFuture.zipWith(
repositoriesFuture,
(organizations, repositories) -> new GithubInfo(user, organizations, repositories)
);
});
}
58. | 번개같이 비동기 프로그래밍 훑어보기 : 코루틴Coroutines
class GithubOperations {
suspend fun fetchUser(username: String): User {..}
suspend fun fetchOrganizations(user: User): List<Organization> {..}
suspend fun fetchRepositories(user: User): List<Repository> {..}
}
suspend fun getGithubInfo(username: String, accessToken: String) = coroutineScope {
val operations = GithubOperations(accessToken)
val user: User = operations.fetchUser(username)
val organizations = async { operations.fetchOrganizations(user) }
val repositories = async { operations.fetchRepositories(user) }
GithubInfo(user, organizations.await(), repositories.await())
}
60. | 함수형 스타일을 품은 스프링5
2005년 05월, 스프링 1.2 (Java 1.3+, support Java 5)
2006년 10월, 스프링 2
2013년 12월, 스프링 3 (Java 5+)
2013년 12월, 스프링 4 (Java 6+)
2007년 11월, 스프링 2.5 (Java 1.4.2+, support Java 6) 2017년 10월, 스프링 5 (Java 8+)
2022년 10월, 스프링 6 (Java 17, Jakarta EE 9)
61. | 함수형 스타일을 품은 스프링5
2005년 05월, 스프링 1.2 (Java 1.3+, support Java 5)
2006년 10월, 스프링 2
2013년 12월, 스프링 3 (Java 5+)
2013년 12월, 스프링 4 (Java 6+)
2007년 11월, 스프링 2.5 (Java 1.4.2+, support Java 6) 2017년 10월, 스프링 5 (Java 8+)
애노테이션 기반 컴포넌트 스캐닝과 프로그래밍 모델 도입
2022년 10월, 스프링 6 (Java 17, Jakarta EE 9)
애노테이션 기반 컨테이너 구성 도입
62. | 함수형 스타일을 품은 스프링5
2005년 05월, 스프링 1.2 (Java 1.3+, support Java 5)
2006년 10월, 스프링 2
2013년 12월, 스프링 3 (Java 5+)
2013년 12월, 스프링 4 (Java 6+)
2007년 11월, 스프링 2.5 (Java 1.4.2+, support Java 6) 2017년 10월, 스프링 5 (Java 8+)
2022년 10월, 스프링 6 (Java 17, Jakarta EE 9)
경량 함수형 프로그래밍 모델 도입
함수형 빈 정의 지원
코틀린 지원
63. | 경량 함수형 프로그래밍 모델 with WebMvc.fn and WebFlux.fn
import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Configuration
public class FunctionalEndpoints {
@Bean
public RouterFunction<ServerResponse> personRouter(PersonHandler handler) {
return route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
}
@Bean
public PersonHandler personHandler() {
return new PersonHandler();
}
}
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#webmvc-fn
!
64. | 경량 함수형 프로그래밍 모델 with WebMvc.fn and WebFlux.fn
import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Configuration
public class FunctionalEndpoints {
@Bean
public RouterFunction<ServerResponse> personRouter(PersonHandler handler) {
return route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
}
@Bean
public PersonHandler personHandler() {
return new PersonHandler();
}
}
65. | 경량 함수형 프로그래밍 모델 with WebMvc.fn and WebFlux.fn
import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Configuration
public class FunctionalEndpoints {
@Bean
public RouterFunction<ServerResponse> personRouter(PersonHandler handler) {
return route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
}
@Bean
public PersonHandler personHandler() {
return new PersonHandler();
}
}
애노테이션 기반
프로그래밍 모델로 표현하면...
@Controller
@RequestMapping("/person")
class PersonController {
@GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
public Person getPerson() { ... }
@GetMapping(produces = APPLICATION_JSON_VALUE)
public List<Person> listPerson() { ... }
@PostMapping
public void createPerson() { ... }
}
66. | 경량 함수형 프로그래밍 모델 with WebMvc.fn and WebFlux.fn
import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Configuration
public class FunctionalEndpoints {
@Bean
public RouterFunction<ServerResponse> personRouter(PersonHandler handler) {
return route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
}
@Bean
public PersonHandler personHandler() {
return new PersonHandler();
}
}
67. | 경량 함수형 프로그래밍 모델 with WebMvc.fn and WebFlux.fn
import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Configuration
public class FunctionalEndpoints {
@Bean
public RouterFunction<ServerResponse> personRouter(PersonHandler handler) {
return route().path("/person", builder ->
builder.nest(accept(APPLICATION_JSON), nestBuilder ->
nestBuilder.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
).POST(handler::createPerson)
).build();
}
@Bean
public PersonHandler personHandler() {
return new PersonHandler();
}
}
69. | 함수형 빈 정의
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean(MyRepository.class);
applicationContext.registerBean(MyService.class, () -> {
return new MyService(applicationContext.getBean(MyRepository.class))
});
class MyRepository {
}
class MyService {
final MyRepository repository;
public MyService(MyRepository repository) {
this.repository = repository;
}
}
70. | 함수형 빈 정의
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean(MyRepository.class);
applicationContext.registerBean(MyService.class, () -> {
return new MyService(applicationContext.getBean(MyRepository.class))
});
class MyRepository {
}
class MyService {
final MyRepository repository;
public MyService(MyRepository repository) {
this.repository = repository;
}
}
71. | 코틀린과 빈 정의 DSL
import org.springframework.context.support.beans
val dataBeans = beans {
bean("namedParameterJdbcOperations") {
NamedParameterJdbcTemplate(ref<DataSource>())
}
bean("springJdbcPostQueryRepository") {
println("reg SpringJdbcPostQueryRepository")
SpringJdbcPostQueryRepository(ref())
}
}
fun main(args: Array<String>) {
runApplication<ForumApplication>(*args) {
addInitializers(dataBeans)
}
} 빈 정의 DSL로 작성된 빈 구성정보는
ApplicationContextInitializer 객체로 만들어지며,
스프링부트 애플리케이션 실행시 전달할 수 있다.
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#kotlin-bean-definition-dsl
!
72. | 애노테이션과 관례 기반의 자동화된 구성
import org.springframework.boot.autoconfigure.SpringBootApplication
@RestController
@SpringBootApplication
class MyApplication {
@Autowired
private lateinit var myService: MyService
@RequestMapping("/")
fun home(): String {
return myService.say()
}
}
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.auto-configuration
!
73. | 애노테이션과 관례 기반의 자동화된 구성
import org.springframework.boot.autoconfigure.SpringBootApplication
@RestController
@SpringBootApplication
class MyApplication {
@Autowired
private lateinit var myService: MyService
@RequestMapping("/")
fun home(): String {
return myService.say()
}
}
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
# Initializers
# Application Listeners
# Environment Post Processors
# Auto Configuration Import Listeners
# Auto Configuration Import Filters
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,
...
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
# Failure analyzers
# Template availability providers
# DataSource initializer detectors
# Depends on database initialization detectors
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.auto-configuration
!
수십개의 자동화된 구성 전략으로
다양하게 애플리케이션을 구성한다
77. | 명시적으로 스프링부트를 구성하는 두 가지 DSL
spring-boot-autoconfigure
spring-fu-autoconfigure-adapter
JaFu
(Java DSL)
KoFu
(Kotlin DSL)
adapt boot auto-configuration
@configuration to ApplicationContextInitializer
✔ DSL을 통한 명시적인 구성한다
✔ 함수형 스타일로 스프링부트 자동 구성 사용을 지원한다
✔ 자동 클래스 탐지로 활성화되는 기능이 없다
✔ 선언적인 모델과 프로그래밍적인 모델 모두 지원한다
✔ 더 빠르게 시작할 수 있고, 메모리 소비도 적다
✔ 애노테이션과 리플렉션 API를 최소한으로 사용한다
✔ 순수 함수형 스타일로 동작하고, CGLIB 프록시를 쓰지 않는다
78. | 40% 빠른 우싸인 푸
kofu auto-configuration
0
0.5
1
1.5
2
2.5
3
3.5
4
4.5
5
스프링부트 기반 웹 애플리케이션 기동 시간
79. | 애플리케이션 구성 방법 비교
Spring Fu
✔ 명시적인 선언 방식
✔ 함수형 스타일 구성
✔ 람다 기반으로 동작
✔ 실험단계
{
Spring Boot
✔ 관 기반의 자동화된 구성
✔ 애노테이션 기반 구성
✔ 리플렉션 기반으로 동작
✔ 운영단계
{
80. Spring fu is not better or worse that auto-config,
it is different.
82. | 참고자료
✔ Spring Framework Documentation
✔ Spring Fu project
✔ The State of Kotlin Support in Spring
✔ The evolution of Spring Fu
✔ SpringMockK project