Skip to content

Commit

Permalink
인증에 성공한 미션에 대한 게시글 업로드 및 이미지 업로드 기능 리팩토링 (#106)
Browse files Browse the repository at this point in the history
* refactor : 선언적 트랜잭션 -> 프로그래매틱 트랜잭션으로 관리 변경

* refactor : MemberMissionCertifyService 에서 발행한 이벤트 TransactionalEventListener 제거 후 EventListener 적용

* refactor : TransactionTemplate 직접 주입으로 변경

* test : 트랜잭션 관리 변경으로 인한 테스트 코드 추가
  • Loading branch information
oownahcohc authored Sep 4, 2024
1 parent f356edd commit 4a1fd06
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,45 +1,70 @@
package univ.earthbreaker.namu.core.domain.mission;

import java.util.concurrent.atomic.AtomicBoolean;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import univ.earthbreaker.namu.event.point.AddRewardPointEvent;
import univ.earthbreaker.namu.event.image.DeleteUploadedImageEvent;
import univ.earthbreaker.namu.event.EventPublisher;
import univ.earthbreaker.namu.event.image.DeleteUploadedImageEvent;
import univ.earthbreaker.namu.event.point.AddRewardPointEvent;
import univ.earthbreaker.namu.event.post.PostCreateEvent;

@Service
public class MemberMissionCertifyService {

private static final Logger LOGGER = LoggerFactory.getLogger(MemberMissionCertifyService.class);
private static final AtomicBoolean IS_ROLLBACK = new AtomicBoolean(false);

private final MemberMissionFinder memberMissionFinder;
private final MissionCertifyHandler missionCertifyHandler;
private final EventPublisher eventPublisher;
private final TransactionTemplate transactionTemplate;

public MemberMissionCertifyService(
MemberMissionFinder memberMissionFinder,
MissionCertifyHandler missionCertifyHandler,
EventPublisher eventPublisher
EventPublisher eventPublisher,
TransactionTemplate transactionTemplate
) {
this.memberMissionFinder = memberMissionFinder;
this.missionCertifyHandler = missionCertifyHandler;
this.eventPublisher = eventPublisher;
this.transactionTemplate = transactionTemplate;
}

@Transactional
public void successMission(
@NotNull MissionCompleteCommand missionCommand,
@NotNull CertifiedMissionPostCommand postCommand
) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
try {
MemberMission memberMission = memberMissionFinder.find(
missionCommand.getMemberNo(), missionCommand.getMissionNo());
MemberMission successMission = missionCertifyHandler.success(memberMission);
publishRewardEventForSuccessMission(successMission);
publishCreatePostEventForSuccessMission(postCommand, successMission);
} catch (Exception e) {
LOGGER.info("예외 발생으로 인한 트랜잭션 롤백 및 이미지 삭제 : {}", e.getMessage());
status.setRollbackOnly();
IS_ROLLBACK.set(status.isRollbackOnly());
}
}
});
publishDeleteUploadImageEventWhenTransactionRollback(postCommand.getImagePathKey());
MemberMission memberMission = memberMissionFinder.find(missionCommand.getMemberNo(), missionCommand.getMissionNo());
MemberMission successMission = missionCertifyHandler.success(memberMission);
publishRewardEventForSuccessMission(successMission);
publishCreatePostEventForSuccessMission(postCommand, successMission);
}

private void publishDeleteUploadImageEventWhenTransactionRollback(@NotNull String imagePathKey) {
eventPublisher.publish(new DeleteUploadedImageEvent(imagePathKey));
if (IS_ROLLBACK.get()) {
eventPublisher.publish(new DeleteUploadedImageEvent(imagePathKey));
}
}

private void publishRewardEventForSuccessMission(@NotNull MemberMission successMission) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package univ.earthbreaker.namu.core.domain.point;

import org.jetbrains.annotations.NotNull;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
Expand All @@ -17,7 +18,7 @@ public EnergyPointEventHandler(EnergyPointRepository energyPointRepository) {
this.energyPointRepository = energyPointRepository;
}

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@EventListener
public void giveRewardPoint(@NotNull AddRewardPointEvent event) {
PointUpdateDbCommand command = new PointUpdateDbCommand(event.memberNo(), event.point());
energyPointRepository.receivePoint(command);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package univ.earthbreaker.namu.core.domain.post;

import org.jetbrains.annotations.NotNull;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import univ.earthbreaker.namu.event.post.PostCreateEvent;

Expand All @@ -18,7 +17,7 @@ public PostCreateEventHandler(PostMemberBridge postMemberBridge, PostRepository
this.postRepository = postRepository;
}

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@EventListener
public void createPost(@NotNull PostCreateEvent event) {
PostMemberBridge.PostMemberDto postMemberInfo = postMemberBridge.findMemberInfo(event.memberNo());
PostCreateDbCommand createCommand = new PostCreateDbCommand(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package univ.earthbreaker.namu.external.aws.image;

import org.jetbrains.annotations.NotNull;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import univ.earthbreaker.namu.event.image.DeleteUploadedImageEvent;

Expand All @@ -16,7 +15,7 @@ public AwsS3ImageEventHandler(ImageManager imageManager) {
this.imageManager = imageManager;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
@EventListener
public void deleteImage(@NotNull DeleteUploadedImageEvent event) {
imageManager.delete(event.imagePathKey());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package univ.earthbreaker.namu.core.domain.mission;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static univ.earthbreaker.namu.core.domain.mission.MissionFixture.DEFAULT_MISSION_NO;
Expand All @@ -13,6 +17,9 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import univ.earthbreaker.namu.event.EventPublisher;
import univ.earthbreaker.namu.event.image.DeleteUploadedImageEvent;
Expand All @@ -28,19 +35,27 @@ class MemberMissionCertifyServiceTest {
private @Mock MemberMissionFinder memberMissionFinder;
private @Mock MissionCertifyHandler missionCertifyHandler;
private @Mock EventPublisher eventPublisher;
private @Mock TransactionTemplate transactionTemplate;
private @InjectMocks MemberMissionCertifyService memberMissionCertifyService;

@DisplayName("""
회원의 인증 성공한 미션을 찾아와 '미션 성공'상태로 변경하고,
보상 포인트 지급 이벤트와 게시글 업로드 이벤트, 롤백 발생 시 업로드한 이미지 삭제 이벤트를 발행한다""")
@DisplayName("회원의 인증 성공한 미션을 찾아와 '미션 성공'상태로 변경하고, 보상 포인트 지급 이벤트와 게시글 업로드 이벤트를 발행한다")
@Test
void successMission() {
void successMission_and_publishEventInTransaction() {
// given
when(memberMissionFinder.find(MEMBER_NO, DEFAULT_MISSION_NO))
.thenReturn(DEFAULT_MISSION_READY);
MemberMission successMission = DEFAULT_MISSION_READY.success();
when(missionCertifyHandler.success(DEFAULT_MISSION_READY))
.thenReturn(successMission);
when(transactionTemplate.execute(any(TransactionCallbackWithoutResult.class)))
.thenAnswer(invocation -> {
TransactionCallbackWithoutResult callback = invocation.getArgument(0);
TransactionStatus status = mock(TransactionStatus.class);

when(memberMissionFinder.find(MEMBER_NO, DEFAULT_MISSION_NO))
.thenReturn(DEFAULT_MISSION_READY);
when(missionCertifyHandler.success(DEFAULT_MISSION_READY))
.thenReturn(DEFAULT_MISSION_READY.success());

callback.doInTransaction(status);

return null;
});

// when
memberMissionCertifyService.successMission(
Expand All @@ -49,18 +64,49 @@ void successMission() {
);

// then
MemberMission successMission = DEFAULT_MISSION_READY.success();
assertAll(
() -> verify(eventPublisher)
.publish(new AddRewardPointEvent(successMission.getMemberNo(), successMission.getRewardPoint())),
() -> verify(eventPublisher)
.publish(new PostCreateEvent(
MEMBER_NO, successMission.getActivity(), MISSION_POST_CONTENT,
MISSION_POST_IMAGE_PATH_KEY, successMission.getNo()
)),
() -> verify(eventPublisher).publish(new DeleteUploadedImageEvent(MISSION_POST_IMAGE_PATH_KEY))
))
);
}

@DisplayName("트랜잭션 내에서 예외가 발생하면 트랜잭션을 롤백하고 이미지 삭제 이벤트를 발행한다")
@Test
void rollbackTransactionAndTriggerImageDeletionEventOnException() {
// given
when(transactionTemplate.execute(any(TransactionCallbackWithoutResult.class)))
.thenAnswer(invocation -> {
TransactionCallbackWithoutResult callback = invocation.getArgument(0);
TransactionStatus status = mock(TransactionStatus.class);

doThrow(new RuntimeException("테스트 런타임 예외 발생!"))
.when(missionCertifyHandler).success(any(MemberMission.class));
doAnswer(invocationOnMock -> {
when(status.isRollbackOnly()).thenReturn(true);
return null;
}).when(status).setRollbackOnly();

callback.doInTransaction(status);

return null;
});

// when
memberMissionCertifyService.successMission(
new MissionCompleteCommand(MEMBER_NO, DEFAULT_MISSION_NO),
new CertifiedMissionPostCommand(MEMBER_NO, MISSION_POST_CONTENT, MISSION_POST_IMAGE_PATH_KEY)
);

// then
verify(eventPublisher).publish(new DeleteUploadedImageEvent(MISSION_POST_IMAGE_PATH_KEY));
}

@DisplayName("회원의 인증 성공한 미션을 찾아와 '미션 실패'상태로 변경한다")
@Test
void failureMission() {
Expand Down

0 comments on commit 4a1fd06

Please sign in to comment.