Skip to content

Commit

Permalink
🐞 BugFix - RabbitMQ와 Optimistic Lock을 μ΄μš©ν•œ λ™μ‹œμ„± 문제 ν•΄κ²° (#176)
Browse files Browse the repository at this point in the history
* 🐞 BugFix/#175 - add: RabbitMQ μ˜μ‘΄μ„± μΆ”κ°€

* 🐞 BugFix/#175 - feat: RabbitMQConfig κ΅¬ν˜„

* 🐞 BugFix/#175 - refactor: Optimistic Lock 적용

* 🐞 BugFix/#175 - feat: comment 생성 μ‹œ λ™μ‹œμ„± 문제 ν•΄κ²°

* 🐞 BugFix/#175 - feat: like 생성 μ‹œ λ™μ‹œμ„± 문제 ν•΄κ²°
  • Loading branch information
dongkyeomjang authored Nov 27, 2024
1 parent 4664556 commit bb5fe1b
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 74 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ dependencies {
// Secheduler
implementation 'org.springframework.boot:spring-boot-starter-quartz'

// RabbitMQ
implementation group: 'org.springframework.amqp', name: 'spring-amqp', version: '3.1.7'
implementation group: 'org.springframework.amqp', name: 'spring-rabbit', version: '3.1.7'

// Testing Dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/com/daon/onjung/core/config/RabbitMQConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.daon.onjung.core.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableRabbit
public class RabbitMQConfig {

@Bean
public DirectExchange boardExchange() {
return new DirectExchange("board-exchange");
}

@Bean
public Queue likeQueue() {
return new Queue("like-queue", true);
}

@Bean
public Queue commentQueue1() {
return new Queue("comment-queue-1", true);
}

@Bean Queue commentQueue2() {
return new Queue("comment-queue-2", true);
}

@Bean
public Binding boardQueue1Binding(DirectExchange boardExchange, Queue commentQueue1) {
return BindingBuilder.bind(commentQueue1).to(boardExchange).with("board.1");
}

@Bean
public Binding boardQueue2Binding(DirectExchange boardExchange, Queue commentQueue2) {
return BindingBuilder.bind(commentQueue2).to(boardExchange).with("board.0");
}

@Bean
public Jackson2JsonMessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter()); // JSON 컨버터 μ„€μ •
return rabbitTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public enum ErrorCode {
TOKEN_GENERATION_ERROR(40106, HttpStatus.UNAUTHORIZED, "토큰 생성에 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."),
TOKEN_UNKNOWN_ERROR(40107, HttpStatus.UNAUTHORIZED, "μ•Œ 수 μ—†λŠ” ν† ν°μž…λ‹ˆλ‹€."),

// Conflict Error
OPTIMISTIC_EXCEPTION(40900, HttpStatus.CONFLICT, "Optimistic Locking μ˜ˆμ™Έκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€."),

// Internal Server Error
INTERNAL_SERVER_ERROR(50000, HttpStatus.INTERNAL_SERVER_ERROR, "μ„œλ²„ λ‚΄λΆ€ μ—λŸ¬μž…λ‹ˆλ‹€."),
INTERNAL_DATA_ERROR(50001, HttpStatus.INTERNAL_SERVER_ERROR, "μ„œλ²„ λ‚΄λΆ€ 데이터 μ—λŸ¬μž…λ‹ˆλ‹€."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ public ResponseDto<Void> likeBoard(
@AccountID UUID accountId,
@PathVariable Long id
) {
if (createOrDeleteLikeUseCase.execute(accountId, id)) {
return ResponseDto.created(null);
} else {
return ResponseDto.ok(null);
}
createOrDeleteLikeUseCase.execute(accountId, id);
return ResponseDto.ok(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.daon.onjung.suggestion.application.controller.consumer;

import com.daon.onjung.account.domain.User;
import com.daon.onjung.account.repository.mysql.UserRepository;
import com.daon.onjung.core.exception.error.ErrorCode;
import com.daon.onjung.core.exception.type.CommonException;
import com.daon.onjung.suggestion.application.dto.request.CommentMessage;
import com.daon.onjung.suggestion.domain.Board;
import com.daon.onjung.suggestion.domain.Comment;
import com.daon.onjung.suggestion.domain.service.BoardService;
import com.daon.onjung.suggestion.domain.service.CommentService;
import com.daon.onjung.suggestion.repository.mysql.BoardRepository;
import com.daon.onjung.suggestion.repository.mysql.CommentRepository;
import lombok.RequiredArgsConstructor;
import org.hibernate.dialect.lock.OptimisticEntityLockException;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class CommentV1Consumer {

private final BoardRepository boardRepository;
private final CommentRepository commentRepository;
private final UserRepository userRepository;

private final CommentService commentService;
private final BoardService boardService;

@Transactional
@RabbitListener(queues = "comment-queue-1")
public void processCommentMessage1(CommentMessage commentMessage) {
try {

// κ²Œμ‹œκΈ€ 쑰회
Board board = boardRepository.findById(commentMessage.boardId())
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// μœ μ € 쑰회
User user = userRepository.findById(commentMessage.userId())
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// λŒ“κΈ€ 생성
Comment comment = commentService.createComment(
commentMessage.content(),
user,
board
);
commentRepository.save(comment);

// κ²Œμ‹œκΈ€ λŒ“κΈ€ 수 증가
board = boardService.addCommentCount(board);
boardRepository.save(board);
} catch (OptimisticEntityLockException e) {
throw new CommonException(ErrorCode.OPTIMISTIC_EXCEPTION);
}
}

@Transactional
@RabbitListener(queues = "comment-queue-2")
public void processCommentMessage2(CommentMessage commentMessage) {
try {

// κ²Œμ‹œκΈ€ 쑰회
Board board = boardRepository.findById(commentMessage.boardId())
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// μœ μ € 쑰회
User user = userRepository.findById(commentMessage.userId())
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// λŒ“κΈ€ 생성
Comment comment = commentService.createComment(
commentMessage.content(),
user,
board
);
commentRepository.save(comment);

// κ²Œμ‹œκΈ€ λŒ“κΈ€ 수 증가
board = boardService.addCommentCount(board);
boardRepository.save(board);
} catch (OptimisticEntityLockException e) {
throw new CommonException(ErrorCode.OPTIMISTIC_EXCEPTION);
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.daon.onjung.suggestion.application.controller.consumer;

import com.daon.onjung.account.domain.User;
import com.daon.onjung.account.repository.mysql.UserRepository;
import com.daon.onjung.core.exception.error.ErrorCode;
import com.daon.onjung.core.exception.type.CommonException;
import com.daon.onjung.suggestion.application.dto.request.LikeMessage;
import com.daon.onjung.suggestion.domain.Board;
import com.daon.onjung.suggestion.domain.Like;
import com.daon.onjung.suggestion.domain.service.BoardService;
import com.daon.onjung.suggestion.domain.service.LikeService;
import com.daon.onjung.suggestion.repository.mysql.BoardRepository;
import com.daon.onjung.suggestion.repository.mysql.LikeRepository;
import lombok.RequiredArgsConstructor;
import org.hibernate.dialect.lock.OptimisticEntityLockException;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class LikeV1Consumer {

private final LikeRepository likeRepository;
private final BoardRepository boardRepository;
private final UserRepository userRepository;

private final LikeService likeService;
private final BoardService boardService;

@Transactional
@RabbitListener(queues = "like-queue")
public void processLikeMessage(LikeMessage likeMessage) {
try {

// κ²Œμ‹œκΈ€ 쑰회
Board board = boardRepository.findById(likeMessage.boardId())
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// μœ μ € 쑰회
User user = userRepository.findById(likeMessage.userId())
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// μ’‹μ•„μš”κ°€ μ‘΄μž¬ν•˜λŠ”μ§€ 확인
Like like = likeRepository.findByBoardAndUser(board, user)
.orElse(null);

if (like != null) {

// μ’‹μ•„μš”κ°€ 이미 μ‘΄μž¬ν•˜λ©΄ μ‚­μ œ
likeRepository.delete(like);
// κ²Œμ‹œκΈ€ μ’‹μ•„μš” 수 κ°μ†Œ
board = boardService.subtractLikeCount(board);
boardRepository.save(board);
} else {

// μ’‹μ•„μš”κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ 생성
likeRepository.save(likeService.createLike(user, board));

// κ²Œμ‹œκΈ€ μ’‹μ•„μš” 수 증가
board = boardService.addLikeCount(board);
boardRepository.save(board);
}
} catch (OptimisticEntityLockException e) {
throw new CommonException(ErrorCode.OPTIMISTIC_EXCEPTION);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.daon.onjung.suggestion.application.controller.producer;

import com.daon.onjung.suggestion.application.dto.request.CommentMessage;
import com.daon.onjung.suggestion.application.usecase.SendCommentRequestUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CommentV1Producer {

private final SendCommentRequestUseCase sendCommentRequestUseCase;

public void sendComment (CommentMessage commentMessage) {
sendCommentRequestUseCase.execute(commentMessage);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.daon.onjung.suggestion.application.controller.producer;

import com.daon.onjung.suggestion.application.dto.request.LikeMessage;
import com.daon.onjung.suggestion.application.usecase.SendLikeRequestUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LikeV1Producer {

private final SendLikeRequestUseCase sendLikeRequestUseCase;

public void sendLike (LikeMessage likeMessage) {
sendLikeRequestUseCase.execute(likeMessage);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.daon.onjung.suggestion.application.dto.request;

import lombok.Builder;

import java.util.UUID;

@Builder
public record CommentMessage(
String content,
Long boardId,
UUID userId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.daon.onjung.suggestion.application.dto.request;

import lombok.Builder;

import java.util.UUID;

@Builder
public record LikeMessage(
Long boardId,
UUID userId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@
import com.daon.onjung.account.repository.mysql.UserRepository;
import com.daon.onjung.core.exception.error.ErrorCode;
import com.daon.onjung.core.exception.type.CommonException;
import com.daon.onjung.suggestion.application.controller.producer.CommentV1Producer;
import com.daon.onjung.suggestion.application.dto.request.CommentMessage;
import com.daon.onjung.suggestion.application.dto.request.CreateCommentRequestDto;
import com.daon.onjung.suggestion.application.usecase.CreateCommentUseCase;
import com.daon.onjung.suggestion.domain.Board;
import com.daon.onjung.suggestion.domain.Comment;
import com.daon.onjung.suggestion.domain.service.BoardService;
import com.daon.onjung.suggestion.domain.service.CommentService;
import com.daon.onjung.suggestion.repository.mysql.BoardRepository;
import com.daon.onjung.suggestion.repository.mysql.CommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -24,10 +22,8 @@ public class CreateCommentService implements CreateCommentUseCase {

private final UserRepository userRepository;
private final BoardRepository boardRepository;
private final CommentRepository commentRepository;

private final CommentService commentService;
private final BoardService boardService;
private final CommentV1Producer commentProducer;

@Override
@Transactional
Expand All @@ -41,16 +37,12 @@ public void execute(UUID accountId, Long boardId, CreateCommentRequestDto reques
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE));

// λŒ“κΈ€ 생성
Comment comment = commentService.createComment(
requestDto.content(),
user,
board
// λŒ“κΈ€ 생성 μš”μ²­ λ°œμ†‘
commentProducer.sendComment(CommentMessage.builder()
.content(requestDto.content())
.userId(user.getId())
.boardId(board.getId())
.build()
);
commentRepository.save(comment);

// κ²Œμ‹œκΈ€ λŒ“κΈ€ 수 증가
board = boardService.addCommentCount(board);
boardRepository.save(board);
}
}
Loading

0 comments on commit bb5fe1b

Please sign in to comment.