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

쿠폰을 잘못 발급해서 일부 쿠폰은 바로 소멸시키고 새로운 쿠폰으로 교체한다. #28

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
33398b3
chore(all) : 전역 build.gradle 에서 spring web 의존성 제거
this-is-spear Jun 8, 2024
5e6b118
chore(batch-coupon-exchange) : 쿠폰 교환 기본 설정
this-is-spear Jun 9, 2024
5ea6a79
feature(batch-coupon-exchange) : CouponEntity 구현
this-is-spear Jun 9, 2024
be706f9
feature(batch-coupon-exchange) : CouponExchangeApplication 설정
this-is-spear Jun 9, 2024
47b8694
test(batch-coupon-exchange) : SpringBootInitializer 주석 추가
this-is-spear Jun 9, 2024
8b59b10
feature(batch-coupon-exchange) : reader 샘플 코드 구현
this-is-spear Jun 9, 2024
4e148c6
feature(batch-coupon-exchange) : processor 샘플 코드 구현
this-is-spear Jun 9, 2024
b995625
feature(batch-coupon-exchange) : writer 샘플 코드 구현
this-is-spear Jun 9, 2024
e0aa221
feature(batch-coupon-exchange) : step 샘플 코드 구현
this-is-spear Jun 9, 2024
f9fb098
feature(batch-coupon-exchange) : job 샘플 코드 구현
this-is-spear Jun 9, 2024
1fc1aad
test(batch-coupon-exchange) : job 테스트 코드 작성
this-is-spear Jun 9, 2024
564c449
Merge remote-tracking branch 'refs/remotes/origin/main' into 4-쿠폰을-잘못…
this-is-spear Jun 9, 2024
52d806c
chore(coupon) : MemberCouponUseState 에서 EXPIRED 상태 제거
this-is-spear Jun 10, 2024
53a7812
feature(batch) : JOB, STEP 플로우 구현
this-is-spear Jun 15, 2024
67f8942
refactor(batch) : 데이터베이스 스키마 수정 batch, coupon 분리
this-is-spear Jun 23, 2024
5d6907c
chore(batch) : commit size 기본 값 세팅
this-is-spear Jun 23, 2024
e8b187a
fix(batch) : test 시 작업 중복 실행하는 현상 해결
this-is-spear Jun 23, 2024
c2a3618
feature(batch) : 데이터베이스에 쿠폰 생성 이력 추가
this-is-spear Jun 23, 2024
4113d8b
feature(batch) : 쿠푠 교환 단계 구현
this-is-spear Jun 24, 2024
4890c6d
refactor(batch) : 사용하지 않는 테이블 ExchangeUserHistory 제거
this-is-spear Jun 24, 2024
dc65c7c
feature(batch) : 알림톡 전송 단계 선언
this-is-spear Jun 24, 2024
2be83c1
feature(batch) : 시도 중 실패하는 경우 생성된 쿠폰 ID로 이후 작업 처리할 수 있도록 주입 방법 추가
this-is-spear Jun 25, 2024
bc9ba0e
test(batch) : 변경된 동작에 맞게 테스트 코드 수정
this-is-spear Jun 25, 2024
4dc167b
chore(batch) : prod 프로파일에 dialect 추가
this-is-spear Jul 20, 2024
1e69f63
chore(batch) : 리드미 파일 추가
this-is-spear Jul 20, 2024
bd74ac8
refactor(batch) : 쿠폰 이름 toString 메서드에도 값 반환하도록 수정
this-is-spear Jul 20, 2024
b7bc1ce
refactor(batch) : CouponService#findCouponById 쿠폰 이름 조회 로직 추가
this-is-spear Jul 21, 2024
81c399d
test(batch) : Fixtures 사용자 식별자 고유하게 생성되도록 수정
this-is-spear Jul 21, 2024
20ed30f
refactor(batch) : ExchangedCouponIdStorage 내부 이름 명확할 수 있도록 수정
this-is-spear Jul 21, 2024
4cb4c01
feature(batch) : CreateExchangeNotificationTemplateProcessors 알림 전송 템…
this-is-spear Jul 21, 2024
6aaffaf
feature(batch) : CreateExchangeNotificationTemplateProcessor 구현
this-is-spear Jul 21, 2024
973e6c9
feature(batch) : NotificationWriters 구현
this-is-spear Jul 21, 2024
5406ec3
feature(batch) : Client 생성
this-is-spear Jul 21, 2024
d38f5a3
feature(batch) : ExchangeCouponSteps 에서 알림톡 템플릿 구현
this-is-spear Jul 21, 2024
4d1da98
feature(batch) : feign 클라이언트 추가
this-is-spear Jul 21, 2024
f1cb57b
chore(batch) : stub runner 의존성 제거
this-is-spear Jul 21, 2024
d4341ca
test(batch) : stub runner 코드 제거
this-is-spear Jul 21, 2024
67e8b6f
test(batch) : 테스트 환경에서 fake 객체 사용하도록 설정
this-is-spear Jul 21, 2024
166d0ef
test(batch) : Fixtures 패키지 추가
this-is-spear Jul 21, 2024
e5f45ef
test(batch) : test 프로파일 실행되도록 수정
this-is-spear Jul 21, 2024
39fba20
refactor : test 환경인 경우 feign 설정되지 않도록 수정
this-is-spear Jul 21, 2024
1d270e9
test(batch) : 도커가 없다면 실행하지 않도록 수정
this-is-spear Jul 21, 2024
b5ad69b
chore(action) : 액션 파일 수정
this-is-spear Jul 21, 2024
cbe1b47
Merge branch '4-쿠폰을-잘못-발급해서-일부-쿠폰은-바로-소멸시키고-새로운-쿠폰으로-교체하고-싶어졌음' of ht…
this-is-spear Jul 21, 2024
bf14ae2
Revert "chore(action) : 액션 파일 수정"
this-is-spear Jul 21, 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
17 changes: 17 additions & 0 deletions batch-coupon-exchang/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

### 배치 실행 명령어

```shell
export PROD_DATASOURCE_URL=jdbc:mysql://{DB_HOST}:{DP_PORT}/batch?serverTimezone=Asia/Seoul
export PROD_DATASOURCE_USERNAME={USERNAME}
export PROD_DATASOURCE_PASSWORD={PASSWORD}

java -jar batch-coupon-exchang/build/libs/batch-coupon-exchang-1.0-SNAPSHOT.jar \
--spring.profiles.active=prod \
name='100% 할인 보상 쿠폰' \
description='배송 오류로 인한 쿠폰 교환' \
amountType='RATE' \
amount=100 \
expiredCouponId=13
```

29 changes: 29 additions & 0 deletions batch-coupon-exchang/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
group = "com.example.estdelivery"
version = "1.0-SNAPSHOT"

tasks.withType<Jar> {
enabled = true
}

repositories {
mavenCentral()
}

dependencies {
runtimeOnly("com.mysql:mysql-connector-j")
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.batch:spring-batch-test")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
testImplementation("org.testcontainers:mysql")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter-kotlin:1.0.16")
}

tasks.test {
useJUnitPlatform()
}

kotlin {
jvmToolchain(17)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.estdelivery

import javax.sql.DataSource
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.PlatformTransactionManager


/**
* datasource 를 빈으로 등록하고 EnableBatchProcessing 애너테이션을 사용하면 순환 의존성으로 인해 datasource 를 외부에서 주입받기 어렵다.
* DefaultBatchConfiguration 로 의존성을 주입하자.
*/
@Configuration
class BatchConfiguration(
private val dataSource: DataSource,
private val transactionManager: PlatformTransactionManager,
) : DefaultBatchConfiguration() {
override fun getDataSource(): DataSource {
return dataSource
}

override fun getTransactionManager(): PlatformTransactionManager {
return transactionManager
}
Comment on lines +22 to +24
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배치 데이터베이스와 쿠폰 데이터베이스가 분리되면 트랜잭션 매니저는 어떻게 관리해야 할까?

Copy link
Owner Author

@this-is-spear this-is-spear Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래에서 확인했듯이 버전으로 실행 이력을 관리한다. Job 정보가 Serializable하게 생성되면 Job 식별자에 맞게 개별적으로 Step 실행 정보가 생성된다.

Step에서 청크 사이즈나 병렬 처리에도 정상적인 데이터를 유지할 수 있는 이유는 Step에서 실행되는 버전 정보 덕분이다. 커밋 단위로 버전이 업데이트되므로 순차적으로 Step 실행 이력을 저장할 수 있다. 일반적으로 Step의 커밋 수 + 2 가 버전 정보다. 아마 처음 실행되면서 +1 마지막 종료되면서 +1 로 추가 카운팅이 되는 것처럼 보인다.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ 다시 확인해보니 트랜잭션을 걸고 있다. AbstractJobRepositoryFactoryBean 에서 getObject 메서드를 호출하는 시점에 트랜잭션을 걸게 된다. Job 정보는 JobRepository에 의해서 관리되고 JobRepository 내부 정보는 AbstractJobRepositoryFactoryBean 에서 getObject 로 호출해서 변경된다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.estdelivery

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "batch.chunk")
data class ChunkSizeProperty(
val commitSize: Int,
val processSize: Int,
val writeSize: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.estdelivery

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication

@SpringBootApplication
@EnableConfigurationProperties(
ChunkSizeProperty::class
)
class CouponExchangeApplication

fun main(args: Array<String>) {
runApplication<CouponExchangeApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.estdelivery

import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile

@Configuration
@Profile("!test")
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트를 위한 소스 코드 없애기

@EnableFeignClients
class FeignClientConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.estdelivery

import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaRepositories

@Configuration
@EnableJpaRepositories(
basePackages = ["com.example.estdelivery.job.step.service.repository"]
)
class JpaConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.estdelivery

import org.springframework.batch.core.explore.JobExplorer
import org.springframework.batch.core.launch.JobLauncher
import org.springframework.batch.core.repository.JobRepository
import org.springframework.boot.autoconfigure.batch.BatchProperties
import org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.util.StringUtils

/**
* CommandLineRunner 를 이용하면 spring boot 가 제공하는 기본 기능을 이용할 수 없다.
* 예를 들어 JPA 관련 설정 DB 관련 설정이다. 이런 auto configure 가 필요없다면 해당 클래스를 직접 생성하지 않아도 된다.
*/
@Configuration
@EnableConfigurationProperties(BatchProperties::class)
class RunnerConfiguration {

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.batch.job", name = ["enabled"])
fun jobLauncherApplicationRunner(
jobLauncher: JobLauncher,
jobExplorer: JobExplorer,
jobRepository: JobRepository,
properties: BatchProperties
): JobLauncherApplicationRunner {
val runner = JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository)
val jobNames = properties.job.name
if (StringUtils.hasText(jobNames)) {
runner.setJobName(jobNames)
}
return runner
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.estdelivery.client

interface AlimTalkClient {
fun sendAlimTalk(alimTalkRequest: AlimTalkRequest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.estdelivery.client

import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody


@FeignClient(
name = "alim",
url = "http://localhost:8090"
)
interface AlimTalkFeignClient : AlimTalkClient {
@PostMapping("/alim")
override fun sendAlimTalk(@RequestBody alimTalkRequest: AlimTalkRequest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.estdelivery.client

data class AlimTalkRequest(
val sender: String,
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.estdelivery.client

interface MemberClient{
fun findMemberById(memberId: Long): MemberResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.estdelivery.client

import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable

@FeignClient(
name = "member",
url = "http://localhost:8081"
)
interface MemberFeignClient : MemberClient {
@GetMapping("/members/{memberId}")
override fun findMemberById(@PathVariable memberId: Long): MemberResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.estdelivery.client

import com.example.estdelivery.domain.PhoneNumber
import com.example.estdelivery.domain.UserName

data class MemberResponse(
val phone: PhoneNumber,
val name: UserName,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.estdelivery.domain

import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.NamedQuery
import jakarta.persistence.Table

@Entity
@NamedQuery(
name = "couponFinaAll",
query = "SELECT c FROM Coupon c"
)
@Table(name = "coupon", catalog = "coupon")
class Coupon(
val name: String,
val description: String,
@Enumerated(EnumType.STRING)
val amountType: CouponStateAmountType,
@Enumerated(EnumType.STRING)
val type: CouponStateType,
val amount: Int,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Coupon) return false

if (id != other.id) return false

return true
}

override fun hashCode(): Int {
return id.hashCode()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.estdelivery.domain

data class CouponExchangeNotificationTemplate(
private val receivingPhoneNumber: PhoneNumber,
private val receivingName: UserName,
private val couponNameBeforeExchange: String,
private val couponNameAfterExchange: String,
) {
val sendMessage: String
get(): String = """
[이스트딜리버리]
${receivingName}님, 보관중인 쿠폰에 문제가 생겨 다음과 같이 변경됩니다.

사유 : ${receivingName}님의 쿠폰이 만료되었습니다.
변경 전 쿠폰 : $couponNameBeforeExchange
변경 후 쿠폰 : $couponNameAfterExchange
""".trimIndent()

val sender: String
get(): String = receivingPhoneNumber.number
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.estdelivery.domain

enum class CouponStateAmountType {
RATE,
FIX,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.estdelivery.domain

enum class CouponStateType {
PUBLISHED,
HANDOUT,
EVENT,
EXPIRED,
REWARD,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.estdelivery.domain

class ExchangeCoupon(
val owner: UserName,
val ownerPhone: PhoneNumber,
val memberCoupon: MemberCoupon,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.estdelivery.domain

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name = "exchange_coupon_history", catalog = "coupon")
class ExchangeCouponHistory(
val expiredCouponId: Long,
val rewardCouponId: Long,
val jobExecutionId: Long,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.example.estdelivery.domain

import com.example.estdelivery.domain.MemberCouponUseState.UNUSED
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.NamedQuery
import jakarta.persistence.Table

const val UNUSED_MEMBER_COUPON_FIND_ALL = "unusedMemberCouponFinaAll"

@Entity
@NamedQuery(
name = UNUSED_MEMBER_COUPON_FIND_ALL,
query = """
SELECT c
FROM MemberCoupon c
WHERE c.couponId = :couponId
"""
)
@Table(name = "member_coupon", catalog = "coupon")
class MemberCoupon(
val couponId: Long,
val memberId: Long,
@Enumerated(EnumType.STRING)
var status: MemberCouponUseState = UNUSED,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MemberCoupon) return false

if (other.id == 0L) return false
if (id != other.id) return false

return true
}

override fun hashCode(): Int {
return id.hashCode()
}

fun exchange(exchangedCouponId: Long): MemberCoupon {
return MemberCoupon(
couponId = exchangedCouponId,
memberId = memberId,
id = id,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.estdelivery.domain

enum class MemberCouponUseState {
USED,
UNUSED,
;
}
Loading
Loading