Skip to content

Commit

Permalink
feat: AWS SQS 및 lambda 기능 구현
Browse files Browse the repository at this point in the history
chore: Main-Class 경로 입력
  • Loading branch information
seonghooni committed Aug 1, 2024
1 parent 218f6ab commit ed4ac97
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 109 deletions.
17 changes: 17 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ public ResponseEntity<String> 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")
public ResponseEntity<String> 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();
}

}
7 changes: 7 additions & 0 deletions src/main/java/com/goormy/hackathon/handler/LikeHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.goormy.hackathon.handler;

import org.springframework.cloud.function.adapter.aws.FunctionInvoker;

public class LikeHandler extends FunctionInvoker{

}
118 changes: 118 additions & 0 deletions src/main/java/com/goormy/hackathon/lambda/LikeFunction.java
Original file line number Diff line number Diff line change
@@ -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<String> postLikeKeySet = likeRedisRepository.findAllKeys();

// 2. Key마다 postId, userId, value를 조회하는 과정
for (String key: postLikeKeySet) {

// 2-1. Key로 Hash 자료구조를 조회함. field = userId / value = 1 or -1
Map<Object, Object> result = likeRedisRepository.findByKey(key);

// 2-2. key를 파싱하여 postId를 구함
String[] split = key.split(":");
Long postId = Long.valueOf(split[1]);

for (Map.Entry<Object, Object> 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);
}
}
}
140 changes: 38 additions & 102 deletions src/main/java/com/goormy/hackathon/service/LikeService.java
Original file line number Diff line number Diff line change
@@ -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<String> postLikeKeySet = likeRedisRepository.findAllKeys();

// 2. Key마다 postId, userId, value를 조회하는 과정
for (String key: postLikeKeySet) {

// 2-1. Key로 Hash 자료구조를 조회함. field = userId / value = 1 or -1
Map<Object, Object> result = likeRedisRepository.findByKey(key);

// 2-2. key를 파싱하여 postId를 구함
String[] split = key.split(":");
Long postId = Long.valueOf(split[1]);

for (Map.Entry<Object, Object> 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);
}
}
}
15 changes: 12 additions & 3 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down

0 comments on commit ed4ac97

Please sign in to comment.