Easy Understanding

스프링부트로 페이 시스템 구축 도전해보기(초보 난이도) (6) - Spring Redis Caching, Scheduler 적용해보기 본문

Spring

스프링부트로 페이 시스템 구축 도전해보기(초보 난이도) (6) - Spring Redis Caching, Scheduler 적용해보기

appleg1226 2020. 10. 28. 19:36

초보 주의

예전 개발 경험이 적을 때 작성한 거라서 지금 저의 시점으로 보니 너무 도움이 안 되는 글이네요...

솔직히 지우고 싶으나 풋풋함을 기억하기 위해서 남겨둡니다. 

(심지어 지금 결제 도메인 개발 중이라 더 부끄러움)

 

이제 무엇을 더 적용해봐야할까

아직 시스템엔 추가해야 하는 기능과, 개선하고자 하는 것들이 남아 있습니다.

제가 이번에 적용하려고 하는 것은

 

1. 최근에 기록된 거래들만 빠르게 확인할 수 있는 캐싱 시스템

2. 하루 내에 송금을 받지 않았을 경우에 송금 취소하고 원래대로 돌려놓기

 

위와 같은 두 가지 기능을 기존 시스템에 보완해볼 예정입니다.

 


간단한 캐싱 적용해보기

캐싱은 자주 사용되는 데이터베이스에 대한 빠른 접근을 위해 사용합니다.

 

여기에서는 거래 내역을 조회하는 것에 캐싱을 적용해 읽는 속도를 빠르게 하려고 합니다.

 

최근 거래가 오래된 거래보다 조회 빈도가 높을 것이기 때문에

 

3일 이내에 조회를 한 결과에 대해서 캐싱을 하도록 하겠습니다.

 

캐싱에 많이 이용되는 데이터베이스라면 Redis를 바로 꼽을 수 있습니다.

Redis는 메모리에 저장되는 DB이기 때문에 빠르고 가볍다는 점이 장점이기 때문에 캐싱을 목적으로

사용되는 경우가 많습니다.

 

그래서 저는 Spring Data Redis를 의존성에 추가해주고

Caching 기능을 추가하도록 하겠습니다.

Redis는 docker를 통해서 설치해줬고,

기본 포트를 사용하면 스프링에서 알아서 Redis를 찾아서 연결합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

 

그리고 메인 클래스에는 @EnableCaching 어노테이션을 붙여줍니다.

@SpringBootApplication
@EnableCaching
public class UserServiceApplication{
	... 생략
}

 

다음으로는 캐싱에 대한 설정을 따로 RedisCacheConfig.java 에 진행했습니다.

설정은 특별한 것은 추가하지 않았으며,

entryTtl에 72시간을 넣어 줌으로서, 캐시의 유효기간을 설정해 주었습니다.

@Configuration
public class RedisCacheConfig {

    @Autowired
    RedisConnectionFactory connectionFactory;

    @Bean
    public CacheManager redisCacheManager() {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofHours(72));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(redisCacheConfiguration).build();
    }
}

 

이제 위의 설정을 가지고 아래처럼 각 메서드에 @Cacheable 을 넣어주면, 

이제 이 메서드에서는 캐싱이 진행됩니다.

@Cacheable(value = "exchange", key = "#userId")
public List<Exchange> findAllExchanges(String userId){
    return exchangeRepository.findAllByMyId(userId)
            .stream()
            .sorted(Comparator.comparing(Exchange::getExchangeDate, Comparator.reverseOrder()))
            .collect(Collectors.toList());
}

순서는 다음과 같습니다.

1. 만약 Redis에서 해당 key값에 해당하는 List<Exchange>를 찾을 수 없으면 메서드를 실행한 뒤 그 값을

   Redis에 저장한다.

2. 만약 Redis에서 해당 key값에 해당하는 List<Exchange>를 찾았으면 메서드를 거치지 않고 바로 그 값을

   반환한다.

3. 만약 결과가 업데이트되면 그 값을 Redis cache에 반영한다

4. 유효기간이 지나면 없어진다.

 

캐싱을 적용하는 과정은 쉽습니다.

그렇지만 성능적으로 어떤 것에 캐싱을 적용해야 효율적인지를 파악하고 그 세부 설정을 정하는 것은

더 세세하게 공부하며 적용해야 합니다.


완료되지 않은 송금 요청 취소하기(Kotlin)

앞에서 송금 요청을 했을 때의 단계는 다음과 같았습니다.

- 보낸 사람의 계좌에서는 빠져나간다.

- 그리고 그 상태는 아직 isComplete = false 상태이다.

 

그러나 만약에 하루가 지나도 그것이 false 상태로 남아있다면 이 송금 요청을 다시 되돌려야 합니다.

 

그래서 어떻게 해야할지 곰곰히 생각해봤습니다.

이걸 언제마다 검사를 해야하지?

이걸 어떤 방법과 흐름을 이용해야하지? 라는 생각을 해봤고,

 

저의 결론은 Spring Scheduler를 이용하는 것이었습니다.

스케쥴러는 내가 원하는 시간과 간격마다 내부 메서드를 자동으로 실행시켜 줍니다.

 

그래서 Scheduler를 이용해서 매 분, 어제 동시간대에 진행된 송금 요청 중 false인 요청을 찾아서

그것을 취소하는 작업을 만들기로 했습니다.


Spring Scheduler를 이용하여 취소 처리하기

스프링 Scheduler를 사용하기 위해서는 main 클래스에 @EnableScheduling을 선언해줘야 합니다.

@SpringBootApplication
@EnableScheduling
class TransferServiceApplication

이렇게 한 다음 클래스를 하나 만들고 그 내부를 다음과 같이 만듭니다.

@Component
class TransferChecker @Autowired constructor(val transferHandler: TransferHandler){

    companion object{
        private val log = LoggerFactory.getLogger(DefaultController::class.java)
    }

    @Scheduled(cron = "0 0/1 * * * *", zone = "Asia/Seoul")
    fun checkNotCompleted(){
        val nowDateTime = LocalDateTime.now()
        log.info("now time: $nowDateTime")
        transferHandler.cancelNotCompleted(nowDateTime)
    }
}

 

이름은 TransferChecker라는 클래스로 만들었고, TransferHandler라는 service 클래스를 주입 받았습니다.

주로 사용될 메서드는 checkNotCompleted라는 메서드이며 위의 Scheduled를 선언하면 됩니다.

 

이렇게 되면 해당 어노테이션이 표기된 메서드는 하나의 독립된 쓰레드에서 계속해서 실행됩니다.

 

cron = " 0 0/1 * * * * " 이렇게 되어있는 것은 얼마의 주기로 아래의 메서드를 실행할 것인지를

나타낸 것입니다.

자세한 cron식은 여기에서는 다루지 않겠습니다.

다만 제가 한 것은 1분에 한 번씩 내부 메서드를 실행하는 것을 나타낸 표현식입니다.

1분에 한 번씩, 해당 메서드를 실행합니다.

 

transferHandler.cancelNotCompleted는 부적으로는 다음의 세 단계로 진행이 됩니다.

fun cancelNotCompleted(nowDateTime: LocalDateTime){
    // 1. 하루가 지났지만 현재 끝나지 않은 송금 찾기
    val resultList = findNotCompleted(nowDateTime)

    // 2. 보낸 사람에게 돈 돌려 보내기
    resultList.map { exchange -> refundOne(exchange)}

    // 3. 보낸 사람들에게 문자 보내기
    resultList.map { exchange -> messageSender.sendUncompletedMessage(exchange) }
}

 

1. 먼저 DB에서 하루 전 이 시간에 요청된 송금 요청들을 찾습니다.

 

2. 그리고 그 리스트를 map과 람다를 이용해서 각 요청별로 돈을 보낸 사람에게 돌리는 함수를 실행합니다.

 

3. 그리고 그 리스트를 가지고 이번엔 요청이 취소되었다는 메시지를 보내는 함수를 실행합니다.

 

private fun findNotCompleted(nowDateTime: LocalDateTime) : List<Exchange> {
    val date1: LocalDateTime = LocalDateTime.of(nowDateTime.year, nowDateTime.month, nowDateTime.dayOfMonth - 1,
            nowDateTime.hour, nowDateTime.minute, 0)
    val date2: LocalDateTime = LocalDateTime.of(nowDateTime.year, nowDateTime.month, nowDateTime.dayOfMonth - 1,
            nowDateTime.hour, nowDateTime.minute + 1, 0)
    return exchangeRepository.findAllByExchangeDateBetweenAndIsCompleteFalse(date1, date2)
}

private fun refundOne(exchange: Exchange){
    val result = payUserRepository.findByIdOrNull(exchange.myId) ?: throw NoSuchElementException("Not Found User")
    result.payMoney += exchange.money
    payUserRepository.save(result)
    log.info(result.userId + "의 계정에 돈을 환불했습니다.")
}

 

위의 람다식에서 사용하는 첫 번째 메서드는 어제 동일 시간에 이루어진 요청들을 찾는 메서드입니다.

내부의 findAllByExchangeDateBetweenAndIsCompleteFalse(date1, date2) 는

JPA query method문법을 따른 메서드입니다.

find All By (ExchangeDate between date1 and date2) and (iscomplete is false)

라고 나눠볼 수도 있을 것 같습니다.

 

메서드를 저렇게 작성하면 스프링에서 알아서 쿼리가 날아갑니다.

 

두 번째 메서드는 해당 유저의 돈을 다시 추가시키는 메서드입니다.

이렇게 진행하면 매 분 각 송금 요청들을 처리할 수 있습니다.

 

 

(실제 구현 코드)

github.com/appleg1226/sample-pay-system