Server-Side Kotlin by Ktor

5155c9bc5b30d9e2a8fd677695887861?s=47 kaonash
August 24, 2019

Server-Side Kotlin by Ktor

5155c9bc5b30d9e2a8fd677695887861?s=128

kaonash

August 24, 2019
Tweet

Transcript

  1. ɹɹɹɹ Server-Side Kotlin By UPSIDERɹਗ਼ਫݦ

  2. SELF INTRODUCTION • ਗ਼ਫݦ(@kaonash_) • Lead Software Engineer @ UPSIDER

    • Ҡॅ͍ͨ͠ܥΤϯδχΞʢ2022೥ɺ௕໺ʹҠॅ༧ఆʣ • Love: ୌ / Kitkat / Kotlin
  3. SELF INTRODUCTION UZABASEɿاۀɾۀք෼ੳϓϥοτϑΥʔϜͷ։ൃɻServer-side Javaɻ ↓ NewsPicksʢࣾ಺స੶ʣɿܦࡁχϡʔεΞϓϦͷ։ൃɻServer-side͸JavaɺAndroidΞϓϦ։ൃ ͰKotlinॳମݧɻ ↓ Bizreachɿ৽نࣄۀͱͯ͠ैۀһσʔλϕʔεαʔϏεͷ։ൃɻServer-side Kotlin(Spring

    Boot) ↓ UPSIDERɿ૑ۀҰ೥ͷελʔτΞοϓʹͯB to BͷFinTechϓϩμΫτΛ։ൃɻServer-side Kotlin(Ktor)
  4. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  5. ࠓ೔࿩͞ͳ͍͜ͱ • Ktorͷৄ͍͠࢓૊Έ • ຊ൪ӡ༻ͯ͠Έͨײ૝ʢ·ͩϦϦʔεલͳͷͰɾɾɾʣ

  6. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  7. Server-Side Kotlinͬͯ࢖͑Δͷʁ ɾJavaΛ࠾༻ͨ͠έʔεͱൺ΂ͯ΋શવҧ࿨ײͳ͍ ɹɹɹͱ͍͏͔ਖ਼௚ɺҰ౓Kotlinʹ׳ΕͪΌ͏ͱ΋͏JavaͰॻ͖ͨ͘ͳ͘ͳΔ ɾܕਪ࿦΍֦ுؔ਺ͳͲͷ͓͔͛ͰඇৗʹεοΩϦ͔͚Δ ɾnull-safety࠷ߴ ɾJava੡ͷϥΠϒϥϦ͕࢖͑ΔͨΊɺΤίγεςϜʹ͍ͭͯ΋৺഑ෆཁ ɾͨͩ͠ɺJava੡ϥΠϒϥϦར༻࣌ʹ͸མͱ͕݀͠ɾɾɾ

  8. Java੡ϥΠϒϥϦར༻࣌ͷ஫ҙ఺ ɾJavaͷϝιουΛݺͼग़͢ͱɺ໭Γ஋͸T!ܕ (=T΋͘͠͸T?)ʹͳΔ ɾϥΠϒϥϦଆͰରԠͯ͠ͳ͍ͱnull-safetyͷੈք͕յΕΔ val message = "test" val encrypted

    = StringEncrypter.encrypt(message) // StringEncrypter͕JavaϥΠϒϥϦͩͱ println(encrypted.length) // ͜͜Ͱ͵ΔΆʹͳΔ͔΋
  9. ʮ͔ͤͬ͘Kotlinʹͨ͠ͷʹɾɾɾʯ ײ͸ਖ਼௚ͪΐͬͱ͋Δ

  10. Kotlin੡ϥΠϒϥϦͳΒ҆৺ʂ

  11. Ͱ΋Server-sideͰ࢖͑ΔΑ͏ͳ Kotlin੡ͷϥΠϒϥϦͳΜͯɾɾɾ

  12. ࠓΊͬͪΌ૿͑ͯ·͢ʂ

  13. • Language : Kotlin • WebϑϨʔϜϫʔΫ : Ktor server •

    O/R Mapper : Exposed • DI : Koin • Http client : Ktor client • JSON Parser : Jackson • Testing : JUnit5 / AssertJ / MockK ฐࣾͷٕज़ελοΫ Server ΄΅Pure Kotlin!
  14. Server-side Kotlin͸ʮͰ͖Δʁʯ͔Β ʮԿΛબ΂͹͍͍ʁʯʮͲ͏΍ͬͨΒ͍͍ʁʯ΁

  15. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  16. WHAT IS • JetBrains͕։ൃͨܰ͠ྔɾඇಉظWebϑϨʔϜϫʔΫ • ಡΈํ͸ʮ͚͍ͨʔʯʢ࠷ॳ͸ʮͨ͜ʔʯͩͱࢥͬͯͨʣ • Pure Kotlin •

    PluggableʹඞཁͳػೳͷΈΛ௥Ճ͍͚ͯ͠Δ • 2018೥11݄ʹ1.0͕ϦϦʔεʂʢݱࡏͷ࠷৽͸1.2.3ʣ • Server͚ͩ͡Όͳͯ͘Client΋͋Δ https://ktor.io/clients/index.html
  17. Spring Bootͱͷ˒ͷࠩ ·ͩ·ͩ͜Ε͔Β͚ͩͲɺ͍ͩͿ஫໨͕ू·͖ͬͯͯΔ

  18. ͳͥKtorΛબΜͩͷ͔ʁ

  19. ٕज़બఆΛͨ͠λΠϛϯάɿ2018೥9݄

  20. ީิ • SpringBoot • Jooby • Ktor

  21. SpringBoot • ͍Θͣͱ஌ΕͨJavaքͷσϑΝΫτελϯμʔυɻServer-side KotlinͰ΋࠾༻ࣄྫ͕ଟ͘ଘࡏɻ • KotlinରԠ • લ৬ʢ౰࣌͸μϒϧϫʔΫͩͬͨͷͰݱ৬ʣ΋SpringBootͰServer-Side Kotlin΍ͬͯͨͷͰܦݧ͸͋ Δ

    • શ෦ೖΓͰศརͳͷ͕ͩͦͷ෼ىಈ͕࣌ؒ௕͘ɺͦͷؒʹTwitterͱ͔Twitterͱ͔Twitterͱ͔Λݟͨ Γͯ͠͠·͏ • Ξϊςʔγϣϯ஍ࠈ͕͋·Γ޷͖ʹͳΕͳ͍ɻɻɻʢݸਓͷײ૝Ͱ͢ʣ • ΫϦʔϯΞʔΩςΫνϟΛݕ౼͍ͯͨͨ͠Ίɺશମ͕ΨοπϦSpringʹґଘ͢Δͷ͕ͪΐͬͱ໰୊
  22. ࣮ࡍͷΞʔΩςΫνϟ ΫϦʔϯΞʔΩςΫνϟͷߟ͑Ͱ͸WebϑϨʔϜϫʔΫ͸ʮৄࡉʯͰ͋ΓϏδωεϩ δοΫ͔Β੾Γ཭͢΂͖΋ͷɻ Ͱ΋SpringBootͩͱDI΍Β@transactional΍ΒͰશମ͕ΨοπϦSpringʹґଘͯ͠͠·͏ɻ

  23. Jooby • ܰྔߴ଎ɺPluggableͳJava੡ϚΠΫϩϑϨʔϜϫʔΫɻ ࢥ૝͕Ktorͱ͔ͳΓ͍ۙɻ • KotlinରԠ • ଞࣾͷ࠾༻ࣄྫ΋͋Δ • ࠷ॳ͸ୈҰީิͩͬͨ

  24. Ktor • ٕज़બఆ౰࣌ͷόʔδϣϯ͸0.9.5 • JetBrains੡ͱ͍͏͜ͱͰɺࠓޙ΋ϝϯς͞ΕΔͩΖ͏ͱ͍͏҆৺ײ • GithubͷIssuesΛνΣοΫͯ͠Έ͕ͨɺServerଆͰ͸͋·Γେ͖ͳ໰୊͸࢒ͬͯͳ͞ ͦ͏ • ʮ0.9ͷ࣍͸1.0͡ΌͶʁʯˠ݁Ռతʹ͸౰ͨͬͯͨ

    • Jooby΋͍͍͕ɺͲ͏ͤͳΒPure KotlinͷϑϨʔϜϫʔΫΛ࢖ͬͯΈ͍ͨ • ʮૣΊʹ࠾༻͢Ε͹ɺٕज़తʹ஫໨ΛूΊͯ࠾༻ͱ͔Ͱ΋༗རͦ͏ʯͱ͍͏ଧࢉ
  25. ݁࿦ ʮͳΜ͔Ktor࢖ͬͯΈ͍ͨʯ

  26. ࣮ࡍ࢖ͬͯΈͯͷײ૝ • ىಈૣ͍ͷ࠷&ߴʢ.ktϑΝΠϧ͕300ݸ͘Β͍ͷঢ়ଶͰϏϧ υऴΘ͔ͬͯΒ1.5ඵ͘Β͍Ͱىಈ׬ྃʣ • ඞཁͳ΋ͷ͸Ұ௨Γἧ͍ͬͯΔ • ֦ுؔ਺ɾߴ֊ؔ਺͕;ΜͩΜʹ࢖ΘΕ͍ͯΔͷͰɺ׳Ε ͯͳ͍ਓ͸࠷ॳͪΐͬͱށ࿭͏͔΋ʁ •

    શମͱͯ͠͸ͱͯ΋ຬ଍
  27. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  28. ͳʹ͸ͱ΋͋ΕHello World

  29. Hello World! Ktor serverͷىಈํ๏͸͍͔ͭ͋͘Δ͕ɺҰ൪؆୯ͳํ๏ fun main() { embeddedServer(Netty, 8080) {

    install(Routing) { get("/") { call.respondText("Hello World!") } } }.start() }
  30. Hello World! fun main() { embeddedServer(Netty, 8080) { install(Routing) {

    get("/") { call.respondText("Hello World!") } } }.start() } NettyΛར༻ͯ͠ΞϓϦέʔγϣϯΛ࡞੒ port൪߸͸8080 Get methodͰpath"/"͕ݺͼग़͞ΕͨΒ responseͱͯ͠"Hello World!"ͱ͍͏จࣈྻΛฦ٫͢Δ ͜ͷblockͰroutingΛఆٛ
  31. ΑΓॊೈͳىಈํ๏

  32. EngineMain

  33. EngineMain • EngineMain͸Ktor͕༻ҙͨ͠Ϋϥε • ར༻͢ΔEngineʹΑͬͯ4छྨ༻ҙ͞Ε͍ͯΔ • io.ktor.server.cio.EngineMain • io.ktor.server.tomcat.EngineMain •

    io.ktor.server.jetty.EngineMain • io.ktor.server.netty.EngineMain •mainؔ਺Λ͍࣋ͬͯΔͷͰɺαʔόʔىಈ࣌͸͜ͷϝιουΛ࣮ߦ •ىಈ࣌ʹࣗಈతʹapplication.confͱ͍͏ઃఆϑΝΠϧΛಡΈࠐΈɺͦͷઃఆ ಺༰ʹΑͬͯىಈ࣌ͷॲཧΛܾఆ
  34. EngineMain • ར༻͢Δػೳ͸ɺApplicationΫϥεͷ֦ுؔ਺ʹͯఆٛ͢Δ fun Application.module() { install(Routing) { get("/") {

    call.respondText("Hello World!") } } } Application.kt ͜ͷϝιου໊͸ͳΜͰ΋͍͍ installϝιουΛ௥Ճ͍ͯ͘͜͠ͱͰɺඞཁͳػೳΛ޷͖ʹ૊ΈࠐΊΔ
  35. EngineMain • ઃఆϑΝΠϧapplication.confʹɺىಈͷઃఆ৘ใΛهड़ ktor { deployment { port = 8080

    } application { modules = [ ApplicationKt.module ] } } application.conf (ύοέʔδ໊.)Ϋϥε໊(ϑΝΠϧ໊ͷ຤ඌʹKtΛ͚ͭΔ).֦ுؔ਺໊
  36. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  37. Routing • RoutingΛinstall͢Δ͜ͱͰɺrouteొ࿥͕Ͱ͖Δ • call.parameters͔ΒϦΫΤετύϥϝʔλ΍ύεύϥϝʔλ ΛऔಘՄೳʢͨͩ͠ܕ͸͢΂ͯStringʹͳΔʣ fun Application.module() { install(Routing)

    { get("/users/{userId}") { // ܕ͸ඞͣStringͳͷͰඞཁʹԠͯ͡Ωϟετ͢Δ val userId = call.parameters["userId"].toLong() } } }
  38. ΋ͬͱλΠϓηʔϑʹ ύϥϝʔλʔΛѻ͍͍ͨ

  39. Locations

  40. Locations(※ݱࡏ͸·ͩexperimental) • ύϥϝʔλΛλΠϓηʔϑʹѻ͑Δ • Int, Long, Float, Double, Boolean, String,

    enum͕࢖༻Մೳ @Location("/users/{userId}") data class UserLocation(val userId: Long) fun Application.module() { install(Locations) install(Routing) { get<UserLocation> { param -> // param͸UserLocationܕʹͳΔͷͰɺuserIdΛͦͷ··LongܕͰऔಘͰ͖Δ val userId = param.userId } } }
  41. Locationͷωετ΋Ͱ͖Δ @Location("/users/{userId}") data class UserLocation(val userId: Long) { @Location("/email") data

    class EmailLocation(val userLocation: UserLocation) } • ؔ࿈ͨ͠routeΛ·ͱΊΒΕΔͷ͸ϝϦοτ͕ͩɺ਺͕૿͑ͯ͘ Δͱͪ͝Όͪ͝Όͯ͠Θ͔ΓͮΒ͘ͳΔ • ʮKtorͷRoutingఆٛͷϕετϓϥΫςΟεΛߟ͑ͯΈΔʯʹৄ ͘͠ॻ͖·ͨ͠ʢͪͳΈʹલ৬ͷTechϒϩάʣ
 https://tech.bizreach.co.jp/posts/324/ktor-routing/
  42. ΋͏Ұาલ΁

  43. ϓϦϛςΟϒҎ֎ͷܕ΁ͷࣗಈม׵

  44. ͨͱ͑͹ValueObject • ͜Μͳײ͡ͰɺValueObject΋ࣗಈͰม׵Ͱ͖Δͱศར data class UserId(val userId: Long) @Location("/users/{userId}") data

    class UserLocation(val userId: UserId) fun Application.module() { install(Locations) install(Routing) { get<UserLocation> { param -> val userId = param.userId // UserIdܕͰͦͷ··औಘͰ͖Δʂ } } }
  45. DataConversionΛ࢖͏ͱͰ͖·͢ fun Application.module() { install(DataConversion) { convert<UserId> { decode {

    values, _ -> values.singleOrNull()?.let { UserId(it.toLong()) } } encode { id -> when (id) { is UserId -> listOf(id.value.toString()) else -> throw DataConversionException("Can not convert.") } } } } }
  46. ͿͬͪΌ͚ɺSpringBootͱൺ΂ͯͲ͏ʁ • ֤Controllerʹ͚ͭΔ@RequestMapping ΞϊςʔγϣϯʹrouteΛهࡌ͢Δํࣜ • ਺͕૿͑Δͱʮ͜ͷAPIɺͲ͜ͷ ControllerݺΜͰΔΜ͚ͩͬʯ͕୳ͮ͠ Β͍໰୊ SpringBoot Ktor

    • routeΛ·ͱΊͯఆٛ͢Δํࣜ • LocationsΛ࢖Θͳ͍ͱλΠϓηʔϑʹ ѻ͑ͳͯͭ͘ΒΈ • Locations͸ఆٛͷ࢓ํΛؾΛ͚ͭͳ͍ ͱΊͬͪΌಡΈͮΒ͘ͳΔ Ұ௕Ұ୹ɻݸਓతʹ͸Ktorͷ΍Γํͷ΄͏͕޷͖͕ͩɺ Locationsͷ࢖͍উखΛ΋͏গ͠վળͯ͠΄͍͠
  47. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  48. ೝূॲཧ • ϕʔγοΫೝূ / Digestೝূ / JWT / LDAPೝূ /

    OAuthΛαϙʔτɻ • install(Authentication)͢Ε͹ར༻Մೳʢ௨ৗͷktorҎ֎ʹktor_authΛ dependenciesʹ௥Ճ͢Δ͜ͱ͕ඞཁʣ • Routingఆٛʹͯೝূ͕ඞཁͳrouteʹରͯ͠authenticate{}ϒϩοΫͰғΉͱɺ ೝূ͕௨͍ͬͯΔ৔߹͚ͩॲཧΛ࣮ߦ͢Δ͜ͱ͕Մೳɻ
  49. Routingଆ fun Application.module() { install(Routing) { // ͜͜͸ೝূෆཁ post(“/login") {

    // ϩάΠϯॲཧɻ੒ޭ࣌ʹ͸tokenΛੜ੒ͯ͠ฦ٫ } authenticate { // ͜ͷϒϩοΫ಺͸ೝূ͕௨ͬͯͳ͍ͱ࣮ߦͰ͖ͳ͍ get("/users/{userId}") { // ͜ΕͰϩάΠϯϢʔβʔͷ৘ใΛऔಘͰ͖Δ val user = authentication.principal<LoginUser>()!! ... } } } }
  50. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  51. DIʹ͍ͭͯ • DIࣗମ͸Ktorͷػೳͱͯ͠ఏڙ͞Ε͍ͯͳ͍ͷͰɺ޷͖ͳϥ ΠϒϥϦΛ࢖͍͍ͬͯ • KtorͷެࣜϖʔδʹKodeinΛ࢖ͬͨαϯϓϧ͕͋Δ • Koin͸ࣗલͰKtor༻ʹApplication.installͰ͖ΔFeatureΛఏڙ͠ ͓ͯΓɺ͜ΕΛ࢖͏ͱKtorىಈ࣌ʹࣗಈͰDIίϯςφΛى ಈͯ͘͠ΕΔ

  52. KoinΛ࢖ͬͨྫ fun Application.module() { install(Koin) { modules( module { single

    { UserController() } } ) } install(Routing) { val userController: UserController by inject() get("users") { call.respond(userController.find()) } } } ͜ͷதͰDI͢ΔϞδϡʔϧΛఆ͓ٛͯ͘͠ͱ औಘॲཧΛinject()ʹҠৡ͢Δ͜ͱͰΦϒδΣΫτ͕औಘͰ͖Δʂ
  53. AGENDA • Server-Side Kotlinͷݱঢ় • ٕज़બఆͷϙΠϯτɿͳͥKtorΛબΜͩͷ͔ʁ • ࣮ફKtor • ىಈͷํ๏

    • Routing • ೝূ • DI • Testing
  54. TestEngine • Ktorʹ༻ҙ͞Ε͍ͯΔɺԾ૝తʹαʔόʔΛىಈ͢ΔͨΊͷςετ༻ Engine • ىಈ࣌͸withTestApplication()Λ࢖༻͢Δ • ࣮ࡍͷHTTPϦΫΤετ͸ड͚෇͚ͳ͍͕ɺٙࣅϦΫΤετΛඈ͹͢͜ͱ ʹΑͬͯఆٛͨ͠routeΛݺͼग़͢͜ͱ͕Ͱ͖Δ

  55. TestEngineͷ࢖༻ྫ withTestApplication(Application::module) { handleRequest(HttpMethod.Post, "/login") { addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) setBody(listOf( "email"

    to "test1@test.com", "password" to "loginPass" ).formUrlEncode()) }.apply { assertThat(response.status()).isEqualTo(HttpStatusCode.OK) } } ىಈ͢ΔΞϓϦέʔγϣϯͷϝιουΛࢦఆ Httpϝιουͱpathͷࢦఆ HeaderͱBodyͷࢦఆ handleRequestͷ໭Γ஋TestApplicationCallܕ͕ ϓϩύςΟͱͯ͠responseΛอ͍࣋ͯ͠Δ
  56. MockKͱKoinΛ࢖ͬͨMocking

  57. ͨͱ͑͹ɺ͜͏͍͏ॲཧΛ୯ମͰςετ͢Δʹ͸ʁ install(Routing) { val userController: UserController by inject() @Location("/users/{userId}") data

    class UserLocation(val userId: UserId) get<UserLocation> { call.respond(userController.find(userId)) } } ͍ͭ͜ΛMockԽ͍ͨ͠
  58. MockKͱKoin val userController = mockk<UserController>(relaxed = true) val module =

    module { single { userController } } @Test fun `Return user data when called request with user id`() { val userId = UserId(1) every { userController.find(userId) } returns UserData(1, "ࢁాଠ࿠") withTestApplication(Application::module) { startKoin { modules(module) } handleRequest(HttpMethod.Get, "/users/1").apply { assertThat(response.status()).isEqualTo(HttpStatusCode.OK) … } } } MockΦϒδΣΫτΛ࡞੒͠ɺKoinϞδϡʔϧʹొ࿥ MockΦϒδΣΫτ͕ݺ͹Εͨ࣌ͷڍಈΛఆٛ KoinΛىಈ
  59. ׬ᘳɾɾɾͱࢥ͍͖΍

  60. ౖΒΕͨ

  61. ͳΜͰʁ • Application.moduleͰinstall(Koin)͢Δͱɺىಈ࣌ʹࣗಈతʹstartKoin͕ݺ͹ΕΔ fun Application.module() { install(Koin) { modules( module

    { single { UserController() } } ) } } • TestEngineͰ΋Ծ૝తʹKtorΛىಈ͍ͯ͠ΔͨΊɺಉ͡Α͏ʹstartKoin͕૸Δ • MockΦϒδΣΫτΛΠϯδΣΫτ͢ΔͨΊʹstartKoin͢ΔͱೋॏىಈʹͳΔ
  62. ͱ͍͏͜ͱͰɺ1ߦ௥Ճ val userController = mockk<UserController>(relaxed = true) val module =

    module { single { userController } } @Test fun `Return user data when called request with user id`() { val userId = UserId(1) every { userController.find(userId) } returns UserData(1, "ࢁాଠ࿠") withTestApplication(Application::module) { stopKoin() startKoin { modules(module) } handleRequest(HttpMethod.Get, "/users/1").apply { assertThat(response.status()).isEqualTo(HttpStatusCode.OK) … } } }
  63. ࠶࣮ߦ

  64. Let’s enjoy Server-Side Kotlin and Ktor!!

  65. ࠷ޙʹએ఻

  66. WE ARE HIRING!! • Server side: Kotlin(Ktor) • Front: Nuxt.js,

    Typescript • Backend: Go • ࣄۀ಺༰ɿ๏ਓ޲͚VISAΧʔυΛ࢖ͬͨBtoBαʔϏεʢৄࡉ͸ۙ೔ެ։ʣ • ෭ۀ׻ܴ(Քಇि2ఔ౓ʙOKʣ • ϑϧϦϞʔτOKʢΦϑΟε͸࿡ຊ໦ʣ • ͪΐͬͱͰ΋ؾʹͳΔਓ͸࠙਌ձɺ΋͘͠͸Twitterʹͯ@kaonash_·Ͱʂ
  67. Thank you!!