728x90
반응형

선착순 쿠폰 발급이나 티켓 예매와 같이 동시에 트래픽이 많이 들어올 경우 동시성 문제가 발생하기 마련이다.
이러한 문제를 해결하기 위해 주로 락을 사용하여 동시성 문제를 해결한다. 

이때 한가지 조심해야할 부분이 있다. 


 

동시성 문제가 발생하는 코드 1

@Transactional
fun issue(couponId: Long, userId: Long){
    val coupon = findCoupon(couponId);
    coupon.issue()
    saveCouponIssue(couponId, userId)
}


사용자가 쿠폰 발급 버튼을 누르면 해당하는 쿠폰을 찾아 쿠폰을 발급하는 간단한 로직이다.
동시에 여러명의 유저가 쿠폰발급을 누른다면 락이 존재하지 않기 때문에 쿠폰개수에 대해 동시성 문제가 발생하여 의도한대로 코드가 동작하지 않을 것이다.

 


동시성 문제가 발생하는 코드? 2


분산환경과 상관없이 테스트용으로 간단히 synchronize를 걸어 오직 하나의 스레드만이 접근할 수 있도록 변경했다.

    @Transactional
    fun issue(couponId: Long, userId: Long){
        synchronized(this){
            val coupon = findCoupon(couponId);
            coupon.issue()
            saveCouponIssue(couponId, userId)
        }
    }

 

이렇게 하면 결과가 어떻게 될까? 


여전히 동시성 문제가 발생한다.
그 이유는 @Transcation과 관련되어 있다.
@Transaction은 AOP를 기반으로 함수가 시작하기전에 transcation을 열고 함수가 끝나는 시점에 commit을 진행한다.

위 코드는 함수 내부에서 락을 열고 락을 닫기 때문에 transcation이 commit되기전에 다른 스레드가 접근할 수 있게된다.
따라서 여전히 동시성 문제는 해결되지 않는다.

이러한 문제를 해결하기 위해선 해당 함수를 호출하는 곳에서 락을 걸어 해결해야한다.

 

fun issueRequestV1(requestDto: CouponIssueRequestDto){
    synchronized(this){
        couponIssueService.issue(requestDto.couponId, requestDto.userId)
    }
    
    log.info("쿠폰 발급 완료, couponId: ${requestDto.couponId}, userId: ${requestDto.userId}")
}

 

728x90
반응형
728x90
반응형

다음과 같이 구성되어있다고 가정하자

익명객체를 만들고 익명객체의 함수에는 @Transactional이 붙어있으며 해당 함수는 Transcation이 필요한 함수를 호출한다.

@FunctionalInterface
interface TempHandleInterface {

    fun handle(something: String): String
}

 

import jakarta.annotation.PostConstruct
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.UUID

@Service
class TempClass(
    private val tempService: TempService,
) {

    private val tempHandlers: MutableMap<String, TempHandleInterface> = mutableMapOf();

    @PostConstruct
    fun init(){
        tempHandlers["secretHandler"] = object : TempHandleInterface{
            @Transactional
            override fun handle(something: String): String {
                return tempService.saveNeedToTransaction(UUID.randomUUID().toString() + something + "secret")
            }
        }
    }
}

 

 

import org.springframework.stereotype.Service

@Service
class TempService {

    fun saveNeedToTransaction(something : String): String {
        //need to save something on transcation
        return "Success"
    }
}

 

과연 saveNeedToTransaction는 Transaction이 적용될까??? 

적용되지 않는다 Spring AOP는 주로 어노테이션기반으로 작동하는데 이때 Spring AOP가 프록시를 만들어주는 대상은 Spring Bean에서 관리되는 되어야한다, 하지만 익명객체는 Spring Bean으로 등록되어 있지 않기 떄문에 @Transcational을 포함한 Spring Aop 기반 어노테이션이 동작하지 않는다.
따라서 호출되는 saveNeedToTransaction에 @Transactional을 붙여서 사용해야한다

 

728x90
반응형
728x90
반응형

 

문제인식

Book
{
@Id
val id : String 
val title: String
val author: User
}


User
{
id: String
name: String 
}

대충 다음과 같은 구조에서 
mongodb에서 Aggregation한뒤 Book class로 Mapping해서 data를 가져오면 전부 잘 가져와지는데 id 만 null로 나온다.

참고로 spring data mongo는 기본적으로 id property strategy를 _id로 지정한다.

웃긴게 mongoTemplate할때는 id를 넣어도 잘되는데   @Query로 query는 무조건 _id를 넣어줘야한다

id 관련해서 버그가 참 많은 것 같다.


해결 방법 1

User의 id 에 @Field("id") 를 명시하기 

 

해결 방법 2

graphql을 사용하거나 다른 모듈의 인터페이스를 사용할 경우 

fun Aggregation.embeddedIdSet(propertyName: String) {
    this.pipeline.add(
        Aggregation.addFields()
            .addField("$propertyName._id").withValue("\$$propertyName.id")
            .build()
    )
}

다음과 같이 addField를 사용하자

728x90
반응형
728x90
반응형

BeanPostProcessor란? 

한국어로 그대로 번역하면 빈 후 처리기로 
스프링 빈이 생성되기 전후(초기화 전후)에 IOC 컨테이너에 빈으로 등록되기전에
빈을 intercept해서 특정로직을 처리가능하게 해주는 스프링이 지원하는 객체이다. 

주로 AOP를 사용하기위해 Proxy 객체를 생성해 IOC 컨테이너에 실제 빈 대신에 
프록시를 넣을 떄 사용한다. 
이 뿐만 아니라 다양하게 특정 빈이 초기화 되었을 때
어떤 로직을 실행한다던지 등 여러가지로 구현가능하다. 


BeanPostProcessor는 다른 빈 등록보다 선행된다.

 

예시

BeanPostProcess를 상속받아 구현 가능하다.

class LogBeanPostProcessor(
    private val packageName : String,
    private val advisor: Advisor
) : BeanPostProcessor {

    override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {

        val beanPackage = bean.javaClass.packageName

        if(beanPackage != packageName){
            return super.postProcessAfterInitialization(bean, beanName)
        }

        val proxyFactory = ProxyFactory(bean)
        proxyFactory.addAdvisor(advisor)
        val proxy = proxyFactory.getProxy()

        return super.postProcessAfterInitialization(proxy, beanName)
    }
}

구현한 BeanPostProcessor를 빈으로 등록하자

@Configuration
class LogPostProcessorConfiguration {

    @Bean
    fun logBeanPostProcessor(logService: LogServiceDirtyCode): LogBeanPostProcessor {
        return LogBeanPostProcessor("com.study.aop.item", getAdvisor(logService))
    }

    fun getAdvisor(logService: LogServiceDirtyCode): Advisor {
        val logAdvice = LogAdvice(logService)
        val pointCut = NameMatchMethodPointcut()
        pointCut.setMappedNames("creat*", "get*", "save*", "find*")
        return DefaultPointcutAdvisor(logAdvice)
    }

}

 


cf). spring aop library 를 depedency에 추가하면 @Aspect나 Advisor를 component로 등록하면 자동으로 빈후처리기가 포인트 컷을 통해 프록시를 생성한다.

728x90
반응형
728x90
반응형

프록시 팩토리란 

Spring에서 지원하는 프록시 생성해주는 클래스로 
프록시를 사용할 구현체(타겟)과 Advice, PointCut을 지정해서 프록시를 생성할 수 있다.

만약 타겟이 interface라면 jdk dynamic proxy를 생성하고

타겟이 구현체라면 cglib를 생성한다.

둘중에 하나만 사용하도록 설정도 가능하다.

최근 spring은 cglib를 기본값으로 사용하고 있다.

 

Advice란

Advice란 프록시 로직을 의미한다 (로그, 캐싱, 권한 확인)

 

Pointcut이란

Advice를 지정할 패키지,클래스,메소드 범위이다. 

 

즉 Advice(로직) PointCut(범위)를 생성하고 ProxyFactory에 넘겨주면 알맞게 proxy를 생성해준다.

참고로 Advice와 PointCut을 합쳐 만든 객체 Advisor를 주로 사용한다. 

 

예시

Advice  만들고

class LogAdvice(
    private val logService: LogServiceDirtyCode
) : MethodInterceptor{

    override fun invoke(invocation: MethodInvocation): Any? {
        val method = invocation.method
        val className = method.declaringClass.name
        val methodName = method.name
        var trace: Trace? = null

        return runCatching {
            trace = logService.begin(className + methodName)
            invocation.proceed()
        }.onSuccess {
            logService.finish(trace!!)
        }.onFailure {
            logService.execption(trace!!, it)
            throw it
        }.getOrThrow()
    }
}

 

PointCut과 생성한 Advice를 통해 Advisor를 만들고

private fun getAdvisor(logService: LogServiceDirtyCode): Advisor {
    val pointCut = NameMatchMethodPointcut()
    pointCut.setMappedNames("creat*", "get*", "save*", "find*")
    return DefaultPointcutAdvisor(pointCut, LogAdvice(logService))
}

 

ProxyFactory에 타겟과 advisor를 통해 프록시를 만들고 프록시를 대신 빈에 등록한다.

@Bean
fun itemController(logService: LogServiceDirtyCode): ItemController {
    val proxyFactory = ProxyFactory(ItemControllerRealSubject(itemService(logService)))
    proxyFactory.addAdvisor(getAdvisor(logService))
    return proxyFactory.getProxy() as ItemController
}

 

728x90
반응형
728x90
반응형

 

문제 발생 

프로젝트에서 12시마다 특정 주기를 가진 아이템을 새로 생성해주는 스케쥴러가 있다.

서버를 k8s에  pod가 4개인 replica로 배포했는데 12시마다 4개의 아이템이 중복 생성되는 것을 확인.

 

해결 방법 구색

Shced Lock을 통해 하나의 작업에 대해 여러 스케줄러가 동시에 접근하지 못하도록 Lock을 걸 수 있음 

 

SchedLock이란 

스프링 스케줄러가 태스크를 시작하기전에 (아마 AOP 를 통해서겠지) DB에 요청을 보내 Lock이 걸렸는지 확인한다.

Lock이 걸렸다면 태스크를 Wait(기다리기)하지 않고 Drop(아에 실행 시키지 않음)한다.

Lock이 걸리지 않았다면 task를 수행한다.

SchedLock은 DB table 을 필요해 하는데 다음과 같다.

@Document(SchedLock.COLLECTION_NAME)
data class SchedLock(
	@Id
    val name: String,
    val lockUntil: LocalDateTime,
    val lockedAt: LocalDateTime,
    val lockedBy: String
) {
    companion object {
        const val COLLECTION_NAME = "schedLock"
    }
}

 

 ShcedLock이 Lock에 대한 조회를 할때 name을 사용하기 때문에 name은 unqiue해야한다. (name이 unique 하지 않다면 Lock에 대한 정보를 가져올 수 없겠지요?)

 ShcedLock은 Lock을 걸수 있는 최대시간과 적어도 Lock을 유지시켜주는 최소시간을 설정 할 수 있게해준다.

 최대시간은 Lock에 데드락이 걸리거나 stuck이 된 경우 Lock을 풀어준 역할을 하고\

 최소시간은 락이 최소시간안에 풀리지 않게 하는 역할을 한다.

 

 @EnableSchedulerLock

ShcedLock을 활성화시켜주는 Annotation으로 defaultLockAtMostFor(아까 말한 최대시간) 과 defaultLockAtLeastFor (최소시간)을 attribute로 가진다. 

예를들어 최대시간을 10초 최소시간을 1초로 잡는다면
@EnableSchedulerLock(defaultLockAtMostFor = "PT10S", defaultLockAtLeastFor = "PT1S") 다음과 같이 설정하면된다. 

 

 @SchedulerLock

 @SchedulerLock은 스케줄러 태스크 함수위에 명시하며 해당 스케줄러는 태스크를 실행시키기전에 Lock에 여부를 확인한다. 똑같이 lockAtMostFor (최대시간) 과 lockAtLeastFor (최소시간)을 attribute로 가지며 명시하지 않는다면 @ EnableSchedulerLock에서 설정한 default 값이 들어간다.

 

 

구현 코드

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT10S", defaultLockAtLeastFor = "PT1S")
class SchedulerConfig(
    private val mongoTemplate: MongoTemplate
) {

    @Bean
    fun lockProvider(): LockProvider {
        val lockCollection = if (mongoTemplate.collectionExists(SchedLock::class.java)) {
            mongoTemplate.getCollection(SchedLock.COLLECTION_NAME)
        } else {
            mongoTemplate.createCollection(SchedLock::class.java)
        }
        return MongoLockProvider(lockCollection)
    }
}
@Scheduled(cron = "0 5 0 * * *", zone = "Asia/Seoul")
@SchedulerLock(name = "taskService_task")
fun task() {
    doSomething()
}

 

 

    implementation("net.javacrumbs.shedlock:shedlock-spring:4.44.0")
    implementation("net.javacrumbs.shedlock:shedlock-provider-mongo:4.44.0")

 

참조:

 https://bootcamptoprod.com/reliable-task-scheduling-with-shedlock/#using-shedlock-with-spring-scheduler-and-mongodb

 https://github.com/lukas-krecan/ShedLock

728x90
반응형
728x90
반응형

How to use switch case on spring mongo data mongotemplate

 

프로젝트에서 mongodb를 사용하는데 

project시 분기를 태워서 나타내야하는 field가 존재했다. 
예를들어 

 student {

id: ObjectId,

name: String,

stauts: int,

} 이라는 객체가 있다고 가정하자 

status가
0일때 ABSENT,
1 일때 ATTENDANCE,
-1 일떄 FIRE 로 표현해야한다. 

물론 data를 그냥 받아서 for문을 돌려서 바꿔줄 수 있지만 project를 통해 충분히 구현이 가능하고 성능으로 봐도 더 좋을 것이라 생각했다. 
mogodb stage에서는 아주 간단했다.
이곳을 참조하면 쉽게 구현할 수 있다.  https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/

 

$switch (aggregation) — MongoDB Manual

Docs Home → MongoDB Manual $switchEvaluates a series of case expressions. When it finds an expression which evaluates to true, $switch executes a specified expression and breaks out of the control flow.$switch has the following syntax:$switch: { branches

www.mongodb.com

이것을 Spring data mongo의 mongoTemplate에서 어떻게 사용할지가 고민이 되었다.  구글링을 해보거나 GPT에게 시켜보았을 때 마땅히 Project와 swtich에 대한 내용이 없거나 틀리고 Document 단으로 내려가는 코드가 많아 작성하게되었다.

Project에서  ConditionalOperators.switchCases 라는 operator를 지원하길래 순조롭게 진행 될 줄 알았으나 CaseOperator.`when`()의 인자의 type이 Criteria가 아닌 AggregationExpression이여서 애좀 써서 코드를 작성했다.

 Aggregation.project().and(
                ConditionalOperators.switchCases(
                    CaseOperator.`when`(ComparisonOperators.valueOf("status").equalTo(ToInt.toInt("0"))).then("ABSENT"),
                    CaseOperator.`when`(ComparisonOperators.valueOf("status").equalTo(ToInt.toInt("1")))
                        .then("ATTENDANCE"),
                    CaseOperator.`when`(ComparisonOperators.valueOf("status").equalTo(ToInt.toInt("-1")))
                        .then("FILRE"),
                ).defaultTo("ABSENT")

 

건승을 빕니다.

728x90
반응형
728x90
반응형

스프링 & 코틀린 프로젝트에서 

gradle build가 자꾸

'compilejava' task (current target is 11) and 'compilekotlin' task (current target is 1.8) jvm target compatibility should be set to the same java version. error가 뜨면서

stuck이 되는 문제가 있었다.

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

이 적혀있음에도 계속 오류가났다. 

그래서 java 버전이 잘못되었나 확인도 해보았고, gradle에 이것저것도 추가해보고 컴퓨터도 껏다켜보고 intelij setting gralde도 다시 로드해보고 했으나 계속 문제가 반복되었다.

 

해결방법은 .idea를 지우고 다시 로드했더니 해결되었다.. 

망할 intelij 버그였다.

 

 

 

728x90
반응형
728x90
반응형

파일, 이미지, 폴더 등  저장소로 IPFS 를 사용하고 있는데 
스프링 빈으로 등록하여 사용시 자꾸 IPFS 데몬이 꺼지는 문제점이 발생

코드는 다음과 같다.

@Configuration
class IpfsConfig(
    private val appProperties: AppProperties,
) {

    @Bean
    fun ipfs(): IPFS{
        return IPFS(appProperties.ipfsHost, 5001)
    }
}

왜 그런지 고민을 좀 해보다가 @Bean의 destroy method가 shutdown이나 close를 추론해서 자동으로 빈이 사라지기 전에 호출한다는 것이 기억이 났다. 찾아보니 IPFS Class에 shutdownMethod가 있었고 이것을 자동으로 호출해서 발생하는 문제였다 .

다음과 같이 수정하면 된다.

@Configuration
class IpfsConfig(
    private val appProperties: AppProperties,
) {

    //자동으로 shutdown을 호출해서 daemon을 shutdown 시킴
    @Bean(destroyMethod = "")
    fun ipfs(): IPFS{
        return IPFS(appProperties.ipfsHost, 5001)
    }
}

 

https://github.com/ipfs-shipyard/java-ipfs-http-client

 

 

 

728x90
반응형
728x90
반응형

개발 도중 @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 을 사용하여

isDeleted를 is_deleted로 보냈는데 날라가는 메세지를 보니 deleted로 날라갔다.

찾아보니 직렬화 하는 도중에 is가 제거된다.. 

@get:JsonProperty("is_deleted") 
@param:JsonProperty("is_deleted")
 var isDeleted: Boolean? = false,

를 사용하면 is를 사용할 수 있다.

 

참고로 요즘 개발할 때에는 형용사 앞에  is 붙이는 걸 선호하지 않는다. 어차피 값은 true 혹은 false일 것이기 떄문.

 

 

728x90
반응형

+ Recent posts