diff --git a/build.gradle b/build.gradle index 9ea1e0a..28137bb 100644 --- a/build.gradle +++ b/build.gradle @@ -23,10 +23,27 @@ repositories { mavenCentral() } +jar { + manifest { + attributes( + 'Manifest-Version': '1.0', + 'Main-Class': 'com.goormy.hackathon.HackathonApplication' + ) + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1") + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' + implementation 'redis.clients:jedis:4.0.1' + implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' + implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws:3.2.7' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/goormy/hackathon/controller/LikeController.java b/src/main/java/com/goormy/hackathon/controller/LikeController.java index 36b1a1e..4d89a2b 100644 --- a/src/main/java/com/goormy/hackathon/controller/LikeController.java +++ b/src/main/java/com/goormy/hackathon/controller/LikeController.java @@ -23,9 +23,9 @@ public ResponseEntity addLike( @RequestHeader(name = "userId") Long userId, @RequestParam(name = "postId") Long postId) { - likeService.addLike(postId, userId); + likeService.sendLikeRequest(userId,postId); - return ResponseEntity.ok("success"); + return ResponseEntity.noContent().build(); } @DeleteMapping("/likes") @@ -33,9 +33,9 @@ public ResponseEntity cancelLike( @RequestHeader(name = "userId") Long userId, @RequestParam(name = "postId") Long postId) { - likeService.cancelLike(postId, userId); + likeService.sendCancelLikeRequest(userId,postId); - return ResponseEntity.ok("success"); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/goormy/hackathon/handler/LikeHandler.java b/src/main/java/com/goormy/hackathon/handler/LikeHandler.java new file mode 100644 index 0000000..e0aafc2 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/handler/LikeHandler.java @@ -0,0 +1,7 @@ +package com.goormy.hackathon.handler; + +import org.springframework.cloud.function.adapter.aws.FunctionInvoker; + +public class LikeHandler extends FunctionInvoker{ + +} diff --git a/src/main/java/com/goormy/hackathon/lambda/LikeFunction.java b/src/main/java/com/goormy/hackathon/lambda/LikeFunction.java new file mode 100644 index 0000000..d6790bf --- /dev/null +++ b/src/main/java/com/goormy/hackathon/lambda/LikeFunction.java @@ -0,0 +1,118 @@ +package com.goormy.hackathon.lambda; + +import com.goormy.hackathon.entity.Like; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.LikeRedisRepository; +import com.goormy.hackathon.repository.LikeRepository; +import com.goormy.hackathon.repository.PostRepository; +import com.goormy.hackathon.repository.UserRepository; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LikeFunction { + + private final LikeRedisRepository likeRedisRepository; + private final LikeRepository likeRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + /** + * @description 좋아요 정보를 Redis 캐시에 업데이트 + * */ + @Transactional + public void addLike(Long postId, Long userId) { + // 캐시에서 좋아요에 대한 정보를 먼저 조회함 + Integer findPostLike = likeRedisRepository.findPostLikeFromCache(postId, userId); + + if (findPostLike == null) { + // 캐시에 좋아요에 대한 정보가 없다면, + // Key = postlike:{postId}, Field = {userId}, Value = 1 로 '좋아요' 정보 생성 + likeRedisRepository.update(postId, userId, 1); + }else if (findPostLike == -1) { + // '좋아요 취소' 정보가 있는 상태라면 + // '좋아요'를 다시 누른 것이기 때문에 '취소 정보'를 삭제 + likeRedisRepository.delete(postId,userId); + } + } + + /** + * @description 좋아요 취소 정보를 Redis 캐시에 업데이트 + * */ + @Transactional + public void cancelLike(Long postId, Long userId) { + // 캐시에서 좋아요 정보를 먼저 조회함 + Integer findPostLike = likeRedisRepository.findPostLikeFromCache(postId, userId); + + // 캐시에 좋아요에 대한 정보가 없다면, + // Key = postlike:{postId}, Field = {userId}, Value = -1 로 '좋아요 취소' 정보 생성 + if (findPostLike == null) { + likeRedisRepository.update(postId,userId,-1); + }else if (findPostLike == 1) { + // '좋아요'라는 정보가 있는 상태라면 + // '좋아요 취소'를 다시 누른 것이기 때문에 '좋아요' 정보를 삭젳 + likeRedisRepository.delete(postId,userId); + } + } + + /** + * @description 좋아요 정보가 있는지 조회 (1. Redis 조회 2. RDB 조회) + * */ + public boolean findLike(Long postId, Long userId) { + // 1. 캐시로부터 '좋아요'에 대한 정보를 조회함 + Integer value = likeRedisRepository.findPostLikeFromCache(postId, userId); + + if (value == null) { // 캐시에 정보가 없다면 DB에서 조회되는지 여부에 따라 true/false 리턴 + return likeRepository.isExistByPostIdAndUserId(postId, userId); + }else if (value == -1) { // 캐시에 '좋아요 삭제' 정보가 있다면 false 리턴 + return false; + }else{ // 캐시에 '좋아요 추가' 정보가 있다면 true 리턴 + return true; + } + } + + /** + * @description Redis에 있는 '좋아요' 정보들을 RDB에 반영하는 함수 + * */ + @Transactional + public void dumpToDB() { + // 1. "postlike:{postId} 형식의 모든 key 목록을 불러옴 + Set postLikeKeySet = likeRedisRepository.findAllKeys(); + + // 2. Key마다 postId, userId, value를 조회하는 과정 + for (String key: postLikeKeySet) { + + // 2-1. Key로 Hash 자료구조를 조회함. field = userId / value = 1 or -1 + Map result = likeRedisRepository.findByKey(key); + + // 2-2. key를 파싱하여 postId를 구함 + String[] split = key.split(":"); + Long postId = Long.valueOf(split[1]); + + for (Map.Entry entry : result.entrySet()) { + // 2-3. field를 형변환하여 userId를 구함 + Long userId = Long.valueOf(String.valueOf(entry.getKey())); + // 2-4. value를 형변환하여 1 또는 -1 값을 얻게 됨 + Integer value = Integer.valueOf(String.valueOf(entry.getValue())); + + // 3. value 값에 따라 DB에 어떻게 반영할지 결정하여 처리함 + if (value == 1) { // 3-1. 좋아요를 추가한 상태였다면 RDB에 insert 쿼리 발생 + User user = userRepository.getReferenceById(userId); + Post post = postRepository.getReferenceById(postId); + likeRepository.save(new Like(user, post)); + }else if (value == -1) { // 3-2. 좋아요를 취소한 상태였다면 RDB에 delete 쿼리 발생 + likeRepository.deleteByPostIdAndUserId(postId, userId); + } + } + + // 4. 해당 Key에 대해 RDB에 반영하는 과정을 마쳤으므로, + likeRedisRepository.delete(key); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/service/LikeService.java b/src/main/java/com/goormy/hackathon/service/LikeService.java index d56cb91..e1b1c41 100644 --- a/src/main/java/com/goormy/hackathon/service/LikeService.java +++ b/src/main/java/com/goormy/hackathon/service/LikeService.java @@ -1,119 +1,55 @@ package com.goormy.hackathon.service; -import com.goormy.hackathon.entity.Like; -import com.goormy.hackathon.entity.Post; -import com.goormy.hackathon.entity.User; -import com.goormy.hackathon.repository.LikeRedisRepository; -import com.goormy.hackathon.repository.LikeRepository; -import com.goormy.hackathon.repository.PostRepository; -import com.goormy.hackathon.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goormy.hackathon.lambda.LikeFunction; import java.util.Map; -import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageResponse; @Service -@Transactional(readOnly = true) -@RequiredArgsConstructor public class LikeService { - private final LikeRedisRepository likeRedisRepository; - private final LikeRepository likeRepository; - private final UserRepository userRepository; - private final PostRepository postRepository; - - /** - * @description 좋아요 정보를 Redis 캐시에 업데이트 - * */ - @Transactional - public void addLike(Long postId, Long userId) { - // 캐시에서 좋아요에 대한 정보를 먼저 조회함 - Integer findPostLike = likeRedisRepository.findPostLikeFromCache(postId, userId); + private static final Logger logger = LoggerFactory.getLogger(LikeFunction.class); - if (findPostLike == null) { - // 캐시에 좋아요에 대한 정보가 없다면, - // Key = postlike:{postId}, Field = {userId}, Value = 1 로 '좋아요' 정보 생성 - likeRedisRepository.update(postId, userId, 1); - }else if (findPostLike == -1) { - // '좋아요 취소' 정보가 있는 상태라면 - // '좋아요'를 다시 누른 것이기 때문에 '취소 정보'를 삭제 - likeRedisRepository.delete(postId,userId); - } - } + @Autowired + private SqsClient sqsClient; - /** - * @description 좋아요 취소 정보를 Redis 캐시에 업데이트 - * */ - @Transactional - public void cancelLike(Long postId, Long userId) { - // 캐시에서 좋아요 정보를 먼저 조회함 - Integer findPostLike = likeRedisRepository.findPostLikeFromCache(postId, userId); + @Value("${spring.cloud.aws.sqs.queue-url}") + private String queueUrl; - // 캐시에 좋아요에 대한 정보가 없다면, - // Key = postlike:{postId}, Field = {userId}, Value = -1 로 '좋아요 취소' 정보 생성 - if (findPostLike == null) { - likeRedisRepository.update(postId,userId,-1); - }else if (findPostLike == 1) { - // '좋아요'라는 정보가 있는 상태라면 - // '좋아요 취소'를 다시 누른 것이기 때문에 '좋아요' 정보를 삭젳 - likeRedisRepository.delete(postId,userId); - } + public void sendLikeRequest(Long userId, Long postId) { + sendRequest(userId, postId, "like"); } - /** - * @description 좋아요 정보가 있는지 조회 (1. Redis 조회 2. RDB 조회) - * */ - public boolean findLike(Long postId, Long userId) { - // 1. 캐시로부터 '좋아요'에 대한 정보를 조회함 - Integer value = likeRedisRepository.findPostLikeFromCache(postId, userId); - - if (value == null) { // 캐시에 정보가 없다면 DB에서 조회되는지 여부에 따라 true/false 리턴 - return likeRepository.isExistByPostIdAndUserId(postId, userId); - }else if (value == -1) { // 캐시에 '좋아요 삭제' 정보가 있다면 false 리턴 - return false; - }else{ // 캐시에 '좋아요 추가' 정보가 있다면 true 리턴 - return true; - } + public void sendCancelLikeRequest(Long userId, Long postId) { + sendRequest(userId, postId, "unlike"); } - /** - * @description Redis에 있는 '좋아요' 정보들을 RDB에 반영하는 함수 - * */ - @Transactional - public void dumpToDB() { - // 1. "postlike:{postId} 형식의 모든 key 목록을 불러옴 - Set postLikeKeySet = likeRedisRepository.findAllKeys(); - - // 2. Key마다 postId, userId, value를 조회하는 과정 - for (String key: postLikeKeySet) { - - // 2-1. Key로 Hash 자료구조를 조회함. field = userId / value = 1 or -1 - Map result = likeRedisRepository.findByKey(key); - - // 2-2. key를 파싱하여 postId를 구함 - String[] split = key.split(":"); - Long postId = Long.valueOf(split[1]); - - for (Map.Entry entry : result.entrySet()) { - // 2-3. field를 형변환하여 userId를 구함 - Long userId = Long.valueOf(String.valueOf(entry.getKey())); - // 2-4. value를 형변환하여 1 또는 -1 값을 얻게 됨 - Integer value = Integer.valueOf(String.valueOf(entry.getValue())); - - // 3. value 값에 따라 DB에 어떻게 반영할지 결정하여 처리함 - if (value == 1) { // 3-1. 좋아요를 추가한 상태였다면 RDB에 insert 쿼리 발생 - User user = userRepository.getReferenceById(userId); - Post post = postRepository.getReferenceById(postId); - likeRepository.save(new Like(user, post)); - }else if (value == -1) { // 3-2. 좋아요를 취소한 상태였다면 RDB에 delete 쿼리 발생 - likeRepository.deleteByPostIdAndUserId(postId, userId); - } - } - - // 4. 해당 Key에 대해 RDB에 반영하는 과정을 마쳤으므로, - likeRedisRepository.delete(key); + public void sendRequest(Long userId, Long postId, String action) { + try{ + ObjectMapper objectMapper = new ObjectMapper(); + String messageBody = objectMapper.writeValueAsString(Map.of( + "userId", userId, + "postId", postId, + "action", action + )); + SendMessageRequest sendMsgRequest = SendMessageRequest.builder() + .queueUrl(queueUrl) + .messageBody(messageBody) + .build(); + + logger.info("메시지 송신 - action: {}, user Id: {}, postId: {}", action, userId, postId); + SendMessageResponse sendMsgResponse = sqsClient.sendMessage(sendMsgRequest); + logger.info("메시지가 전달되었습니다: {}, Message ID: {}, HTTP Status: {}", + messageBody, sendMsgResponse.messageId(), sendMsgResponse.sdkHttpResponse().statusCode()); + } catch (Exception e) { + logger.error("메시지 전송 실패", e); } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 98edb09..f80dd0a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,10 +4,10 @@ spring: host: localhost port: 6379 datasource: - url: ${MYSQL_URL:jdbc:mysql://localhost}:${MYSQL_PORT:3306}/${MYSQL_SCHEMA:dev} + url: ${MYSQL_URL:jdbc:mysql://gureumi-rds.cfgaqgkoy31u.ap-northeast-2.rds.amazonaws.com}:${MYSQL_PORT:3306}/${MYSQL_SCHEMA:gureumi-rds} driver-class-name: com.mysql.cj.jdbc.Driver - username: ${MYSQL_USERNAME:root} - password: ${MYSQL_PASSWORD:1234} + username: ${MYSQL_USERNAME:admin} + password: ${MYSQL_PASSWORD:rnfmalelql3} jpa: hibernate: ddl-auto: update @@ -20,6 +20,15 @@ spring: include: - redis - swagger + cloud: + aws: + region: + static: ${{ secrets.AWS_REGION }} + credentials: + access-key: ${{ secrets.AWS_ACCESS_KEY_ID }} + secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + sqs: + queue-url: https://sqs.ap-northeast-2.amazonaws.com/008971650206/GoormySQSForLike logging: level: org: