게시글 작성 API 로직이 ‘포인트 차감 → 게시글 저장 → 활동 점수 적립’ 구성으로 이루어져 있다. ‘포인트 차감, 게시글 저장, 활동 점수 적립’은 기획상 하나로 묶인 작업이기 때문에 이 중에 하나라도 실패하면 전체가 실패해야 한다. 즉, 활동 점수 적립에 실패하면 게시글 저장도 실패해야 하고 포인트 차감도 실패해야 하는 게 정상일 것이다. 정말 이렇게 작동하는 지 확인해보자.
활동 점수 증가 API에 요청을 보내면 에러가 발생하도록 만들기
활동 점수를 증가시키다 실패하는 경우를 가정하기 위해, 에러 발생시키는 코드를 추가하자.
service/UserService
@Service
public class UserService {
...
@Transactional
public void addActivityScore(
AddActivityScoreRequestDto addActivityScoreRequestDto
) {
User user = userRepository.findById(addActivityScoreRequestDto.getUserId())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
user.addActivityScore(addActivityScoreRequestDto.getScore());
userRepository.save(user);
throw new RuntimeException("에러 발생");
}
}
user-service 서버 재실행시키기
테스트를 위해 DB 데이터 정리하기
활동 점수 0으로 바꾸기
포인트 데이터 1000점으로 맞춰놓기
게시글 데이터 전부 삭제하기
게시글 생성 API 요청 보내기
게시글 작성 API 로직의 ‘포인트 차감 → 게시글 저장 → 활동 점수 적립’ 중 ‘활동 점수 적립’ 단계에서 에러가 발생했기 때문에 게시글 작성 API 자체가 실패했다. 이렇게만 봤을 때는 의도한대로 ‘활동 점수 적립’이 실패함으로써 ‘게시글 작성’도 실패하고, ‘포인트 차감’도 실패한 것처럼 보인다. 정말 그렇게 작동했는 지 DB를 확인해보자.
DB 확인하기
DB를 보면 게시글 작성도 되지 않았고 사용자 활동 점수도 쌓이지 않았는데, 포인트 차감은 된 걸 확인할 수 있다. 왜 이런 현상이 발생한 걸까?
✅ 원인 분석
모놀리식 아키텍처와 달리 MSA 환경에서는 서비스마다 DB가 분리되어 있다. 그래서 서로 다른 DB에서 처리하는 여러 작업(포인트 차감, 게시글 작성, 활동 점수 적립)을 하나의 트랜잭션으로 묶을 수 없다. 그러다보니 에러가 발생했을 때 어떤 작업은 실패했는데, 어떤 작업은 성공하는 모순된 상황이 발생한 것이다. 코드를 보면서 조금 더 자세히 살펴보자.
BoardService의 ‘게시글 작성’ 비즈니스 로직
@Transactional
public void create(CreateBoardRequestDto createBoardRequestDto) {
// 게시글 작성 전 100 포인트 차감
pointClient.deductPoints(createBoardRequestDto.getUserId(), 100);
// 게시글 작성
Board board = new Board(
createBoardRequestDto.getTitle(),
createBoardRequestDto.getContent(),
createBoardRequestDto.getUserId()
);
this.boardRepository.save(board); // 롤백
// 게시글 작성 시 작성자에게 활동 점수 10점 부여
userClient.addActivityScore(createBoardRequestDto.getUserId(), 10); // 에러 발생
}
userClient.addActivityScore(...)에서 에러가 발생하는 순간 @Transactional 어노테이션 때문에 게시글 저장(this.boardRepostiory.save())이 롤백(rollback)된다.
PointService의 ‘포인트 차감’ 비즈니스 로직
@Transactional
public void deductPoints(DeductPointRequestDto deductPointRequestDto) {
Point point = pointRepository.findByUserId(deductPointRequestDto.getUserId())
.orElseThrow(() -> new IllegalArgumentException("사용자의 포인트 정보를 찾을 수 없습니다."));
point.deductAmount(deductPointRequestDto.getAmount());
pointRepository.save(point);
}
‘포인트 적립’ 비즈니스 로직에서는 에러가 발생하지 않았기 때문에
포인트 적립(pointRepository.save())이 롤백(rollback)되지 않고 커밋(commit)된다.
UserService의 ‘활동 점수 증가’ 비즈니스 로직
@Transactional
public void addActivityScore(
AddActivityScoreRequestDto addActivityScoreRequestDto
) {
User user = userRepository.findById(addActivityScoreRequestDto.getUserId())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
user.addActivityScore(addActivityScoreRequestDto.getScore());
userRepository.save(user); // 롤백
throw new RuntimeException("에러 발생");
}
‘활동 점수 증가’ 비즈니스 로직에서는 에러가 발생했기 때문에 @Transactional에 의해
‘활동 점수 증가(userRepository.save())’가 롤백(rollback)된다.