-
Notifications
You must be signed in to change notification settings - Fork 1
feat: AI 이미지 생성권 구매 시스템 구현 #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
df34a1f
33825d7
5e2e7d4
a1db722
b10d40b
37f08cb
94b7d6d
86e4cff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,9 +75,12 @@ public enum ErrorCode { | |
| AI_REQUEST_NOT_FOUND(HttpStatus.NOT_FOUND, "A-001", "해당 AI 요청을 찾을 수 없습니다."), | ||
| CREATED_AI_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "A-002", "생성된 AI 이미지를 찾을 수 없습니다."), | ||
| AI_IMAGE_DATA_INCONSISTENCY(HttpStatus.INTERNAL_SERVER_ERROR, "A-003", "AI 이미지 데이터 일관성 오류가 발생했습니다."), | ||
| AI_IMAGE_PERMISSION_NOT_FOUND(HttpStatus.FORBIDDEN, "A-004", "AI 이미지 생성 권한이 없습니다."), | ||
| AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "A-005", "티켓 처리에 실패했습니다."), | ||
| AI_IMAGE_PERMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "AI-004", "AI 이미지 생성권을 구매한 이력이 없습니다."), | ||
| AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "AI-005", "티켓 처리에 실패했습니다."), | ||
| AI_REQUEST_TICKET_NOT_ENOUGH_EXCEPTION(HttpStatus.BAD_REQUEST, "AI-006", "티켓이 부족합니다."), | ||
| REMAINING_GENERATIONS_NOT_ENOUGH_EXCEPTION(HttpStatus.BAD_REQUEST, "AI-007", "AI 이미지 생성권이 부족합니다."), | ||
| AI_IMAGE_PERMISSION_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "AI-008", "이미지 생성권 처리에 실패하였습니다."), | ||
| AI_IMAGE_PERMISSION_ALREADY_EXISTS_EXCEPTION(HttpStatus.CONFLICT, "AI-009", "이미 AI 이미지 생성권을 구매했습니다."), | ||
| ; | ||
|
Comment on lines
+78
to
84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAI 에러 코드(78~84) 추가/수정 전반은 타당. 다만 prefix/상태/중복 관점 점검 제안
🏁 Script executed: #!/bin/bash
set -euo pipefail
# 리포지토리 전역의 "대문자-세자리" 패턴 에러코드를 수집하고 중복을 출력
rg -nP --type=java -g '!**/build/**' -g '!**/out/**' -g '!**/target/**' '"[A-Z]+-\d{3}"' \
| sed -nE 's/.*"([A-Z]+-[0-9]{3})".*/\1/p' \
| sort | uniq -c | awk '$1>1 {printf("DUP\t%s\t(%d occurrences)\n",$2,$1)} END{if(NR==0) print "NO_CODES_FOUND"}'Length of output: 423 글로벌 에러 코드 중복 발견 – 중복 해소 및 일관성 재검토가 필요합니다
🤖 Prompt for AI Agents |
||
|
|
||
| private final HttpStatus status; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package hanium.modic.backend.common.redis.distributedLock; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Collection; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.TimeUnit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.redisson.RedissonMultiLock; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.redisson.api.RLock; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.redisson.api.RedissonClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hanium.modic.backend.common.error.exception.LockException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.Builder; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.Getter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class DistributionLockExecutor { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RedissonClient redissonClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final AopForTransaction aopForTransaction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final LockOptions DEFAULT_OPTS = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LockOptions.builder().waitTime(3).leaseTime(10).timeUnit(TimeUnit.SECONDS).build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 단일 키 락 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void withLock(String key, Runnable block) throws LockException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withLock(key, DEFAULT_OPTS, block); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void withLock(String key, LockOptions opts, Runnable block) throws LockException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RLock lock = redissonClient.getLock(key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| executeWith(lock, key, opts, block); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 다중 키 락(데드락 방지 위해 키 정렬 권장) */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void withMultiLock(Collection<String> keys, Runnable block) throws LockException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withMultiLock(keys, DEFAULT_OPTS, block); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void withMultiLock(Collection<String> keys, LockOptions opts, Runnable block) throws LockException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<String> sorted = keys.stream().sorted().toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RLock[] locks = sorted.stream().map(redissonClient::getLock).toArray(RLock[]::new); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RedissonMultiLock multiLock = new RedissonMultiLock(locks); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| executeWith(multiLock, sorted.toString(), opts, block); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 공통 실행부: tryLock → 트랜잭션 내 block.run() → 안전 unlock */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void executeWith(RLock lock, String logKey, LockOptions opts, Runnable block) throws LockException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boolean acquired = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| acquired = lock.tryLock(opts.getWaitTime(), opts.getLeaseTime(), opts.getTimeUnit()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!acquired) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new LockException(new InterruptedException("락 획득 실패: " + logKey)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aopForTransaction.proceed(block); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (InterruptedException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Thread.currentThread().interrupt(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new LockException(e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (acquired && lock.isHeldByCurrentThread()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.unlock(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (IllegalMonitorStateException ignored) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("락이 이미 해제됨: {}", logKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 임차(lease) 만료로 인한 조기 해제 위험 제거 + requiresNewTx 옵션 실제 반영.
아래처럼 수정하면
@@
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
@@
- private void executeWith(RLock lock, String logKey, LockOptions opts, Runnable block) throws LockException {
+ private void executeWith(RLock lock, String logKey, LockOptions opts, Runnable block) throws LockException {
boolean acquired = false;
try {
- acquired = lock.tryLock(opts.getWaitTime(), opts.getLeaseTime(), opts.getTimeUnit());
+ // leaseTime <= 0 이면 watchdog(자동 연장) 사용
+ if (opts.getLeaseTime() <= 0) {
+ acquired = lock.tryLock(opts.getWaitTime(), opts.getTimeUnit());
+ } else {
+ acquired = lock.tryLock(opts.getWaitTime(), opts.getLeaseTime(), opts.getTimeUnit());
+ }
if (!acquired) {
- throw new LockException(new InterruptedException("락 획득 실패: " + logKey));
+ throw new LockException(new TimeoutException(
+ "락 획득 실패(대기 " + opts.getWaitTime() + " " + opts.getTimeUnit() + "): " + logKey));
}
- aopForTransaction.proceed(block);
+ if (opts.isRequiresNewTx()) {
+ aopForTransaction.proceed(block); // REQUIRES_NEW
+ } else {
+ // 기존 트랜잭션에 참여(또는 비트랜잭션 수행)
+ block.run();
+ }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockException(e);
} finally {
try {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
} catch (IllegalMonitorStateException ignored) {
log.info("락이 이미 해제됨: {}", logKey);
}
}
}참고: requiresNewTx=false 경로는 호출부가 이미 @transactional이라면 그 트랜잭션에 자연스럽게 참여합니다. 필요 시 AopForTransaction에 REQUIRED 전파 메서드를 추가하는 대안도 있습니다. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,148 +1,56 @@ | ||
| package hanium.modic.backend.common.redis.distributedLock; | ||
|
|
||
| import static org.springframework.transaction.annotation.Propagation.*; | ||
|
|
||
| import java.util.List; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import org.redisson.RedissonMultiLock; | ||
| import org.redisson.api.RLock; | ||
| import org.redisson.api.RedissonClient; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import hanium.modic.backend.common.error.exception.LockException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| /** | ||
| * Service 계층에서 Lock 제어를 위한 객체 | ||
| * 내부적으로 DistributionLockExecutor로 락을 제어하고 있음. | ||
| * 새로운 도메인에 대한 락이 필요하면 이 객체를 수정하여 도입 | ||
| */ | ||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class LockManager { | ||
|
|
||
| private final AopForTransaction aopForTransaction; | ||
| private final RedissonClient redissonClient; | ||
|
|
||
| private static final TimeUnit timeUnit = TimeUnit.SECONDS; // 락 시간 단위 | ||
| private static final long waitTime = 5L; // 락 획득 대기 시간(5초) | ||
| private static final long leaseTime = 3L; // 락 유지 시간(3초) | ||
| private static final long postLikeWaitTime = 2L; // 좋아요 락 대기 시간(2초) | ||
| private static final long postLikeLeaseTime = 1L; // 좋아요 락 유지 시간(1초) | ||
| private static final String REDISSON_USER_LOCK_PREFIX = "USER_LOCK:"; | ||
| private static final String REDISSON_POST_LIKE_LOCK_PREFIX = "POST_LIKE:"; | ||
| private static final String REDISSON_USER_TICKET_LOCK_PREFIX = "USER_TICKET_LOCK:"; | ||
| private final DistributionLockExecutor exec; | ||
|
|
||
| // 유저 단일 락 | ||
| public void userLock( | ||
| final long userId, | ||
| Runnable block | ||
| ) throws LockException { | ||
| String key = REDISSON_USER_LOCK_PREFIX + userId; | ||
| RLock rLock = redissonClient.getLock(key); | ||
| private final LockOptions POST_LIKE_OPTS = | ||
| LockOptions.builder().waitTime(1).leaseTime(3).timeUnit(TimeUnit.SECONDS).build(); | ||
|
|
||
| try { | ||
| boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit); | ||
| if (!available) { | ||
| throw new LockException(new InterruptedException("단일 락 획득 실패")); | ||
| } | ||
| private final String USER_PREFIX = "lock:user:"; | ||
| private final String USER_TICKET_PREFIX = "lock:user:ticket:"; | ||
| private final String POST_LIKE_PREFIX = "lock:postlike:"; | ||
| private final String AI_PERMISSION_PREFIX = "lock:ai:perm:"; | ||
|
|
||
| aopForTransaction.proceed(block); // lock 범위 안에서 트랜잭션 적용 후 로직 처리 | ||
| } catch (InterruptedException e) { | ||
| throw new LockException(e); // 락 획득 실패시 Exception 발생 | ||
| } finally { | ||
| try { | ||
| rLock.unlock(); | ||
| } catch (IllegalMonitorStateException e) { | ||
| log.info("분산 락이 이미 해제되었습니다. :key({})", key); | ||
| } | ||
| } | ||
| public void userLock(long userId, Runnable block) throws LockException { | ||
| exec.withLock(USER_PREFIX + userId, block); | ||
| } | ||
|
|
||
| // 여러 유저 동시 락 | ||
| @Transactional(propagation = REQUIRES_NEW) | ||
| public void multipleUserLock( | ||
| final List<Long> userIds, | ||
| Runnable block | ||
| ) throws LockException { | ||
| List<String> sortedKeys = userIds.stream() | ||
| .sorted() // 데드락 방지용 정렬 | ||
| .map(id -> REDISSON_USER_LOCK_PREFIX + id) | ||
| .toList(); | ||
|
|
||
| List<RLock> locks = sortedKeys.stream() | ||
| .map(redissonClient::getLock) | ||
| public void multipleUserLock(List<Long> userIds, Runnable block) throws LockException { | ||
| List<String> keys = userIds.stream() | ||
| .map(id -> USER_PREFIX + id) | ||
| .toList(); | ||
|
|
||
| RedissonMultiLock multiLock = new RedissonMultiLock(locks.toArray(new RLock[0])); | ||
|
|
||
| try { | ||
| boolean available = multiLock.tryLock(waitTime, leaseTime, timeUnit); | ||
| if (!available) { | ||
| throw new LockException(new InterruptedException("멀티 락 획득 실패")); | ||
| } | ||
|
|
||
| aopForTransaction.proceed(block); // lock 범위 안에서 트랜잭션 적용 후 로직 처리 | ||
| } catch (InterruptedException e) { | ||
| throw new LockException(e); // 락 획득 실패시 Exception 발생 | ||
| } finally { | ||
| try { | ||
| multiLock.unlock(); | ||
| } catch (IllegalMonitorStateException e) { | ||
| log.info("멀티 분산 락이 이미 해제되었습니다. : keys={}", sortedKeys); | ||
| } | ||
| } | ||
| exec.withMultiLock(keys, block); | ||
| } | ||
|
|
||
| // 게시글 좋아요 락 | ||
| public void postLikeLock( | ||
| final Long userId, | ||
| final Long postId, | ||
| Runnable block | ||
| ) throws LockException { | ||
| String key = REDISSON_POST_LIKE_LOCK_PREFIX + userId + ":" + postId; | ||
| RLock rLock = redissonClient.getLock(key); | ||
|
|
||
| try { | ||
| boolean available = rLock.tryLock(postLikeWaitTime, postLikeLeaseTime, timeUnit); | ||
| if (!available) { | ||
| throw new LockException(new InterruptedException("좋아요 락 획득 실패")); | ||
| } | ||
|
|
||
| aopForTransaction.proceed(block); // lock 범위 안에서 트랜잭션 적용 후 로직 처리 | ||
| } catch (InterruptedException e) { | ||
| throw new LockException(e); // 락 획득 실패시 Exception 발생 | ||
| } finally { | ||
| try { | ||
| rLock.unlock(); | ||
| } catch (IllegalMonitorStateException e) { | ||
| log.info("좋아요 분산 락이 이미 해제되었습니다. :key({})", key); | ||
| } | ||
| } | ||
| public void postLikeLock(long userId, long postId, Runnable block) throws LockException { | ||
| exec.withLock(POST_LIKE_PREFIX + userId + ":" + postId, | ||
| POST_LIKE_OPTS, | ||
| block); | ||
| } | ||
|
|
||
| // AI 요청 티켓 락 | ||
| public void aiRequestTicketLock( | ||
| final Long userId, | ||
| Runnable block | ||
| ) throws LockException { | ||
| String key = REDISSON_USER_TICKET_LOCK_PREFIX + userId; | ||
| RLock rLock = redissonClient.getLock(key); | ||
|
|
||
| try { | ||
| boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit); | ||
| if (!available) { | ||
| throw new LockException(new InterruptedException("유저 티켓 락 획득 실패")); | ||
| } | ||
| public void aiRequestTicketLock(long userId, Runnable block) throws LockException { | ||
| exec.withLock(USER_TICKET_PREFIX + userId, block); | ||
| } | ||
|
|
||
| aopForTransaction.proceed(block); // lock 범위 안에서 트랜잭션 적용 후 로직 처리 | ||
| } catch (InterruptedException e) { | ||
| throw new LockException(e); // 락 획득 실패시 Exception 발생 | ||
| } finally { | ||
| try { | ||
| rLock.unlock(); | ||
| } catch (IllegalMonitorStateException e) { | ||
| log.info("유저 티켓 분산 락이 이미 해제되었습니다. :key({})", key); | ||
| } | ||
| } | ||
| public void aiImagePermissionLock(long userId, long postId, Runnable block) throws LockException { | ||
| exec.withLock(AI_PERMISSION_PREFIX + userId + ":" + postId, block); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package hanium.modic.backend.common.redis.distributedLock; | ||
|
|
||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| /** Lock에 대한 세부 설정을 도와주는 객체 */ | ||
| @Getter | ||
| @Builder | ||
| public class LockOptions { | ||
| private final long waitTime; | ||
| private final long leaseTime; | ||
| private final TimeUnit timeUnit; | ||
| private final boolean requiresNewTx; // 필요 시 트랜잭션 분리 제어(선택) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
DB 컬럼명 명시 변경에 따른 마이그레이션/쿼리 영향 점검 필요
@Column(name = "create_at"),@Column(name = "update_at")로 명시되면서, 과거에 네이밍 전략(default)로 매핑되던 컬럼명과 불일치할 수 있습니다. 기존 스키마/인덱스/뷰/네이티브 쿼리/ETL이createAt혹은 다른 스펠링을 참조하지 않는지 확인해 주세요. Flyway/Liquibase 마이그레이션도 함께 반영되어야 합니다.다음 스크립트로 레포 내 잠재적 참조를 빠르게 점검할 수 있습니다:
🏁 Script executed:
Length of output: 6936
컬럼명 변경(createAt→create_at, updateAt→update_at) 반영 필수
BaseEntity에서 명시한@Column(name = "create_at"),@Column(name = "update_at")적용에 따라, 아래 항목들을 반드시 점검 및 수정해야 합니다.네이티브 SQL 쿼리
AiImagePermissionRepository.java(INSERT INTO ai_image_permissions …create_at,update_at)FollowEntityRepository.java(INSERT INTO follows …create_at,update_at)JPA 쿼리 및 메소드
PostReviewCommentRepository.java(c.createAt,ORDER BY c.createAt)FollowEntityRepository.java(ORDER BY f.createAt)DB 마이그레이션 스크립트
createAt·updateAt참조 여부 확인위 변경 사항을 반영한 후, 로컬 및 CI 환경에서 컬럼명 불일치로 인한 쿼리 에러가 없는지 충분히 검증해 주세요.
🤖 Prompt for AI Agents