Skip to content

Commit

Permalink
Merge pull request #139 from dnd-side-project/refactor/#138-redisson
Browse files Browse the repository at this point in the history
refactor: 분산락 Redisson으로 적용 및 AOP로 적용
  • Loading branch information
f1v3-dev authored Oct 18, 2024
2 parents 08e171e + 21dc47a commit 8346a4f
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 72 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.16.3'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.dnd.jjakkak.domain.schedule.dto.request.ScheduleUpdateRequestDto;
import com.dnd.jjakkak.domain.schedule.dto.response.ScheduleAssignResponseDto;
import com.dnd.jjakkak.domain.schedule.dto.response.ScheduleResponseDto;
import com.dnd.jjakkak.domain.schedule.facade.ScheduleFacade;
import com.dnd.jjakkak.domain.schedule.service.ScheduleService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,7 +23,6 @@
public class ScheduleController {

private final ScheduleService scheduleService;
private final ScheduleFacade scheduleFacade;

/**
* 회원의 일정을 모임에 할당하는 메서드입니다.
Expand All @@ -38,8 +36,7 @@ public class ScheduleController {
public ResponseEntity<Void> assignScheduleToMember(@PathVariable("meetingUuid") String meetingUuid,
@AuthenticationPrincipal Long memberId,
@Valid @RequestBody ScheduleAssignRequestDto requestDto) {

scheduleFacade.assignScheduleToMember(memberId, meetingUuid, requestDto);
scheduleService.assignScheduleToMember(memberId, meetingUuid, requestDto);
return ResponseEntity.ok().build();
}

Expand All @@ -53,8 +50,8 @@ public ResponseEntity<Void> assignScheduleToMember(@PathVariable("meetingUuid")
@PostMapping("/guests")
public ResponseEntity<ScheduleAssignResponseDto> assignScheduleToGuest(@PathVariable("meetingUuid") String meetingUuid,
@Valid @RequestBody ScheduleAssignRequestDto requestDto) {
ScheduleAssignResponseDto responseDto = scheduleService.assignScheduleToGuest(meetingUuid, requestDto);

ScheduleAssignResponseDto responseDto = scheduleFacade.assignScheduleToGuest(meetingUuid, requestDto);
return ResponseEntity.ok(responseDto);
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.dnd.jjakkak.domain.schedule.exception.ScheduleAlreadyAssignedException;
import com.dnd.jjakkak.domain.schedule.exception.ScheduleNotFoundException;
import com.dnd.jjakkak.domain.schedule.repository.ScheduleRepository;
import com.dnd.jjakkak.global.annotation.redisson.RedissonLock;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -72,16 +73,13 @@ public void createDefaultSchedule(Meeting meeting) {
* @param requestDto 일정 할당 요청 DTO
* @return 일정 할당 응답 DTO (UUID)
*/
@Transactional
@RedissonLock(key = "#meetingUuid")
public ScheduleAssignResponseDto assignScheduleToGuest(String meetingUuid, ScheduleAssignRequestDto requestDto) {


// 이미 모임의 인원이 다 찼는가? (400 Bad Request)
if (meetingRepository.checkMeetingFull(meetingUuid)) {
throw new MeetingFullException();
}


// meetingId로 할당되지 않은 schedule 조회
Schedule schedule = scheduleRepository.findNotAssignedScheduleByMeetingUuid(meetingUuid)
.orElseThrow(ScheduleNotFoundException::new);
Expand All @@ -105,7 +103,7 @@ public ScheduleAssignResponseDto assignScheduleToGuest(String meetingUuid, Sched
* @param meetingUuid 모임 UUID
* @param requestDto 일정 할당 요청 DTO
*/
@Transactional
@RedissonLock(key = "#meetingUuid")
public void assignScheduleToMember(Long memberId, String meetingUuid, ScheduleAssignRequestDto requestDto) {

// 이미 모임의 인원이 다 찼는가? (400 Bad Request)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dnd.jjakkak.global.annotation.redisson;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Redisson AOP 어노테이션입니다.
*
* <li>적용하려는 메소드에 Transactional을 지우고 RedissonLock을 적용하면 됩니다.</li>
* <li>ex) @RedissonLock(value = "#meetingUuid")</li>
*
* @author 류태웅
* @version 2024. 10. 15.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {

/**
* Lock Key
*/
String key();

/***
* Lock 획득을 시도하는 최대 시간 (ms)
*/
long waitTime() default 5000L;

/**
* 락을 획득한 후, 점유하는 최대 시간
*/
long leaseTime() default 3000L;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.dnd.jjakkak.global.annotation.redisson;

import com.dnd.jjakkak.global.util.CustomSpringELParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
* Redisson Lock Aspect 클래스입니다.
*
* @author 류태웅, 정승조
* @version 2024. 10. 15.
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RedissonLockAspect {
private final RedissonClient redissonClient;
private final TransactionAspect transactionAspect;

/**
* '@RedissonLock' 어노테이션을 사용한 메서드에 대한 Advice.
*/
@Around("@annotation(com.dnd.jjakkak.global.annotation.redisson.RedissonLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock annotation = method.getAnnotation(RedissonLock.class);

// todo: prefix 변경 필요 (현재는 모임 관련 로직에서만 사용)
String lockKey = "meeting-" + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), annotation.key());

RLock lock = redissonClient.getLock(lockKey);
try {
boolean lockable = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), TimeUnit.MILLISECONDS);

if (!lockable) {
throw new IllegalArgumentException();
}

return transactionAspect.proceed(joinPoint);
} catch (InterruptedException e) {
// todo : 재시도 로직이 필요한가?
log.error("Redisson Lock Exception", e);
throw new RuntimeException();
} finally {
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.dnd.jjakkak.global.annotation.redisson;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
* 트랜잭션 분리를 위한 TransactionAspect 클래스입니다.
*
* @author 류태웅
* @version 2024. 10. 15.
*/

@Component
public class TransactionAspect {

/**
* 트랜잭션을 분리하기 위한 메서드 (다른 트랜잭션과 분리)
*
* <li> REQUIRES_NEW 를 통해 별도의 트랜잭션으로 관리한다.</li>
* <li> timeout 시간 내에 수행이 되어야 한다. (아니면 롤백) </li>
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 2)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* Redis 설정 클래스입니다.
*
* @author 류태웅
* @version 2024. 10. 15.
*/
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.dnd.jjakkak.global.config.redis;

import lombok.RequiredArgsConstructor;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Redisson 설정 클래스입니다.
*
* @author 류태웅
* @version 2024. 10. 15.
*/
@Configuration
@RequiredArgsConstructor
public class RedissonConfig {

private static final String REDISSON_HOST_PREFIX = "redis://";
private final RedisProperties redisProperties;

/**
* Redisson Client 등록 메서드.
*/
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort());
return Redisson.create(config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.dnd.jjakkak.global.util;

import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

/**
* 어노테이션에 작성된 Spring EL 표현식을 파싱하는 클래스입니다.
*
* @author 류태웅
* @version 2024. 10. 15.
*/
public class CustomSpringELParser {

private CustomSpringELParser() {
throw new IllegalStateException("Utility class");
}

public static Object getDynamicValue(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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.dnd.jjakkak.config.JjakkakMockUser;
import com.dnd.jjakkak.domain.meeting.exception.MeetingNotFoundException;
import com.dnd.jjakkak.domain.schedule.ScheduleDummy;
import com.dnd.jjakkak.domain.schedule.facade.ScheduleFacade;
import com.dnd.jjakkak.domain.schedule.service.ScheduleService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
Expand Down Expand Up @@ -40,9 +39,6 @@ class ScheduleControllerTest extends AbstractRestDocsTest {
@MockBean
ScheduleService scheduleService;

@MockBean
ScheduleFacade scheduleFacade;

@Autowired
ObjectMapper objectMapper;

Expand Down

0 comments on commit 8346a4f

Please sign in to comment.