๋ด ํ๋ก์ ํธ์ Payment๋ฅผ ๊ฐ๋ฐํ๋ฉด์ ๊ฐ์ฅ ๊ธฐ๋ณธ ์ค์ ๊ธฐ๋ณธ์ด ๋๋ ๋ฌธ์ ๋ฅผ ์ง๋ฉดํ์๋ค.
๊ทธ๊ฒ์ ๋ฐ๋ก ๋์์ฑ ๋ฌธ์ !
์คํ๋ง๋ถํธ์ ๋ด์ฅ ์๋ฒ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํฐ์บฃ, ์ธ๋ํ ์ฐ ๋ฑ๋ฑ์ WAS๋ก ๋์๊ฐ๋๋ฐ ์ด WAS๋ ๋ฉํฐ์ค๋ ๋ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ค.
A๋ผ๋ ์ํ (์ฌ๊ณ 3๊ฐ) ์ [๊ฐ]๊ตฐ์ด 2๊ฐ ๊ตฌ๋งคํ๋ ค ํ๋ค. ๋์์ [๋]๊ตฐ์ด 2๊ฐ ๊ตฌ๋งคํ๋ ค ํ๋ค.
๋ฏธ์ธํ๊ฒ ๋๋ง 0.00001์ด์ ์ฐจ์ด๊ฐ ์์ ์ ์๋ค.
๊ฒฐ๊ตญ ๊ฐ๊ฐ์ ์ค๋ ๋๊ฐ ๊ฐ์ ์ํ์ ์ฌ๊ณ ๋ฅผ ์กฐํํ๋ค. ์๋๋๋ก๋ผ๋ฉด ํ ๋ช
์ ๋ชป ์ฌ์ผ ์ ์์ด๋ค.
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ด ๋ญ๊ฐ ์์๊น?
1. Synchronized
์๋ฐ๋ก ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ด๋ค.
Thread-Safe ํ๊ธฐ ๋๋ฌธ์ ๋งค์ฐ ์ข์๋ณด์ด๋, ์๋ฒ๊ฐ ์ฆ์ค๋ ๊ฒฝ์ฐ ์๋ฏธ๊ฐ ์์ด์ง๋ค.
2. Database Lock
Database์ Lock์ ๊ฑธ ์ ์๋ ๋ฐฉ๋ฒ์ด๋ค.
ํ
์ด๋ธ์ Lock์ ๊ฑธ๊ฑฐ๋ Mysql์ Innodb์์ง์ ๋ ์ฝ๋ Lock์ ์ ๊ณตํ๋ฏ๋ก ์ด๋ฅผ ํตํด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
ํ์ง๋ง DB์ ์ง์ ์ ์ธ Lock์ ๊ฑฐ๋ ๊ฒ์ ๊ฒฐ์ฝ ์ข์ ๋ฐฉ๋ฒ์ ์๋๋ค. ๊ต์ฐฉ ์ํ(deadlock)์ ๋น ์ง ์๋ ์๊ณ ,
๋ฌด์๋ณด๋ค ์ฑ๋ฅ์ ์ธ ๋ถ๋ถ์์ ์๋๊ฐ ๋๋ ค์ง ์ ์๋ค๋ ๋ฌธ์ ์ ์ด ์๋ค.
3. Redisson์ ์ด์ฉํ ๋ถ์ฐ Lock
Redis๋ฅผ ์ด์ฉํด์ Lock์ ๊ฑธ ์ ์๋ ๋ฐฉ๋ฒ์ด ์๋ค.
์๋ฒ๊ฐ ์ฌ๋ฌ ๋์ผ ๊ฒฝ์ฐ์๋ ๋ฌธ์ ์์ด ๋ถ์ฐ Lock์ ๊ฑธ์ด ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ ๋ณด์ฅํ ์ ์๋ค.
์๋ฐ์์ ์ ๊ณตํ๋ Redisson ํด๋ผ์ด์ธํธ๋ฅผ ์ด์ฉํด ๋ถ์ฐ Lock์ ํ๋ํด ๋ถ์ฐ ํ๊ฒฝ์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
4. ๊ตฌํ ๊ณผ์
Build.gradle ์์กด์ฑ ์ถ๊ฐ
// Redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.17.6'
Redisson Configuration
- Redisson Client์ ์ค์ ์ ์ถ๊ฐํด Bean์ผ๋ก ๋ฑ๋กํ๋ค.
- ์์ bean๋ค์ ๊ธฐ์กด์ ์ฌ์ฉํ๋ Lettuce ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๊ธฐ ๋๋ฌธ์ด๊ณ , ์ถ๊ฐํ๊ณ ์ ํ๋ Redisson client๋ฅผ Bean์ผ๋ก ๋ฑ๋กํ์.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.connectionTimeout}")
private Long connectionTimeout;
/**
* redis์ connection์ ์์ฑํด ์ฃผ๋ ๊ฐ์ฒด
* @return LettuceConnectionFactory
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration =
new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(password);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
/**
* redis ์๋ฒ์ ํต์ ๋ฐ ์ ์ ์๊ฒ redis ๋ชจ๋ ๊ธฐ๋ฅ ์ ๊ณตํ๊ธฐ ์ํด ์ถ์ํ๋ฅผ ํตํด
* ์คํผ๋ ์ด์
์ ๊ณต
* @return redisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate =
new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
stringRedisTemplate.setValueSerializer(new StringRedisSerializer());
return stringRedisTemplate;
}
@Bean
public RedissonClient redissonClient() {
String address = "redis://" + host + ":" + port;
Config config = new Config();
config.useSingleServer()
.setAddress(address)
.setPassword(password)
.setConnectTimeout(connectionTimeout.intValue());
return Redisson.create(config);
}
}
Lock์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ง์ ์ฝ๋์ ์ ์ฉ ์ํค๋ฉด ํ์ ์ด์์ ๋น์ฆ๋์ค๊ฐ ํ๋์ ๋ฉ์๋์ ๋ค์ด๊ฐ ๊ฒ ๊ฐ๋ค.
๊ทธ๋์ AOP๋ฅผ ์ด์ฉํด์ ๋ถ์ฐ Lock์ ํ์๋ก ํ๋ ๊ณณ์ ์ ์ฉ ์ํค๊ธฐ ์ํด ์ ๋
ธํ
์ด์
์ ์ ์ธํด ์ ์ฉํ๋ค.
์ ๋ ธํ ์ด์ ์ ์
- Lock ์ ์ง ์๊ฐ๊ณผ ๋๊ธฐ ์๊ฐ์ ํ๋กํผํฐ๋ก ์ ์
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLocked {
/**
* redisson lock ์ ์ง ์๊ฐ <br>
* ๋จ์ : milliseconds
*/
long leaseTime() default 1000;
/**
* redisson lock ํ๋ ๋๊ธฐ ์๊ฐ <br>
* ๋จ์ : milliseconds
*/
long waitTime() default 3000;
}
AOP ๊ตฌํ
- Lock์ key๋ ํ์์ ๋ฐ๋ผ ์๋ช ์ ํ๋ฉด ๋ ๊ฒ์ด๋ค.
- tryLock์ ํตํด lock์ ํ๋ ์ฌ๋ถ ํ์ ๊ณผ ํ๋์ ์งํํ๊ณ , @Around๋ก ์ ์ํด ์ค๊ฐ์ ํต์ฌ๊ด์ฌ์ฌ๋ฅผ ์คํํ๋ค.
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RedisLockAspect {
private final RedissonClient redissonClient;
private static final String LOCK_SUFFIX = ":lock";
@Around("@annotation(com.daniel.mychickenbreastshop.global.aspect.annotation.RedisLocked)")
public Object executeWithLock(ProceedingJoinPoint joinPoint) {
String key = getLockableKey(joinPoint);
return execute(key, joinPoint);
}
private String getLockableKey(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String methodName = method.getName();
String className = method.getDeclaringClass().toString();
return methodName + className + LOCK_SUFFIX;
}
private Object execute(String key, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
long leaseTime = method.getAnnotation(RedisLocked.class).leaseTime();
long waitTime = method.getAnnotation(RedisLocked.class).waitTime();
RLock lock = redissonClient.getLock(key);
Object result;
try {
boolean tryLock = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
if (!tryLock) {
log.error("Lock ํ๋์ ์คํจํ์ต๋๋ค.");
}
result = joinPoint.proceed();
} catch (Throwable e) {
throw new InternalErrorException(e);
} finally {
unlock(lock);
log.info("Redis unlocked!");
}
return result;
}
private void unlock(RLock lock) {
if (lock != null && lock.isLocked()) {
lock.unlock();
}
}
}
์ ๋ ธํ ์ด์ ํ์ฉ
- ์ ๋ ธํ ์ด์ ์ ํ์ฉํด ๋ถ์ฐ ๋ฝ์ด ํ์ํ ๋ฉ์๋์ ๋ฌ์์ฃผ๋ฉด ๋๋ค.
- ํธ๋์ญ์ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๊ณณ์ AOP๋ ํ๋ก์ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ํธ๋์ญ์ ์ ํ๋ฅผ ๋ฐ๋ก ์ ์ฉํ๋ค.
- ์ด๋ ๊ฒ ํ์ง ์์ ๊ฒฝ์ฐ ์ง์ ์ปค๋ฐ๊ณผ ๋กค๋ฐฑ์ ํด์ค์ผ ํ๋ค.
@Override
@RedisLocked(leaseTime = 3000)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public PaymentResult cancelPayment(PayCancelRequestDto payCancelRequestDto, String loginId) {
์ด๋ ๊ฒ ๊ฐ๋จํ๊ฒ Redisson์ ์ด์ฉํ์ฌ ๋ถ์ฐ Lock์ ๊ตฌํํด๋ดค๋ค.
์ข ๋ ๋์ ์ฑ๋ฅ๊ณผ ๋์ ๋ฐฉ๋ฒ์ ์ฐพ๋ ๊ฒ์ ์ฐธ ์ฝ์ง ์์ ๊ฒ ๊ฐ๋ค. ๋, ์ต์ ์ ๋ฐฉ๋ฒ์ ๊ณ์ ์ฐพ๋๋ผ ์๋ ์กฐ์ฐจ ์ ํ๋ ๊ฒ์ด ์๋๋ผ ์ผ๋จ ๋จ๊ณ์ ๋ง๋ ์๋๋ฅผ ๋ฐ์๋๊ฐ๋ฉฐ ํ์์ ์ํด ์ต์ ์ ๋ฐฉ๋ฒ์ ์ ์ฉ ์ํค๋ ๊ณผ์ ์ด ํ์ํ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ๋ค.
ํธ๋์ญ์ ๊ด๋ จ ์์ธ ๋ฌธ์ ํด๊ฒฐ ์ฒ๋ฆฌ ํฌ์คํ !
[Redisson] ํธ๋์ญ์ ๋ฌธ์ ๋ฐ์ ๋ฐ ํด๊ฒฐ
์ง๋ ํฌ์คํธ [Redisson]์ ์ด์ฉํ ๋ถ์ฐ Lock ๊ตฌํ & ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ ๋ด ํ๋ก์ ํธ์ Payment๋ฅผ ๊ฐ๋ฐํ๋ฉด์ ๊ฐ์ฅ ๊ธฐ๋ณธ ์ค์ ๊ธฐ๋ณธ์ด ๋๋ ๋ฌธ์ ๋ฅผ ์ง๋ฉดํ์๋ค. ๊ทธ๊ฒ์ ๋ฐ๋ก ๋์์ฑ ๋ฌธ์ ! ์คํ๋ง๋ถํธ์ ๋ด
sweeeetgoguma.tistory.com
์ฐธ๊ณ ์๋ฃ
https://it-hhhj2.tistory.com/102
redis ์ค์น ๋ฐ redisson์ ์ด์ฉํ ๋ถ์ฐ๋ฝ ๊ตฌํ
์ค์น redis ์ค์น ์ ๋ธ๋ก๊ทธ ๋ฐ๋ผ ์์กฐ๋กญ๊ฒ ์ค์น ํ ํ์ธ ์๋ฃ Redis์ ๋ถ์ฐ๋ฝ ๋ถ์ฐ๋ฝ(Distributed Lock) ์ฌ๋ฌ ๋ ๋ฆฝ๋ ํ๋ก์ธ์ค์์ ํ๋์ ๊ณต์ ์์์ ์ ๊ทผํ ๋, ๋ฐ์ดํฐ์ ๊ฒฐํจ์ด ๋ฐ์ํ์ง ์๋๋ก ์์
it-hhhj2.tistory.com
https://devroach.tistory.com/82
Redisson ์ผ๋ก ๋ถ์ฐ Lock ๊ตฌํํ๊ธฐ
ํ์ฌ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ถ์ฐ๋ฝ์ด ํ์ํ ๊ฒ ๊ฐ๋ค๋ ํ๋จ์ด ๋ค์์ต๋๋ค. ๋ถ์ฐ๋ฝ์ด๋ ๋ฌด์์ผ๊น์? ์์ฃผ ๊ฐ๋ณ๊ฒ ์ค๋ช ํ์๋ฉด ์๋ฅผ ๋ค์ด, ๊ฐ๊ฒ๋ ํ๋์ ์ฃผ๋ฌธ๋ง ๋ฐ
devroach.tistory.com
'๐ Spring Framework > Spring Project' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Refactor] ํจํค์ง ๊ตฌ์กฐ์ ์์กด์ฑ (2) | 2022.10.14 |
---|---|
[Redisson] ํธ๋์ญ์ ๋ฌธ์ ๋ฐ์ ๋ฐ ํด๊ฒฐ (0) | 2022.10.01 |
๊ฒฐ์ API ๋ฆฌํฉํ ๋ง - [2] (feat. WebClient) (7) | 2022.09.22 |
๊ฒฐ์ API ๋ฆฌํฉํ ๋ง - [1] (feat. ์ ๋ต ํจํด) (2) | 2022.09.20 |
๋์์ฑ ์กฐํ ๋ฌธ์ ํด๊ฒฐ ๋ฐ ์ฑ๋ฅ์ ๊ดํ ๊ณ ๋ฏผ [Lock, Queue, Redis] (0) | 2022.09.14 |