From d738f3a69e3ea028b9f846d8587c998b89ebb54b Mon Sep 17 00:00:00 2001 From: junhokim Date: Sun, 12 Oct 2025 01:54:28 +0900 Subject: [PATCH] =?UTF-8?q?Spring=20Batch=EC=99=80=20Es=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 5 +- .../kotlin/com/retrip/map/MapApplication.kt | 2 +- .../in/service/LocationIndexService.kt | 17 +++++++ .../in/usecase/LocationIndexUseCase.kt | 7 +++ .../repository/LocationElasticRepository.kt | 11 ++++ .../out/repository/LocationQueryRepository.kt | 3 ++ .../com/retrip/map/domain/entity/Location.kt | 2 + .../adapter/in/batch/DestinationReader.kt | 14 ----- .../infra/adapter/in/batch/MapBatchConfig.kt | 51 +++++++++++++++++++ .../infra/adapter/in/batch/MapProcessor.kt | 15 ++++++ .../map/infra/adapter/in/batch/MapReader.kt | 18 +++++++ .../map/infra/adapter/in/batch/MapWriter.kt | 17 +++++++ .../mysql/query/LocationQuerydslRepository.kt | 12 +++++ .../elasticsearch/entity/LocationDocument.kt | 49 ++++++++++++++++++ .../map/infra/config/ElasticsearchConfig.kt | 40 +++++++++++++++ src/main/resources/application.properties | 1 - src/main/resources/application.yml | 23 +++++++++ 17 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/retrip/map/application/in/service/LocationIndexService.kt create mode 100644 src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt create mode 100644 src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt delete mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/in/batch/DestinationReader.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt create mode 100644 src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/build.gradle.kts b/build.gradle.kts index 2611549..4354866 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,14 +35,15 @@ dependencies { implementation("org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.5") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5") implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta") + kapt("com.querydsl:querydsl-apt:5.1.0:jakarta") // annotationProcessor → kapt로 변경 implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("com.h2database:h2") + runtimeOnly("com.mysql:mysql-connector-j") compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") - + testImplementation("org.springframework.batch:spring-batch-test") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/src/main/kotlin/com/retrip/map/MapApplication.kt b/src/main/kotlin/com/retrip/map/MapApplication.kt index a740071..697ae39 100644 --- a/src/main/kotlin/com/retrip/map/MapApplication.kt +++ b/src/main/kotlin/com/retrip/map/MapApplication.kt @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class MapApplication +class MapApplication() fun main(args: Array) { runApplication(*args) diff --git a/src/main/kotlin/com/retrip/map/application/in/service/LocationIndexService.kt b/src/main/kotlin/com/retrip/map/application/in/service/LocationIndexService.kt new file mode 100644 index 0000000..9be94d7 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/in/service/LocationIndexService.kt @@ -0,0 +1,17 @@ +package com.retrip.map.application.`in`.service + +import com.retrip.map.application.`in`.usecase.LocationIndexUseCase +import com.retrip.map.application.out.repository.LocationElasticRepository +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class LocationIndexService( + private val locationElasticRepository: LocationElasticRepository +) : LocationIndexUseCase { + override fun indexLocationDocuments(documents: List?) { + documents?.let { locationElasticRepository.saveAll(it) } + } +} diff --git a/src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt new file mode 100644 index 0000000..d5f43c4 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/in/usecase/LocationIndexUseCase.kt @@ -0,0 +1,7 @@ +package com.retrip.map.application.`in`.usecase + +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument + +interface LocationIndexUseCase { + fun indexLocationDocuments(documents: List?) +} diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt new file mode 100644 index 0000000..43abbf3 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationElasticRepository.kt @@ -0,0 +1,11 @@ +package com.retrip.map.application.out.repository + +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface LocationElasticRepository: ElasticsearchRepository { + +} diff --git a/src/main/kotlin/com/retrip/map/application/out/repository/LocationQueryRepository.kt b/src/main/kotlin/com/retrip/map/application/out/repository/LocationQueryRepository.kt index 55c47ee..652ed5c 100644 --- a/src/main/kotlin/com/retrip/map/application/out/repository/LocationQueryRepository.kt +++ b/src/main/kotlin/com/retrip/map/application/out/repository/LocationQueryRepository.kt @@ -1,10 +1,13 @@ package com.retrip.map.application.out.repository import com.retrip.map.application.`in`.response.LocationResponse +import com.retrip.map.domain.entity.Location import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import java.time.LocalDate import java.util.UUID interface LocationQueryRepository { fun findLocations(id: UUID?, page: Pageable): Page + fun findLocationsByEditedAt( editedAt: LocalDate): List } diff --git a/src/main/kotlin/com/retrip/map/domain/entity/Location.kt b/src/main/kotlin/com/retrip/map/domain/entity/Location.kt index bb9bb58..e35e294 100644 --- a/src/main/kotlin/com/retrip/map/domain/entity/Location.kt +++ b/src/main/kotlin/com/retrip/map/domain/entity/Location.kt @@ -9,6 +9,7 @@ import jakarta.persistence.Column import jakarta.persistence.Embedded import jakarta.persistence.Entity import jakarta.persistence.Id +import jakarta.persistence.Table import jakarta.persistence.Version import lombok.AccessLevel import lombok.NoArgsConstructor @@ -16,6 +17,7 @@ import lombok.Setter import java.util.* @Entity +@Table(name = "location") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Setter(value = AccessLevel.PROTECTED) class Location( diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/DestinationReader.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/DestinationReader.kt deleted file mode 100644 index 1d786f1..0000000 --- a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/DestinationReader.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.retrip.map.infra.adapter.`in`.batch - -import com.retrip.map.domain.entity.Location -import org.springframework.batch.item.ItemStreamReader -import org.springframework.stereotype.Component - -@Component -class DestinationReader( -): ItemStreamReader { - override fun read(): Location? { - return null - } - -} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt new file mode 100644 index 0000000..fad0e93 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapBatchConfig.kt @@ -0,0 +1,51 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import com.retrip.map.domain.entity.Location +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import org.springframework.batch.core.Job +import org.springframework.batch.core.Step +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing +import org.springframework.batch.core.job.builder.JobBuilder +import org.springframework.batch.core.repository.JobRepository +import org.springframework.batch.core.step.builder.StepBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.transaction.PlatformTransactionManager + +@Configuration +//@EnableBatchProcessing +class MapBatchConfig( + @Value("\${spring.batch.job.name}") + private val jobName: String, + private val reader: MapReader, + private val processor: MapProcessor, + private val writer: MapWriter, +) { + + @Bean + fun mapJob( + jobRepository: JobRepository, + mapStep: Step + ): Job { + return JobBuilder(jobName, jobRepository) + .start(mapStep) + .build() + + } + + @Bean + fun mapStep( + jobRepository: JobRepository, + transactionManager: PlatformTransactionManager + ): Step { + return StepBuilder("map-step", jobRepository) + .chunk, List>(10, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .build() + } + + +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt new file mode 100644 index 0000000..65d5fdb --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapProcessor.kt @@ -0,0 +1,15 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import com.retrip.map.domain.entity.Location +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import org.springframework.batch.item.ItemProcessor +import org.springframework.stereotype.Component + +@Component +class MapProcessor() + : ItemProcessor, List> { + override fun process(item: List): List? { + return item.map { LocationDocument.of(it) } + + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt new file mode 100644 index 0000000..adcdba6 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapReader.kt @@ -0,0 +1,18 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import com.retrip.map.application.out.repository.LocationQueryRepository +import com.retrip.map.domain.entity.Location +import org.springframework.batch.item.ItemReader +import org.springframework.stereotype.Component +import java.time.LocalDate + +@Component +class MapReader( + val locationQueryRepository: LocationQueryRepository + +): ItemReader> { + override fun read(): List? { + println("TEST") + return locationQueryRepository.findLocationsByEditedAt(LocalDate.now()) + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt new file mode 100644 index 0000000..1fb1c6c --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/in/batch/MapWriter.kt @@ -0,0 +1,17 @@ +package com.retrip.map.infra.adapter.`in`.batch + +import com.retrip.map.application.`in`.usecase.LocationIndexUseCase +import com.retrip.map.infra.adapter.out.search.elasticsearch.entity.LocationDocument +import org.springframework.batch.item.Chunk +import org.springframework.batch.item.ItemWriter +import org.springframework.stereotype.Component + +@Component +class MapWriter( + val locationIndexUseCase: LocationIndexUseCase +) : ItemWriter> { + override fun write(chunk: Chunk?>) { + chunk.items.forEach { documents -> locationIndexUseCase.indexLocationDocuments(documents) } + println("TEST 완료") + } +} diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationQuerydslRepository.kt b/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationQuerydslRepository.kt index 4d08522..2836d76 100644 --- a/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationQuerydslRepository.kt +++ b/src/main/kotlin/com/retrip/map/infra/adapter/out/persistence/mysql/query/LocationQuerydslRepository.kt @@ -5,11 +5,13 @@ import com.querydsl.core.types.Projections import com.querydsl.jpa.impl.JPAQueryFactory import com.retrip.map.application.`in`.response.LocationResponse import com.retrip.map.application.out.repository.LocationQueryRepository +import com.retrip.map.domain.entity.Location import com.retrip.map.domain.entity.QLocation.location import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable import org.springframework.stereotype.Repository +import java.time.LocalDate import java.util.* @@ -49,6 +51,16 @@ class LocationQuerydslRepository( return PageImpl(locations, page, count ?: 0) } + override fun findLocationsByEditedAt(editedAt: LocalDate): List { + val startOfDay = editedAt.atStartOfDay() + val endOfDay = editedAt.plusDays(1).atStartOfDay() + return query.selectFrom(location) + .where( + location.editedAt.goe(startOfDay), + location.createdAt.lt(endOfDay) + ).fetch() + } + private fun eqLocation(id: UUID?): Predicate? { return id?.let { location.id.eq(it) } } diff --git a/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt b/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt new file mode 100644 index 0000000..04cd57b --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/adapter/out/search/elasticsearch/entity/LocationDocument.kt @@ -0,0 +1,49 @@ +package com.retrip.map.infra.adapter.out.search.elasticsearch.entity + +import com.retrip.map.domain.entity.Location +import com.retrip.map.domain.exception.LocationNotFoundException +import com.retrip.map.domain.exception.common.RequireException +import jakarta.persistence.Id +import org.springframework.data.elasticsearch.annotations.Document +import org.springframework.data.elasticsearch.annotations.Field +import org.springframework.data.elasticsearch.annotations.FieldType +import java.util.* + +@Document(indexName = "locations") +data class LocationDocument( + @Id + val id: UUID, + @Field(type = FieldType.Text) + val name: String, + @Field(type = FieldType.Keyword) + val category: String, + @Field(type = FieldType.Keyword) + val description: String?, + @Field(type = FieldType.Keyword) + val telephone: String?, + @Field(type = FieldType.Keyword) + val address: String?, + @Field(type = FieldType.Keyword) + val roadAddress: String?, + @Field(type = FieldType.Double) + val latitude: Double?, + @Field(type = FieldType.Double) + val longitude: Double? +) { + companion object { + fun of(location: Location): LocationDocument { + return LocationDocument( + location.id ?: throw LocationNotFoundException(), + location.name?.value ?: throw RequireException(), + location.category?.value ?: throw RequireException(), + location.description?.value, + location.telephone, + location.address?.address, + location.address?.roadAddress, + location.geoPoint?.latitude, + location.geoPoint?.longitude, + ) + } + } + +} diff --git a/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt b/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt new file mode 100644 index 0000000..0cbf1e4 --- /dev/null +++ b/src/main/kotlin/com/retrip/map/infra/config/ElasticsearchConfig.kt @@ -0,0 +1,40 @@ +package com.retrip.map.infra.config + +import co.elastic.clients.elasticsearch.ElasticsearchClient +import co.elastic.clients.json.jackson.JacksonJsonpMapper +import co.elastic.clients.transport.rest_client.RestClientTransport +import org.apache.http.HttpHost +import org.apache.http.auth.AuthScope +import org.apache.http.auth.UsernamePasswordCredentials +import org.apache.http.impl.client.BasicCredentialsProvider +import org.apache.http.ssl.SSLContexts +import org.elasticsearch.client.RestClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class ElasticsearchConfig { + @Bean + fun elasticsearchClient(): ElasticsearchClient { + val credentialsProvider = BasicCredentialsProvider() + credentialsProvider.setCredentials( + AuthScope.ANY, + UsernamePasswordCredentials("elastic", "61tkSdZs6lUk2Mfa+nq5") + ) + + val sslContext = SSLContexts.custom() + .loadTrustMaterial(null) { _, _ -> true } // 개발용 + .build() + + val restClient = RestClient.builder(HttpHost("localhost", 9200, "https")) + .setHttpClientConfigCallback { httpClientBuilder -> + httpClientBuilder + .setSSLContext(sslContext) + .setDefaultCredentialsProvider(credentialsProvider) // ✅ 여기 수정 + } + .build() + + val transport = RestClientTransport(restClient, JacksonJsonpMapper()) + return ElasticsearchClient(transport) + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 115f979..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=map diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..7191076 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,23 @@ +spring: + application: + name: map +############# Data Source ###### + datasource: + url: jdbc:mysql://localhost:3306/map?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + username: root + password: test1234 + driver-class-name: com.mysql.cj.jdbc.Driver + h2: + console: + enabled: true + path: /h2-console +############# Batch ############ + batch: + job: + name: "retrip-map" + jdbc: + initialize-schema: always + jpa: + show-sql: true + hibernate: + ddl-auto: create