분산 락(Distributed Lock)이란
여러 개의 서버(분산 환경)에서 하나의 리소스(데이터, 파일, 프로세스 등)에 대한 동시 접근을 제어하는 락
싱글 서버 환경에서는 synchronized, ReentrantLock 등의 Java 락을 사용하면 되지만,
멀티 서버 환경에서는 서로 다른 서버에서 동일한 리소스에 접근할 가능성이 있기 때문에
공유 저장소(Redis, Zookeeper, DB 등)를 이용한 락이 필요합니다.
- 분산 락은 여러 서버에서 동일한 리소스에 접근하지 못하도록 제어하는 기능
- Redis, Zookeeper, DB를 활용하여 구현 가능하지만, Redis 기반 락이 속도와 유지보수 측면에서 유리
- Redisson 라이브러리를 활용하면 TTL 및 락 해제 보장 기능이 포함되어 있어 안정적인 운영 가능
- TTL을 잘못 설정하면 락이 영구적으로 걸릴 위험이 있으므로 설정 시 주의 필요
분산 락이 필요한 이유
> 동시성 문제 발생
👉 예를 들어, AI 기반 웹툰 생성 서비스에서 동일한 웹툰 생성 요청이 여러 번 들어온 경우를 가정해 보겠습니다.

위와 같은 상황이 발생하면, 하나의 웹툰 프로젝트에 대해 중복으로 3번 생성이 실행되는 문제가 발생할 수 있습니다.
이를 방지하기 위해 분산 환경에서도 하나의 요청만 실행되도록 보장하는 기법이 분산 락입니다.
👉 SSE 기반 웹툰 생성 서비스에서 중복 생성 방지를 위해 Redis 기반의 분산 락(Redisson)을 적용하는 것이 가장 적합합니다. 🚀
분산 락의 주요 기능
- 동일한 리소스에 대해 한 번에 하나의 서버만 접근 가능하도록 보장
- 서버 장애 시 락이 자동으로 해제되도록 TTL(Time-to-Live) 설정 지원
- 락 획득 실패 시 대기 및 재시도 기능 제공
분산 락 적용 시 고려할 점

분산 락을 구현하는 방법

1. Redis를 활용한 분산 락
1) Redis SETNX (Set if Not Exists) 사용
Redis에서는 SETNX (SET if Not Exists) 명령어를 사용하여 락을 구현할 수 있습니다.
하지만 이 방식만 사용하면 서버 장애 시 락이 해제되지 않는 문제가 발생할 수 있습니다.
따라서, Redisson과 같은 라이브러리를 사용하면 자동 해제 기능까지 제공됩니다.
(1) 락 획득
SET webtoon_processing_lock:123 "locked" NX PX 30000
- NX: 키가 존재하지 않을 경우에만 설정 (즉, 이미 락이 존재하면 설정되지 않음)
- PX 30000: 30초 후 자동 만료 (TTL 설정)
(2) 락 해제
DEL webtoon_processing_lock:123
Redisson?
Redis 기반의 분산 락을 쉽게 사용할 수 있도록 제공하는 라이브러리
✅ DB가 아닌 Redis에서 락을 설정하는 코드
✅ Redisson을 활용하여 여러 개의 Redis 락을 하나의 락처럼 관리하는 기능을 수행
✅ Redis의 SETNX, EXPIRE, DEL 등을 사용하여 락을 획득하고 해제
✅ DB 트랜잭션과는 무관하며, DB 자체를 락하는 기능은 포함되지 않음
- private final RedissonClient redissonClient;
- RLock lock = redissonClient.getLock("webtoon_processing_lock:" + projectId);
- boolean available = lock.tryLock(5, 30, TimeUnit.SECONDS);
- lock.unlock();
RedissonMultiLock -> Redis Lock 사용
- Redis의 SETNX + EXPIRE를 이용한 분산 락
- tryLockAsync() 메서드에서 lock.tryLockAsync()를 호출하여 Redis에 락을 시도합니다.
- 내부적으로 Redis의 SETNX(set if not exists) 명령어를 사용하여 락을 설정합니다.
- TTL(Time-To-Live)을 설정하여 자동 해제 기능을 추가합니다.
- 락의 유지 및 해제도 Redis에서 수행
- lock.unlockAsync()를 호출하면 Redis에서 해당 락 키를 삭제하여 락을 해제합니다.
- expireAsync()를 사용하여 TTL을 연장할 수도 있습니다.
- 여러 개의 Redis 락을 하나로 묶어 관리
- RedissonMultiLock은 여러 개의 RLock을 관리하므로, 여러 개의 Redis 락을 하나의 락처럼 동작하도록 합니다.
- 예를 들어, A, B, C 세 개의 Redis 락이 있을 때, A와 B를 획득했지만 C를 획득하지 못하면 A와 B도 해제하도록 관리합니다.
데이터베이스(DB)를 락하지 않는 이유
DB와 직접적인 관련이 없습니다.
DB 락을 걸려면 일반적으로 트랜잭션을 이용한 SELECT ... FOR UPDATE, LOCK TABLE 같은 DB 락 기능을 사용해야 합니다.
Redis에 저장된 키-값을 이용하여 락을 관리하는 것이고, 데이터베이스의 행(row)이나 테이블을 직접 락하는 것이 아닙니다.
RedissonClient

RLock

MultiLock
- RLock을 구현체
Redis를 락하는 것이지, DB(데이터베이스)를 락하지 않음
- 여러 개의 RLock을 그룹화하고 하나의 락처럼 동작하도록 관리하는 기능
- Redis에 여러 개의 키를 사용하여 락을 설정하고, 이 락들을 하나의 트랜잭션처럼 다루는 기능


2. Java에서 Redisson을 이용한 분산 락
✅ Gradle에 redisson-spring-boot-starter 추가
✅ Spring Boot에서 RedissonClient를 빈으로 등록
✅ Redis를 활용한 분산 락 적용
✅ Redisson의 RLock을 활용하여 동시 실행 방지
👉 여러 서버에서 동일한 웹툰 생성 요청이 동시에 실행되지 않도록 방지할 수 있음! 🚀
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.webtoonmaker'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.redisson:redisson-spring-boot-starter:3.23.3'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml
spring:
data:
redis:
port: 6379
host: localhost
redisson-config.yml
singleServerConfig:
address: "redis://localhost:6379"
password: null
database: 0
timeout: 3000
connectionMinimumIdleSize: 10
connectionPoolSize: 64
RedissonConfig 설정
package com.webtoonmaker.api;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 단일 Redis 서버를 사용하는 설정-> Redis 서버 주소 지정
config.useSingleServer().setAddress("redis://localhost:6379");
// 클러스터 환경이라면 .useClusterServers()
// config.useClusterServers()
// .addNodeAddress("redis://node1:6379", "redis://node2:6379");
return Redisson.create(config);
}
}
package com.webtoonmaker.api;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/**
* docker run --name redis -d -p 6379:6379 redis
* curl -X POST "http://localhost:8080/webtoon/generate?projectId=123"
* Redis에서 확인 >>> redis-cli >> KEYS *
*/
public class WebtoonService {
private final RedissonClient redissonClient;
public WebtoonService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void generateWebtoon(String projectId) {
RLock lock = redissonClient.getLock("webtoon_processing_lock:" + projectId);
try {
// 락 획득 시도 (최대 5초 대기, 30초 후 자동 해제)
boolean available = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!available) {
throw new RuntimeException("이미 실행 중입니다.");
}
// 웹툰 생성 실행
System.out.println("웹툰 생성 시작: " + projectId);
Thread.sleep(10000); // 웹툰 생성 로직 (10초 대기)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 락 해제
lock.unlock();
System.out.println("웹툰 생성 완료: " + projectId);
}
}
}
'학습 기록 (Learning Logs) > Today I Learned' 카테고리의 다른 글
인공지능의 진화 (0) | 2025.03.02 |
---|---|
reddit 기능 파악 (0) | 2025.03.01 |
난생 처음 인공지능 입문 (0) | 2025.02.22 |
nestJs (0) | 2025.01.26 |
MDC (0) | 2024.12.26 |