Skip to content

Commit

Permalink
Weekly/8/issue#144 동시성 제어 (#189)
Browse files Browse the repository at this point in the history
* Feat: RedissonConfig 설정

* Feat: Redisson Lock 구현(임시)

* Fix: RaceCondition 테스트 오류 수정

1. RedissonLock의 value로 hostId만 넘기기
2. RaceConditionTest를 SpringBootTest로 테스트
3. BeforeEach로 Redis 초기화

* Refactor: GroupMember 조회 fetch join 적용

* Refactor: random GroupMember 조회 fetch join 적용

* Refactor: 방문자 조회수 비동기 로직 추가

* Feat: 질문 지목 시 count 증가 동시성 제어

* Refactor: merge weekly9

* Refactor: 누락된 코드 추가

* Refactor: 누락된 코드 추가

* Refactor: 코드 수정

* Refactor: redissonClient 제거

* Refactor: redis 설정 변경

* Refactor: redis 설정 변경

* HotFix: 이미지 파일 resize 비율 조정

* Refactor: RedissonClient 사용 테스트 ActiveProfile 설정

* Refactor: RedissonClient 사용 테스트 Profile 설정

* Fix: Profile() 메서드에 적용

* Fix: ProfileIntegrationTest, RaceConditionTest 주석처리

* Fix: 충돌 해결

* Refactor: Random Question Response 수정

* Chore: 불필요한 Import 제거

* Import: 충돌 해결

---------

Co-authored-by: hjinshin <gudwls818@gmail.com>
Co-authored-by: Kwon Da woon <82216606+momnpa333@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent 3f16ea6 commit 2b849d8
Show file tree
Hide file tree
Showing 27 changed files with 363 additions and 165 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {

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

// embedded redis - 테스트 시 적용
testImplementation('it.ozimov:embedded-redis:0.7.3') {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/supernova/whokie/global/annotation/RedissonLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package supernova.whokie.global.annotation;

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

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
String value();
long waitTime() default 5000L;
long leaseTime() default 2000L;
}
55 changes: 55 additions & 0 deletions src/main/java/supernova/whokie/global/aop/RedissonAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package supernova.whokie.global.aop;

import lombok.RequiredArgsConstructor;
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.redisson.client.RedisException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import supernova.whokie.global.annotation.RedissonLock;

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

@Aspect
@Component
@RequiredArgsConstructor
public class RedissonAspect {

private final RedissonClient redissonClient;

@Around("@annotation(supernova.whokie.global.annotation.RedissonLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock annotation = method.getAnnotation(RedissonLock.class);
String lockKey = method.getName() + getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), annotation.value());
RLock lock = redissonClient.getLock(lockKey);
try {
boolean lockable = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), TimeUnit.MILLISECONDS);
if (!lockable) {
return false;
}
return joinPoint.proceed();
} catch (RedisException e) {
throw new Exception("Temporary errors failed to access the service");
} finally {
lock.unlock();
}
}

public 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);
}
}
27 changes: 12 additions & 15 deletions src/main/java/supernova/whokie/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
package supernova.whokie.global.config;

import lombok.RequiredArgsConstructor;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import supernova.whokie.global.property.RedisProperties;

@Profile("redis")
@Configuration
@RequiredArgsConstructor
public class RedisConfig {

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

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port());
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisProperties.host() + ":" + redisProperties.port());
return Redisson.create(config);
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);

// Key는 String으로 저장
redisTemplate.setKeySerializer(new StringRedisSerializer());
// Value는 기본적으로 Jdk 직렬화로 저장
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
public RedisConnectionFactory redisConnectionFactory(RedissonClient redissonClient) {
return new RedissonConnectionFactory(redissonClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public interface GroupMemberRepository extends JpaRepository<GroupMember, Long>
@Query("SELECT g FROM GroupMember g WHERE g.group.id = :groupId")
Page<GroupMember> findAllByGroupId(Pageable pageable, Long groupId);

@Query("SELECT g FROM GroupMember g WHERE g.user.id != :userId AND g.group.id = :groupId ORDER BY function('RAND')")
List<GroupMember> getRandomGroupMember(@Param("userId") Long userId,
@Query("SELECT g FROM GroupMember g JOIN FETCH g.user WHERE g.user.id != :userId AND g.group.id = :groupId ORDER BY function('RAND')")
List<GroupMember> getRandomGroupMemberJoinFetch(@Param("userId") Long userId,
@Param("groupId") Long groupId, Pageable pageable);

Boolean existsByUserIdAndGroupId(Long userId, Long groupId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public Page<GroupMember> getGroupMembers(Pageable pageable, Long userId, Long gr
@Transactional(readOnly = true)
public List<GroupMember> getRandomGroupMembersByGroupId(Long userId, Long groupId,
Pageable pageable) {
return groupMemberRepository.getRandomGroupMember(userId,
return groupMemberRepository.getRandomGroupMemberJoinFetch(userId,
groupId, pageable);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ public QuestionResponse.CommonQuestions getCommonQuestions(
@Authenticate Long userId,
@PageableDefault(page = 0, size = 5) Pageable pageable
) {
List<QuestionModel.CommonQuestion> commonQuestions = questionService.getCommonQuestion(
userId, pageable);
List<QuestionModel.CommonQuestion> commonQuestions = questionService.getCommonQuestion(pageable);
return QuestionResponse.CommonQuestions.from(commonQuestions);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

import lombok.Builder;
import org.springframework.data.domain.Page;
import supernova.whokie.groupmember.controller.dto.GroupMemberResponse;
import supernova.whokie.question.Question;
import supernova.whokie.question.QuestionStatus;
import supernova.whokie.question.service.dto.QuestionModel;
import supernova.whokie.user.service.dto.UserModel;

import java.time.LocalDate;
import java.util.List;
Expand All @@ -32,19 +29,13 @@ public static GroupQuestions from(List<QuestionModel.GroupQuestion> model) {
@Builder
public record GroupQuestion(
Long questionId,
String content,
List<GroupMemberResponse.Option> users
String content
) {

public static GroupQuestion from(QuestionModel.GroupQuestion model) {
return GroupQuestion.builder()
.questionId(model.questionId())
.content(model.content())
.users(
model.groupMembers().stream()
.map(GroupMemberResponse.Option::from)
.toList()
)
.build();
}
}
Expand All @@ -53,31 +44,24 @@ public static GroupQuestion from(QuestionModel.GroupQuestion model) {
public record CommonQuestions(
List<CommonQuestion> questions
) {
public static CommonQuestions from(List<QuestionModel.CommonQuestion> commonQuestions) {
public static CommonQuestions from(List<QuestionModel.CommonQuestion> models) {
List<CommonQuestion> commonQuestions = models.stream().map(CommonQuestion::from).toList();
return CommonQuestions.builder()
.questions(
commonQuestions.stream().map(
commonQuestion -> CommonQuestion.builder()
.questionId(commonQuestion.questionId())
.content(commonQuestion.content())
.users(commonQuestion.users())
.build()
).toList()).build();
.questions(commonQuestions)
.build();
}

}

@Builder
public record CommonQuestion(
Long questionId,
String content,
List<UserModel.PickedInfo> users
String content
) {
public static CommonQuestion from(Question question, List<UserModel.PickedInfo> friendList) {
public static CommonQuestion from(QuestionModel.CommonQuestion question) {
return CommonQuestion.builder()
.questionId(question.getId())
.content(question.getContent())
.users(friendList)
.questionId(question.questionId())
.content(question.content())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,37 @@
package supernova.whokie.question.service;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import supernova.whokie.friend.Friend;
import supernova.whokie.friend.service.FriendReaderService;
import supernova.whokie.global.constants.MessageConstants;
import supernova.whokie.global.exception.EntityNotFoundException;
import supernova.whokie.groupmember.GroupMember;
import supernova.whokie.groupmember.service.GroupMemberReaderService;
import supernova.whokie.groupmember.service.dto.GroupMemberModel;
import supernova.whokie.question.Question;
import supernova.whokie.question.QuestionStatus;
import supernova.whokie.question.constants.QuestionConstants;
import supernova.whokie.question.service.dto.QuestionCommand;
import supernova.whokie.question.service.dto.QuestionModel;
import supernova.whokie.s3.service.S3Service;
import supernova.whokie.user.Users;
import supernova.whokie.user.service.UserReaderService;
import supernova.whokie.user.service.dto.UserModel;

import java.util.List;

@Service
@RequiredArgsConstructor
public class QuestionService {

private final GroupMemberReaderService groupMemberReaderService;
private final QuestionReaderService questionReaderService;
private final FriendReaderService friendReaderService;
private final UserReaderService userReaderService;
private final QuestionWriterService questionWriterService;
private final S3Service s3Service;

@Transactional(readOnly = true)
public List<QuestionModel.CommonQuestion> getCommonQuestion(Long userId, Pageable pageable) {

Users user = userReaderService.getUserById(userId);

public List<QuestionModel.CommonQuestion> getCommonQuestion(Pageable pageable) {
List<Question> randomQuestions = questionReaderService.getRandomQuestions(pageable);
Pageable friendPageable = PageRequest.of(0, QuestionConstants.FRIEND_LIMIT);

List<Friend> friends = friendReaderService.findRandomFriendsByHostUser(user.getId(), friendPageable);
List<UserModel.PickedInfo> pickerModels = friends.stream()
.map(friend -> {
Users friendUser = friend.getFriendUser();
String imageUrl = friendUser.getImageUrl();
if (friendUser.isImageUrlStoredInS3()) {
imageUrl = s3Service.getSignedUrl(imageUrl);
}
return UserModel.PickedInfo.from(friendUser, imageUrl);
}).toList();

return randomQuestions.stream()
.map(question -> QuestionModel.CommonQuestion.from(question, pickerModels))
.map(QuestionModel.CommonQuestion::from)
.toList();
}

Expand Down Expand Up @@ -84,22 +60,8 @@ public List<QuestionModel.GroupQuestion> getGroupQuestions(Long userId, Long gro
List<Question> randomQuestions = questionReaderService.getRandomGroupQuestions(groupId,
pageable);

Pageable GroupMemberpageable = PageRequest.of(0, QuestionConstants.FRIEND_LIMIT);

List<GroupMember> groupMembers = groupMemberReaderService.getRandomGroupMembersByGroupId(userId, groupId, GroupMemberpageable);
List<GroupMemberModel.Option> memberModels = groupMembers.stream()
.map(member -> {
String imageUrl = member.getUser().getImageUrl();
if ( member.getUser().isImageUrlStoredInS3()) {
imageUrl = s3Service.getSignedUrl(imageUrl);
}
return GroupMemberModel.Option.from(member, imageUrl);
})
.toList();

return randomQuestions.stream()
.map(question ->
QuestionModel.GroupQuestion.from(question, memberModels))
.map(QuestionModel.GroupQuestion::from)
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
package supernova.whokie.question.service.dto;

import lombok.Builder;
import supernova.whokie.groupmember.service.dto.GroupMemberModel;
import supernova.whokie.question.Question;
import supernova.whokie.question.QuestionStatus;
import supernova.whokie.user.service.dto.UserModel;

import java.time.LocalDate;
import java.util.List;

public class QuestionModel {

@Builder
public record CommonQuestion(
Long questionId,
String content,
List<UserModel.PickedInfo> users
String content
) {

public static QuestionModel.CommonQuestion from(Question question, List<UserModel.PickedInfo> pickers) {
public static QuestionModel.CommonQuestion from(Question question) {
return CommonQuestion.builder()
.questionId(question.getId())
.content(question.getContent())
.users(pickers)
.build();
}

Expand Down Expand Up @@ -54,16 +49,14 @@ public static QuestionModel.Info from(Question question, QuestionStatus status)
@Builder
public record GroupQuestion(
Long questionId,
String content,
List<GroupMemberModel.Option> groupMembers
String content
) {

public static QuestionModel.GroupQuestion from(
Question question, List<GroupMemberModel.Option> groupMembers) {
Question question) {
return GroupQuestion.builder()
.questionId(question.getId())
.content(question.getContent())
.groupMembers(groupMembers)
.build();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package supernova.whokie.ranking.infrastructure.repoistory;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import supernova.whokie.group.Groups;
import supernova.whokie.ranking.Ranking;
import supernova.whokie.user.Users;
Expand All @@ -17,4 +19,8 @@ public interface RankingRepository extends JpaRepository<Ranking, Long> {
List<Ranking> findAllByGroupIdFetchJoinUsers(Long groupId);

Optional<Ranking> findByUsersAndQuestionAndGroups(Users users, String question, Groups groups);

@Modifying
@Query("UPDATE Ranking r SET r.count = r.count + 1 WHERE r.id = :id")
void incrementCount(@Param("id") Long id);
}
Loading

0 comments on commit 2b849d8

Please sign in to comment.