From 25f553a06456763eb341edb7da5014c9b4682144 Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Tue, 7 Jan 2025 01:30:16 +0900 Subject: [PATCH 01/10] [feat] apply optimistic lock --- .../domain/store/model/Store.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java index 78345239..cc22edf3 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java @@ -1,6 +1,17 @@ package org.hankki.hankkiserver.domain.store.model; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Version; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -11,9 +22,6 @@ import org.hankki.hankkiserver.domain.universitystore.model.UniversityStore; import org.hibernate.annotations.BatchSize; -import java.util.ArrayList; -import java.util.List; - @Entity @Getter @BatchSize(size = 100) @@ -57,6 +65,9 @@ public class Store extends BaseTimeEntity { @Column(nullable = false) private boolean isDeleted; + @Version + private Long version; + @Builder private Store (String name, Point point, String address, StoreCategory category, int lowestPrice, int heartCount, boolean isDeleted) { this.name = name; From e7b224aa090e9c7a28f525e9a26f350f588e5afb Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Tue, 7 Jan 2025 12:08:36 +0900 Subject: [PATCH 02/10] [chore] add aop dependency --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e8ec5591..8865cd81 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,9 @@ dependencies { // Discord Webhook implementation 'com.github.napstr:logback-discord-appender:1.0.0' + + // aop + implementation 'org.springframework.boot:spring-boot-starter-aop' } tasks.named('test') { @@ -93,4 +96,4 @@ tasks.register('copyYml', Copy) { include "*.yml" into 'src/main/resources' } -} \ No newline at end of file +} From 8f52b2c9fbb2bc5923db56f2fd011db6f86ec490 Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Tue, 7 Jan 2025 12:12:46 +0900 Subject: [PATCH 03/10] [fix] set default version --- .../java/org/hankki/hankkiserver/domain/store/model/Store.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java index cc22edf3..bd8d98da 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java @@ -66,7 +66,7 @@ public class Store extends BaseTimeEntity { private boolean isDeleted; @Version - private Long version; + private Long version = 0L; @Builder private Store (String name, Point point, String address, StoreCategory category, int lowestPrice, int heartCount, boolean isDeleted) { From dd6704de17fa3a0205df09678c09378be3bd6eb0 Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Tue, 7 Jan 2025 12:39:50 +0900 Subject: [PATCH 04/10] [feat] make custom retry annotation --- .../hankkiserver/api/common/annotation/Retry.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/org/hankki/hankkiserver/api/common/annotation/Retry.java diff --git a/src/main/java/org/hankki/hankkiserver/api/common/annotation/Retry.java b/src/main/java/org/hankki/hankkiserver/api/common/annotation/Retry.java new file mode 100644 index 00000000..3745cb21 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/common/annotation/Retry.java @@ -0,0 +1,15 @@ +package org.hankki.hankkiserver.api.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) +@Retention(RetentionPolicy.RUNTIME) +public @interface Retry { + + int maxAttempts() default 3; + + int backoff() default 100; +} From 8a19f8a99757caa44f35383a12edd7f445684d8d Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Tue, 7 Jan 2025 12:40:03 +0900 Subject: [PATCH 05/10] [feat] logic for retry --- .../api/store/aspect/RetryAspect.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java diff --git a/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java b/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java new file mode 100644 index 00000000..d7ea4db4 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java @@ -0,0 +1,32 @@ +package org.hankki.hankkiserver.api.store.aspect; + +import jakarta.persistence.OptimisticLockException; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.hankki.hankkiserver.api.common.annotation.Retry; +import org.hankki.hankkiserver.common.code.HeartErrorCode; +import org.hankki.hankkiserver.common.exception.ConflictException; +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 RetryAspect { + + @Around("@annotation(retry)") + public Object retryOptimisticLock(final ProceedingJoinPoint joinPoint, final Retry retry) throws Throwable { + for (int attempt = 0; attempt < retry.maxAttempts(); attempt++) { + try { + return joinPoint.proceed(); + } catch (OptimisticLockException | ObjectOptimisticLockingFailureException | StaleObjectStateException e) { + Thread.sleep(retry.backoff()); + } + } + throw new ConflictException(HeartErrorCode.ALREADY_EXISTED_HEART); + } +} From ea4b74b2d1039d7aeedfbf9dd70827191ee970dc Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Tue, 7 Jan 2025 12:40:12 +0900 Subject: [PATCH 06/10] [feat] apply retry logic --- .../hankkiserver/api/store/service/HeartCommandService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java b/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java index 592a982f..c27fb066 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.hankki.hankkiserver.api.auth.service.UserFinder; +import org.hankki.hankkiserver.api.common.annotation.Retry; import org.hankki.hankkiserver.api.store.service.command.HeartDeleteCommand; import org.hankki.hankkiserver.api.store.service.command.HeartPostCommand; import org.hankki.hankkiserver.api.store.service.response.HeartCreateResponse; @@ -16,7 +17,6 @@ @Service @RequiredArgsConstructor -@Transactional public class HeartCommandService { private final HeartUpdater heartUpdater; @@ -25,6 +25,8 @@ public class HeartCommandService { private final UserFinder userFinder; private final StoreFinder storeFinder; + @Retry + @Transactional public HeartCreateResponse createHeart(final HeartPostCommand heartPostCommand) { User user = userFinder.getUserReference(heartPostCommand.userId()); Store store = storeFinder.findByIdWhereDeletedIsFalse(heartPostCommand.storeId()); @@ -34,6 +36,8 @@ public HeartCreateResponse createHeart(final HeartPostCommand heartPostCommand) return HeartCreateResponse.of(store); } + @Retry + @Transactional public HeartDeleteResponse deleteHeart(final HeartDeleteCommand heartDeleteCommand) { User user = userFinder.getUserReference(heartDeleteCommand.userId()); Store store = storeFinder.findByIdWhereDeletedIsFalse(heartDeleteCommand.storeId()); From 53d574e08de895f9d6bac78932c637325f5c38f7 Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Wed, 8 Jan 2025 01:04:07 +0900 Subject: [PATCH 07/10] [chore] change error message --- .../org/hankki/hankkiserver/api/store/aspect/RetryAspect.java | 4 ++-- .../org/hankki/hankkiserver/common/code/HeartErrorCode.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java b/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java index d7ea4db4..bb20c266 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java @@ -6,7 +6,7 @@ import org.aspectj.lang.annotation.Aspect; import org.hankki.hankkiserver.api.common.annotation.Retry; import org.hankki.hankkiserver.common.code.HeartErrorCode; -import org.hankki.hankkiserver.common.exception.ConflictException; +import org.hankki.hankkiserver.common.exception.BadRequestException; import org.hibernate.StaleObjectStateException; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -27,6 +27,6 @@ public Object retryOptimisticLock(final ProceedingJoinPoint joinPoint, final Ret Thread.sleep(retry.backoff()); } } - throw new ConflictException(HeartErrorCode.ALREADY_EXISTED_HEART); + throw new BadRequestException(HeartErrorCode.HEART_COUNT_CONCURRENCY_ERROR); } } diff --git a/src/main/java/org/hankki/hankkiserver/common/code/HeartErrorCode.java b/src/main/java/org/hankki/hankkiserver/common/code/HeartErrorCode.java index 2578d04c..a284eb01 100644 --- a/src/main/java/org/hankki/hankkiserver/common/code/HeartErrorCode.java +++ b/src/main/java/org/hankki/hankkiserver/common/code/HeartErrorCode.java @@ -9,7 +9,8 @@ public enum HeartErrorCode implements ErrorCode { ALREADY_EXISTED_HEART(HttpStatus.CONFLICT, "이미 좋아요 한 가게입니다."), - ALREADY_NO_HEART(HttpStatus.CONFLICT, "이미 좋아요를 취소한 가게입니다."); + ALREADY_NO_HEART(HttpStatus.CONFLICT, "이미 좋아요를 취소한 가게입니다."), + HEART_COUNT_CONCURRENCY_ERROR(HttpStatus.CONFLICT, "좋아요 처리 중 충돌이 발생했습니다. 잠시 후 다시 시도해주세요."); private final HttpStatus httpStatus; private final String message; From a418fd647e2ece46fa729b14a32fa116feb42141 Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Sat, 11 Jan 2025 09:20:39 +0900 Subject: [PATCH 08/10] [refac] move to other package --- .../hankkiserver/{api/store => common}/aspect/RetryAspect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/org/hankki/hankkiserver/{api/store => common}/aspect/RetryAspect.java (96%) diff --git a/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java b/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java similarity index 96% rename from src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java rename to src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java index bb20c266..bcea1951 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/aspect/RetryAspect.java +++ b/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java @@ -1,4 +1,4 @@ -package org.hankki.hankkiserver.api.store.aspect; +package org.hankki.hankkiserver.common.aspect; import jakarta.persistence.OptimisticLockException; import org.aspectj.lang.ProceedingJoinPoint; From 747aa7bc1a5149d87d295e0f3bd69a2b2323040e Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Sat, 11 Jan 2025 09:26:02 +0900 Subject: [PATCH 09/10] [refac] change throwing exception class --- .../org/hankki/hankkiserver/common/aspect/RetryAspect.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java b/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java index bcea1951..e97d22ce 100644 --- a/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java +++ b/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java @@ -6,7 +6,7 @@ import org.aspectj.lang.annotation.Aspect; import org.hankki.hankkiserver.api.common.annotation.Retry; import org.hankki.hankkiserver.common.code.HeartErrorCode; -import org.hankki.hankkiserver.common.exception.BadRequestException; +import org.hankki.hankkiserver.common.exception.ConflictException; import org.hibernate.StaleObjectStateException; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -27,6 +27,6 @@ public Object retryOptimisticLock(final ProceedingJoinPoint joinPoint, final Ret Thread.sleep(retry.backoff()); } } - throw new BadRequestException(HeartErrorCode.HEART_COUNT_CONCURRENCY_ERROR); + throw new ConflictException(HeartErrorCode.HEART_COUNT_CONCURRENCY_ERROR); } } From a46ed37bd386fa8e0a95aa7f72f4cca79992c2d4 Mon Sep 17 00:00:00 2001 From: kgy1008 Date: Sat, 11 Jan 2025 09:42:59 +0900 Subject: [PATCH 10/10] [refac] simplify exception handling --- .../org/hankki/hankkiserver/common/aspect/RetryAspect.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java b/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java index e97d22ce..448ecf25 100644 --- a/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java +++ b/src/main/java/org/hankki/hankkiserver/common/aspect/RetryAspect.java @@ -1,13 +1,11 @@ package org.hankki.hankkiserver.common.aspect; -import jakarta.persistence.OptimisticLockException; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.hankki.hankkiserver.api.common.annotation.Retry; import org.hankki.hankkiserver.common.code.HeartErrorCode; import org.hankki.hankkiserver.common.exception.ConflictException; -import org.hibernate.StaleObjectStateException; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.orm.ObjectOptimisticLockingFailureException; @@ -23,7 +21,7 @@ public Object retryOptimisticLock(final ProceedingJoinPoint joinPoint, final Ret for (int attempt = 0; attempt < retry.maxAttempts(); attempt++) { try { return joinPoint.proceed(); - } catch (OptimisticLockException | ObjectOptimisticLockingFailureException | StaleObjectStateException e) { + } catch (ObjectOptimisticLockingFailureException e) { Thread.sleep(retry.backoff()); } }