<문제 상황>
https://backend-newbie.tistory.com/44 , 이전 글에서 API를 설계해 보면서 분산락이 필요해 보이는 상황이 생겼다.

사용자가 해당 API를 호출 했을 때, 응답에 필요한 데이터가 준비된 상황이라면 200 OK와 응답 데이터를, 준비되지 않은 상황이라면 202 Accepted를 응답으로 보내주고 서버 내부에서 데이터를 준비하는 작업을 실행하는 상황이다.

어떤 사용자가 최초로 맞팔탐지기 조회 요청을 하게 되면, 실제 응답을 위해 데이터를 받아오는 데 5초 정도의 시간이 걸린다고 가정했다.
이 때, 사용자측에서 기다리지 않고 계속 동일한 API를 호출하면, API 서버 쪽에서는 아직 응답을 위한 데이터가 캐시에 존재하지 않는다고 판단하여 계속 깃허브에서 데이터를 받아오고 캐시에 저장하는 동일한 작업을 수행하게 된다.

follow:{userId} 같은 이름의 락을 추가해서 이런 문제를 해결해 보고자 했다.

문제는 이런 락을 API 서버 내부에서 구현할 경우 API 서버가 여러대인 환경에서 똑바로 작동하지 않는 문제가 발생한다.
<MySQL 네임드 락을 이용하는 경우>
이런 문제를 해결하기 위해 가장 먼저 생각해 본 방법은 MySQL의 네임드 락을 이용하는 것이었다. 이를 이용하면 테이블이나 레코드가 아니라 사용자가 지정한 문자열에 대해 락을 획득하고 반납할 수 있다.
# follow:1 이라는 문자열에 대해 락을 획득한다.
# 이미 누군가 락을 사용중이라면 락 획득을 위해 0초 대기한다.
SELECT GET_LOCK('follow:1', 0);
# follow:1 이라는 문자열에 대해 락이 설정되어 있는지 확인한다.
SELECT IS_FREE_LOCK('follow:1');
# follow:1 이라는 문자열에 대한 락을 반납한다.
SELECT RELEASE_LOCK('follow:1');
이는 MySQL에서 함수 레벨로 위와 같이 제공하고 있다.
@Component
@RequiredArgsConstructor
public class MysqlNamedLockManager {
private final JdbcTemplate jdbcTemplate;
public boolean tryLock(String key, int timeoutSec) {
Integer result = jdbcTemplate.queryForObject(
"SELECT GET_LOCK(?, ?)", Integer.class, key, timeoutSec);
return result != null && result == 1;
}
public boolean unlock(String key) {
Integer result = jdbcTemplate.queryForObject(
"SELECT RELEASE_LOCK(?)", Integer.class, key);
return result != null && result == 1;
}
}
JDBC를 이용해 간단한 네임드 락 매니저를 만들어 실험을 진행해 봤다.
// 클라이언트에게 이미 202 Accepted 전달 완료
boolean lock = mysqlNamedLockManager.tryLock("follow:" + userId, 0);
log.info("락 경합 결과 : {}", lock);
if(lock) {
ThreadUtils.sleep(5000);
log.info("데이터 수집 작업 진행됨");
}
위와 같이 간단한 로직을 작성후 테스트 해봤다. 락 반납은 일부러 하지 않았고, 여러번의 요청을 한번에 보내면 최초 요청에서만 네임드 락을 획득하고 나머지는 모두 실패하기를 기대했다.

하지만 전혀 의도대로 작동하지 않고 중복된 데이터 수집 작업이 모두 수행되었다.

이게 무슨일인가 하고 공식 문서를 살펴보니 GET_LOCK()을 이용해 획득한 락은 세션이 종료될 때 자동으로 해제된다고 나와 있었다.

내가 테스트를 위해 만들었던 네임드락 매니저는 내부적으로 JdbcTemplate를 이용했고 이는 또 내부적으로 DB와의 커넥션을 맺기위해 DataSource 인터페이스를 생성자에서 이용하고 있었다.

실제로는 히카리CP를 이용해서 커넥션을 가져오고 있었다.

따라서 실제로는 위와 같이 커넥션 풀에서 커넥션을 획득해 DB에서 GET_LOCK을 수행하고 더 이상 해당 세션에서 진행할 작업이 없어서 세션은 종료되고 커넥션은 반환된다. 즉, 네임드 락을 획득하자 마자 락을 반납하고 있는 상황이었다.
데이터 조회 API가 호출 될 떄 해당 API에서는 별도의 스레드 풀에 깃허브 API를 이용해서 데이터를 수집해오는 작업을 제출만 하고 종료되기 때문에 @Transactional을 써서 세션을 유지 시킬수도 없었다. @Transactional을 걸어도 DB 세션을 비동기 작업이 끝날 때까지 붙잡아두는 건 불가능하다.

MySQL 공식문서를 살펴보면 한 개의 세션에서 여러개의 네임드 락을 획득 할 수 있다고 나와있다.

그래서 API 서버가 시작 될 때, 특정 커넥션 한 개를 DB와 연결해 두고 절대 종료 시키지 않고 Named Lock을 얻기 위한 전용 커넥션으로 사용하는 방법을 생각해 봤다.
문제는 이럴 경우, API 서버의 인스턴스 수 만큼 DB 서버쪽에도 죽지 않는 세션이 열려 있어야 하고 DB 서버가 다운이라도 된다면 다시 시작 되었을 때 이 세션들을 복구할 방법이 필요했다. 굳이 이런 문제들을 감수하면서 MySQL의 네임드락을 사용할 필요는 없어 보였다.
그냥 RDB에 락을 PK로 두는 테이블을 하는 구조로 만들까도 고민해 봤는데 클라이언트에서 같은 API를 데이터가 조회할때 까지 폴링 + 사용자가 강제 갱신 버튼까지 누를 수 있는 상황이라 생각보다 락 획득이 많이 발생할 것 같아서 Redis를 이용한 분산락으로넘어가기로 결정했다.
<참고 자료>
https://dev.mysql.com/doc/refman/8.4/en/locking-functions.html
MySQL :: MySQL 8.4 Reference Manual :: 14.14 Locking Functions
This section describes functions used to manipulate user-level locks. Table 14.19 Locking Functions GET_LOCK(str,timeout) Tries to obtain a lock with a name given by the string str, using a timeout of timeout seconds. A negative timeout value means infin
dev.mysql.com
https://techblog.woowahan.com/2631/
MySQL을 이용한 분산락으로 여러 서버에 걸친 동시성 관리 | 우아한형제들 기술블로그
안녕하세요. 비즈인프라개발팀 권순규입니다. 현재 광고시스템에서 사용하고 있는 MySQL을 이용한 분산락에 대해 설명드리고자 합니다. 분산락을 적용하게된 원인 현재 테이블은 아래 그림과 같
techblog.woowahan.com
'토이 프로젝트 > 깃허브 맞팔 탐지기' 카테고리의 다른 글
| 깃허브 맞팔 탐지기 [05] - Redis 글로벌 캐시 도입 (0) | 2025.08.28 |
|---|---|
| 깃허브 맞팔 탐지기 [04] - 저장할 데이터 분석 후 캐시 저장소 선정 (0) | 2025.08.27 |
| 깃허브 맞팔 탐지기 [03] - Redisson 분산락 도입 (0) | 2025.08.24 |
| 깃허브 맞팔 탐지기 [01] - 문제상황 분석 & API 설계 (0) | 2025.08.22 |