Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 아티클 이메일 전송 배치 구현 #82

Merged
merged 41 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6f47cdc
feat: email 모듈 생성및 필요 의존성 추가
belljun3395 Jun 21, 2024
04b6212
feat: email 모듈 구성 설정 파일 추가
belljun3395 Jun 21, 2024
2ed6ba3
feat: MailConfig 선언
belljun3395 Jun 21, 2024
e647f72
feat: ThymeleafConfig 구현
belljun3395 Jun 21, 2024
e583ffa
feat: MailSenderConfig 구현
belljun3395 Jun 21, 2024
d175efe
feat: SendEmailSender 구현
belljun3395 Jun 21, 2024
1a5176a
feat: SendArticleEmailService 구현
belljun3395 Jun 21, 2024
612675f
refactor: 테스트에서 MailProperties를 통해 자동으로 설정 값이 매핑되지 않아 수정
belljun3395 Jun 22, 2024
745c388
feat: SendAEmailTestSpec 구현
belljun3395 Jun 22, 2024
03f62b2
chore: 이메일 테스트를 위한 리소스 추가
belljun3395 Jun 22, 2024
6e974f6
test: SendArticleEmailServiceTest 구현
belljun3395 Jun 22, 2024
991baa6
Merge branch 'feat/#66_belljun3395' into feat/#76_belljun3395
belljun3395 Jun 22, 2024
3000856
feat: batch 모듈 추가
belljun3395 Jun 23, 2024
9a4fdcc
feat: batch 모듈 필요 의존성 추가
belljun3395 Jun 23, 2024
2ce7a8c
feat: Subscription 테이블에 progress 칼럼 추가
belljun3395 Jun 23, 2024
a5adeaf
feat: SendMailArgs equalsAndHashCode 추가
belljun3395 Jun 23, 2024
183e136
feat: WorkBookSubscriberItem 구현
belljun3395 Jun 23, 2024
3df5752
feat: WorkBookSubscriberReader 구현
belljun3395 Jun 23, 2024
b01a51b
feat: WorkBookSubscriberWriter 구현
belljun3395 Jun 23, 2024
7d246af
test: WorkBookSubscriberWriterTest 구현
belljun3395 Jun 23, 2024
d8d331c
feat: BatchSendArticleEmailService 구현
belljun3395 Jun 23, 2024
f231a6a
feat: batch 테스트를 위한 설정 코드 추가
belljun3395 Jun 23, 2024
2a55046
feat: BatchServiceLogAspect 구현
belljun3395 Jun 23, 2024
3e0ff64
feat: api 모듈에 batch 모듈 추가
belljun3395 Jun 23, 2024
4be9b18
feat: BatchController 구현
belljun3395 Jun 23, 2024
b457fd5
Merge branch 'main' into feat/#76_belljun3395
belljun3395 Jun 23, 2024
8a422fc
feat: batch call execution 테이블 추가
belljun3395 Jun 23, 2024
dab223c
feat: BatchCallExecutionService 구현
belljun3395 Jun 23, 2024
db4e733
refactor: 성공과 실패 경우 기록할 수 있도록 리펙토링
belljun3395 Jun 23, 2024
dd5b78d
refactor: 배치 콜 수행 결과 기록하도록 리펙토링
belljun3395 Jun 23, 2024
76962a1
refactor: BatchReaderWriterTestSpec -> BatchTestSpec으로 이름 수정
belljun3395 Jun 23, 2024
057c6d7
feat: BatchTestSpec에 ObjectMapper 추가
belljun3395 Jun 23, 2024
ffeabd6
test: BatchCallExecutionServiceTest 구현
belljun3395 Jun 23, 2024
fba0b97
feat: BatchSendArticleEmailService에 @Transactional 어노테이션 추가
belljun3395 Jun 23, 2024
ac6ac8e
feat: WorkBookSubscriberReader에 readOnly @Transactional 어노테이션 추가
belljun3395 Jun 23, 2024
827f752
refactor: problem -> article 패키지 이동
belljun3395 Jun 23, 2024
58b3a54
Merge branch 'feat/#66_belljun3395' into feat/#76_belljun3395
belljun3395 Jun 23, 2024
69661a1
Merge branch 'main' into feat/#76_belljun3395
belljun3395 Jun 23, 2024
84b54dc
refactor: problem -> article 패키지 이동 반영
belljun3395 Jun 23, 2024
78a5245
refactor: flyway 스키마 버전 수정
belljun3395 Jun 23, 2024
c5d2752
Merge branch 'main' into feat/#76_belljun3395
belljun3395 Jun 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
dependencies {
/** module */
implementation(project(":api-repo"))
implementation(project(":batch"))

/** spring starter */
implementation("org.springframework.boot:spring-boot-starter-webflux")
Expand Down
3 changes: 2 additions & 1 deletion api/src/main/kotlin/com/few/api/config/ApiConfig.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.few.api.config

import com.few.api.repo.config.ApiRepoConfig
import com.few.batch.config.BatchConfig
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import

@Configuration
@ComponentScan(basePackages = [ApiConfig.BASE_PACKAGE])
@Import(ApiRepoConfig::class)
@Import(ApiRepoConfig::class, BatchConfig::class)
class ApiConfig {
companion object {
const val BASE_PACKAGE = "com.few.api"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.few.api.web.controller.batch

import com.few.batch.service.article.BatchSendArticleEmailService
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Validated
@RestController
@RequestMapping("/batch")
class BatchController(
private val batchSendArticleEmailService: BatchSendArticleEmailService
) {

// todo add check permission
@PostMapping("/article")
fun batchArticle() {
batchSendArticleEmailService.execute()
}
}
145 changes: 145 additions & 0 deletions batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
tasks.getByName("bootJar") {
enabled = false
}

tasks.getByName("jar") {
enabled = true
}

plugins {
/** jooq */
id("org.jooq.jooq-codegen-gradle") version DependencyVersion.JOOQ
}

sourceSets {
main {
java {
val mainDir = "src/main/kotlin"
val jooqDir = "src/generated"
srcDirs(mainDir, jooqDir)
}
}
}

dependencies {
/** module */
implementation(project(":email"))

/** spring starter */
api("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("com.mysql:mysql-connector-j")

/** jooq */
implementation("org.springframework.boot:spring-boot-starter-jooq")
implementation("org.jooq:jooq:${DependencyVersion.JOOQ}")
implementation("org.jooq:jooq-meta:${DependencyVersion.JOOQ}")
implementation("org.jooq:jooq-codegen:${DependencyVersion.JOOQ}")
jooqCodegen("org.jooq:jooq-meta-extensions:${DependencyVersion.JOOQ}")

/** test container */
implementation(platform("org.testcontainers:testcontainers-bom:${DependencyVersion.TEST_CONTAINER}"))
testImplementation("org.testcontainers:mysql")

/** aspectj */
implementation("org.aspectj:aspectjweaver")

/** test flyway */
testImplementation("org.flywaydb:flyway-core:${DependencyVersion.FLYWAY}")
testImplementation("org.flywaydb:flyway-mysql")
}

/** copy data migration */
tasks.create("copyDataMigration") {
doLast {
val root = rootDir
val flyWayResourceDir = "/db/migration/entity"
val dataMigrationDir = "$root/data/$flyWayResourceDir"
File(dataMigrationDir).walkTopDown().forEach {
if (it.isFile) {
it.copyTo(
File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"),
true
)
}
}
}
}

/** copy data migration before compile kotlin */
tasks.getByName("compileKotlin") {
dependsOn("copyDataMigration")
}

/** jooq codegen after copy data migration */
tasks.getByName("jooqCodegen") {
dependsOn("copyDataMigration")
}

jooq {
configuration {
generator {
database {
name = "org.jooq.meta.extensions.ddl.DDLDatabase"
properties {
// Specify the location of your SQL script.
// You may use ant-style file matching, e.g. /path/**/to/*.sql
//
// Where:
// - ** matches any directory subtree
// - * matches any number of characters in a directory / file name
// - ? matches a single character in a directory / file name
property {
key = "scripts"
value = "src/main/resources/db/migration/**/*.sql"
}

// The sort order of the scripts within a directory, where:
//
// - semantic: sorts versions, e.g. v-3.10.0 is after v-3.9.0 (default)
// - alphanumeric: sorts strings, e.g. v-3.10.0 is before v-3.9.0
// - flyway: sorts files the same way as flyway does
// - none: doesn't sort directory contents after fetching them from the directory
property {
key = "sort"
value = "flyway"
}

// The default schema for unqualified objects:
//
// - public: all unqualified objects are located in the PUBLIC (upper case) schema
// - none: all unqualified objects are located in the default schema (default)
//
// This configuration can be overridden with the schema mapping feature
property {
key = "unqualifiedSchema"
value = "none"
}

// The default name case for unquoted objects:
//
// - as_is: unquoted object names are kept unquoted
// - upper: unquoted object names are turned into upper case (most databases)
// - lower: unquoted object names are turned into lower case (e.g. PostgreSQL)
property {
key = "defaultNameCase"
value = "as_is"
}
}
}

generate {
isDeprecated = false
isRecords = true
isImmutablePojos = true
isFluentSetters = true
isJavaTimeTypes = true
}

target {
packageName = "jooq.jooq_dsl"
directory = "src/generated"
encoding = "UTF-8"
}
}
}
}
22 changes: 22 additions & 0 deletions batch/src/main/kotlin/com/few/batch/config/BatchConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.few.batch.config

import com.few.email.config.MailConfig
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import

@Configuration
@ComponentScan(basePackages = [BatchConfig.BASE_PACKAGE])
@EnableAutoConfiguration(exclude = [DataSourceAutoConfiguration::class])
@Import(MailConfig::class)
class BatchConfig {
companion object {
const val BASE_PACKAGE = "com.few.batch"
const val SERVICE_NAME = "few"
const val MODULE_NAME = "few-batch"
const val BEAN_NAME_PREFIX = "fewBatch"
const val PROPERTY_PREFIX = SERVICE_NAME + "." + MODULE_NAME
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.few.batch.log

import jooq.jooq_dsl.Tables.BATCH_CALL_EXECUTION
import org.jooq.DSLContext
import org.jooq.JSON
import org.springframework.stereotype.Service

@Service
class BatchCallExecutionService(
private val dslContext: DSLContext
) {
fun execute(status: Boolean, jsonDescription: String) {
dslContext.insertInto(BATCH_CALL_EXECUTION)
.set(BATCH_CALL_EXECUTION.STATUS, status)
.set(BATCH_CALL_EXECUTION.DESCRIPTION, JSON.valueOf(jsonDescription))
.execute()
}
}
37 changes: 37 additions & 0 deletions batch/src/main/kotlin/com/few/batch/log/BatchServiceLogAspect.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.few.batch.log

import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.util.*

@Aspect
@Component
class BatchServiceLogAspect {
private val log = LoggerFactory.getLogger(BatchServiceLogAspect::class.java)

@Pointcut(value = "execution(* com.few.batch.service..*.execute(..))")
fun batchServiceDao() {}

@Around("batchServiceDao()")
@Throws(Throwable::class)
fun requestLogging(joinPoint: ProceedingJoinPoint): Any? {
val signature = joinPoint.signature
val splitByDot =
signature.declaringTypeName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val serviceName = splitByDot[splitByDot.size - 1]
val args = joinPoint.args

log.trace("{} execute with {}", serviceName, args)
val startTime = System.currentTimeMillis()
val proceed = joinPoint.proceed()
val elapsedTime = System.currentTimeMillis() - startTime
log.debug("{} finished in {}ms", serviceName, elapsedTime)

return proceed
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.few.batch.service.article

import com.fasterxml.jackson.databind.ObjectMapper
import com.few.batch.log.BatchCallExecutionService
import com.few.batch.service.article.reader.WorkBookSubscriberReader
import com.few.batch.service.article.writer.WorkBookSubscriberWriter
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class BatchSendArticleEmailService(
private val workBookSubscriberReader: WorkBookSubscriberReader,
private val workBookSubscriberWriter: WorkBookSubscriberWriter,
private val batchCallExecutionService: BatchCallExecutionService,
private val objectMapper: ObjectMapper
) {
@Transactional
fun execute() {
workBookSubscriberReader.execute().let { item ->
workBookSubscriberWriter.execute(item).let { execution ->
objectMapper.writeValueAsString(execution).let { json ->
if (!json.contains("fail")) {
batchCallExecutionService.execute(false, json)
} else {
batchCallExecutionService.execute(true, json)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.few.batch.service.article.dto

fun List<WorkBookSubscriberItem>.toMemberIds(): List<Long> {
return this.map { it.memberId }
}

fun List<WorkBookSubscriberItem>.toTargetWorkBookIds(): List<Long> {
return this.map { it.targetWorkBookId }
}

/** key: 구독자들이 구독한 학습지 ID, value: 구독자들의 학습지 구독 진행률 */
fun List<WorkBookSubscriberItem>.toTargetWorkBookProgress(): Map<Long, List<Long>> {
return this.stream().collect(
{ mutableMapOf<Long, MutableList<Long>>() },
{ map, dto ->
if (map.containsKey(dto.targetWorkBookId)) {
map[dto.targetWorkBookId]?.add(dto.progress)
} else {
map[dto.targetWorkBookId] = mutableListOf(dto.progress)
}
},
{ map1, map2 ->
map2.forEach { (key, value) ->
if (map1.containsKey(key)) {
map1[key]?.addAll(value)
} else {
map1[key] = value
}
}
}
)
}

data class WorkBookSubscriberItem(
/** 회원 ID */
val memberId: Long,
/** 학습지 ID */
val targetWorkBookId: Long,
/** 진행률 */
val progress: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.few.batch.service.article.reader

import com.few.batch.service.article.dto.WorkBookSubscriberItem
import jooq.jooq_dsl.tables.Subscription
import org.jooq.DSLContext
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class WorkBookSubscriberReader(
private val dslContext: DSLContext
) {

/** 구독 테이블에서 학습지를 구독하고 있는 회원의 정보를 조회한다.*/
@Transactional(readOnly = true)
fun execute(): List<WorkBookSubscriberItem> {
val subscriptionT = Subscription.SUBSCRIPTION

return dslContext.select(
subscriptionT.MEMBER_ID.`as`(WorkBookSubscriberItem::memberId.name),
subscriptionT.TARGET_WORKBOOK_ID.`as`(WorkBookSubscriberItem::targetWorkBookId.name),
subscriptionT.PROGRESS.`as`(WorkBookSubscriberItem::progress.name)
)
.from(subscriptionT)
.where(subscriptionT.TARGET_MEMBER_ID.isNull)
.and(subscriptionT.DELETED_AT.isNull)
.fetchInto(WorkBookSubscriberItem::class.java)
}
}
Loading
Loading