SlideShare ist ein Scribd-Unternehmen logo
1 von 82
Downloaden Sie, um offline zu lesen
#살아있다 #자프링외길12년차 #코프링2개월생존기
발 표 자 : 박용권(당근마켓 동네모임 팀)
밋업자료 :
밋업코드 :
#살아있다
#자프링외길12년차
박 용 권
#코프링2개월생존기
| 2016년, 첫 만남
| 코틀린의 철학
Kotlin in Action
Dmitry Jemerov and Svetlana Isakova
https://www.manning.com/books/kotlin-in-action
!
코틀린은 자바와의 상호운용성에 초점을 맞춘 실용적이고 간결하며
안전한 언어이다.
| 코틸린의 철학 : 간결성
/* 데이터 보관을 목적으로 사용하는 클래스가 필요할 때는 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)
| 코틸린의 철학 : 안전성
// 널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
}
| 2021년, 다시 만나다
|
지금부터 코드가 무-척 많이 나옵니다
#이것은
#자바인가
#코틀린인가
| 그만 널 잊으라고
fun from(posts: Array<Post?>): Array<PostDto> {
return posts.map({ post ->
if (post == null) {
throw Error("Post object is null")
}
if (post.id == null) {
throw Error("Id field is null in post object")
}
PostDto(
post.id,
post.text,
post.author.id,
post.createdAt,
post.updatedAt
)
}).toTypedArray()
}
| 그만 널 잊으라고 : 안전한 호출 연산자: ?.
https://kotlinlang.org/docs/null-safety.html#safe-calls
!
fun from(posts: Array<Post?>): Array<PostDto> {
return posts.map({ post ->
if (post?.id == null) {
throw Error("Post object or id field is null")
}
PostDto(
post.id,
post.text,
post.author.id,
post.createdAt,
post.updatedAt
)
}).toTypedArray()
}
| 그만 널 잊으라고 : 엘비스 연산자: ?:
https://kotlinlang.org/docs/null-safety.html#elvis-operator
!
fun from(posts: Array<Post?>): Array<PostDto> {
return posts.map({ post ->
PostDto(
post?.id ?: throw Error("Post object or id field is null"),
post.text,
post.author.id,
post.createdAt,
post.updatedAt
)
}).toTypedArray()
}
| 그만 널 잊으라고 : 널 아님 단언: !!
https://kotlinlang.org/docs/null-safety.html#the-operator
!
fun from(posts: Array<Post?>): Array<PostDto> {
return posts.map({ post ->
PostDto(
post?.id!!,
post.text,
post.author.id,
post.createdAt,
post.updatedAt
)
}).toTypedArray()
}
| 다 놀았니? 이제 할 일을 하자
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")
}
}
| 다 놀았니? 이제 할 일을 하자 : 문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")
}
}
| 다 놀았니? 이제 할 일을 하자 : 문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")
}
}
| 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
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")
}
}
| 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
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>()
}
| 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
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")
}
| 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
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
!
| 다 놀았니? 이제 할 일을 하자 : 봉인해서 까먹지 않기
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
!
| 오버하지마
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())
}
| 오버하지마
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))
| 오버하지마 : 이름 붙인 인자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)
)
| 오버하지마 : 기본 인자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())
}
| 오버하지마 : 기본 인자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())
}
| 오버하지마 : 기본 인자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)
)
| 너의 이름은
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
}
| 너의 이름은
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
}
| 너의 이름은 : 익명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
)
}
}
| 너의 이름은 : 함수형 인터페이스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
}
| 너의 이름은 : 함수 타입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
}
| 품행제로
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
}
}
}
| 품행제로 : 린트lint와 코딩 컨벤션Coding conventions
https://kotlinlang.org/docs/coding-conventions.html
!
#표현력
#풍부한
#아이
| 뭣이 중헌디
@SpringBootConfiguration
@EnableJdbcRepositories
class DataConfigurations : AbstractJdbcConfiguration() {
@Bean
fun dataSource(environment: Environment): DataSource {
val type = environment.getRequiredProperty(
"type",
EmbeddedDatabaseType::class.java
)
val scriptEncoding = environment.getProperty("script-encoding", "utf-8")
val separator = environment.getProperty("separator", ";")
val scripts = environment.getProperty("scripts", List::class.java)
?.map { it.toString() }
?.toTypedArray()
val builder = EmbeddedDatabaseBuilder()
builder.setType(type)
builder.setScriptEncoding(scriptEncoding)
builder.setSeparator(separator)
builder.addScripts(*scripts ?: emptyArray())
return builder.build()
}
}
| 뭣이 중헌디 : 스코프 함수로 가독성 높이기
@SpringBootConfiguration
@EnableJdbcRepositories
class DataConfigurations : AbstractJdbcConfiguration() {
@Bean
fun dataSource(environment: Environment): DataSource {
val type = environment.getRequiredProperty(
"type",
EmbeddedDatabaseType::class.java
)
val scriptEncoding = environment.getProperty("script-encoding", "utf-8")
val separator = environment.getProperty("separator", ";")
val scripts = environment.getProperty("scripts", List::class.java)
?.map { it.toString() }
?.toTypedArray()
return EmbeddedDatabaseBuilder().apply {
setType(type)
setScriptEncoding(scriptEncoding)
setSeparator(separator)
addScripts(*scripts ?: emptyArray())
}.build()
}
}
| 뭣이 중헌디 : 스코프 함수Scope Function
val arawn = Traveler("arawn", "Seoul", 1000)
arawn.moveTo("New York")
arawn.pay(10)
val grizz = Traveler("Grizz", "Seoul", 1000).let {
it.moveTo("London")
it.pay(10)
}
val dan = Traveler("Dan").apply {
moveTo("Vancouver")
earn(50)
}
travelerRepository.findByName("Root")?.run {
moveTo("Firenze")
}
| 뭣이 중헌디 : 편하고, 안전하게 꺼내쓰기 with Kotlin support
@SpringBootConfiguration
@EnableJdbcRepositories
class DataConfigurations : AbstractJdbcConfiguration() {
@Bean
fun dataSource(environment: Environment): DataSource {
val type = environment.getRequiredProperty(
"type",
EmbeddedDatabaseType::class.java
)
val scriptEncoding = environment.getProperty("script-encoding", "utf-8")
val separator = environment.getProperty("separator", ";")
val scripts = environment.getProperty("scripts", List::class.java)
?.map { it.toString() }
?.toTypedArray()
return EmbeddedDatabaseBuilder().apply {
setType(type)
setScriptEncoding(scriptEncoding)
setSeparator(separator)
addScripts(*scripts ?: emptyArray())
}.build()
}
}
| 뭣이 중헌디 : 편하고, 안전하게 꺼내쓰기 with Kotlin support
@SpringBootConfiguration
@EnableJdbcRepositories
class DataConfigurations : AbstractJdbcConfiguration() {
@Bean
fun dataSource(environment: Environment): DataSource {
val type = environment.getRequiredProperty<EmbeddedDatabaseType>("type")
val scriptEncoding = environment.get("scriptEncoding") ?: "utf-8"
val separator = environment.get("separator") ?: ";"
val scripts = environment.getProperty<Array<String>>("scripts")
return EmbeddedDatabaseBuilder().apply {
setType(type)
setScriptEncoding(scriptEncoding)
setSeparator(separator)
addScripts(*scripts ?: emptyArray())
}.build()
}
}
|
@SpringBootConfiguration
@EnableJdbcRepositories
class DataConfigurations : AbstractJdbcConfiguration() {
@Bean
fun dataSource(environment: Environment): DataSource {
val type = environment.getRequiredProperty<EmbeddedDatabaseType>("type")
val scriptEncoding = environment.get("scriptEncoding") ?: "utf-8"
val separator = environment.get("separator") ?: ";"
val scripts = environment.getProperty<Array<String>>("scripts")
return EmbeddedDatabaseBuilder().apply {
setType(type)
setScriptEncoding(scriptEncoding)
setSeparator(separator)
addScripts(*scripts ?: emptyArray())
}.build()
}
}
뭣이 중헌디 : 확장함수Extension functions
/**
* Extension for [PropertyResolver.getProperty] providing
* a `getProperty<Foo>(...)` variant returning a nullable `Foo`.
*/
inline fun <reified T> PropertyResolver.getProperty(key: String) : T? =
getProperty(key, T::class.java)
스프링은 PropertyResolver를 확장해 몇가지 편의 기능을
코틀린의 확장 함수로 제공한다
Environment 는 PropertyResolver 를 상속한다
https://kotlinlang.org/docs/extensions.html#extension-functions
!
| 뭣이 중헌디이
class SpringJdbcPostQueryRepository(
val namedParameterJdbcOperations: NamedParameterJdbcOperations
) : PostQueryRepository {
val jdbcOperations: JdbcOperations
get() = namedParameterJdbcOperations.jdbcOperations
override fun findByTopic(topic: Topic): Array<Post> {
return jdbcOperations.query(SQL_findByTopic, RowMapper(){
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")
)
}, arrayOf(topic.id)).toTypedArray()
}
companion object {
const val SQL_findByTopic = "..."
}
}
| 뭣이 중헌디이 : 할 일만 간결하게 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 = "..."
}
}
| 너니까 너답게
@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")
)
}
}
| 너니까 너답게 : 생성자 주입 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")
)
}
}
| 너니까 너답게 : 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")
)
}
}
| 너니까 너답게 : 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") }
}
}
}
|
@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)
✔ 코드의 가독성과 유지 보수성을 좋게 유지할 수 있다
✔ 코드 자동완성 기능을 누릴 수 있다
✔ 컴파일 시점에 문법 오류를 알 수 있다
| 너니까 너답게 : 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") }
}
}
}
| 너니까 너답게 : 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") }
}
}
}
#기다려
#먹어
| 헤어짐을 아는 그대에게
@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)
)
}
}
}
| 헤어짐을 아는 그대에게 : 비동기 요청 처리
@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)도 지원
| 헤어짐을 아는 그대에게 : 코루틴으로 비동기 처리 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))
)
}
}
| 번개같이 비동기 프로그래밍 훑어보기
GithubInfo getGithubInfo(String username, String accessToken)
깃헙 사용자 정보외 조직, 저자소 정보를 조회하기
사용자 정보로 조직 정보 조회
사용자 정보 조회
사용자 정보로 저장소 정보 조회
결과
| 번개같이 비동기 프로그래밍 훑어보기 : 자바 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) {..}
}
| 번개같이 비동기 프로그래밍 훑어보기 : 리액티브 라이브러리 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)
);
});
}
| 번개같이 비동기 프로그래밍 훑어보기 : 코루틴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())
}
One more thing...
| 함수형 스타일을 품은 스프링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)
| 함수형 스타일을 품은 스프링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)
애노테이션 기반 컨테이너 구성 도입
| 함수형 스타일을 품은 스프링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)
경량 함수형 프로그래밍 모델 도입
함수형 빈 정의 지원
코틀린 지원
| 경량 함수형 프로그래밍 모델 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
!
| 경량 함수형 프로그래밍 모델 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();
}
}
| 경량 함수형 프로그래밍 모델 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() { ... }
}
| 경량 함수형 프로그래밍 모델 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();
}
}
| 경량 함수형 프로그래밍 모델 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();
}
}
| 코틀린과 라우터 DSL
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#router-dsl
!
import org.springframework.web.servlet.function.router
@Configuration
class FunctionalEndpoints {
@Bean
fun personRouter(personHandler: PersonHandler) = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", personHandler::getPerson)
GET(personHandler::listPeople)
}
POST(personHandler::createPerson)
}
}
@Bean
fun personHandler() = PersonHandler()
}
| 함수형 빈 정의
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;
}
}
| 함수형 빈 정의
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;
}
}
| 코틀린과 빈 정의 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
!
| 애노테이션과 관례 기반의 자동화된 구성
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
!
| 애노테이션과 관례 기반의 자동화된 구성
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
!
수십개의 자동화된 구성 전략으로
다양하게 애플리케이션을 구성한다
| 질문, 하나...
명확하게 구성하기 위해서는 어떻게 해야할까요?
| 질문, 둘...
스프링 애플리케이션 구성을 효과적으로 하려면
어떻게 해야할까요?
| 스프링 푸Spring Fu, 스프링부트를 구성하는 DSL
https://github.com/spring-projects-experimental/spring-fu
!
import org.springframework.fu.kofu.webApplication
import org.springframework.fu.kofu.webmvc.webMvc
val myApplication = webApplication {
beans {
bean<MyRepository>()
bean {
MyService(ref())
}
webMvc {
router {
val service = ref<MyService>()
GET("/") {
ServerResponse.ok().body(service.say())
}
}
converters {
string()
}
}
}
}
fun main(args: Array<String>) {
myApplication.run(args)
}
| 명시적으로 스프링부트를 구성하는 두 가지 DSL
spring-boot-autoconfigure
spring-fu-autoconfigure-adapter
JaFu
(Java DSL)
KoFu
(Kotlin DSL)
adapt boot auto-configuration
@configuration to ApplicationContextInitializer
✔ DSL을 통한 명시적인 구성한다
✔ 함수형 스타일로 스프링부트 자동 구성 사용을 지원한다
✔ 자동 클래스 탐지로 활성화되는 기능이 없다
✔ 선언적인 모델과 프로그래밍적인 모델 모두 지원한다
✔ 더 빠르게 시작할 수 있고, 메모리 소비도 적다
✔ 애노테이션과 리플렉션 API를 최소한으로 사용한다
✔ 순수 함수형 스타일로 동작하고, CGLIB 프록시를 쓰지 않는다
| 40% 빠른 우싸인 푸
kofu auto-configuration
0
0.5
1
1.5
2
2.5
3
3.5
4
4.5
5
스프링부트 기반 웹 애플리케이션 기동 시간
| 애플리케이션 구성 방법 비교
Spring Fu
✔ 명시적인 선언 방식
✔ 함수형 스타일 구성
✔ 람다 기반으로 동작
✔ 실험단계
{
Spring Boot
✔ 관 기반의 자동화된 구성
✔ 애노테이션 기반 구성
✔ 리플렉션 기반으로 동작
✔ 운영단계
{
Spring fu is not better or worse that auto-config,
it is different.
end { '#살아있다' }
| 참고자료
✔ Spring Framework Documentation
✔ Spring Fu project
✔ The State of Kotlin Support in Spring
✔ The evolution of Spring Fu
✔ SpringMockK project

Weitere ähnliche Inhalte

Was ist angesagt?

Easy data-with-spring-data-jpa
Easy data-with-spring-data-jpaEasy data-with-spring-data-jpa
Easy data-with-spring-data-jpa
Staples
 
A year with event sourcing and CQRS
A year with event sourcing and CQRSA year with event sourcing and CQRS
A year with event sourcing and CQRS
Steve Pember
 

Was ist angesagt? (20)

Spark + S3 + R3를 이용한 데이터 분석 시스템 만들기
Spark + S3 + R3를 이용한 데이터 분석 시스템 만들기Spark + S3 + R3를 이용한 데이터 분석 시스템 만들기
Spark + S3 + R3를 이용한 데이터 분석 시스템 만들기
 
Rest and the hypermedia constraint
Rest and the hypermedia constraintRest and the hypermedia constraint
Rest and the hypermedia constraint
 
Real World Event Sourcing and CQRS
Real World Event Sourcing and CQRSReal World Event Sourcing and CQRS
Real World Event Sourcing and CQRS
 
Redis + Apache Spark = Swiss Army Knife Meets Kitchen Sink
Redis + Apache Spark = Swiss Army Knife Meets Kitchen SinkRedis + Apache Spark = Swiss Army Knife Meets Kitchen Sink
Redis + Apache Spark = Swiss Army Knife Meets Kitchen Sink
 
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
 
Easy data-with-spring-data-jpa
Easy data-with-spring-data-jpaEasy data-with-spring-data-jpa
Easy data-with-spring-data-jpa
 
Solid NodeJS with TypeScript, Jest & NestJS
Solid NodeJS with TypeScript, Jest & NestJSSolid NodeJS with TypeScript, Jest & NestJS
Solid NodeJS with TypeScript, Jest & NestJS
 
[수정본] 우아한 객체지향
[수정본] 우아한 객체지향[수정본] 우아한 객체지향
[수정본] 우아한 객체지향
 
webservice scaling for newbie
webservice scaling for newbiewebservice scaling for newbie
webservice scaling for newbie
 
Omnisearch in AEM 6.2 - Search All the Things
Omnisearch in AEM 6.2 - Search All the ThingsOmnisearch in AEM 6.2 - Search All the Things
Omnisearch in AEM 6.2 - Search All the Things
 
Spring Data JPA from 0-100 in 60 minutes
Spring Data JPA from 0-100 in 60 minutesSpring Data JPA from 0-100 in 60 minutes
Spring Data JPA from 0-100 in 60 minutes
 
API Gateway를 이용한 토큰 기반 인증 아키텍처
API Gateway를 이용한 토큰 기반 인증 아키텍처API Gateway를 이용한 토큰 기반 인증 아키텍처
API Gateway를 이용한 토큰 기반 인증 아키텍처
 
Little Big Data #1. 바닥부터 시작하는 데이터 인프라
Little Big Data #1. 바닥부터 시작하는 데이터 인프라Little Big Data #1. 바닥부터 시작하는 데이터 인프라
Little Big Data #1. 바닥부터 시작하는 데이터 인프라
 
[2019] PAYCO 쇼핑 마이크로서비스 아키텍처(MSA) 전환기
[2019] PAYCO 쇼핑 마이크로서비스 아키텍처(MSA) 전환기[2019] PAYCO 쇼핑 마이크로서비스 아키텍처(MSA) 전환기
[2019] PAYCO 쇼핑 마이크로서비스 아키텍처(MSA) 전환기
 
A year with event sourcing and CQRS
A year with event sourcing and CQRSA year with event sourcing and CQRS
A year with event sourcing and CQRS
 
Defending against Java Deserialization Vulnerabilities
 Defending against Java Deserialization Vulnerabilities Defending against Java Deserialization Vulnerabilities
Defending against Java Deserialization Vulnerabilities
 
L'API Collector dans tous ses états
L'API Collector dans tous ses étatsL'API Collector dans tous ses états
L'API Collector dans tous ses états
 
Spring Data JPA
Spring Data JPASpring Data JPA
Spring Data JPA
 
엘라스틱서치, 로그스태시, 키바나
엘라스틱서치, 로그스태시, 키바나엘라스틱서치, 로그스태시, 키바나
엘라스틱서치, 로그스태시, 키바나
 
Spring Boot
Spring BootSpring Boot
Spring Boot
 

Ähnlich wie #살아있다 #자프링외길12년차 #코프링2개월생존기

AST Transformations
AST TransformationsAST Transformations
AST Transformations
HamletDRC
 
AST Transformations at JFokus
AST Transformations at JFokusAST Transformations at JFokus
AST Transformations at JFokus
HamletDRC
 
JJUG CCC 2011 Spring
JJUG CCC 2011 SpringJJUG CCC 2011 Spring
JJUG CCC 2011 Spring
Kiyotaka Oku
 

Ähnlich wie #살아있다 #자프링외길12년차 #코프링2개월생존기 (20)

AST Transformations
AST TransformationsAST Transformations
AST Transformations
 
AST Transformations at JFokus
AST Transformations at JFokusAST Transformations at JFokus
AST Transformations at JFokus
 
Groovy Ast Transformations (greach)
Groovy Ast Transformations (greach)Groovy Ast Transformations (greach)
Groovy Ast Transformations (greach)
 
Kotlin for Android Developers - Victor Kropp - Codemotion Rome 2018
Kotlin for Android Developers - Victor Kropp - Codemotion Rome 2018Kotlin for Android Developers - Victor Kropp - Codemotion Rome 2018
Kotlin for Android Developers - Victor Kropp - Codemotion Rome 2018
 
Persisting Data on SQLite using Room
Persisting Data on SQLite using RoomPersisting Data on SQLite using Room
Persisting Data on SQLite using Room
 
Privet Kotlin (Windy City DevFest)
Privet Kotlin (Windy City DevFest)Privet Kotlin (Windy City DevFest)
Privet Kotlin (Windy City DevFest)
 
Codable routing
Codable routingCodable routing
Codable routing
 
Improving Correctness with Types Kats Conf
Improving Correctness with Types Kats ConfImproving Correctness with Types Kats Conf
Improving Correctness with Types Kats Conf
 
Ast transformations
Ast transformationsAst transformations
Ast transformations
 
Meetup di GDG Italia - Leonardo Pirro - Codemotion Rome 2018
Meetup di GDG Italia - Leonardo Pirro -  Codemotion Rome 2018 Meetup di GDG Italia - Leonardo Pirro -  Codemotion Rome 2018
Meetup di GDG Italia - Leonardo Pirro - Codemotion Rome 2018
 
{"JSON, Swift and Type Safety" : "It's a wrap"}
{"JSON, Swift and Type Safety" : "It's a wrap"}{"JSON, Swift and Type Safety" : "It's a wrap"}
{"JSON, Swift and Type Safety" : "It's a wrap"}
 
Imugi: Compiler made with Python
Imugi: Compiler made with PythonImugi: Compiler made with Python
Imugi: Compiler made with Python
 
Swift와 Objective-C를 함께 쓰는 방법
Swift와 Objective-C를 함께 쓰는 방법Swift와 Objective-C를 함께 쓰는 방법
Swift와 Objective-C를 함께 쓰는 방법
 
Improving Correctness with Types
Improving Correctness with TypesImproving Correctness with Types
Improving Correctness with Types
 
JJUG CCC 2011 Spring
JJUG CCC 2011 SpringJJUG CCC 2011 Spring
JJUG CCC 2011 Spring
 
Leap Ahead with Redis 6.2
Leap Ahead with Redis 6.2Leap Ahead with Redis 6.2
Leap Ahead with Redis 6.2
 
Wrapper class
Wrapper classWrapper class
Wrapper class
 
Improving Correctness With Type - Goto Con Berlin
Improving Correctness With Type - Goto Con BerlinImproving Correctness With Type - Goto Con Berlin
Improving Correctness With Type - Goto Con Berlin
 
What's in Kotlin for us - Alexandre Greschon, MyHeritage
What's in Kotlin for us - Alexandre Greschon, MyHeritageWhat's in Kotlin for us - Alexandre Greschon, MyHeritage
What's in Kotlin for us - Alexandre Greschon, MyHeritage
 
Kotlin coroutines and spring framework
Kotlin coroutines and spring frameworkKotlin coroutines and spring framework
Kotlin coroutines and spring framework
 

Mehr von Arawn Park

Mehr von Arawn Park (16)

우린 같은 곳을 바라 보고 있나요?
우린 같은 곳을 바라 보고 있나요?우린 같은 곳을 바라 보고 있나요?
우린 같은 곳을 바라 보고 있나요?
 
코틀린 멀티플랫폼, 미지와의 조우
코틀린 멀티플랫폼, 미지와의 조우코틀린 멀티플랫폼, 미지와의 조우
코틀린 멀티플랫폼, 미지와의 조우
 
kotlinx.serialization
kotlinx.serializationkotlinx.serialization
kotlinx.serialization
 
우아한 모노리스
우아한 모노리스우아한 모노리스
우아한 모노리스
 
잘 키운 모노리스 하나 열 마이크로서비스 안 부럽다
잘 키운 모노리스 하나 열 마이크로서비스 안 부럽다잘 키운 모노리스 하나 열 마이크로서비스 안 부럽다
잘 키운 모노리스 하나 열 마이크로서비스 안 부럽다
 
점진적인 레거시 웹 애플리케이션 개선 과정
점진적인 레거시 웹 애플리케이션 개선 과정점진적인 레거시 웹 애플리케이션 개선 과정
점진적인 레거시 웹 애플리케이션 개선 과정
 
Introduction to Kotlin
Introduction to KotlinIntroduction to Kotlin
Introduction to Kotlin
 
Reactive Web - Servlet & Async, Non-blocking I/O
Reactive Web - Servlet & Async, Non-blocking I/OReactive Web - Servlet & Async, Non-blocking I/O
Reactive Web - Servlet & Async, Non-blocking I/O
 
Spring framework 4.x
Spring framework 4.xSpring framework 4.x
Spring framework 4.x
 
씹고 뜯고 맛보고 즐기는 스트림 API
씹고 뜯고 맛보고 즐기는 스트림 API씹고 뜯고 맛보고 즐기는 스트림 API
씹고 뜯고 맛보고 즐기는 스트림 API
 
Spring framework 3.2 > 4.0 — themes and trends
Spring framework 3.2 > 4.0 — themes and trendsSpring framework 3.2 > 4.0 — themes and trends
Spring framework 3.2 > 4.0 — themes and trends
 
overview of spring4
overview of spring4overview of spring4
overview of spring4
 
조금 더 좋은 개발자가 된다는 것( 부제: 컨퍼런스의 발표자가 된다는 것 )
조금 더 좋은 개발자가 된다는 것( 부제: 컨퍼런스의 발표자가 된다는 것 )조금 더 좋은 개발자가 된다는 것( 부제: 컨퍼런스의 발표자가 된다는 것 )
조금 더 좋은 개발자가 된다는 것( 부제: 컨퍼런스의 발표자가 된다는 것 )
 
Resource Handling in Spring MVC
Resource Handling in Spring MVCResource Handling in Spring MVC
Resource Handling in Spring MVC
 
[Spring Camp 2013] Java Configuration 없인 못살아!
[Spring Camp 2013] Java Configuration 없인 못살아![Spring Camp 2013] Java Configuration 없인 못살아!
[Spring Camp 2013] Java Configuration 없인 못살아!
 
Vagrant와 chef로 개발서버 구축 자동화하기
Vagrant와 chef로 개발서버 구축 자동화하기Vagrant와 chef로 개발서버 구축 자동화하기
Vagrant와 chef로 개발서버 구축 자동화하기
 

Kürzlich hochgeladen

%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
masabamasaba
 
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Medical / Health Care (+971588192166) Mifepristone and Misoprostol tablets 200mg
 
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
masabamasaba
 
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
masabamasaba
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
masabamasaba
 
%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...
%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...
%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...
masabamasaba
 

Kürzlich hochgeladen (20)

Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
Direct Style Effect Systems -The Print[A] Example- A Comprehension AidDirect Style Effect Systems -The Print[A] Example- A Comprehension Aid
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
 
WSO2Con2024 - From Code To Cloud: Fast Track Your Cloud Native Journey with C...
WSO2Con2024 - From Code To Cloud: Fast Track Your Cloud Native Journey with C...WSO2Con2024 - From Code To Cloud: Fast Track Your Cloud Native Journey with C...
WSO2Con2024 - From Code To Cloud: Fast Track Your Cloud Native Journey with C...
 
%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand
 
8257 interfacing 2 in microprocessor for btech students
8257 interfacing 2 in microprocessor for btech students8257 interfacing 2 in microprocessor for btech students
8257 interfacing 2 in microprocessor for btech students
 
%in Harare+277-882-255-28 abortion pills for sale in Harare
%in Harare+277-882-255-28 abortion pills for sale in Harare%in Harare+277-882-255-28 abortion pills for sale in Harare
%in Harare+277-882-255-28 abortion pills for sale in Harare
 
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
 
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
 
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
 
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
 
Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...
Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...
Devoxx UK 2024 - Going serverless with Quarkus, GraalVM native images and AWS...
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
 
Announcing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK SoftwareAnnouncing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK Software
 
%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg
%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg
%in Rustenburg+277-882-255-28 abortion pills for sale in Rustenburg
 
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
 
Architecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the pastArchitecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the past
 
%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...
%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...
%+27788225528 love spells in Boston Psychic Readings, Attraction spells,Bring...
 
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital TransformationWSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
 

#살아있다 #자프링외길12년차 #코프링2개월생존기

  • 1. #살아있다 #자프링외길12년차 #코프링2개월생존기 발 표 자 : 박용권(당근마켓 동네모임 팀) 밋업자료 : 밋업코드 :
  • 3. | 2016년, 첫 만남
  • 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 }
  • 7. | 2021년, 다시 만나다
  • 8. | 지금부터 코드가 무-척 많이 나옵니다
  • 10. | 그만 널 잊으라고 fun from(posts: Array<Post?>): Array<PostDto> { return posts.map({ post -> if (post == null) { throw Error("Post object is null") } if (post.id == null) { throw Error("Id field is null in post object") } PostDto( post.id, post.text, post.author.id, post.createdAt, post.updatedAt ) }).toTypedArray() }
  • 11. | 그만 널 잊으라고 : 안전한 호출 연산자: ?. https://kotlinlang.org/docs/null-safety.html#safe-calls ! fun from(posts: Array<Post?>): Array<PostDto> { return posts.map({ post -> if (post?.id == null) { throw Error("Post object or id field is null") } PostDto( post.id, post.text, post.author.id, post.createdAt, post.updatedAt ) }).toTypedArray() }
  • 12. | 그만 널 잊으라고 : 엘비스 연산자: ?: https://kotlinlang.org/docs/null-safety.html#elvis-operator ! fun from(posts: Array<Post?>): Array<PostDto> { return posts.map({ post -> PostDto( post?.id ?: throw Error("Post object or id field is null"), post.text, post.author.id, post.createdAt, post.updatedAt ) }).toTypedArray() }
  • 13. | 그만 널 잊으라고 : 널 아님 단언: !! https://kotlinlang.org/docs/null-safety.html#the-operator ! fun from(posts: Array<Post?>): Array<PostDto> { return posts.map({ post -> PostDto( post?.id!!, post.text, post.author.id, post.createdAt, post.updatedAt ) }).toTypedArray() }
  • 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 } } }
  • 34. | 품행제로 : 린트lint와 코딩 컨벤션Coding conventions https://kotlinlang.org/docs/coding-conventions.html !
  • 36. | 뭣이 중헌디 @SpringBootConfiguration @EnableJdbcRepositories class DataConfigurations : AbstractJdbcConfiguration() { @Bean fun dataSource(environment: Environment): DataSource { val type = environment.getRequiredProperty( "type", EmbeddedDatabaseType::class.java ) val scriptEncoding = environment.getProperty("script-encoding", "utf-8") val separator = environment.getProperty("separator", ";") val scripts = environment.getProperty("scripts", List::class.java) ?.map { it.toString() } ?.toTypedArray() val builder = EmbeddedDatabaseBuilder() builder.setType(type) builder.setScriptEncoding(scriptEncoding) builder.setSeparator(separator) builder.addScripts(*scripts ?: emptyArray()) return builder.build() } }
  • 37. | 뭣이 중헌디 : 스코프 함수로 가독성 높이기 @SpringBootConfiguration @EnableJdbcRepositories class DataConfigurations : AbstractJdbcConfiguration() { @Bean fun dataSource(environment: Environment): DataSource { val type = environment.getRequiredProperty( "type", EmbeddedDatabaseType::class.java ) val scriptEncoding = environment.getProperty("script-encoding", "utf-8") val separator = environment.getProperty("separator", ";") val scripts = environment.getProperty("scripts", List::class.java) ?.map { it.toString() } ?.toTypedArray() return EmbeddedDatabaseBuilder().apply { setType(type) setScriptEncoding(scriptEncoding) setSeparator(separator) addScripts(*scripts ?: emptyArray()) }.build() } }
  • 38. | 뭣이 중헌디 : 스코프 함수Scope Function val arawn = Traveler("arawn", "Seoul", 1000) arawn.moveTo("New York") arawn.pay(10) val grizz = Traveler("Grizz", "Seoul", 1000).let { it.moveTo("London") it.pay(10) } val dan = Traveler("Dan").apply { moveTo("Vancouver") earn(50) } travelerRepository.findByName("Root")?.run { moveTo("Firenze") }
  • 39. | 뭣이 중헌디 : 편하고, 안전하게 꺼내쓰기 with Kotlin support @SpringBootConfiguration @EnableJdbcRepositories class DataConfigurations : AbstractJdbcConfiguration() { @Bean fun dataSource(environment: Environment): DataSource { val type = environment.getRequiredProperty( "type", EmbeddedDatabaseType::class.java ) val scriptEncoding = environment.getProperty("script-encoding", "utf-8") val separator = environment.getProperty("separator", ";") val scripts = environment.getProperty("scripts", List::class.java) ?.map { it.toString() } ?.toTypedArray() return EmbeddedDatabaseBuilder().apply { setType(type) setScriptEncoding(scriptEncoding) setSeparator(separator) addScripts(*scripts ?: emptyArray()) }.build() } }
  • 40. | 뭣이 중헌디 : 편하고, 안전하게 꺼내쓰기 with Kotlin support @SpringBootConfiguration @EnableJdbcRepositories class DataConfigurations : AbstractJdbcConfiguration() { @Bean fun dataSource(environment: Environment): DataSource { val type = environment.getRequiredProperty<EmbeddedDatabaseType>("type") val scriptEncoding = environment.get("scriptEncoding") ?: "utf-8" val separator = environment.get("separator") ?: ";" val scripts = environment.getProperty<Array<String>>("scripts") return EmbeddedDatabaseBuilder().apply { setType(type) setScriptEncoding(scriptEncoding) setSeparator(separator) addScripts(*scripts ?: emptyArray()) }.build() } }
  • 41. | @SpringBootConfiguration @EnableJdbcRepositories class DataConfigurations : AbstractJdbcConfiguration() { @Bean fun dataSource(environment: Environment): DataSource { val type = environment.getRequiredProperty<EmbeddedDatabaseType>("type") val scriptEncoding = environment.get("scriptEncoding") ?: "utf-8" val separator = environment.get("separator") ?: ";" val scripts = environment.getProperty<Array<String>>("scripts") return EmbeddedDatabaseBuilder().apply { setType(type) setScriptEncoding(scriptEncoding) setSeparator(separator) addScripts(*scripts ?: emptyArray()) }.build() } } 뭣이 중헌디 : 확장함수Extension functions /** * Extension for [PropertyResolver.getProperty] providing * a `getProperty<Foo>(...)` variant returning a nullable `Foo`. */ inline fun <reified T> PropertyResolver.getProperty(key: String) : T? = getProperty(key, T::class.java) 스프링은 PropertyResolver를 확장해 몇가지 편의 기능을 코틀린의 확장 함수로 제공한다 Environment 는 PropertyResolver 를 상속한다 https://kotlinlang.org/docs/extensions.html#extension-functions !
  • 42. | 뭣이 중헌디이 class SpringJdbcPostQueryRepository( val namedParameterJdbcOperations: NamedParameterJdbcOperations ) : PostQueryRepository { val jdbcOperations: JdbcOperations get() = namedParameterJdbcOperations.jdbcOperations override fun findByTopic(topic: Topic): Array<Post> { return jdbcOperations.query(SQL_findByTopic, RowMapper(){ 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") ) }, arrayOf(topic.id)).toTypedArray() } companion object { const val SQL_findByTopic = "..." } }
  • 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(); } }
  • 68. | 코틀린과 라우터 DSL https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#router-dsl ! import org.springframework.web.servlet.function.router @Configuration class FunctionalEndpoints { @Bean fun personRouter(personHandler: PersonHandler) = router { "/person".nest { accept(APPLICATION_JSON).nest { GET("/{id}", personHandler::getPerson) GET(personHandler::listPeople) } POST(personHandler::createPerson) } } @Bean fun personHandler() = 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 ! 수십개의 자동화된 구성 전략으로 다양하게 애플리케이션을 구성한다
  • 74. | 질문, 하나... 명확하게 구성하기 위해서는 어떻게 해야할까요?
  • 75. | 질문, 둘... 스프링 애플리케이션 구성을 효과적으로 하려면 어떻게 해야할까요?
  • 76. | 스프링 푸Spring Fu, 스프링부트를 구성하는 DSL https://github.com/spring-projects-experimental/spring-fu ! import org.springframework.fu.kofu.webApplication import org.springframework.fu.kofu.webmvc.webMvc val myApplication = webApplication { beans { bean<MyRepository>() bean { MyService(ref()) } webMvc { router { val service = ref<MyService>() GET("/") { ServerResponse.ok().body(service.say()) } } converters { string() } } } } fun main(args: Array<String>) { myApplication.run(args) }
  • 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