O slideshow foi denunciado.
Seu SlideShare está sendo baixado. ×

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

Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio

Confira estes a seguir

1 de 82 Anúncio

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

Baixar para ler offline

자프링(자바 + 스프링) 외길 12년차 서버 개발자가 코프링(코틀린 + 스프링)을 만난 후 코틀린의 특징과 스프링의 코틀린 지원을 알아가며 코프링 월드에서 살아남은 이야기…

코드 저장소: https://github.com/arawn/kotlin-support-in-spring

자프링(자바 + 스프링) 외길 12년차 서버 개발자가 코프링(코틀린 + 스프링)을 만난 후 코틀린의 특징과 스프링의 코틀린 지원을 알아가며 코프링 월드에서 살아남은 이야기…

코드 저장소: https://github.com/arawn/kotlin-support-in-spring

Anúncio
Anúncio

Mais Conteúdo rRelacionado

Diapositivos para si (20)

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

Anúncio

Mais de Arawn Park (17)

Mais recentes (20)

Anúncio

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

  1. 1. #살아있다 #자프링외길12년차 #코프링2개월생존기 발 표 자 : 박용권(당근마켓 동네모임 팀) 밋업자료 : 밋업코드 :
  2. 2. #살아있다 #자프링외길12년차 박 용 권 #코프링2개월생존기
  3. 3. | 2016년, 첫 만남
  4. 4. | 코틀린의 철학 Kotlin in Action Dmitry Jemerov and Svetlana Isakova https://www.manning.com/books/kotlin-in-action ! 코틀린은 자바와의 상호운용성에 초점을 맞춘 실용적이고 간결하며 안전한 언어이다.
  5. 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. 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. 7. | 2021년, 다시 만나다
  8. 8. | 지금부터 코드가 무-척 많이 나옵니다
  9. 9. #이것은 #자바인가 #코틀린인가
  10. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 34. | 품행제로 : 린트lint와 코딩 컨벤션Coding conventions https://kotlinlang.org/docs/coding-conventions.html !
  35. 35. #표현력 #풍부한 #아이
  36. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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") } } } }
  51. 51. #기다려 #먹어
  52. 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. 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. 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. 55. | 번개같이 비동기 프로그래밍 훑어보기 GithubInfo getGithubInfo(String username, String accessToken) 깃헙 사용자 정보외 조직, 저자소 정보를 조회하기 사용자 정보로 조직 정보 조회 사용자 정보 조회 사용자 정보로 저장소 정보 조회 결과
  56. 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. 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. 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()) }
  59. 59. One more thing...
  60. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 74. | 질문, 하나... 명확하게 구성하기 위해서는 어떻게 해야할까요?
  75. 75. | 질문, 둘... 스프링 애플리케이션 구성을 효과적으로 하려면 어떻게 해야할까요?
  76. 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. 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. 78. | 40% 빠른 우싸인 푸 kofu auto-configuration 0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 스프링부트 기반 웹 애플리케이션 기동 시간
  79. 79. | 애플리케이션 구성 방법 비교 Spring Fu ✔ 명시적인 선언 방식 ✔ 함수형 스타일 구성 ✔ 람다 기반으로 동작 ✔ 실험단계 { Spring Boot ✔ 관 기반의 자동화된 구성 ✔ 애노테이션 기반 구성 ✔ 리플렉션 기반으로 동작 ✔ 운영단계 {
  80. 80. Spring fu is not better or worse that auto-config, it is different.
  81. 81. end { '#살아있다' }
  82. 82. | 참고자료 ✔ Spring Framework Documentation ✔ Spring Fu project ✔ The State of Kotlin Support in Spring ✔ The evolution of Spring Fu ✔ SpringMockK project

×