0. ๋์์ฑ ์ด์์ ํด๊ฒฐ ๋ฐฉ๋ฒ
๋์์ฑ ์ด์๋ ์ฌ๋ฌ ์ค๋ ๋๋ ํ๋ก์ธ์ค๊ฐ ๋์์ ๊ฐ์ ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ค ํ ๋ ๋ฐ์ํ๋ค.
๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์๋ค.
์ด๋ฒ ํ๋ก์ ํธ์์ ๋ฐ์ํ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํด ๋ณด๋ ค๊ณ ํ๋ค.
๋จผ์ , Spring Boot ํ๊ฒฝ์์ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ ์ ์๋ ๋ช ๊ฐ์ง ๋ฐฉ๋ฒ์ ์์๋ณด๊ฒ ๋ค.
0-1. ๋ฝ (Lock)
๋ฝ์ ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ๊ณต์ ๋ฆฌ์์ค์ ๋ํ ๋์ ์ ๊ทผ์ ์ ์ดํ๋ ๋๊ธฐํ ๋ฉ์ปค๋์ฆ์ด๋ค.
๋ฝ์ ํตํด ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ์ ์งํ๊ณ Race Condition์ ๋ฐฉ์งํ๋ค.
0-1-1. ๋๊ด์ ๋ฝ (Optimistic Lock)
๋๊ด์ ๋ฝ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋ ๋ฝ์ ๊ฑธ์ง ์๋๋ค. ๋ณดํต ๋ฒ์ ๋ฒํธ๋ ํ์์คํฌํ๋ฅผ ์ด์ฉํ์ฌ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ ๊ฐ์งํ๋ค.
์ถฉ๋์ด ๊ฐ์ง๋๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํค๊ณ , ์ฌ์๋๋ฅผ ์คํํ๋ค.
๋ณดํต ์ฝ๊ธฐ ์์ ์ด ๋ง๊ณ ์ถฉ๋์ด ์ ์ ํ๊ฒฝ์์ ์ฑ๋ฅ์ด ์ข๋ค.
๋์์ฑ์ด ๋๊ณ Deadlock์ด ๋ฐ์ํ์ง ์์ง๋ง, ์ฌ์๋ ๋งค์ปค๋์ฆ์ ์ํด ์ฑ๋ฅ ์ ํ์ ์ฐ๋ ค๊ฐ ์๋ค.
0-1-2. ๋น๊ด์ ๋ฝ(Pessimistic Lock)
์ถฉ๋์ด ์์ฃผ ์ผ์ด๋๋ ์ํฉ์์, ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฑฐ๋ ์์ ํ๊ธฐ ์ ์ ๋ช ์์ ์ผ๋ก ๋ฝ์ ๊ฑฐ๋ ๋ฐฉ๋ฒ์ด๋ค.
๋ฝ์ ํ๋ํ์ง ๋ชปํ์ ๊ฒฝ์ฐ, ๋ฝ์ด ํด์ ๋ ๋๊น์ง ๋๊ธฐํ๋ค.
๋ฐ์ดํฐ์ ์ผ๊ด์ฑ์ ๊ฐํ๊ฒ ๋ณด์ฅํ ์ ์๋ค.
0-2. ๋๊ธฐํ(Synchronization)
Java์๋ ๋์์ฑ์ ์ ์ดํ ์ ์๋ synchronized๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
๋๊ธฐํ๋ ์์ญ์ ํ ๋ฒ์ ํ๋์ ์ค๋ ๋๋ง ์ ๊ทผํ ์ ์๋ค.
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public void incrementWithBlock() {
synchronized(this) {
count++;
}
}
}
synchronized๋ฅผ ํตํด ํ ๋ฒ์ ํ๋์ ์ค๋ ๋์ ์ํด์๋ง ์คํ๋ ์ ์๋ ํจ์๋ก ์ฌ์ฉํ ์ ์๋ค.
0-3. ์์์ ์ฐ์ฐ(Atomic Operations)
์์์ ์ฐ์ฐ์ java.util์ atomic์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด๋ค.
import java.util.concurrent.atomic.AtomicInteger;
public class SimpleAtomicExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(SimpleAtomicExample::incrementCounter);
Thread t2 = new Thread(SimpleAtomicExample::incrementCounter);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.get());
}
private static void incrementCounter() {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
}
}
์ ์์์์ AtomicInteger๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๊ทธ๋ฅ int๋ฅผ ์ฌ์ฉํ๋ค๋ฉด Race Condition์ ์ํด ์ต์ข ๊ฐ์ 2000์ด ๋์ง ์์์ ๊ฒ์ด๋ค.
0-4. ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค ์กฐ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ค์์ ๋์์ฑ์ ์ ์ดํ๋ ๋ฐฉ๋ฒ์ผ๋ก, ํธ๋์ญ์ ๋ค์ด ์๋ก์๊ฒ ์ด๋ ์ ๋์ ์ํฅ์ ๋ฏธ์น ์ ์๋์ง๋ฅผ ์ ์ํ๋ค.
๊ฒฉ๋ฆฌ ์์ค์ด ๋์์๋ก ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ํฅ์๋์ง๋ง, ๋์์ฑ์ ๋ฎ์์ง๋ค.
- READ UNCOMMITTED: ๊ฐ์ฅ ๋ฎ์ ๊ฒฉ๋ฆฌ ์์ค. ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ปค๋ฐ๋์ง ์์ ๋ณ๊ฒฝ์ฌํญ๋ ์ฝ์ ์ ์๋ค.
- READ COMMITTED: ์ปค๋ฐ๋ ๋ฐ์ดํฐ๋ง ์ฝ์ ์ ์๋ค.
- REPEATABLE READ: ํธ๋์ญ์ ๋ด์์ ๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ฉด ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฅํ๋ค.
- SERIALIZABLE: ๊ฐ์ฅ ๋์ ๊ฒฉ๋ฆฌ ์์ค. ํธ๋์ญ์ ์ ์์ ํ ๋ถ๋ฆฌํ์ฌ ์์ฐจ์ ์ผ๋ก ์คํํ ๊ฒ๊ณผ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฅํ๋ค.
1. WAS ํ๊ฒฝ์์ ๊ฐ์ฅ ํจ์จ์ ์ธ ํด๊ฒฐ ๋ฐฉ๋ฒ
WASํ๊ฒฝ, ํนํ ์ฌ๋ฌ ์๋ฒ๊ฐ ๋์ํ๋ ๋ถ์ฐ ํ๊ฒฝ์์๋ ๋ถ์ฐ ๋ฝ(Distributed Lock)์ด ํจ๊ณผ์ ์ด๋ค.
Java์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ํ๊ณ๊ฐ ๋ง๊ณ , ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค ์ค์ ์ ๋์ ๊ฒฉ๋ฆฌ ์์ค์ ์ํด ๋์์ฑ์ด ํฌ๊ฒ ์ ํ๋ ์ ์๋ค.
๋ฐ๋ผ์ ๋ถ์ฐ ๋น๊ด์ ๋ฝ์ด ํจ์จ์ ์ด๋ค.
2. Redis ํด๋ผ์ด์ธํธ ์ ํ
๋น๊ด์ ๋ฝ์ Redis๋ฅผ ํ์ฉํ์ฌ ๊ตฌํํ ๊ณํ์ธ๋ฐ, Redis๋ ์ฌ๋ฌ ํด๋ผ์ด์ธํธ๊ฐ ์๋ค.
2-1. Lettuce
- Spring Boot 2.0 ์ดํ ๊ธฐ๋ณธ Redis ํด๋ผ์ด์ธํธ
- ๋น๋๊ธฐ ์ง์, ๋ฐ์ด๋ ์ฑ๋ฅ
- ์ค๋ ๋ ์์
2-2. Jedis
- ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ธ API๋ก ์ฌ์ฉ์ด ์ฌ์
- ๋๊ธฐ์ ์์ ๋ง ์ง์
2-3. Redisson
- ๋ถ์ฐ ๋ฝ, ์ธ๋งํฌ์ด, ๋ฆฌ๋ ์ ์ถ ๋ฑ ๋ค์ํ ๋ถ์ฐ ๊ฐ์ฒด ๋ฐ ์๋น์ค ์ ๊ณต
- ๋น๋๊ธฐ ๋ฐ ๋ฐ์ํ ์ธํฐํ์ด์ค ์ง์
๊ธฐ๋ณธ์ ์ผ๋ก Jedis๋ณด๋ค Lettuce๊ฐ ๋น๋๊ธฐ๋ฅผ ์ง์ํ๋ฉด์ ์ฑ๋ฅ์ด ๋ ์ฐ์ํ๊ธฐ ๋๋ฌธ์, Jedis ๋ณด๋ค Lettuce๊ฐ ๋ ์ข์ ์ ํ์ด๋ค.
2-4. Lettuce vs Redisson
Lettuce
Lettuce๋ setnx(SET if Not eXist) ๋ช ๋ น์ด๋ฅผ ํตํด ์คํ ๋ฝ์ ๊ตฌํํ๊ฒ ๋๋๋ฐ, ๋ง์ฝ timeout์ ์ง์ ํ์ง ์๋๋ค๋ฉด ๋ฌดํ ๋ฃจํ์ ๋ ์ ์๋ค.
timeout์ ์ค์ ํ๋๋ผ๋ ๋ฝ ํ๋์ ๋ฌดํ์ ์ผ๋ก ์๋ํ๋ค ๋ณด๋ฉด Redis์ ๋ง์ ๋ถํ๊ฐ ๊ฑธ๋ฆฌ๊ฒ ๋๊ณ , ์ด๋ ์ฑ๋ฅ ์ ํ๋ก ์ด์ด์ง ์ ์๋ค.
Redisson
Redisson์ Non-Blocking I/O ์์ ์ ์ฌ์ฉํ๋ค.
Redisson์ ํน์ง์ ์ง์ Redis์ ๋ช ๋ น์ด๋ฅผ ์ ๊ณตํ๋ ๊ฒ์ด ์๋, Bucket, Map, Lock๊ณผ ๊ฐ์ ๊ตฌํ์ฒด๋ฅผ ํตํด์ ์ฌ์ฉํ๋ค.
๋ํ, Redisson์ ๊ตฌํ์ฒด์๋ ํ์์์์ด ๊ตฌํ๋์ด ์๋ค.
Redisson์ ๊ตฌํ์ฒด์ธ RLock์์ ํ์์์ ์๊ฐ์ ์ ๋ฌํ ์ ์๋ค.
๋ฝ ํ๋์ ์คํจํ๊ฒ ๋๋ค๋ฉด false๋ฅผ ๋ฐํํ๊ฒ ๋๋ค.
์ถ๊ฐ๋ก, Redisson์ Lettuce์ ๋ค๋ฅด๊ฒ ์คํ ๋ฝ์ ์ฌ์ฉํ์ง ์๊ณ Pub/Sub ๊ตฌ์กฐ๋ก ๋ฝ์ ํ๋ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ์ฒดํฌํ๋ค.
๋ฝ์ด ํด์ ๋๋ ์์ ์ Subscriber๋ค์๊ฒ ๋ฝ ํ๋ ์๋ ๊ฐ๋ฅ์ ์๋ ค์ค์ผ๋ก์จ, Redis์ ์์ฒญ๋๋ ๋ถํ๊ฐ ์ค์ด๋ค๊ฒ ๋๋ค.
Lua Script
Redisson์ Lua ์คํฌ๋ฆฝํธ๋ก atomic ์ฐ์ฐ์ ์ง์ํ๋ฉด์, ๋ฝ์ ํ๋ ๊ฐ๋ฅ ์ฌ๋ถ์ ํ์ธ์ ์์์ฑ์ ๋ณด์ฅํ๊ฒ ๋๋ค.
ํธ๋์ญ์ ์ ๋ช ๋ น์ด๋ฅผ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ๋ ๊ธฐ๋ฅ์ด๋ฏ๋ก, ๋ช ๋ น์ด์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ ๋ค๋ฅธ ์ฐ์ฐ์ ํ์ฉํ๋ atomicํ ์ฐ์ฐ์ ๊ตฌํํ๊ธฐ ์ด๋ ค์ด๋ฐ,
Lua ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ค.
3. ๊ตฌํ
๊ตฌํ์ ์คํ๋ง AOP์ SpEL์ ํ์ฉํ๋ค.
3-1. ์์กด์ฑ
implementation 'org.redisson:redisson-spring-boot-starter:3.16.4'
3-2. RedissonConfig
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();
config.useSingleServer().setAddress("redis://localhost:6379");
return Redisson.create(config);
}
}
Redis ์๋ฒ๋ฅผ ์ฐ๊ฒฐํด์ฃผ๋ ํด๋ผ์ด์ธํธ๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํด๋๋ค.
3-3. @RedissonLock
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonLock {
String value();
long waitTime() default 5000L;
long leaseTime() default 3000L;
}
๋ฝ์ด ํ์ํ ํจ์๋ ํธ๋์ญ์ ์ ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ ์ฌ์ฉํ ์ฉ๋์ ์ธํฐํ์ด์ค์ด๋ค.
3-4. RedisLockSpELParser
public final class RedisLockSpELParser {
public static Object getLockKey(String[] parameterNames, Object[] args, String key) {
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return parser.parseExpression(key).getValue(context, Object.class);
}
}
ํด๋น ํด๋์ค๋ SpEL(Spring Expression Language) ์ ์ฌ์ฉํ์ฌ ๋์ ์ผ๋ก ๋ฝ ํค๋ฅผ ์์ฑํ๋ ๋ฉ์๋๋ฅผ ๊ฐ๊ณ ์๋ค.
3-5. RedissonLockAspect
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RedissonLockAspect {
private final RedissonClient redissonClient;
@Around("@annotation(com.ddonghyeo.example.global.annotation.RedissonLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock redissonLock = method.getAnnotation(RedissonLock.class);
//๋ฝ ํค ์์ฑ
String lockKey =
method.getName() + ":" + RedisLockSpELParser.getLockKey(signature.getParameterNames(),
joinPoint.getArgs(), redissonLock.value());
long waitTime = redissonLock.waitTime();
long leaseTime = redissonLock.leaseTime();
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;
try {
isLocked = lock.tryLock(waitTime, leaseTime, MILLISECONDS);
if (isLocked) {
log.info("[Redisson Lock] ๋ฝ ํ๋ ์ฑ๊ณต ---> {}", lockKey);
return joinPoint.proceed(); //๋ฝ ํ๋์ ์ฑ๊ณตํ๋ค๋ฉด ์๋ ๋ก์ง ์คํ
} else {
log.error("[Redisson Lock] ๋ฝ ํ๋ ์คํจ ---> {}", lockKey);
throw new CustomException(CommonErrorCode.FAILED_TO_ACQUIRE_LOCK);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("[Redisson Lock] ๋ฝ ํ๋ ์ค ์ธํฐ๋ฝํธ ๋ฐ์ ---> {}", lockKey);
throw new CustomException(CommonErrorCode.FAILED_TO_ACQUIRE_LOCK);
} finally {
if (isLocked) {
lock.unlock(); //๋ฝ ํด์
log.info("[Redisson Lock] ๋ฝ์ ํด์ ํ๋๋ฐ ์ฑ๊ณต ---> {}", lockKey);
}
}
}
}
๋ฏธ๋ฆฌ ๋ง๋ค์ด๋์๋ RedisLockSpELParser๋ฅผ ํตํด ๋ฝ ํค๋ฅผ ์์ฑํ๊ณ ํ๋์ ์๋ํ๋ค.
Redisson์ RLock ๋ฉ์๋ tryLock์ ๋ฝ ํ๋ ์คํจ ์ false๋ฅผ ๋ฆฌํดํ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ด์ฉํ์ฌ ๋ฝ ํค ํ๋ ์ฑ๊ณต ์ฌ๋ถ๋ฅผ ํ๋จํ๋ค.
๋ง์ฝ ํค ํ๋์ ์ฑ๊ณตํ๋ค๋ฉด joinPoint.proceed() ๋ฅผ ํตํด ์๋ ์คํํ๋ ค ํ๋ ๋ก์ง์ ์คํํ๋ค.
3-6. ์ฌ์ฉ
์ด๋ ๊ฒ ๊ตฌํ๋ ๋ฝ์ ์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
@RedissonLock(value = "#productId", waitTime = 5000L, leaseTime = 3000L)
public OrderResult createOrder(Long productId, Long userId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException("Product not found"));
if (product.getStock() < quantity) {
throw new InsufficientStockException("Not enough stock");
}
product.decreaseStock(quantity);
productRepository.save(product);
Order order = Order.builder()
.userId(userId)
.productId(productId)
.quantity(quantity)
.status(OrderStatus.CREATED)
.build();
orderRepository.save(order);
return new OrderResult(order.getId(), "Order created successfully");
}
๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉ์๊ฐ ์ฃผ๋ฌธ์ ์๋ํ ๋์ ๊ฐ์ ๋์์ฑ ์ด์๊ฐ ์๋ ๋ฉ์๋์ @RedissonLock์ ์ฌ์ฉํ๋ค.
4. ์ฃผ์ํ ์
- leaseTime์ ๋๋ฌด ์งง๊ฒ ์ค์ ํ๋ค๋ฉด, ์์
์ด ์๋ฃ๋๊ธฐ ์ ๊น์ง ํด์ ๋ ์ ์์ผ๋ฏ๋ก ์ถฉ๋ถํ ๊ธธ๊ฒ ์ค์ ํ๋ ๊ฒ์ด ์ข๋ค.
- ๋ฐ๋๋ก ๋๋ฌด ๊ธธ๊ฒ ์ค์ ํ๋ค๋ฉด ์์คํ ์ฅ์ ์ ๋ฝ์ด ์ค๋ซ๋์ ํด์ ๋์ง ์์ ์ ์๋ค.
- AOP๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ํ๋ก์ ๋ก์ง์ด ์คํ๋๋ค. ๋์์ฑ์ด ๋์์๋ก ๊ณผ๋ถํ๊ฐ ์ผ์ด๋ ์ ์๋ค. ์ด๋ค ์ํฉ์ด๋ ์ง ์๋ฒ ์ํฉ์ ๋ง๊ฒ ๊ตฌํํด์ผ ํ๋ค.
์ฐธ๊ณ
https://javadoc.io/doc/org.redisson/redisson/latest/index.html
https://github.com/redisson/redisson/wiki/Table-of-Content
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html