Upgrade to Pro — share decks privately, control downloads, hide ads and more …

これならできる!Kotlin・Spring・DDDを活用したAll in oneのマイクロサー...

これならできる!Kotlin・Spring・DDDを活用したAll in oneのマイクロサービス開発術

Avatar for 株式会社出前館

株式会社出前館

June 08, 2025
Tweet

More Decks by 株式会社出前館

Other Decks in Technology

Transcript

  1. build.gradle.ktsイメージ plugins {...} allprojects { project } subprojects { project

    } project(":app-api") {...} project(":app-batch") {...} project(":app-subscriber") {...} project(":usecase") {...} project(":infra") {...} project(":domain-model") {...} project(":test-common") {...} project(":test-common-spring") {...} project(":test-gatling") {...}
  2. @Data data class Shop( val shopId: ShopId, val shopCode: String,

    val shopName: String, val shopImageUrl: String, val shopCategory: String, ) { init { if (shopName.isBlank() || shopImageUrl.isBlank() || shopCategory.isBlank()) { throw DomainModelException(“ShopCopy items must not be Empty.”) } } }
  3. data class Shop( val shopId: ShopId, val shopName: String, val

    shopImageUrl: String, val shopCategory: String, ) { companion object { fun fromApiResponse( shopId: ShopId, shopName: String, shopImageUrl: String, shopCategory: String, ): Shop { return Shop( shopId = shopId, shopName = shopName, shopImageUrl = shopImageUrl, shopCategory = shopCategory, ) } } }
  4. data class Shop( val shopId: ShopId, val shopCode: String, val

    shopName: String, val shopImageUrl: String, val shopCategory: String, ) { init { if (shopName.isBlank() || shopImageUrl.isBlank() || shopCategory.isBlank()) { throw DomainModelException(“ShopCopy items must not be Empty.”) } } }
  5. @Component class AdShopReportGetUseCase( private val hogeMysqlRepository: hogeMysqlRepository, private val fugaExternalApiRepository:

    FugaExternalApiRepository, ) { fun execute(shopIds: List<ShopId>): List<AdShopReport> { val shops = hogeMysqlRepository.getShops( shopIds = shopIds, isExcludeDeleted = true, ) ?: run { throw InvalidInputParameterException( message = "invalid input parameter: shopIds=$shopIds", ) } val reports = fugaExternalApiRepository.getReport( shopIds = shopIds, ) // add more buisiness logic if needed val adShopReports = AdShopReport.listOf( shops = shops, reports = reports, ) return adShopReports } }
  6. • • • • mybatisGenerate // build.gradle.kts val mybatisConfig: Configuration

    by configurations.creating dependencies { mybatisConfig(“org.mybatis.generator:mybatis-generator-core:some-version") mybatisConfig("mysql:mysql-connector-java:some-version") } tasks.register<JavaExec>("mybatisGenerate") { classpath = mybatisConfig mainClass.set("org.mybatis.generator.api.ShellRunner") args = listOf( "-configfile", "path/to/config.xml", "-overwrite" ) }
  7. // define HTTP Interface interface HogeExternalApiClient { @GetExchange("/ad-shops/{ad_shop_id}") fun getAdShops(

    @RequestHeader("Authorization") token: String, @PathVariable("ad_shop_id") adShopId: String, ): ResponseEntity<GetAdShopsResponseData> } // call HTTP Interface @Repository class HogeExternalApiRepository( private val hogeExternalApiClient: HogeExternalApiClient, ) { fun getAdShops(token: String, adShopId: String): List<AdShop> { val response = hogeExternalApiClient.getAdShops(token, adShopId) } }
  8. @Bean fun hogeExternalApiClient(): HogeExternalApiClient { val httpClient = HttpClients .custom()

    .setConnectionManager(PoolingHttpClientConnectionManagerBuilder .create() // setting for Apache connection manager .build()) // setting for Apache Http client .build() val restClient = RestClient.builder() .requestFactory(HttpComponentsClientHttpRequestFactory(httpClient)) .baseUrl("http://localhost:8080") // setting for RestClient .build() return HttpServiceProxyFactory .builderFor(RestClientAdapter.create(restClient)) .build() .createClient(HogeApiClient::class.java) }
  9. • • • • • • • • • •

    • • • openApiGenerate
  10. @Component @ConditionalOnProperty(prefix = "batch", name = ["name"], havingValue = BatchName.HOGE)

    class HogeBatch( private val hogeUseCase: HogeUseCase, ) : ApplicationRunner { override fun run(args: ApplicationArguments) { try { logger.info { "${this.javaClass.simpleName} started." } val start = System.currentTimeMillis() hogeUseCase.execute() val elapsed = System.currentTimeMillis() - start logger.info { "${this.javaClass.simpleName} ended. (${elapsed}ms)" } } catch (ex: Exception) { logger.error(ex) { "${this.javaClass.simpleName} failed." } throw ex } } }
  11. // test class CalculatorTest : StringSpec({ "addition should return correct

    result" { Calculator().add(2, 3) shouldBe 5 } "subtraction should return correct result" { Calculator().subtract(5, 3) shouldBe 2 } }) // test target class Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b }
  12. // test using Should clause class CalculatorTest : ShouldSpec({ should("return

    the correct result when adding two numbers") { Calculator().add(2, 3) shouldBe 5 } should("return the correct result when subtracting two numbers") { Calculator().subtract(5, 3) shouldBe 2 } }) // test target class Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b }
  13. // test like Given-When-Then class CalculatorTest : BehaviorSpec({ given("a calculator")

    { `when`("adding two numbers") { then("it should return the correct result") { Calculator().add(2, 3) shouldBe 5 } } `when`("subtracting two numbers") { then("it should return the correct result") { Calculator().subtract(5, 3) shouldBe 2 } } } }) // test target class Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b }
  14. class CalculatorTest : FunSpec({ data class TestPattern( val a: Int,

    val b: Int, val expectedAddition: Int, val expectedSubtraction: Int ) context("Calculator operations") { withData( TestPattern(a = 2, b = 3, expectedAddition = 5, expectedSubtraction = -1), TestPattern(a = 10, b = 5, expectedAddition = 15, expectedSubtraction = 5), TestPattern(a = 0, b = 0, expectedAddition = 0, expectedSubtraction = 0) ) { testCase -> val calculator = Calculator() calculator.add(testCase.a, testCase.b) shouldBe testCase.expectedAddition calculator.subtract(testCase.a, testCase.b) shouldBe testCase.expectedSubtraction } } })
  15. # docker-compose.yml sample-mysql: image: mysql:8.0.28-oracle ports: - “3306:3306” environment: MYSQL_DATABASE:

    sample MYSQL_USER: user001 volumes: - ./sample-mysql:/docker-entrypoint-initdb.d sample-wiremock: image: wiremock/wiremock:3.1.0 ports: - "18180:18180" volumes: - ./sample-wiremock:/home/wiremock command: - "--port=18180" class HogeTest : StringSpec({ val composeContainer = DockerComposeContainer(File(“../tool/docker-compose.yml”)) .withExposedService(“sample-mysql”, 3306, Wait.forListeningPort()) .withExposedService(“sample-wiremock”, 18180, Wait.forListeningPort()) .apply { start() } “sample test" { 1 + 1 shouldBe 2 } })
  16. // libs.versions.toml [versions] gatling = "3.13.5” [plugins] gatling = {

    id = "io.gatling.gradle", version.ref = "gatling" } [libraries] gatling-charts-highcharts = { module = "io.gatling.highcharts:gatling-charts-highcharts", version.ref = "gatling" } gatling-app = { module = "io.gatling:gatling-app", version.ref = "gatling" } // build.gradle.kts project(":test-gatling") { apply(plugin = rootProject.libs.plugins.gatling.get().pluginId) dependencies { gatling(rootProject.libs.gatling.charts.highcharts) gatling(rootProject.libs.gatling.app) } gatling { System.getenv().forEach { environment.put(it.key, it.value) // pass environment variables to gatling app } } }
  17. class HogeApiSimulations : Simulation() { val scenario1 = scenario(“scenario1”) .exec(http(“hoge-api-request”)

    .post(“/v1/hoges”) .body(StringBody(“”“{”key1“: ”XXX“, “key2”: 999}“”“.trimMargin())) .check(status().shouldBe(200)) ) val httpProtocol = http.baseUrl(”https://hoge.sample.com“) .header(”content-type“, ”application/json“) .header(”X-Api-Key“, System.getenv(”X_API_KEY“)) init { setUp( scenario1.injectOpen(rampUsersPerSec(10.0).to(100.0).during(60)) .andThen(scenario1.injectOpen(constantUsersPerSec(100.0).during(300))) ).protocols(httpProtocol) } }
  18. # gradle/libs.versions.toml [versions] javaLanguageVersion = “21” springBoot = “3.4.0” springRetry

    = "2.0.11" [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } [libraries] spring-retry = { module = "org.springframework.retry:spring-retry", version.ref = "springRetry" } spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springBoot" } spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } [bundles] spring-boot-setting = [ "spring-boot-starter", "spring-boot-starter-web”, "spring-retry" ]
  19. # build.gradle.kts project(":app-api") { kotlin { jvmToolchain { this.languageVersion.set(JavaLanguageVersion.of(rootProject.libs.versions.javaLanguageVersion.get())) }

    apply(plugin = rootProject.libs.plugins.spring.boot.get().pluginId) dependencies { implementation(rootProject.libs.bundles.spring.boot.setting) }
  20. // ControllerExtensions.kt fun HogesPostRequest.toDomainModel(): Hoge { return HogeModel( hogeId =

    HogeId(this.hogeId), fuga = Fuga.of(this.fuga), ) } // HogeController.kt @RestController class HogeController(private val hogeUseCase: HogeUseCase) : HogesApi { override fun hogesPost(hogesPostRequest: HogesPostRequest): ResponseEntity<HogesPostResponse> { hogeUseCase.create(hogesPostRequest.toDomainModel()) return ResponseEntity.ok(HogesPostResponse(code = "OK")) } }
  21. // build.gradle.kts tasks.register<Exec>("dockerComposeUp") { commandLine("docker-compose", "-f", "./docker-compose-test.yml", "up", "-d") }

    tasks.register<Exec>("dockerComposeDown") { commandLine("docker-compose", "-f", "./docker-compose-test.yml", "down", "--rmi", "—volumes") } tasks.named<Test>("test") { dependsOn("dockerComposeUp") finalizedBy("dockerComposeDown") }
  22. // libs.versions.toml [libraries] opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api" }

    // build.gradle.kts project(":sample") { dependencies { implementation(rootProject.libs.opentelemetry.api) } } //OpenTelemetryConfig.kt @Configuration class OpenTelemetryConfig { @Bean fun openTelemetry(): OpenTelemetry { return GlobalOpenTelemetry.get() } } // execute command java -javaagent:/path/to/the/opentelemetry-javaagent.jar –jar sample.jar