From 2138bf11debbbbc3917bc8dcb715677019958a19 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 09:43:09 +0900 Subject: [PATCH 01/15] =?UTF-8?q?[test]=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=8B=9C=EA=B0=84=20=EC=B8=A1=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20ExeTimer=20=EB=A7=88=ED=82=B9?= =?UTF-8?q?=EC=9A=A9=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/cau/puangbe/common/annotation/ExeTimer.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/gdsc/cau/puangbe/common/annotation/ExeTimer.java diff --git a/src/main/java/gdsc/cau/puangbe/common/annotation/ExeTimer.java b/src/main/java/gdsc/cau/puangbe/common/annotation/ExeTimer.java new file mode 100644 index 0000000..36c9445 --- /dev/null +++ b/src/main/java/gdsc/cau/puangbe/common/annotation/ExeTimer.java @@ -0,0 +1,11 @@ +package gdsc.cau.puangbe.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExeTimer { +} \ No newline at end of file From 59dcf5b47b9e1f20c8758c35204f402ade379a81 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 09:44:20 +0900 Subject: [PATCH 02/15] =?UTF-8?q?[feat]=20email=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java b/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java index b42bcfd..c46ebca 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java +++ b/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java @@ -61,4 +61,9 @@ public void finishStatus() { this.status = RequestStatus.FINISHED; this.updateDate = LocalDateTime.now(); } + + public void modifyEmail(String email) { + this.email = email; + this.updateDate = LocalDateTime.now(); + } } From e3d3def07c6214f9a5002c5f65a57a9a5f15fd90 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 09:44:53 +0900 Subject: [PATCH 03/15] =?UTF-8?q?[feat]=20email=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4,=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PhotoRequestService.java | 3 +++ .../service/PhotoRequestServiceImpl.java | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestService.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestService.java index c129f39..1fb7fda 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestService.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestService.java @@ -12,4 +12,7 @@ public interface PhotoRequestService { //최근 생성 요청한 이미지의 상태 조회 String getRequestStatus(Long userId); + + // 이미지 처리 요청이 끝나지 않았을 경우 이메일 업데이트 + Long updateEmail(Long userId, String email); } diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 839b87b..3e79c22 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -120,6 +120,22 @@ public String getRequestStatus(Long userId){ return status.name(); } + @Override + @Transactional(readOnly = true) + public Long updateEmail(Long userId, String email) { + log.info("이메일 수정 진입"); + // 가장 최근의 PhotoRequest 조회 + PhotoRequest photoRequest = photoRequestRepository.findTopByUserIdOrderByCreateDateDesc(userId) + .orElseThrow(() -> new BaseException(ResponseCode.PHOTO_REQUEST_NOT_FOUND)); + + photoRequest.modifyEmail(email); + photoRequestRepository.save(photoRequest); + + log.info("이메일 수정 완료: {}", photoRequest.getEmail()); + + return photoRequest.getId(); + } + // 유저id 유효성 검사 private void validateUser(Long userId){ if(!userRepository.existsById(userId)){ From 87696f4807039f0e8ba37ebce22e48bf4df24d6e Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 09:46:25 +0900 Subject: [PATCH 04/15] =?UTF-8?q?[fix]=20redis=20=EC=97=86=EC=9D=B4=20db?= =?UTF-8?q?=EC=97=90=20=EC=A0=91=EA=B7=BC=ED=95=98=EC=97=AC=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=83=9D=EC=84=B1=20=EC=9A=94=EC=B2=AD=EC=9D=98=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=83=81=ED=99=A9=20=ED=8C=8C=EC=95=85=20?= =?UTF-8?q?=ED=9B=84=20=EC=83=88=20=EC=9A=94=EC=B2=AD=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo/service/PhotoServiceImpl.java | 7 +------ .../service/PhotoRequestServiceImpl.java | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 8391738..8b0505d 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -50,12 +50,7 @@ public void uploadPhoto(Long photoRequestId, String imageUrl) { photoResultRepository.save(photoResult); log.info("결과 이미지 URL 업로드 완료: {}", imageUrl); - // Redis 대기열의 user 정보 삭제 - redisTemplate.opsForSet().remove(ConstantUtil.USER_ID_KEY, user.getId()); - redisTemplate.delete(user.getId().toString()); - log.info("Redis 대기열에서 요청 삭제 : {}", user.getId()); - - // 이메일 발송 + // 이메일 발송 - 나중에 분리할 것이므로 제외 EmailInfo emailInfo = EmailInfo.builder() .email(photoRequest.getEmail()) .photoUrl(imageUrl) diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 3e79c22..8ab4e06 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -43,6 +43,17 @@ public class PhotoRequestServiceImpl implements PhotoRequestService { @Transactional public Long createImage(CreateImageDto dto, Long userId){ User user = userRepository.findById(userId).orElseThrow(() -> new BaseException(ResponseCode.USER_NOT_FOUND)); + + // 가장 최신의 photo_request 조회 + PhotoRequest latestRequest = photoRequestRepository + .findTopByUserIdOrderByCreateDateDesc(userId) + .orElseThrow(() -> new RuntimeException("No photo requests found for userId: " + userId)); + + // 상태 체크 + if (latestRequest.getStatus() == RequestStatus.WAITING) { + return latestRequest.getId(); // 상태가 'waiting'이면 requestId 반환 + } + // PhotoRequest 생성 PhotoRequest request = PhotoRequest.builder() .user(user) @@ -79,10 +90,6 @@ public Long createImage(CreateImageDto dto, Long userId){ throw new PhotoRequestException(ResponseCode.JSON_PARSE_ERROR); } - // Redis에 userId 저장하고, userId로 requestId 추적할 수 있도록 함 - redisTemplate.opsForSet().add(ConstantUtil.USER_ID_KEY, userId); - redisTemplate.opsForSet().add(userId.toString(), request.getId()); - log.info("Redis 대기열 등록 완료: {}", userId); return request.getId(); } @@ -107,12 +114,6 @@ public List getRequestImages(Long userId){ public String getRequestStatus(Long userId){ validateUser(userId); - // Redis에 userId가 존재하면 아직 처리 대기 중인 요청이므로 WAITING 반환 - if(Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(ConstantUtil.USER_ID_KEY, userId))){ - log.info("사용자의 요청 상태 조회, 현재 대기 중: {}", userId); - return RequestStatus.WAITING.name(); - } - RequestStatus status = photoRequestRepository.findTopByUserIdOrderByCreateDateDesc(userId) .orElseThrow(() -> new BaseException(ResponseCode.PHOTO_REQUEST_NOT_FOUND)) .getStatus(); From cb21978a242a6f1c865b784f917c8df2a86a168a Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 09:47:07 +0900 Subject: [PATCH 05/15] =?UTF-8?q?[test]=20photo=5Frequest=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=88=98=EC=A0=95=20/=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=84=EC=86=A1=EC=9D=B4=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=97=90=20=EC=9D=BC=EC=96=B4=EB=82=98?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PhotoConcurrencyTeset.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java diff --git a/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java b/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java new file mode 100644 index 0000000..5962db7 --- /dev/null +++ b/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java @@ -0,0 +1,70 @@ +package gdsc.cau.puangbe.photorequest.service; + +import gdsc.cau.puangbe.photo.service.PhotoService; +import gdsc.cau.puangbe.photo.service.PhotoServiceImpl; +import gdsc.cau.puangbe.photorequest.dto.CreateImageDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +@SpringBootTest +public class PhotoConcurrencyTeset { + + @Autowired + private PhotoServiceImpl photoService; + + @Autowired + private PhotoRequestServiceImpl photoRequestService; + + @Test + void 수정_조회가_동시에_발생하는_경우_테스트() throws InterruptedException { + + Long userId = 1000L; // 테스트할 사용자 ID + String email = "newEmail@gmail.com"; // 변경할 이메일 + + CountDownLatch latch = new CountDownLatch(2); + ExecutorService executor = Executors.newFixedThreadPool(2); + + AtomicInteger successCount = new AtomicInteger(); + AtomicInteger failCount = new AtomicInteger(); + + // uploadPhoto 호출 + executor.submit(() -> { + try { + photoService.uploadPhoto(998L, "http://example.com/image.jpg"); + successCount.incrementAndGet(); + } catch (Exception e) { + System.out.println(e.getMessage()); + failCount.incrementAndGet(); + } finally { + latch.countDown(); + } + }); + + // modifyEmail 호출 + executor.submit(() -> { + try { + Thread.sleep(100); + photoRequestService.updateEmail(userId, email); + successCount.incrementAndGet(); + } catch (Exception e) { + System.out.println(e.getMessage()); + failCount.incrementAndGet(); + } finally { + latch.countDown(); + } + }); + + latch.await(); // 두 개의 쓰레드가 완료될 때까지 대기 + executor.shutdown(); + + System.out.println("success count: " + successCount.get()); + System.out.println("fail count: " + failCount.get()); + + } +} From 3cb902a38b9f5e1ef4dc4a6046da0177d7bfc934 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 12:23:06 +0900 Subject: [PATCH 06/15] =?UTF-8?q?[test]=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=EC=8B=9C=EA=B0=84=20=EC=B8=A1=EC=A0=95=20Exe?= =?UTF-8?q?cutionTimer=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../puangbe/common/util/ExecutionTimer.java | 37 +++++++++++++++++++ .../photo/service/PhotoServiceImpl.java | 4 +- .../service/PhotoRequestServiceImpl.java | 2 + 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/main/java/gdsc/cau/puangbe/common/util/ExecutionTimer.java diff --git a/src/main/java/gdsc/cau/puangbe/common/util/ExecutionTimer.java b/src/main/java/gdsc/cau/puangbe/common/util/ExecutionTimer.java new file mode 100644 index 0000000..f758df6 --- /dev/null +++ b/src/main/java/gdsc/cau/puangbe/common/util/ExecutionTimer.java @@ -0,0 +1,37 @@ +package gdsc.cau.puangbe.common.util; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +@Slf4j +@Aspect +@Component +public class ExecutionTimer { + // 조인포인트를 어노테이션으로 설정 + @Pointcut("@annotation(gdsc.cau.puangbe.common.annotation.ExeTimer)") + private void timer(){}; + + // 메서드 실행 전,후로 시간을 공유해야 하기 때문 + @Around("timer()") + public void AssumeExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + + StopWatch stopWatch = new StopWatch(); + + stopWatch.start(); + joinPoint.proceed(); // 조인포인트의 메서드 실행 + stopWatch.stop(); + + long totalTimeMillis = stopWatch.getTotalTimeMillis(); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String methodName = signature.getMethod().getName(); + + log.info("실행 메서드: {}, 실행시간 = {}ms", methodName, totalTimeMillis); + } +} diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 8b0505d..841314b 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -1,5 +1,6 @@ package gdsc.cau.puangbe.photo.service; +import gdsc.cau.puangbe.common.annotation.ExeTimer; import gdsc.cau.puangbe.common.enums.RequestStatus; import gdsc.cau.puangbe.common.exception.BaseException; import gdsc.cau.puangbe.common.util.ConstantUtil; @@ -33,6 +34,7 @@ public class PhotoServiceImpl implements PhotoService { private final TemplateEngine templateEngine; // 완성된 요청 id 및 imageUrl을 받아 저장 + @ExeTimer @Override @Transactional public void uploadPhoto(Long photoRequestId, String imageUrl) { @@ -54,7 +56,7 @@ public void uploadPhoto(Long photoRequestId, String imageUrl) { EmailInfo emailInfo = EmailInfo.builder() .email(photoRequest.getEmail()) .photoUrl(imageUrl) - .name(user.getUserName()) + .name(user.getUserName()) // 지연 로딩을 사용하기 때문에 여기서 user.getName을 하는 순간에 Hibernate select .framePageUrl("https://www.google.com/") // TODO : 프론트 분들 링크 관련 답변 오면 프레임 페이지 링크 관련 수정 .build(); diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 8ab4e06..1b50222 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import gdsc.cau.puangbe.common.annotation.ExeTimer; import gdsc.cau.puangbe.common.enums.Gender; import gdsc.cau.puangbe.common.enums.RequestStatus; import gdsc.cau.puangbe.common.exception.BaseException; @@ -121,6 +122,7 @@ public String getRequestStatus(Long userId){ return status.name(); } + @ExeTimer @Override @Transactional(readOnly = true) public Long updateEmail(Long userId, String email) { From f8a4ae9201094b869f21eaec0ddf32a6743c44c6 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 12:31:35 +0900 Subject: [PATCH 07/15] =?UTF-8?q?[refactor]=20uploadPhoto=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EA=B4=80=EB=A0=A8=20=EC=A3=BC=EC=84=9D=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 841314b..4ff1672 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -38,11 +38,14 @@ public class PhotoServiceImpl implements PhotoService { @Override @Transactional public void uploadPhoto(Long photoRequestId, String imageUrl) { + // 예외처리 PhotoRequest photoRequest = photoRequestRepository.findById(photoRequestId) .orElseThrow(() -> new BaseException(ResponseCode.PHOTO_REQUEST_NOT_FOUND)); if (photoRequest.getStatus() == RequestStatus.FINISHED) { throw new BaseException(ResponseCode.URL_ALREADY_UPLOADED); } + + // 결과 이미지 업데이트 User user = photoRequest.getUser(); PhotoResult photoResult = getPhotoResult(photoRequestId); @@ -52,11 +55,11 @@ public void uploadPhoto(Long photoRequestId, String imageUrl) { photoResultRepository.save(photoResult); log.info("결과 이미지 URL 업로드 완료: {}", imageUrl); - // 이메일 발송 - 나중에 분리할 것이므로 제외 + // 이메일 발송 EmailInfo emailInfo = EmailInfo.builder() .email(photoRequest.getEmail()) .photoUrl(imageUrl) - .name(user.getUserName()) // 지연 로딩을 사용하기 때문에 여기서 user.getName을 하는 순간에 Hibernate select + .name(user.getUserName()) .framePageUrl("https://www.google.com/") // TODO : 프론트 분들 링크 관련 답변 오면 프레임 페이지 링크 관련 수정 .build(); From 5fb6f3c00fc17677cb6afdb53c190ddbaee0bf68 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 13:38:24 +0900 Subject: [PATCH 08/15] =?UTF-8?q?[fix]=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...PhotoConcurrencyTeset.java => PhotoConcurrencyTest.java} | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename src/test/java/gdsc/cau/puangbe/photorequest/service/{PhotoConcurrencyTeset.java => PhotoConcurrencyTest.java} (90%) diff --git a/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java b/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java similarity index 90% rename from src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java rename to src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java index 5962db7..2415a9e 100644 --- a/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTeset.java +++ b/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java @@ -1,8 +1,6 @@ package gdsc.cau.puangbe.photorequest.service; -import gdsc.cau.puangbe.photo.service.PhotoService; import gdsc.cau.puangbe.photo.service.PhotoServiceImpl; -import gdsc.cau.puangbe.photorequest.dto.CreateImageDto; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -13,7 +11,7 @@ import java.util.concurrent.atomic.AtomicInteger; @SpringBootTest -public class PhotoConcurrencyTeset { +public class PhotoConcurrencyTest { @Autowired private PhotoServiceImpl photoService; @@ -25,7 +23,7 @@ public class PhotoConcurrencyTeset { void 수정_조회가_동시에_발생하는_경우_테스트() throws InterruptedException { Long userId = 1000L; // 테스트할 사용자 ID - String email = "newEmail@gmail.com"; // 변경할 이메일 + String email = "newUser998@example.com"; // 변경할 이메일 CountDownLatch latch = new CountDownLatch(2); ExecutorService executor = Executors.newFixedThreadPool(2); From 206afa84571882f3cee5df99d6a4b5bd0ebfe821 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 14:37:20 +0900 Subject: [PATCH 09/15] =?UTF-8?q?[fix]=20email=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20transactional=20readonly=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java | 1 - .../puangbe/photorequest/service/PhotoRequestServiceImpl.java | 4 +--- .../puangbe/photorequest/service/PhotoConcurrencyTest.java | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 4ff1672..0b7c91e 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -34,7 +34,6 @@ public class PhotoServiceImpl implements PhotoService { private final TemplateEngine templateEngine; // 완성된 요청 id 및 imageUrl을 받아 저장 - @ExeTimer @Override @Transactional public void uploadPhoto(Long photoRequestId, String imageUrl) { diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 1b50222..15eca0a 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -122,11 +122,9 @@ public String getRequestStatus(Long userId){ return status.name(); } - @ExeTimer @Override - @Transactional(readOnly = true) + @Transactional public Long updateEmail(Long userId, String email) { - log.info("이메일 수정 진입"); // 가장 최근의 PhotoRequest 조회 PhotoRequest photoRequest = photoRequestRepository.findTopByUserIdOrderByCreateDateDesc(userId) .orElseThrow(() -> new BaseException(ResponseCode.PHOTO_REQUEST_NOT_FOUND)); diff --git a/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java b/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java index 2415a9e..fb83bec 100644 --- a/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java +++ b/src/test/java/gdsc/cau/puangbe/photorequest/service/PhotoConcurrencyTest.java @@ -63,6 +63,5 @@ public class PhotoConcurrencyTest { System.out.println("success count: " + successCount.get()); System.out.println("fail count: " + failCount.get()); - } } From ddbef83f6f4886239bf210467d497af7b69e3813 Mon Sep 17 00:00:00 2001 From: rumos Date: Wed, 4 Sep 2024 19:42:44 +0900 Subject: [PATCH 10/15] =?UTF-8?q?[fix]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../puangbe/photorequest/service/PhotoRequestServiceImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 15eca0a..579b8a5 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -132,8 +132,6 @@ public Long updateEmail(Long userId, String email) { photoRequest.modifyEmail(email); photoRequestRepository.save(photoRequest); - log.info("이메일 수정 완료: {}", photoRequest.getEmail()); - return photoRequest.getId(); } From aa6d478ccf1535788fdad4ed6df2f70bf336b70e Mon Sep 17 00:00:00 2001 From: rumos Date: Thu, 5 Sep 2024 14:46:23 +0900 Subject: [PATCH 11/15] =?UTF-8?q?[test]=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=EC=9D=98=20=EC=A0=81=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=ED=95=98=EB=8A=94=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=EC=97=90=20=EB=8C=80=ED=95=B4=20=EC=B5=9C?= =?UTF-8?q?=EC=B4=88=EC=9D=98=20=EC=BB=A4=EB=B0=8B=EB=A7=8C=20=EC=9D=B8?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/cau/puangbe/common/util/ResponseCode.java | 1 + .../gdsc/cau/puangbe/photo/entity/PhotoRequest.java | 12 ++++++++++++ .../photo/repository/PhotoRequestRepository.java | 2 ++ .../cau/puangbe/photo/service/PhotoServiceImpl.java | 7 ++++++- .../service/PhotoRequestServiceImpl.java | 6 ++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/gdsc/cau/puangbe/common/util/ResponseCode.java b/src/main/java/gdsc/cau/puangbe/common/util/ResponseCode.java index 1677c6f..b7131fe 100644 --- a/src/main/java/gdsc/cau/puangbe/common/util/ResponseCode.java +++ b/src/main/java/gdsc/cau/puangbe/common/util/ResponseCode.java @@ -26,6 +26,7 @@ public enum ResponseCode { // 405 Method Not Allowed METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, false, "허용되지 않은 메서드입니다."), + OPTIMISTIC_LOCK_FAILED(HttpStatus.METHOD_NOT_ALLOWED, false, "낙관적 락에 의해 요청이 취소되었습니다."), // 409 Conflict USER_ALREADY_EXISTS(HttpStatus.CONFLICT, false, "이미 존재하는 사용자입니다."), diff --git a/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java b/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java index c46ebca..13db085 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java +++ b/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java @@ -46,6 +46,9 @@ public class PhotoRequest { @OneToMany(mappedBy = "request", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) private List photoUrls = new ArrayList<>(); + @Version + private Long version; + @Builder public PhotoRequest(User user, Gender gender, List urls, String email) { this.user = user; @@ -66,4 +69,13 @@ public void modifyEmail(String email) { this.email = email; this.updateDate = LocalDateTime.now(); } + + @Override + public String toString() { + return "PhotoRequest{" + + "id=" + id + + ", email='" + email + '\'' + + ", version=" + version + + '}'; + } } diff --git a/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java b/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java index 4a8437e..07d1197 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java +++ b/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java @@ -1,7 +1,9 @@ package gdsc.cau.puangbe.photo.repository; import gdsc.cau.puangbe.photo.entity.PhotoRequest; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.stereotype.Repository; import java.util.List; diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 0b7c91e..3845cee 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -12,8 +12,10 @@ import gdsc.cau.puangbe.photo.repository.PhotoRequestRepository; import gdsc.cau.puangbe.user.entity.User; import jakarta.mail.internet.MimeMessage; +import jakarta.persistence.LockModeType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; @@ -36,6 +38,7 @@ public class PhotoServiceImpl implements PhotoService { // 완성된 요청 id 및 imageUrl을 받아 저장 @Override @Transactional + @Lock(LockModeType.OPTIMISTIC) public void uploadPhoto(Long photoRequestId, String imageUrl) { // 예외처리 PhotoRequest photoRequest = photoRequestRepository.findById(photoRequestId) @@ -52,7 +55,9 @@ public void uploadPhoto(Long photoRequestId, String imageUrl) { photoRequestRepository.save(photoRequest); photoResult.update(imageUrl); photoResultRepository.save(photoResult); - log.info("결과 이미지 URL 업로드 완료: {}", imageUrl); + + log.info("uploadPhoto - PhotoRequest version: {}", photoRequest.getVersion()); + // 이메일 발송 EmailInfo emailInfo = EmailInfo.builder() diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 579b8a5..5aa6eff 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -17,8 +17,10 @@ import gdsc.cau.puangbe.photorequest.dto.ImageInfo; import gdsc.cau.puangbe.user.entity.User; import gdsc.cau.puangbe.user.repository.UserRepository; +import jakarta.persistence.LockModeType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -124,7 +126,9 @@ public String getRequestStatus(Long userId){ @Override @Transactional + @Lock(LockModeType.OPTIMISTIC) public Long updateEmail(Long userId, String email) { + // 가장 최근의 PhotoRequest 조회 PhotoRequest photoRequest = photoRequestRepository.findTopByUserIdOrderByCreateDateDesc(userId) .orElseThrow(() -> new BaseException(ResponseCode.PHOTO_REQUEST_NOT_FOUND)); @@ -132,6 +136,8 @@ public Long updateEmail(Long userId, String email) { photoRequest.modifyEmail(email); photoRequestRepository.save(photoRequest); + log.info("updateEmail - PhotoRequest version: {}", photoRequest.getVersion()); + return photoRequest.getId(); } From d0cfdd4477b13693a3b3c0d9b676dc0674d47734 Mon Sep 17 00:00:00 2001 From: rumos Date: Thu, 5 Sep 2024 15:07:42 +0900 Subject: [PATCH 12/15] =?UTF-8?q?[test]=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EC=9E=A0=EA=B8=88=20rollback=20=EB=B0=9C=EC=83=9D=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=AC=EC=8B=9C=EB=8F=84=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Retry=20AOP=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cau/puangbe/common/annotation/Retry.java | 11 ++++++ .../common/util/OptimisticLockRetry.java | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/main/java/gdsc/cau/puangbe/common/annotation/Retry.java create mode 100644 src/main/java/gdsc/cau/puangbe/common/util/OptimisticLockRetry.java diff --git a/src/main/java/gdsc/cau/puangbe/common/annotation/Retry.java b/src/main/java/gdsc/cau/puangbe/common/annotation/Retry.java new file mode 100644 index 0000000..b89703d --- /dev/null +++ b/src/main/java/gdsc/cau/puangbe/common/annotation/Retry.java @@ -0,0 +1,11 @@ +package gdsc.cau.puangbe.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Retry { +} diff --git a/src/main/java/gdsc/cau/puangbe/common/util/OptimisticLockRetry.java b/src/main/java/gdsc/cau/puangbe/common/util/OptimisticLockRetry.java new file mode 100644 index 0000000..f1b1856 --- /dev/null +++ b/src/main/java/gdsc/cau/puangbe/common/util/OptimisticLockRetry.java @@ -0,0 +1,38 @@ +package gdsc.cau.puangbe.common.util; + +import jakarta.persistence.OptimisticLockException; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.hibernate.StaleObjectStateException; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.stereotype.Component; + +@Order(Ordered.LOWEST_PRECEDENCE - 1) +@Aspect +@Component +public class OptimisticLockRetry { + private static final int MAX_RETRIES = 1000; // 최대 1000번 재시도 + private static final int RETRY_DELAY_MS = 100; // 0.1초 간격으로 재시도 + + @Pointcut("@annotation(Retry)") + public void retry() { + } + + @Around("retry()") + public Object retryOptimisticLock(ProceedingJoinPoint joinPoint) throws Throwable { + Exception exceptionHolder = null; + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + return joinPoint.proceed(); + } catch (OptimisticLockException | ObjectOptimisticLockingFailureException | StaleObjectStateException e) { + exceptionHolder = e; + Thread.sleep(RETRY_DELAY_MS); + } + } + throw exceptionHolder; + } +} From 97b10ca96fd6d28cf15599f372999eac03cc20b4 Mon Sep 17 00:00:00 2001 From: rumos Date: Thu, 5 Sep 2024 15:10:12 +0900 Subject: [PATCH 13/15] =?UTF-8?q?[test]=20uploadPhoto,=20updateEmail?= =?UTF-8?q?=EC=97=90=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20AOP=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java | 2 ++ .../puangbe/photorequest/service/PhotoRequestServiceImpl.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 3845cee..72aa2f0 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -1,6 +1,7 @@ package gdsc.cau.puangbe.photo.service; import gdsc.cau.puangbe.common.annotation.ExeTimer; +import gdsc.cau.puangbe.common.annotation.Retry; import gdsc.cau.puangbe.common.enums.RequestStatus; import gdsc.cau.puangbe.common.exception.BaseException; import gdsc.cau.puangbe.common.util.ConstantUtil; @@ -39,6 +40,7 @@ public class PhotoServiceImpl implements PhotoService { @Override @Transactional @Lock(LockModeType.OPTIMISTIC) + @Retry public void uploadPhoto(Long photoRequestId, String imageUrl) { // 예외처리 PhotoRequest photoRequest = photoRequestRepository.findById(photoRequestId) diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 5aa6eff..1f5ec21 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import gdsc.cau.puangbe.common.annotation.ExeTimer; +import gdsc.cau.puangbe.common.annotation.Retry; import gdsc.cau.puangbe.common.enums.Gender; import gdsc.cau.puangbe.common.enums.RequestStatus; import gdsc.cau.puangbe.common.exception.BaseException; @@ -127,6 +128,7 @@ public String getRequestStatus(Long userId){ @Override @Transactional @Lock(LockModeType.OPTIMISTIC) + @Retry public Long updateEmail(Long userId, String email) { // 가장 최근의 PhotoRequest 조회 From 3c69103aa30c322e718f9fb784228b4a8ebf2e52 Mon Sep 17 00:00:00 2001 From: rumos Date: Thu, 5 Sep 2024 15:42:21 +0900 Subject: [PATCH 14/15] =?UTF-8?q?[test]=20spring=EC=9D=98=20@Retryable?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=82=99=EA=B4=80=EC=A0=81=20=EB=9D=BD=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ src/main/java/gdsc/cau/puangbe/PuangbeApplication.java | 2 ++ .../cau/puangbe/photo/service/PhotoServiceImpl.java | 9 ++++++++- .../photorequest/service/PhotoRequestServiceImpl.java | 10 +++++++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d0d5ddd..087489b 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,10 @@ dependencies { implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' // Rabbit MQ implementation 'org.springframework.boot:spring-boot-starter-amqp' + + // spring-retry + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework:spring-aspects' } tasks.named('test') { diff --git a/src/main/java/gdsc/cau/puangbe/PuangbeApplication.java b/src/main/java/gdsc/cau/puangbe/PuangbeApplication.java index 7303131..124aa89 100644 --- a/src/main/java/gdsc/cau/puangbe/PuangbeApplication.java +++ b/src/main/java/gdsc/cau/puangbe/PuangbeApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableRetry @SpringBootApplication @ConfigurationPropertiesScan @EnableScheduling diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 72aa2f0..9e4d178 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -20,6 +20,9 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thymeleaf.TemplateEngine; @@ -40,7 +43,11 @@ public class PhotoServiceImpl implements PhotoService { @Override @Transactional @Lock(LockModeType.OPTIMISTIC) - @Retry + @Retryable( + retryFor = {ObjectOptimisticLockingFailureException.class}, + maxAttempts = 1000, + backoff = @Backoff(100) + ) public void uploadPhoto(Long photoRequestId, String imageUrl) { // 예외처리 PhotoRequest photoRequest = photoRequestRepository.findById(photoRequestId) diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 1f5ec21..63a7092 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -23,6 +23,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -125,10 +128,15 @@ public String getRequestStatus(Long userId){ return status.name(); } + @Override @Transactional @Lock(LockModeType.OPTIMISTIC) - @Retry + @Retryable( + retryFor = {ObjectOptimisticLockingFailureException.class}, + maxAttempts = 1000, + backoff = @Backoff(100) + ) public Long updateEmail(Long userId, String email) { // 가장 최근의 PhotoRequest 조회 From 4210ea382ab87d29255e9a635466dffb1ebf5bdc Mon Sep 17 00:00:00 2001 From: rumos Date: Thu, 5 Sep 2024 19:03:04 +0900 Subject: [PATCH 15/15] =?UTF-8?q?[test]=20photo=5Frequest=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=97=90=20=EB=B9=84=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=EC=9D=84=20=EC=A0=81=EC=9A=A9=ED=95=B4=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../puangbe/photo/entity/PhotoRequest.java | 12 ---------- .../repository/PhotoRequestRepository.java | 3 +++ .../photo/service/PhotoServiceImpl.java | 24 +++++++------------ .../service/PhotoRequestServiceImpl.java | 8 +------ 4 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java b/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java index 13db085..c46ebca 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java +++ b/src/main/java/gdsc/cau/puangbe/photo/entity/PhotoRequest.java @@ -46,9 +46,6 @@ public class PhotoRequest { @OneToMany(mappedBy = "request", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) private List photoUrls = new ArrayList<>(); - @Version - private Long version; - @Builder public PhotoRequest(User user, Gender gender, List urls, String email) { this.user = user; @@ -69,13 +66,4 @@ public void modifyEmail(String email) { this.email = email; this.updateDate = LocalDateTime.now(); } - - @Override - public String toString() { - return "PhotoRequest{" + - "id=" + id + - ", email='" + email + '\'' + - ", version=" + version + - '}'; - } } diff --git a/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java b/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java index 07d1197..bf00b71 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java +++ b/src/main/java/gdsc/cau/puangbe/photo/repository/PhotoRequestRepository.java @@ -12,8 +12,11 @@ @Repository public interface PhotoRequestRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) Optional findById(Long photoRequestId); // 특정 유저의 최근에 만들어진 PhotoRequest 조회 + @Lock(LockModeType.PESSIMISTIC_WRITE) Optional findTopByUserIdOrderByCreateDateDesc(Long photoRequestId); } diff --git a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java index 9e4d178..6909a4d 100644 --- a/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photo/service/PhotoServiceImpl.java @@ -42,12 +42,6 @@ public class PhotoServiceImpl implements PhotoService { // 완성된 요청 id 및 imageUrl을 받아 저장 @Override @Transactional - @Lock(LockModeType.OPTIMISTIC) - @Retryable( - retryFor = {ObjectOptimisticLockingFailureException.class}, - maxAttempts = 1000, - backoff = @Backoff(100) - ) public void uploadPhoto(Long photoRequestId, String imageUrl) { // 예외처리 PhotoRequest photoRequest = photoRequestRepository.findById(photoRequestId) @@ -65,18 +59,18 @@ public void uploadPhoto(Long photoRequestId, String imageUrl) { photoResult.update(imageUrl); photoResultRepository.save(photoResult); - log.info("uploadPhoto - PhotoRequest version: {}", photoRequest.getVersion()); + log.info("upload photo"); // 이메일 발송 - EmailInfo emailInfo = EmailInfo.builder() - .email(photoRequest.getEmail()) - .photoUrl(imageUrl) - .name(user.getUserName()) - .framePageUrl("https://www.google.com/") // TODO : 프론트 분들 링크 관련 답변 오면 프레임 페이지 링크 관련 수정 - .build(); - - sendEmail(emailInfo); +// EmailInfo emailInfo = EmailInfo.builder() +// .email(photoRequest.getEmail()) +// .photoUrl(imageUrl) +// .name(user.getUserName()) +// .framePageUrl("https://www.google.com/") // TODO : 프론트 분들 링크 관련 답변 오면 프레임 페이지 링크 관련 수정 +// .build(); +// +// sendEmail(emailInfo); } // 특정 요청의 imageUrl 조회 diff --git a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java index 63a7092..d8eb6c3 100644 --- a/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java +++ b/src/main/java/gdsc/cau/puangbe/photorequest/service/PhotoRequestServiceImpl.java @@ -131,12 +131,6 @@ public String getRequestStatus(Long userId){ @Override @Transactional - @Lock(LockModeType.OPTIMISTIC) - @Retryable( - retryFor = {ObjectOptimisticLockingFailureException.class}, - maxAttempts = 1000, - backoff = @Backoff(100) - ) public Long updateEmail(Long userId, String email) { // 가장 최근의 PhotoRequest 조회 @@ -146,7 +140,7 @@ public Long updateEmail(Long userId, String email) { photoRequest.modifyEmail(email); photoRequestRepository.save(photoRequest); - log.info("updateEmail - PhotoRequest version: {}", photoRequest.getVersion()); + log.info("update email"); return photoRequest.getId(); }