diff --git a/build.gradle b/build.gradle index 8c610b19..a8088ca0 100644 --- a/build.gradle +++ b/build.gradle @@ -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') { diff --git a/src/main/java/supernova/whokie/global/annotation/RedissonLock.java b/src/main/java/supernova/whokie/global/annotation/RedissonLock.java new file mode 100644 index 00000000..839e8005 --- /dev/null +++ b/src/main/java/supernova/whokie/global/annotation/RedissonLock.java @@ -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; +} diff --git a/src/main/java/supernova/whokie/global/aop/RedissonAspect.java b/src/main/java/supernova/whokie/global/aop/RedissonAspect.java new file mode 100644 index 00000000..088c3055 --- /dev/null +++ b/src/main/java/supernova/whokie/global/aop/RedissonAspect.java @@ -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); + } +} diff --git a/src/main/java/supernova/whokie/global/config/RedisConfig.java b/src/main/java/supernova/whokie/global/config/RedisConfig.java index 21fbe7a1..5aff818d 100644 --- a/src/main/java/supernova/whokie/global/config/RedisConfig.java +++ b/src/main/java/supernova/whokie/global/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate 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); } } diff --git a/src/main/java/supernova/whokie/groupmember/infrastructure/repository/GroupMemberRepository.java b/src/main/java/supernova/whokie/groupmember/infrastructure/repository/GroupMemberRepository.java index 9158be3e..7368c00e 100644 --- a/src/main/java/supernova/whokie/groupmember/infrastructure/repository/GroupMemberRepository.java +++ b/src/main/java/supernova/whokie/groupmember/infrastructure/repository/GroupMemberRepository.java @@ -21,8 +21,8 @@ public interface GroupMemberRepository extends JpaRepository @Query("SELECT g FROM GroupMember g WHERE g.group.id = :groupId") Page 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 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 getRandomGroupMemberJoinFetch(@Param("userId") Long userId, @Param("groupId") Long groupId, Pageable pageable); Boolean existsByUserIdAndGroupId(Long userId, Long groupId); diff --git a/src/main/java/supernova/whokie/groupmember/service/GroupMemberReaderService.java b/src/main/java/supernova/whokie/groupmember/service/GroupMemberReaderService.java index cd09c739..de8659c1 100644 --- a/src/main/java/supernova/whokie/groupmember/service/GroupMemberReaderService.java +++ b/src/main/java/supernova/whokie/groupmember/service/GroupMemberReaderService.java @@ -43,7 +43,7 @@ public Page getGroupMembers(Pageable pageable, Long userId, Long gr @Transactional(readOnly = true) public List getRandomGroupMembersByGroupId(Long userId, Long groupId, Pageable pageable) { - return groupMemberRepository.getRandomGroupMember(userId, + return groupMemberRepository.getRandomGroupMemberJoinFetch(userId, groupId, pageable); } diff --git a/src/main/java/supernova/whokie/question/controller/QuestionController.java b/src/main/java/supernova/whokie/question/controller/QuestionController.java index 260d2e55..436e48b8 100644 --- a/src/main/java/supernova/whokie/question/controller/QuestionController.java +++ b/src/main/java/supernova/whokie/question/controller/QuestionController.java @@ -81,8 +81,7 @@ public QuestionResponse.CommonQuestions getCommonQuestions( @Authenticate Long userId, @PageableDefault(page = 0, size = 5) Pageable pageable ) { - List commonQuestions = questionService.getCommonQuestion( - userId, pageable); + List commonQuestions = questionService.getCommonQuestion(pageable); return QuestionResponse.CommonQuestions.from(commonQuestions); } diff --git a/src/main/java/supernova/whokie/question/controller/dto/QuestionResponse.java b/src/main/java/supernova/whokie/question/controller/dto/QuestionResponse.java index f02029ac..e521829f 100644 --- a/src/main/java/supernova/whokie/question/controller/dto/QuestionResponse.java +++ b/src/main/java/supernova/whokie/question/controller/dto/QuestionResponse.java @@ -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; @@ -32,19 +29,13 @@ public static GroupQuestions from(List model) { @Builder public record GroupQuestion( Long questionId, - String content, - List 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(); } } @@ -53,16 +44,11 @@ public static GroupQuestion from(QuestionModel.GroupQuestion model) { public record CommonQuestions( List questions ) { - public static CommonQuestions from(List commonQuestions) { + public static CommonQuestions from(List models) { + List 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(); } } @@ -70,14 +56,12 @@ public static CommonQuestions from(List commonQues @Builder public record CommonQuestion( Long questionId, - String content, - List users + String content ) { - public static CommonQuestion from(Question question, List 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(); } diff --git a/src/main/java/supernova/whokie/question/service/QuestionService.java b/src/main/java/supernova/whokie/question/service/QuestionService.java index 65496dd1..5c5f7195 100644 --- a/src/main/java/supernova/whokie/question/service/QuestionService.java +++ b/src/main/java/supernova/whokie/question/service/QuestionService.java @@ -1,28 +1,22 @@ 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 @@ -30,32 +24,14 @@ 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 getCommonQuestion(Long userId, Pageable pageable) { - - Users user = userReaderService.getUserById(userId); - + public List getCommonQuestion(Pageable pageable) { List randomQuestions = questionReaderService.getRandomQuestions(pageable); - Pageable friendPageable = PageRequest.of(0, QuestionConstants.FRIEND_LIMIT); - - List friends = friendReaderService.findRandomFriendsByHostUser(user.getId(), friendPageable); - List 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(); } @@ -84,22 +60,8 @@ public List getGroupQuestions(Long userId, Long gro List randomQuestions = questionReaderService.getRandomGroupQuestions(groupId, pageable); - Pageable GroupMemberpageable = PageRequest.of(0, QuestionConstants.FRIEND_LIMIT); - - List groupMembers = groupMemberReaderService.getRandomGroupMembersByGroupId(userId, groupId, GroupMemberpageable); - List 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(); } diff --git a/src/main/java/supernova/whokie/question/service/dto/QuestionModel.java b/src/main/java/supernova/whokie/question/service/dto/QuestionModel.java index 7ab1e05b..d4ece00a 100644 --- a/src/main/java/supernova/whokie/question/service/dto/QuestionModel.java +++ b/src/main/java/supernova/whokie/question/service/dto/QuestionModel.java @@ -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 users + String content ) { - public static QuestionModel.CommonQuestion from(Question question, List pickers) { + public static QuestionModel.CommonQuestion from(Question question) { return CommonQuestion.builder() .questionId(question.getId()) .content(question.getContent()) - .users(pickers) .build(); } @@ -54,16 +49,14 @@ public static QuestionModel.Info from(Question question, QuestionStatus status) @Builder public record GroupQuestion( Long questionId, - String content, - List groupMembers + String content ) { public static QuestionModel.GroupQuestion from( - Question question, List groupMembers) { + Question question) { return GroupQuestion.builder() .questionId(question.getId()) .content(question.getContent()) - .groupMembers(groupMembers) .build(); } } diff --git a/src/main/java/supernova/whokie/ranking/infrastructure/repoistory/RankingRepository.java b/src/main/java/supernova/whokie/ranking/infrastructure/repoistory/RankingRepository.java index 9e22a318..1d13f04f 100644 --- a/src/main/java/supernova/whokie/ranking/infrastructure/repoistory/RankingRepository.java +++ b/src/main/java/supernova/whokie/ranking/infrastructure/repoistory/RankingRepository.java @@ -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; @@ -17,4 +19,8 @@ public interface RankingRepository extends JpaRepository { List findAllByGroupIdFetchJoinUsers(Long groupId); Optional 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); } diff --git a/src/main/java/supernova/whokie/ranking/service/RankingService.java b/src/main/java/supernova/whokie/ranking/service/RankingService.java index 2fca6ba1..bdd3b773 100644 --- a/src/main/java/supernova/whokie/ranking/service/RankingService.java +++ b/src/main/java/supernova/whokie/ranking/service/RankingService.java @@ -1,6 +1,5 @@ package supernova.whokie.ranking.service; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,9 +9,7 @@ import supernova.whokie.ranking.Ranking; import supernova.whokie.ranking.service.dto.RankingModel; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.IntStream; @Service @@ -34,10 +31,6 @@ public List getGroupRanking(Long userId, Long groupId) { if (!groupMemberReaderService.isGroupMemberExist(userId, groupId)) { throw new EntityNotFoundException(MessageConstants.GROUP_MEMBER_NOT_FOUND_MESSAGE); } - - List rankings = rankingReaderService.getTop3RankingByGroupId(groupId); - List> entries = getTop3UsersFromGroup(rankings); - RankingModel.Top3RankingEntries mapEntries = rankingReaderService.getTop3UsersFromGroupByGroupId(groupId); return IntStream.range(0, mapEntries.entries().size()) diff --git a/src/main/java/supernova/whokie/ranking/service/RankingWriterService.java b/src/main/java/supernova/whokie/ranking/service/RankingWriterService.java index b367f84e..c0facbf9 100644 --- a/src/main/java/supernova/whokie/ranking/service/RankingWriterService.java +++ b/src/main/java/supernova/whokie/ranking/service/RankingWriterService.java @@ -34,6 +34,7 @@ public Ranking createRanking(Users user, String question, Groups groups) { public void increaseRankingCountByUserAndQuestionAndGroups(Users user, String question, Groups group) { Ranking ranking = rankingRepository.findByUsersAndQuestionAndGroups(user, question, group) .orElseGet(() -> createRanking(user, question, group)); - ranking.increaseCount(); + rankingRepository.incrementCount(ranking.getId()); + rankingRepository.save(ranking); } } diff --git a/src/main/java/supernova/whokie/redis/service/RedisVisitService.java b/src/main/java/supernova/whokie/redis/service/RedisVisitService.java index b02a1bcf..d1c7d0c5 100644 --- a/src/main/java/supernova/whokie/redis/service/RedisVisitService.java +++ b/src/main/java/supernova/whokie/redis/service/RedisVisitService.java @@ -1,7 +1,11 @@ package supernova.whokie.redis.service; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import supernova.whokie.global.annotation.RedissonLock; import supernova.whokie.profile.service.ProfileVisitReadService; import supernova.whokie.redis.entity.RedisVisitCount; import supernova.whokie.redis.entity.RedisVisitor; @@ -10,10 +14,6 @@ import supernova.whokie.redis.service.dto.RedisCommand; import supernova.whokie.redis.util.RedisUtil; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - @Service @RequiredArgsConstructor public class RedisVisitService { @@ -21,6 +21,7 @@ public class RedisVisitService { private final RedisVisitCountRepository redisVisitCountRepository; private final ProfileVisitReadService profileVisitReadService; + @RedissonLock(value = "#hostId") public RedisVisitCount visitProfile(Long hostId, String visitorIp) { RedisVisitCount redisVisitCount = findVisitCountByHostId(hostId); if(!checkVisited(hostId, visitorIp)) { @@ -80,5 +81,4 @@ public void deleteAllVisitors(List visitors) { List ids = visitors.stream().map(RedisVisitor::getId).toList(); redisVisitorRepository.deleteAllById(ids); } - } diff --git a/src/test/java/supernova/config/EmbeddedRedisConfig.java b/src/test/java/supernova/config/EmbeddedRedisConfig.java index 374d66d6..0b4feb07 100644 --- a/src/test/java/supernova/config/EmbeddedRedisConfig.java +++ b/src/test/java/supernova/config/EmbeddedRedisConfig.java @@ -2,7 +2,11 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; import redis.embedded.RedisServer; import redis.embedded.RedisServerBuilder; @@ -11,6 +15,7 @@ @TestConfiguration public class EmbeddedRedisConfig { private static final int REDIS_PORT = 6379; + private static final String REDIS_HOST = "localhost"; private RedisServer redisServer; @PostConstruct @@ -20,12 +25,26 @@ public void startRedis() throws IOException { .setting("maxmemory 128M") .build(); redisServer.start(); + try { + redisServer.start(); + } catch (Exception e) { + e.printStackTrace(); + } } + @PreDestroy public void stopRedisServer() { if (redisServer != null) { redisServer.stop(); } } + + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + REDIS_HOST + ":" + REDIS_PORT); + return Redisson.create(config); + } } diff --git a/src/test/java/supernova/whokie/WhokieApplicationTests.java b/src/test/java/supernova/whokie/WhokieApplicationTests.java index cec7cd42..3e0aa647 100644 --- a/src/test/java/supernova/whokie/WhokieApplicationTests.java +++ b/src/test/java/supernova/whokie/WhokieApplicationTests.java @@ -2,6 +2,7 @@ import io.awspring.cloud.s3.S3Template; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; @@ -13,7 +14,7 @@ "jwt.secret=abcd" }) -@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class}) class WhokieApplicationTests { @Test diff --git a/src/test/java/supernova/whokie/answer/AnswerIntegrationTest.java b/src/test/java/supernova/whokie/answer/AnswerIntegrationTest.java index ea15f3ce..a82ac0ff 100644 --- a/src/test/java/supernova/whokie/answer/AnswerIntegrationTest.java +++ b/src/test/java/supernova/whokie/answer/AnswerIntegrationTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -43,7 +44,7 @@ "jwt.secret=abcd", "spring.sql.init.mode=never" }) -@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class AnswerIntegrationTest { diff --git a/src/test/java/supernova/whokie/friend/repository/FriendRepositoryTest.java b/src/test/java/supernova/whokie/friend/repository/FriendRepositoryTest.java index dc46ab87..b337280f 100644 --- a/src/test/java/supernova/whokie/friend/repository/FriendRepositoryTest.java +++ b/src/test/java/supernova/whokie/friend/repository/FriendRepositoryTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -29,7 +30,7 @@ "jwt.secret=abcd", "spring.sql.init.mode=never" }) -@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class FriendRepositoryTest { @Autowired diff --git a/src/test/java/supernova/whokie/groupmember/GroupMemberIntegrationTest.java b/src/test/java/supernova/whokie/groupmember/GroupMemberIntegrationTest.java index 93d6ada2..99fc9796 100644 --- a/src/test/java/supernova/whokie/groupmember/GroupMemberIntegrationTest.java +++ b/src/test/java/supernova/whokie/groupmember/GroupMemberIntegrationTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -43,7 +44,7 @@ "jwt.secret=abcd", "spring.sql.init.mode=never" }) -@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class GroupMemberIntegrationTest { diff --git a/src/test/java/supernova/whokie/profile/ProfileIntegrationTest.java b/src/test/java/supernova/whokie/profile/ProfileIntegrationTest.java index f871a98c..dc102cd1 100644 --- a/src/test/java/supernova/whokie/profile/ProfileIntegrationTest.java +++ b/src/test/java/supernova/whokie/profile/ProfileIntegrationTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -32,10 +33,10 @@ @SpringBootTest @AutoConfigureMockMvc -@Import(EmbeddedRedisConfig.class) @TestPropertySource(properties = { "jwt.secret=abcd" }) +@Import(EmbeddedRedisConfig.class) @MockBean({S3Client.class, S3Template.class, S3Presigner.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class ProfileIntegrationTest { @@ -55,11 +56,15 @@ public class ProfileIntegrationTest { @Autowired private MockMvc mockMvc; + @Autowired + private RedissonClient redissonClient; + private Users user; private Profile profile; @BeforeEach void setUp() { + redissonClient.getKeys().flushall(); user = createUser(); profile = createProfile(); ProfileVisitCount profileVisitCount = createProfileVisitCount(); diff --git a/src/test/java/supernova/whokie/question/QuestionIntegrationTest.java b/src/test/java/supernova/whokie/question/QuestionIntegrationTest.java index fcd7e187..e66300b2 100644 --- a/src/test/java/supernova/whokie/question/QuestionIntegrationTest.java +++ b/src/test/java/supernova/whokie/question/QuestionIntegrationTest.java @@ -1,16 +1,10 @@ package supernova.whokie.question; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import io.awspring.cloud.s3.S3Template; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -36,13 +30,18 @@ import supernova.whokie.user.Users; import supernova.whokie.user.infrastructure.repository.UserRepository; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = { "jwt.secret=abcd", "spring.sql.init.mode=never" }) -@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class QuestionIntegrationTest { @@ -107,7 +106,6 @@ void getCommonQuestionTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.questions").isArray()) .andExpect(jsonPath("$.questions.length()").value(5)) - .andExpect(jsonPath("$.questions[0].users.length()").value(5)) .andDo(result -> { String responseContent = result.getResponse().getContentAsString(); System.out.println("questions 내용: " + responseContent); @@ -126,7 +124,6 @@ void getGroupQuestionTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.questions").isArray()) .andExpect(jsonPath("$.questions.length()").value(10)) - .andExpect(jsonPath("$.questions[0].users.length()").value(5)) .andDo(result -> { String responseContent = result.getResponse().getContentAsString(); System.out.println("questions 내용: " + responseContent); diff --git a/src/test/java/supernova/whokie/question/service/QuestionServiceTest.java b/src/test/java/supernova/whokie/question/service/QuestionServiceTest.java index b97cb48c..ad620efd 100644 --- a/src/test/java/supernova/whokie/question/service/QuestionServiceTest.java +++ b/src/test/java/supernova/whokie/question/service/QuestionServiceTest.java @@ -81,20 +81,15 @@ void getCommonQuestionTest() { // given Long userId = 1L; Pageable pageable = PageRequest.of(0, QuestionConstants.QUESTION_LIMIT); - Pageable friendPageable = PageRequest.of(0, QuestionConstants.FRIEND_LIMIT); // when - when(userReaderService.getUserById(eq(userId))).thenReturn(user); when(questionReaderService.getRandomQuestions(eq(pageable))).thenReturn(questions); - when(friendReaderService.findRandomFriendsByHostUser(eq(userId), eq(friendPageable))) - .thenReturn(friends); - List commonQuestions = questionService.getCommonQuestion(userId, pageable); + List commonQuestions = questionService.getCommonQuestion(pageable); // then assertAll( - () -> assertEquals(10, commonQuestions.size()), - () -> assertEquals(5, commonQuestions.get(0).users().size()) + () -> assertEquals(10, commonQuestions.size()) ); } @@ -110,8 +105,6 @@ void getGroupQuestionTest() { .thenReturn(true); when(questionReaderService.getRandomGroupQuestions(eq(groupId), any(Pageable.class))) .thenReturn(questions); - when(groupMemberReaderService.getRandomGroupMembersByGroupId(eq(userId), eq(groupId), any(Pageable.class))) - .thenReturn(groupMembers); List groupQuestionList = questionService.getGroupQuestions( userId, groupId); @@ -120,8 +113,7 @@ void getGroupQuestionTest() { // then assertAll( - () -> assertEquals(10, groupQuestions.questions().size()), - () -> assertEquals(5, groupQuestions.questions().get(0).users().size()) + () -> assertEquals(10, groupQuestions.questions().size()) ); } @@ -142,7 +134,7 @@ void createQuestionTest() { verify(questionWriterService, times(1)).save(any(Question.class)); } - @Test //TODO 수정 + @Test @DisplayName("그룹 질문 승인 테스트") void approveQuestionTest() { // given diff --git a/src/test/java/supernova/whokie/ranking/service/RankingReaderServiceTest.java b/src/test/java/supernova/whokie/ranking/service/RankingReaderServiceTest.java index cbdb83d1..937f85d5 100644 --- a/src/test/java/supernova/whokie/ranking/service/RankingReaderServiceTest.java +++ b/src/test/java/supernova/whokie/ranking/service/RankingReaderServiceTest.java @@ -7,6 +7,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -36,7 +37,7 @@ void setUp() { rankings = createRankings(); } - //@Test + @Test @DisplayName("그룹 내에서 count가 높은 3명 뽑기 테스트") void getTop3UsersFromGroupTest() { // given @@ -56,9 +57,9 @@ void getTop3UsersFromGroupTest() { // then assertAll( () -> assertThat(actuals.entries()).hasSize(2), - () -> assertThat(actuals.entries().get(0).getKey()).isEqualTo(users.get(0).getName()), + () -> assertThat(actuals.entries().get(0).getKey()).isEqualTo(users.get(0).getId()), () -> assertThat(actuals.entries().get(0).getValue()).isEqualTo(finalCount), - () -> assertThat(actuals.entries().get(1).getKey()).isEqualTo(users.get(1).getName()), + () -> assertThat(actuals.entries().get(1).getKey()).isEqualTo(users.get(1).getId()), () -> assertThat(actuals.entries().get(1).getValue()).isEqualTo(finalCount1) ); } diff --git a/src/test/java/supernova/whokie/ranking/service/RankingServiceTest.java b/src/test/java/supernova/whokie/ranking/service/RankingServiceTest.java index e9d25e49..61986e7f 100644 --- a/src/test/java/supernova/whokie/ranking/service/RankingServiceTest.java +++ b/src/test/java/supernova/whokie/ranking/service/RankingServiceTest.java @@ -16,7 +16,6 @@ import supernova.whokie.user.Users; import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -84,26 +83,6 @@ void getGroupRankingTest() { .isThrownBy(() -> rankingService.getGroupRanking(userId, groupId)); } - @Test - @DisplayName("그룹 내에서 count가 높은 3명 뽑기 테스트") - void getTop3UsersFromGroupTest() { - // given - List rankingList = rankings; - int finalCount = rankingList.get(0).getCount() + rankingList.get(1).getCount() + rankingList.get(2).getCount(); - int finalCount1 = rankingList.get(3).getCount(); - - // when - List> actuals = rankingService.getTop3UsersFromGroup(rankingList); - - assertAll( - () -> assertThat(actuals).hasSize(2), - () -> assertThat(actuals.get(0).getKey()).isEqualTo(users.get(0).getName()), - () -> assertThat(actuals.get(0).getValue()).isEqualTo(finalCount), - () -> assertThat(actuals.get(1).getKey()).isEqualTo(users.get(1).getName()), - () -> assertThat(actuals.get(1).getValue()).isEqualTo(finalCount1) - ); - } - private Groups createGroup() { return Groups.builder().id(1L).build(); } diff --git a/src/test/java/supernova/whokie/redis/service/RaceConditionTest.java b/src/test/java/supernova/whokie/redis/service/RaceConditionTest.java new file mode 100644 index 00000000..fcf2e2a9 --- /dev/null +++ b/src/test/java/supernova/whokie/redis/service/RaceConditionTest.java @@ -0,0 +1,195 @@ +package supernova.whokie.redis.service; + +import io.awspring.cloud.s3.S3Template; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import supernova.config.EmbeddedRedisConfig; +import supernova.whokie.group.Groups; +import supernova.whokie.group.infrastructure.repository.GroupRepository; +import supernova.whokie.ranking.Ranking; +import supernova.whokie.ranking.infrastructure.repoistory.RankingRepository; +import supernova.whokie.ranking.service.RankingWriterService; +import supernova.whokie.redis.entity.RedisVisitCount; +import supernova.whokie.redis.infrastructure.repository.RedisVisitCountRepository; +import supernova.whokie.user.Gender; +import supernova.whokie.user.Role; +import supernova.whokie.user.Users; +import supernova.whokie.user.infrastructure.repository.UserRepository; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SpringBootTest +@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@TestPropertySource(properties = { + "jwt.secret=abcd" +}) +@Import(EmbeddedRedisConfig.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class RaceConditionTest { + + @Autowired + private RedisVisitCountRepository redisVisitCountRepository; + + @Autowired + private RedisVisitService redisVisitService; + + @Autowired + private RankingWriterService rankingWriterService; + + @Autowired + RankingRepository rankingRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private RedissonClient redissonClient; + + Users user; + Groups group; + + @BeforeEach + void setUp() { + redissonClient.getKeys().flushall(); + user = createUser(); + group = createGroup(); + } + + @Test + @DisplayName("동시 방문자 수 증가 테스트") + void visitProfileConcurrentlyTest() throws InterruptedException { + // given + RedisVisitCount redisVisitCount = createVisitCount(); + Long hostId = redisVisitCount.getHostId(); + String visitorIp = "visitorIp"; + int oldDailyVisited = redisVisitCount.getDailyVisited(); + int oldTotalVisited = redisVisitCount.getTotalVisited(); + + int threadCount = 100; // 스레드 개수 + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + // when + for (int i = 0; i < threadCount; i++) { + int finalI = i; + executorService.submit(() -> { + try { + redisVisitService.visitProfile(hostId, visitorIp + finalI); + } catch (Exception e) { + e.printStackTrace(); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + executorService.shutdown(); + + // then + RedisVisitCount actual = redisVisitCountRepository.findById(hostId).orElseThrow(); + + assertAll( + () -> assertThat(actual.getDailyVisited()).isEqualTo(oldDailyVisited + threadCount), + () -> assertThat(actual.getTotalVisited()).isEqualTo(oldTotalVisited + threadCount) + ); + } + + @Test + @DisplayName("동시 질문 지목 횟수 증가 테스트") + void AnswerCountConcurrentlyTest() throws InterruptedException { + // given + createRanking(user, group); + int threadCount = 100; // 스레드 개수 + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + // when + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + rankingWriterService.increaseRankingCountByUserAndQuestionAndGroups(user, "test", group); + } catch (Exception e) { + e.printStackTrace(); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + executorService.shutdown(); + + // then + Ranking actual = rankingRepository.findByUsersAndQuestionAndGroups(user, "test", group) + .orElseThrow(); + + assertAll( + () -> assertThat(actual.getCount()).isEqualTo(threadCount) + ); + } + + private RedisVisitCount createVisitCount() { + RedisVisitCount redisVisitCount = RedisVisitCount.builder() + .hostId(1L) + .dailyVisited(0) + .totalVisited(10) + .build(); + redisVisitCountRepository.save(redisVisitCount); + return redisVisitCount; + } + + private Users createUser() { + Users user = Users.builder() + .name("test") + .email("test@gmail.com") + .point(1000) + .age(22) + .kakaoId(1L) + .gender(Gender.M) + .role(Role.USER) + .build(); + + userRepository.save(user); + return user; + } + + private Groups createGroup() { + Groups group = Groups.builder() + .groupName("test") + .description("test") + .groupImageUrl("test") + .build(); + + groupRepository.save(group); + return group; + } + + private void createRanking(Users user, Groups group) { + Ranking ranking = Ranking.builder() + .question("test") + .count(0) + .users(user) + .groups(group) + .build(); + + rankingRepository.save(ranking); + } +} + diff --git a/src/test/java/supernova/whokie/redis/service/RedisVisitServiceTest.java b/src/test/java/supernova/whokie/redis/service/RedisVisitServiceTest.java index b7c9628b..4b0ec4f2 100644 --- a/src/test/java/supernova/whokie/redis/service/RedisVisitServiceTest.java +++ b/src/test/java/supernova/whokie/redis/service/RedisVisitServiceTest.java @@ -15,8 +15,8 @@ import supernova.whokie.profile.service.ProfileVisitReadService; import supernova.whokie.redis.entity.RedisVisitCount; import supernova.whokie.redis.entity.RedisVisitor; -import supernova.whokie.redis.infrastructure.repository.RedisVisitorRepository; import supernova.whokie.redis.infrastructure.repository.RedisVisitCountRepository; +import supernova.whokie.redis.infrastructure.repository.RedisVisitorRepository; import supernova.whokie.redis.service.dto.RedisCommand; import java.util.ArrayList; diff --git a/src/test/java/supernova/whokie/user/UserIntegrationTest.java b/src/test/java/supernova/whokie/user/UserIntegrationTest.java index da813110..e3cb302c 100644 --- a/src/test/java/supernova/whokie/user/UserIntegrationTest.java +++ b/src/test/java/supernova/whokie/user/UserIntegrationTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -26,7 +27,7 @@ @TestPropertySource(properties = { "jwt.secret=abcd" }) -@MockBean({S3Client.class, S3Template.class, S3Presigner.class}) +@MockBean({S3Client.class, S3Template.class, S3Presigner.class, RedissonClient.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class UserIntegrationTest {