[TIL] 최종 프로젝트 (16) - 분산 락 제대로 구현해보기

J쭈디·2025년 3월 6일
0

Sparta_프로젝트

목록 보기
27/35

새벽 내리 분산 락이 적용이 제대로 안된 걸 보고, 아무래도 동기화가 직후에 되는 문제가 크다고 판단이 되어 이번에는 배치를 써보려고 한다.

집계테이블로 업데이트할 때 어느정도 값이 쌓이고 업데이트 되도록 하는 것이다.

1. 분산 락 개선하기

1. 에러파티를 해결하자

와아아... 부하 테스트가 계속 터져버리고 있다. 심지어 어제는 velog도 터져서 이미지도 찾는데 오래걸렸다. (눈물펑펑)
앞으로는 Jmeter에 이름 제대로 써놔야지.. 어떤 결과가 어떤 이미지인지 찾는데 너무 오래 걸렸다.

그리고 정말 바보같은 실수인데, 분산 락 걸어놓고 비관적 락을 안 뺐다는 걸 깨달았다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
정말 바보같은 실수라서 그 부분을 빼고 다시 해보기로 했다.

그 부분을 제대로 하니까 실행은 되는데 그래도 누락이 발생하는 걸 찾았다. 도대체 뭐지 하고 둘러보다가 레디스에서 값을 가져오고 직후에 reset 하는 거에서 문제가 발생할 수 있다는 걸 알게되었다.

    public String updateViewCount(Long jobOpeningId) {
        String lockKey = "JOB_OPENING_VIEW_COUNT_" + jobOpeningId;
        int retryCount = 3; // 최대 3번 재시도
        boolean success = false;

        while (retryCount-- > 0) {
            success = redisDistributedLockManager.tryLockAndRun(
                lockKey,
                10,
                5,
                TimeUnit.SECONDS,
                () -> 
                    // 실제 처리할 비즈니스 로직
                
            redisViewCountManager.resetViewCount(jobOpeningId);

            if (success) {
                // 락 획득에 성공한 케이스
                log.info("JobOpeningId: {} 락 획득 로직 실행 완료", jobOpeningId);
                return "success";
            }

            log.warn("JobOpeningId: {} 락 획득 실패", jobOpeningId);
            try {
                Thread.sleep(100);// 1초 대기
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        log.error("JobOpeningId: {} 조회수 동기화 최종 실패 (모든 재시도 실패)", jobOpeningId);
        return "fail";
    }

원래는 이런 식으로 실제 로직 처리 후, 직후에 리셋을 하는 방식이었는데 이게 락 획득 후 로직 실행 성공일 때만 아니라 락 획득에 실패한 경우에도 리셋이 되는 듯 해서 아래쪽 if문 안에 넣어봤다.

            if (success) {
                // 락 획득에 성공한 케이스
                log.info("JobOpeningId: {} 락 획득 로직 실행 완료", jobOpeningId);
                redisViewCountManager.resetViewCount(jobOpeningId);
                return "success";
            }

이런 식으로 넣었는데 이번엔 제발 정합성이 높아졌으면 좋겠다. (눈물)

근데 이래도 안되길래 여기다 지연을 좀 줘봤다. 몇 초 정도 있다가 초기화 되는 걸로 지연을 해보기로 했다.

어,,,, 이거 리셋 문제가 아닌 거 같다는 생각이 든다. 애초에 업데이트에서 문제가 생긴건가?? (먼 산...) 락이 잘못 걸린 거 같다는 생각이 들면서 두통이 오기 시작했다. ㅋㅋㅋㅋ

2. Jmeter와 실제가 다른 이유 찾기

일단 나는 에러를 잡은 후로는 jmeter에서는 문제가 하나도 없었다. 걍 편하게 통과처리 된거다. 근데 이게 Jmeter로는 메시지만 받으면 통과인데, 이게...

뭔가 이상타, 이상타 하니 결국 이유는 단순히 Save를 안해서!! 였다.
근데 왜 값이 들어가긴 했지? (아이러니)

아무튼 save 적용하고, 레디스를 직접 데이터 소스로 볼 수 있는 방법도 튜터님께서 알려주셔서 추가했고 나름대로 해결이 되어가고 있다.

단, 현재 코드는 레디스 -> 실시간 집계테이블로 동기화(문제발생 가능) -> 채용공고 테이블 이런 순서인데,
중간에 실시간 집계테이블로 동기화하는 걸 삭제해버릴 예정이다.

왜냐하면, 레디스가 sort 등의 기능으로 인해 조회수 관리에 나름 좋다는 걸 알았기 때문이다. 이제 엘라스틱 서치가 적용된 인기순 검색에 대해 기존 DB의 viewCount 값이 적용 되어 있었기 때문에 이 부분을 동기화 하는 주기를 정해야한다고 한다.

현재는 분산 락을 해보려고 나름 애썼는데, 일단 이 부분은 비교용으로 커밋을 해 두기로 했다.

    public String increaseViewCount(Long jobOpeningId) {
        redisViewCountManager.increaseViewCount(jobOpeningId);
        String result = updateViewCount(jobOpeningId);
        return result;
    }

    /**
     * 레디스에 있는 채용공고 조회수를 집계테이블로 업데이트 하는데 사용하는 메서드
     * @param jobOpeningId
     */
    @Transactional
    public String updateViewCount(Long jobOpeningId) {
        String lockKey = "JOB_OPENING_VIEW_COUNT_" + jobOpeningId;
        int retryCount = 3; // 최대 3번 재시도
        boolean success = false;

        while (retryCount-- > 0) {
            success = redisDistributedLockManager.tryLockAndRun(
                lockKey,
                100,
                500,
                TimeUnit.SECONDS,
                () -> {
                    // 실제 처리할 비즈니스 로직
                    Long redisviewCount = redisViewCountManager.getViewCount(jobOpeningId);
                    JobOpeningViewCount jobOpeningViewCount = findByJobOpeningViewCountInJobOpeningId(jobOpeningId);
                    jobOpeningViewCount.increaseViewCount(redisviewCount);
                });

            if (success) {
                // 락 획득에 성공한 케이스
                log.info("JobOpeningId: {} 반영조회수: {} 락 획득 로직 실행 완료", jobOpeningId , redisViewCountManager.getViewCount(jobOpeningId));
                return "success";
            }

            log.warn("JobOpeningId: {} 락 획득 실패", jobOpeningId);
            try {
                Thread.sleep(100);// 1초 대기
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        log.error("JobOpeningId: {} 조회수 동기화 최종 실패 (모든 재시도 실패)", jobOpeningId);
        return "fail";
    }

    private JobOpeningViewCount findByJobOpeningViewCountInJobOpeningId(Long jobOpeningId) {
        JobOpeningViewCount jobOpeningViewCount = jobOpeningViewCountRepository.findByJobOpeningId(
                jobOpeningId)
            .orElseGet(() -> {
                JobOpening jobOpening = jobOpeningFindByService.findById(jobOpeningId);
                return jobOpeningViewCountRepository.save(
                    JobOpeningViewCount.create(jobOpening));
            });
        return jobOpeningViewCount;
    }

나름 애쓰며 분산 락을 구현해봤는데, 동기화 부분에서 만족스럽지 않아서 조금 아쉬웠다. 그리고 레디스를 이렇게 애써서 구현한 후, 그것에 대한 또 다른 피드백을 받았는데... 바로, 갈아엎으란 얘기였다.

2. 레디스에서 엘라스틱 서치까지

오늘은 정말 파란만장한 하루였다. 분명 나는 레디스와 분산 락을 이용한 동시성 제어를 엄청 열심히 했는데, 막상 그걸로 나름 완성을 시키고 나니 다른 문제가 튀어나왔기 때문이다. 그래서 현재 도대체 어떻게 내가 기술적 의사결정을 해왔는지, 또 앞으로는 무엇을 할지 한 번 정리해보고 가기로 했다.

1. 레디스에 대한 기술적 의사결정

처음에는 비관적 락의 테이블 분리 말고 레디스와 분산 락을 사용하여 다중 서버 환경에서도 락이 적용되게 하려고 했었다. 그래서 레디스에서 조회 수를 집계한 후에 분산 락을 이용하여 집계 테이블에 동기화되도록 설정했다.

이 때 직후에 동기화 하는 방식이 정합성 보장도 안되기 때문에 효율적이지 않다는 판단이 되었고, 분산 락을 이용하는 게 아니라 배치를 이용하여 조회 수를 업데이트 하는 방식을 택하기로 했다. 게다가 집계테이블 또한 비관적 락을 사용하지 않는다면 굳이 별도로 두면서 스케줄러를 돌아가게 하는게 전체적인 로직 상(현재 이메일 알림 등 조회 수를 제외한 몇 가지 스케줄러가 더 돌아가고 있음) 불필요한 DB 부하만 유발시킬 것이라는 생각이 들었다.

그리하여 레디스를 집계테이블 대신 구현하되, 정기적으로 채용공고 테이블에 배치 방식으로 업데이트 해보기로 했다. 그런데 여기서 한 가지 문제가 더 있었는데 현재 우리 로직에서는 엘라스틱 서치로 사용자에게 맞춤형 검색을 지원하는 시스템을 개발하고 있었고, 거기에 조회 수가 인기 순으로 검색할 수 있어야 했다.

그런 이유로 인기 검색을 위해 DB에 업데이트 빈도를 엘라스틱 서치의 인덱스 빈도에 따라 조절해야 할 필요가 있어서 그 부분을 개발하는 팀원에게 그 질문을 했다. 그리고 나는 피눈물을 흘렸다. 나는 엘라스틱 서치 부분을 100% 이해하지 못한 상태였으므로 잘 모르는 상태에서 질문을 했는데 현재 엘라스틱 서치와 DB 동기화가 실시간으로 이루어지지 않고 있다는 문제를 겪는다는 걸 알게되었다.

2. 엘라스틱 서치에 대한 기술적 의사결정

결과적으로는 레디스와 조회 수 문제보다도 더 시급한 문제인 엘라스틱 서치와 DB 동기화 문제가 대두되었다. 아무리 레디스를 사용하여 조회 수의 동시성을 보장한다고 해도 일단 엘라스틱 서치의 동기화 문제가 해결되지 않는다는 맹점이 있었기 때문에 그 부분을 해결하면서도 조회 수의 동시성을 해결할 수 있는 방식을 찾아야 했다.

그리고 회의와 튜터님 피드백 등을 반영하여 조회가 발생하게 되면, elasticsearch data stream을 이용하여 조회 이벤트를 타임스탬프 포함하여 저장하게 하고, 1분 간격으로 elasticsearch 데이터를 DB로 업데이트 하는 로직으로 변경하기로 하였다.

오늘 밤도 참 밤이 길 것 같다. 도저히 제대로 잠 못 드는 밤일 것이 확실하다.
이제 elasticsearch data stream을 공부하러 가봐야겠다.

<출처>
https://velog.io/@juhyeon1114/Spring-RedisRedisson-분산락을-활용하여-동시성-문제-해결하기
https://mangkyu.tistory.com/311
https://innovation123.tistory.com/185
https://wildeveloperetrain.tistory.com/280
https://velog.io/@wndid2008/TIL-최종-프로젝트-15-Redis와-분산-락을-이용한-동시성-제어

profile
언제 어느 위치에 있더라도 그 자리의 최선을 다 하는 사람이 되고 싶습니다.

0개의 댓글