Conversation
Redis에 임시 저장소를 사용하는 새로운 이미지 사전 업로드 흐름을 도입하여 클라이언트가 그룹 생성 전에 이미지를 업로드할 수 있도록 합니다. 악용 방지를 위해 Redis를 사용한 그룹 생성 쿨다운 기능을 추가합니다. 이 기능에는 실패 시 쿨다운 잠금을 해제하는 트랜잭션 롤백 처리가 포함됩니다. 사전 업로드된 이미지와 이미지 키를 활용하도록 그룹 생성 프로세스를 재구성하여 보안과 효율성을 향상시킵니다.
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Walkthrough사전 업로드 이미지(v2)와 Redis 기반 모임 생성 쿨다운을 도입했습니다. 이미지 업로드는 WebP로 변환해 S3에 저장하고 메타를 Redis에 보관하며, 모임 생성 시 Redis에서 소비하고 소유자를 검증합니다. Redis 예외 및 JSON 예외 처리기가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Client
participant GroupImageV2Controller
participant GroupImageV2PreUploadService
participant ImageUploadService
participant S3
participant PreUploadedGroupImageRedisRepository
participant Redis
User->>Client: POST /api/v2/groups/images/upload (files)
Client->>GroupImageV2Controller: uploadImages(userDetails, images)
GroupImageV2Controller->>GroupImageV2PreUploadService: uploadGroupImagesV2(uploaderId, images)
loop for each file
GroupImageV2PreUploadService->>ImageUploadService: uploadAsWebpWithSizeUsingBaseName(file, baseName, 440x240)
ImageUploadService->>S3: PUT webp 440x240
S3-->>ImageUploadService: URL
GroupImageV2PreUploadService->>ImageUploadService: uploadAsWebpWithSizeUsingBaseName(file, baseName, 100x100)
ImageUploadService->>S3: PUT webp 100x100
S3-->>ImageUploadService: URL
GroupImageV2PreUploadService->>PreUploadedGroupImageRedisRepository: save(PreUploadedGroupImage)
PreUploadedGroupImageRedisRepository->>Redis: SET key (TTL 2h)
Redis-->>PreUploadedGroupImageRedisRepository: OK
end
GroupImageV2PreUploadService-->>GroupImageV2Controller: PreUploadGroupImageV2Response (imageKeys)
GroupImageV2Controller-->>Client: 201 Created
sequenceDiagram
actor User
participant Client
participant GroupV2Controller
participant GroupV2Service
participant GroupCreateCooldownService
participant PreUploadedGroupImageRedisRepository
participant Redis
participant Database
User->>Client: POST /api/v2/groups/create (request, cooldownSeconds, imageKeys)
Client->>GroupV2Controller: create(userDetails, request, cooldownSeconds)
GroupV2Controller->>GroupV2Service: create(userId, request, cooldownSeconds)
GroupV2Service->>GroupCreateCooldownService: acquireOrThrowWithRollbackRelease(userId, cooldownSeconds)
GroupCreateCooldownService->>Redis: SET key NX EX cooldownSeconds
alt key existed
Redis-->>GroupCreateCooldownService: exists
GroupCreateCooldownService-->>GroupV2Service: throw GROUP_CREATE_COOLDOWN_ACTIVE
GroupV2Service-->>Client: 429 Too Many Requests
else key set
Redis-->>GroupCreateCooldownService: OK
GroupCreateCooldownService-->>GroupV2Service: proceed
loop for each imageKey
GroupV2Service->>PreUploadedGroupImageRedisRepository: consume(imageKey)
PreUploadedGroupImageRedisRepository->>Redis: GETDEL key
Redis-->>PreUploadedGroupImageRedisRepository: PreUploadedGroupImage
PreUploadedGroupImageRedisRepository-->>GroupV2Service: Optional<PreUploadedGroupImage>
alt missing or owner mismatch
GroupV2Service-->>Client: error (PRE_UPLOADED_IMAGE_NOT_FOUND / OWNER_MISMATCH)
else valid
GroupV2Service->>Database: persist group image entity (with imageKey and URLs)
end
end
GroupV2Service->>Database: persist group
Database-->>GroupV2Service: OK
GroupV2Service-->>Client: 201 Created
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 검토 시 주의할 파일/영역:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (10)
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/PreUploadedGroupImage.java (1)
6-14: serialVersionUID 추가를 고려하세요 (선택 사항).Redis에 저장되는 객체에
serialVersionUID를 명시하면 향후 필드 변경 시 역직렬화 호환성을 보장할 수 있습니다. 임시 캐시 용도이므로 필수는 아니지만, 추가하면 안정성이 향상됩니다.🔎 제안 수정사항
public record PreUploadedGroupImage( String imageKey, Long uploaderId, String url440x240, String url100x100, LocalDateTime createdAt ) implements Serializable { + private static final long serialVersionUID = 1L; }src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java (1)
285-295: 도메인별 에러 코드 사용을 재고하세요.전역 예외 핸들러에서
GroupErrorCode.REDIS_READ_FAILED를 사용하고 있습니다. 고려사항:
DataAccessException은 Redis뿐만 아니라 모든 데이터 액세스 계층 예외를 포함할 수 있습니다- 전역 핸들러에서 도메인별 에러 코드를 사용하면 결합도가 높아집니다
AppErrorCode에 범용적인REDIS_CONNECTION_FAILED또는DATA_ACCESS_ERROR를 추가하는 것을 고려해보세요.src/main/java/team/wego/wegobackend/group/v2/application/service/GroupImageV2PreUploadService.java (2)
38-64: 빈 파일 건너뛰기 시 sortOrder 불일치 가능성빈 파일을
continue로 건너뛰면 반환되는result리스트의 인덱스와sortOrder(=i)가 요청 이미지 리스트의 원래 위치와 일치하지 않을 수 있습니다. 예를 들어,[file0, empty, file2]를 업로드하면 결과는[{sortOrder:0}, {sortOrder:2}]가 됩니다.클라이언트가 이 동작을 예상한다면 문제없지만, 그렇지 않으면 혼란을 줄 수 있습니다. 빈 파일에 대해 예외를 던지거나, 건너뛴 후 인덱스를 재조정하는 방안을 고려해 보세요.
49-61: 부분 실패 시 S3 정리 누락 가능성S3 업로드가 루프 중간에 실패하면 이미 업로드된 이미지들이 고아 상태로 남을 수 있습니다. Redis에는 저장되지 않았지만 S3에는 이미 존재하는 상황이 발생할 수 있습니다.
현재 구조에서는 큰 문제가 아닐 수 있지만, 운영 환경에서 S3 스토리지 비용이 누적될 수 있습니다. 필요시 try-catch로 감싸서 실패 시 이전 업로드를 정리하는 보상 로직을 추가할 수 있습니다.
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java (1)
210-238: 이미지 소비 후 생성 실패 시 복구 불가
preUploadedGroupImageRedisRepository.consume()이 Redis에서 이미지를 삭제하는데, 이후 모임 생성이 실패하면(예: 검증 오류) 사용자가 업로드한 이미지를 다시 사용할 수 없습니다. S3에는 이미지가 남아 있지만 Redis 참조가 사라져서 사용자는 이미지를 다시 업로드해야 합니다.이미지 유효성 검증을 먼저 수행하고(
find사용), 모든 검증이 완료된 후에 한꺼번에consume하는 방식을 고려해 보세요.🔎 수정 제안:
// 이미지 생성 if (request.images() != null && !request.images().isEmpty()) { + // 1단계: 모든 이미지 검증 (consume 하지 않음) + List<PreUploadedGroupImage> validatedImages = new ArrayList<>(); for (int i = 0; i < request.images().size(); i++) { CreateGroupImageV2Request imgReq = request.images().get(i); - String imageKey = imgReq.imageKey(); if (imageKey == null || imageKey.isBlank()) { throw new GroupException(GroupErrorCode.GROUP_IMAGE_KEY_REQUIRED); } - - int sortOrder = (imgReq.sortOrder() != null) ? imgReq.sortOrder() : i; - - PreUploadedGroupImage pre = preUploadedGroupImageRedisRepository.consume(imageKey) + PreUploadedGroupImage pre = preUploadedGroupImageRedisRepository.find(imageKey) .orElseThrow(() -> new GroupException( GroupErrorCode.PRE_UPLOADED_IMAGE_NOT_FOUND, imageKey )); - if (!userId.equals(pre.uploaderId())) { throw new GroupException(GroupErrorCode.PRE_UPLOADED_IMAGE_OWNER_MISMATCH, imageKey); } + validatedImages.add(pre); + } + // 2단계: 검증 완료 후 consume 및 생성 + for (int i = 0; i < validatedImages.size(); i++) { + PreUploadedGroupImage pre = validatedImages.get(i); + CreateGroupImageV2Request imgReq = request.images().get(i); + int sortOrder = (imgReq.sortOrder() != null) ? imgReq.sortOrder() : i; + + preUploadedGroupImageRedisRepository.consume(pre.imageKey()); + GroupImageV2.create( group, sortOrder,src/main/java/team/wego/wegobackend/group/v2/infrastructure/redis/PreUploadedGroupImageRedisRepository.java (4)
19-21: imageKey에 대한 입력 검증 추가를 고려하세요.
imageKey가 null이거나 빈 문자열인 경우 예상치 못한 Redis 키가 생성될 수 있습니다. 방어적 프로그래밍 관점에서 검증을 추가하는 것이 좋습니다.🔎 제안하는 수정사항 보기
private String key(String imageKey) { + if (imageKey == null || imageKey.isBlank()) { + throw new IllegalArgumentException("imageKey는 null이거나 비어있을 수 없습니다."); + } return PREFIX + imageKey; }
23-29: Redis 연산에 대한 예외 처리 추가를 고려하세요.Redis 연결 실패나 직렬화 오류 등의 예외가 발생할 수 있습니다.
GroupErrorCode.REDIS_SAVE_FAILED가 정의되어 있으므로, 이를 활용한 예외 처리를 추가하는 것이 좋습니다.🔎 제안하는 수정사항 보기
public void save(PreUploadedGroupImage value) { + try { preUploadedGroupImageRedisTemplate.opsForValue().set( key(value.imageKey()), value, TTL ); + } catch (Exception e) { + throw new GroupException(GroupErrorCode.REDIS_SAVE_FAILED, e); + } }
31-35: Redis 연산에 대한 예외 처리 추가를 고려하세요.Redis 연결 실패나 역직렬화 오류 등의 예외가 발생할 수 있습니다.
GroupErrorCode.REDIS_READ_FAILED를 활용한 예외 처리를 추가하는 것이 좋습니다.🔎 제안하는 수정사항 보기
public Optional<PreUploadedGroupImage> find(String imageKey) { + try { PreUploadedGroupImage value = preUploadedGroupImageRedisTemplate.opsForValue().get(key(imageKey)); return Optional.ofNullable(value); + } catch (Exception e) { + throw new GroupException(GroupErrorCode.REDIS_READ_FAILED, e); + } }
37-41: Redis 연산에 대한 예외 처리 추가를 고려하세요.
getAndDelete()는 원자적 연산으로 동시성 측면에서 안전하지만, Redis 연결 실패나 역직렬화 오류에 대한 예외 처리가 필요합니다.🔎 제안하는 수정사항 보기
public Optional<PreUploadedGroupImage> consume(String imageKey) { + try { PreUploadedGroupImage value = preUploadedGroupImageRedisTemplate.opsForValue().getAndDelete(key(imageKey)); return Optional.ofNullable(value); + } catch (Exception e) { + throw new GroupException(GroupErrorCode.REDIS_READ_FAILED, e); + } }src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1)
12-18: 관련 에러 코드들을 그룹화하는 것을 고려하세요.새로운 에러 코드들이 enum 상단에 추가되었는데, 유지보수성 향상을 위해 관련된 에러 코드들을 함께 배치하는 것이 좋습니다. 예를 들어:
- 쿨다운 관련:
INVALID_COOLDOWN_SECONDS,GROUP_CREATE_COOLDOWN_ACTIVE- 이미지 관련:
PRE_UPLOADED_IMAGE_*,GROUP_IMAGE_KEY_REQUIRED와 기존 이미지 에러들 (Line 19-22, 28, 30, 44-45)- Redis 관련:
REDIS_SAVE_FAILED,REDIS_READ_FAILED
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java(11 hunks)src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/dto/common/PreUploadGroupImageV2Item.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/dto/common/PreUploadedGroupImage.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/dto/request/CreateGroupImageV2Request.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/dto/request/UpdateGroupImageItemV2Request.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/dto/response/PreUploadGroupImageV2Response.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/service/GroupCreateCooldownService.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/service/GroupImageV2PreUploadService.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java(5 hunks)src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupImageV2.java(4 hunks)src/main/java/team/wego/wegobackend/group/v2/infrastructure/redis/PreUploadedGroupImageRedisConfig.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/infrastructure/redis/PreUploadedGroupImageRedisRepository.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/presentation/GroupImageV2Controller.java(1 hunks)src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java(2 hunks)src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java(3 hunks)src/test/http/group/v2/v2-group-create.http(10 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/team/wego/wegobackend/group/v2/infrastructure/redis/PreUploadedGroupImageRedisRepository.java (2)
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java (1)
RequiredArgsConstructor(40-365)src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java (1)
RequiredArgsConstructor(27-106)
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupImageV2PreUploadService.java (3)
src/main/java/team/wego/wegobackend/group/v2/infrastructure/redis/PreUploadedGroupImageRedisRepository.java (1)
RequiredArgsConstructor(10-42)src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java (1)
RequiredArgsConstructor(29-493)src/main/java/team/wego/wegobackend/group/domain/exception/GroupException.java (1)
GroupException(6-15)
🔇 Additional comments (16)
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/PreUploadGroupImageV2Item.java (1)
3-10: LGTM! 깔끔한 DTO 구조입니다.사전 업로드된 이미지 메타데이터를 전달하는 용도로 적절한 구조이며, record 타입 사용이 적합합니다.
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/PreUploadGroupImageV2Response.java (1)
6-10: LGTM! 응답 DTO 구조가 적절합니다.사전 업로드된 이미지 목록을 반환하는 용도로 깔끔하게 설계되었습니다.
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupImageV2.java (4)
42-43: imageKey 필드 제약조건이 적절합니다.UUID 형식의 고유 키를 위한 제약조건(길이 36, 수정 불가, 고유성)이 올바르게 설정되었습니다.
55-63: 생성자 검증 로직이 적절합니다.imageKey에 대한 null/blank 검증이 추가되어 데이터 무결성이 보장됩니다.
65-93: 팩토리 메서드 업데이트가 올바릅니다.imageKey 파라미터 추가와 검증이 적절하게 구현되었습니다. 팩토리와 생성자에 중복 검증이 있지만, 방어적 프로그래밍 관점에서 문제없습니다.
111-113:changeSortOrder메서드는 현재 사용되지 않는 미사용 코드입니다.코드 분석 결과,
changeSortOrder메서드는 어디에서도 호출되지 않고 있습니다. 서비스 계층, 컨트롤러, 테스트 코드 등 전체 코드베이스에서 이 메서드의 사용 사례가 없으며, 그룹 이미지의sortOrder는 생성 시점에만 설정됩니다. 따라서 원래 지적된 서비스 계층의 제약조건 검증 부재에 대한 우려는 현재 코드에서는 발생하지 않습니다. 이 메서드는 제거하거나, 향후 사용 계획이 있다면 적절한 유효성 검사 로직을 추가하여 구현해야 합니다.Likely an incorrect or invalid review comment.
src/main/java/team/wego/wegobackend/group/v2/application/dto/request/UpdateGroupImageItemV2Request.java (1)
3-9: UpdateGroupImageItemV2Request는 현재 코드베이스에서 사용되지 않습니다.이 DTO 레코드는 정의되어 있지만 어떤 서비스, 컨트롤러, 또는 테스트에서도 사용되지 않습니다. 따라서 제안된 검증 로직을 확인할 대상이 없습니다. 이 클래스가 실제로 사용될 때 상호 배타적 검증(id와 imageKey의 동시 존재 방지)을 고려해야 합니다.
Likely an incorrect or invalid review comment.
src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java (2)
390-398: LGTM!포맷팅 변경으로 가독성이 개선되었습니다. 기능 변경 없이 코드 스타일만 정리되었습니다.
Also applies to: 411-413
475-491: LGTM!기존 패턴을 잘 따르고 있으며, 외부에서 제공된
baseName을 사용하여 S3 키를 생성하는 방식이 V2 사전 업로드 흐름에 적합합니다. 입력 파일에 대한 검증(크기, 콘텐츠 타입, 확장자)이 적절히 수행됩니다.src/main/java/team/wego/wegobackend/group/v2/application/service/GroupImageV2PreUploadService.java (1)
69-76: LGTM!최대 3개 이미지 제한 검증이 명확하게 구현되었습니다.
src/test/http/group/v2/v2-group-create.http (1)
29-51: LGTM!V2 이미지 사전 업로드 및
imageKey추출 흐름이 올바르게 구현되었습니다.src/main/java/team/wego/wegobackend/group/v2/infrastructure/redis/PreUploadedGroupImageRedisConfig.java (1)
15-38: LGTM!Redis 템플릿 구성이 올바르게 되어 있습니다. 애플리케이션의
ObjectMapper를 재사용하여 일관된 직렬화 설정을 보장합니다.src/main/java/team/wego/wegobackend/group/v2/application/service/GroupCreateCooldownService.java (2)
25-55: LGTM!
setIfAbsent를 사용한 원자적 잠금 획득과 TTL 기반 남은 시간 계산이 올바르게 구현되었습니다. TTL이-2(키 없음) 또는-1(만료 없음)인 경우의 방어 처리도 적절합니다.
64-80: LGTM!트랜잭션 롤백 시 쿨다운 잠금을 해제하는 패턴이 잘 구현되었습니다. 이를 통해 모임 생성이 실패해도 사용자가 쿨다운에 갇히지 않습니다.
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java (2)
170-177: LGTM!쿨다운 획득을 DB 작업 전에 수행하고, 롤백 시 자동 해제되도록 구성한 것이 적절합니다.
231-237: GroupImageV2에 저장되는 imageKey가 UUID이므로 나중에 S3 이미지 삭제 시 문제가 될 수 있습니다.
pre.imageKey()는 UUID이지만, S3에 실제로 저장되는 키는baseName + "_" + size + ".webp"형식(예:20231215143022_0_<uuid>_440x240.webp)입니다. GroupImageV2.imageKey에 UUID만 저장하면 나중에 GroupImageV2가 삭제될 때 S3에서 이미지를 제대로 삭제할 수 없습니다. imageKey는 S3 삭제에 필요한 전체 키 경로를 저장하거나, variant의 URL에서 키를 추출하는 방식으로 개선이 필요합니다.
| @ExceptionHandler(JsonProcessingException.class) | ||
| public ResponseEntity<ErrorResponse> handleJson(JsonProcessingException ex, HttpServletRequest request) { | ||
| log.error("Jackson 직렬화/역직렬화 실패(500): {}", rootCauseMessage(ex), ex); | ||
| AppException mapped = new AppException(GroupErrorCode.REDIS_READ_FAILED); | ||
| return handleApp(mapped, request); | ||
| } |
There was a problem hiding this comment.
JSON 처리 오류에 부적절한 에러 코드가 사용되고 있습니다.
JsonProcessingException을 GroupErrorCode.REDIS_READ_FAILED로 매핑하는 것은 의미상 부정확합니다. JSON 처리 오류는 Redis와 무관한 컨텍스트(일반 API 응답 직렬화 등)에서도 발생할 수 있습니다.
다음 중 하나를 선택하세요:
AppErrorCode에JSON_PROCESSING_ERROR같은 별도의 에러 코드 추가- 이 핸들러가 Redis 관련 JSON 오류만 처리하도록 의도된 경우, 더 구체적인 예외 타입으로 범위를 좁히거나 주석으로 명확히 설명
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java
around lines 297 to 302, the JsonProcessingException is incorrectly mapped to
GroupErrorCode.REDIS_READ_FAILED; instead add a new error code (e.g.,
AppErrorCode.JSON_PROCESSING_ERROR) to the appropriate enum or error-code class,
update any centralized error metadata (message, HTTP status) for that code, and
change this handler to map JsonProcessingException to the new
JSON_PROCESSING_ERROR; if you prefer not to add a new code, narrow the handler
to Redis-specific JSON exceptions and document intent—either way ensure tests
and any error-to-status mappings are updated accordingly.
| public enum GroupErrorCode implements ErrorCode { | ||
|
|
||
| INVALID_COOLDOWN_SECONDS(HttpStatus.BAD_REQUEST, "모임: 유효하지 않은 쿨다운 정책입니다."), | ||
| GROUP_CREATE_COOLDOWN_ACTIVE(HttpStatus.TOO_MANY_REQUESTS, "모임: 모임 생성은 연속으로 할 수 없습니다. {%s}초 후 다시 시도해 주세요."), |
There was a problem hiding this comment.
메시지 템플릿 포맷 오류를 수정하세요.
{%s} 형식은 String.format()과 호환되지 않습니다. 다른 에러 코드들과 동일하게 %s만 사용해야 합니다.
🔎 제안하는 수정사항 보기
- GROUP_CREATE_COOLDOWN_ACTIVE(HttpStatus.TOO_MANY_REQUESTS, "모임: 모임 생성은 연속으로 할 수 없습니다. {%s}초 후 다시 시도해 주세요."),
+ GROUP_CREATE_COOLDOWN_ACTIVE(HttpStatus.TOO_MANY_REQUESTS, "모임: 모임 생성은 연속으로 할 수 없습니다. %s초 후 다시 시도해 주세요."),🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java
around line 13, the message template uses "{%s}" which is not compatible with
String.format; replace the template token with "%s" to match other error codes
and ensure String.format will interpolate the cooldown seconds correctly.
| public record CreateGroupImageV2Request( | ||
| Integer sortOrder, | ||
| String imageUrl440x240, | ||
| String imageUrl100x100 | ||
| String imageKey, | ||
| Integer sortOrder | ||
| ) {} |
There was a problem hiding this comment.
입력 검증 어노테이션을 추가하세요.
요청 DTO에 필수 검증이 누락되어 있습니다:
imageKey:@NotBlank및 길이 제약 (예:@Size(max=36)) 추가 필요sortOrder:@NotNull및 범위 제약 (예:@Min(0)) 추가 권장
서비스 계층에 도달하기 전에 잘못된 입력을 조기에 차단할 수 있습니다.
🔎 제안 수정사항
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Size;
+
public record CreateGroupImageV2Request(
+ @NotBlank(message = "이미지 키는 필수입니다")
+ @Size(max = 36, message = "이미지 키는 최대 36자입니다")
String imageKey,
+ @NotNull(message = "정렬 순서는 필수입니다")
+ @Min(value = 0, message = "정렬 순서는 0 이상이어야 합니다")
Integer sortOrder
) {}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public record CreateGroupImageV2Request( | |
| Integer sortOrder, | |
| String imageUrl440x240, | |
| String imageUrl100x100 | |
| String imageKey, | |
| Integer sortOrder | |
| ) {} | |
| import jakarta.validation.constraints.NotBlank; | |
| import jakarta.validation.constraints.NotNull; | |
| import jakarta.validation.constraints.Min; | |
| import jakarta.validation.constraints.Size; | |
| public record CreateGroupImageV2Request( | |
| @NotBlank(message = "이미지 키는 필수입니다") | |
| @Size(max = 36, message = "이미지 키는 최대 36자입니다") | |
| String imageKey, | |
| @NotNull(message = "정렬 순서는 필수입니다") | |
| @Min(value = 0, message = "정렬 순서는 0 이상이어야 합니다") | |
| Integer sortOrder | |
| ) {} |
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/application/dto/request/CreateGroupImageV2Request.java
around lines 3 to 6, the request DTO lacks validation annotations; add @NotBlank
and @Size(max=36) to the imageKey component and add @NotNull and @Min(0) to the
sortOrder component (use the project's validation package - javax.validation or
jakarta.validation - and add the necessary imports) so invalid requests are
rejected before reaching the service layer.
| @PostMapping("/upload") | ||
| public ResponseEntity<ApiResponse<PreUploadGroupImageV2Response>> uploadImages( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @RequestParam("images") List<MultipartFile> images | ||
| ) { | ||
| PreUploadGroupImageV2Response response = preUploadService.uploadGroupImagesV2( | ||
| userDetails.getId(), images); | ||
|
|
||
| return ResponseEntity | ||
| .status(HttpStatus.CREATED) | ||
| .body(ApiResponse.success(HttpStatus.CREATED.value(), response)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the GroupImageV2Controller file and examine surrounding context
find . -type f -name "*GroupImageV2*" 2>/dev/null | head -20Repository: WeGo-Together/WeGo_BackEnd
Length of output: 916
🏁 Script executed:
#!/bin/bash
# Search for the preUploadService implementation and validation logic
find . -type f -name "*.java" -path "*/service/*" 2>/dev/null | grep -i "upload\|image" | head -20Repository: WeGo-Together/WeGo_BackEnd
Length of output: 348
🏁 Script executed:
#!/bin/bash
# Search for @Valid, @Validated, and OpenAPI annotations usage in codebase
rg "@Valid|@Validated|@Operation|@Parameter|@Schema" --type java -l 2>/dev/null | head -20Repository: WeGo-Together/WeGo_BackEnd
Length of output: 878
🏁 Script executed:
#!/bin/bash
# Read the full GroupImageV2Controller.java file
cat -n src/main/java/team/wego/wegobackend/group/v2/presentation/GroupImageV2Controller.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 1965
🏁 Script executed:
#!/bin/bash
# Read the GroupImageV2PreUploadService.java to check validation logic
cat -n src/main/java/team/wego/wegobackend/group/v2/application/service/GroupImageV2PreUploadService.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 3644
🏁 Script executed:
#!/bin/bash
# Check if GroupImageV2ControllerDocs exists
find . -type f -name "*GroupImageV2*Docs*" 2>/dev/nullRepository: WeGo-Together/WeGo_BackEnd
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check the structure and annotations used in v1 GroupImageControllerDocs for reference
head -50 src/main/java/team/wego/wegobackend/group/presentation/v1/GroupImageControllerDocs.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 2280
파일 업로드 검증 및 API 문서화를 추가하세요.
현재 엔드포인트에 다음 항목들이 누락되어 있습니다:
-
컨트롤러 수준 검증 부재: 서비스 계층에서 최대 3개 파일 제한을 검증하지만, 컨트롤러 수준에서 조기 검증이 없어 불필요한 요청 처리가 발생합니다.
@Valid와 커스텀 validator를 추가하여 컨트롤러에서 먼저 검증하세요. -
API 문서화 누락:
@Operation,@Parameter,@Tag같은 OpenAPI 어노테이션이 없으며, v1 버전처럼 별도GroupImageV2ControllerDocs인터페이스도 없습니다. API 사용자가 3개 파일 제한 같은 제약사항을 알기 어렵습니다.
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupImageV2Controller.java
around lines 26-37, add controller-level validation and OpenAPI docs: create a
request wrapper DTO (e.g., GroupImageUploadRequest) holding List<MultipartFile>
images annotated with @NotNull and @Size(max=3) or use a custom Constraint
(implement ConstraintValidator to check file count and optionally file
content/type/size), annotate the controller parameter with @Valid to enforce
early validation and return 400 on violations; add OpenAPI annotations (@Tag on
the controller, @Operation on the upload method, and @Parameter describing the
images field and the 3-file limit) or extract docs into a
GroupImageV2ControllerDocs interface (like v1) and have the controller implement
it so API docs clearly show the 3-file constraint and request details.
| @PostMapping("/create") | ||
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> createGroup( | ||
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> create( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @RequestParam(defaultValue = "30") int cooldownSeconds, | ||
| @RequestBody @Valid CreateGroupV2Request request | ||
| ) { | ||
| CreateGroupV2Response response = groupV2Service.create(userDetails.getId(), request); | ||
| CreateGroupV2Response response = groupV2Service.create(userDetails.getId(), request, cooldownSeconds); |
There was a problem hiding this comment.
보안 우려: cooldownSeconds를 클라이언트가 제어 가능
cooldownSeconds를 @RequestParam으로 노출하면 악의적인 사용자가 ?cooldownSeconds=1과 같이 매우 낮은 값을 설정하여 쿨다운 메커니즘을 우회할 수 있습니다. 이는 PR 목표인 "악용 방지를 위한 쿨다운 기능"의 효과를 무력화합니다.
쿨다운 시간은 서버 측 설정(@Value 또는 @ConfigurationProperties)으로 관리하고, 클라이언트 요청에서 제거하는 것이 좋습니다.
🔎 수정 제안:
+@Value("${group.create.cooldown-seconds:30}")
+private int cooldownSeconds;
@PostMapping("/create")
public ResponseEntity<ApiResponse<CreateGroupV2Response>> create(
@AuthenticationPrincipal CustomUserDetails userDetails,
- @RequestParam(defaultValue = "30") int cooldownSeconds,
@RequestBody @Valid CreateGroupV2Request request
) {
CreateGroupV2Response response = groupV2Service.create(userDetails.getId(), request, cooldownSeconds);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @PostMapping("/create") | |
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> createGroup( | |
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> create( | |
| @AuthenticationPrincipal CustomUserDetails userDetails, | |
| @RequestParam(defaultValue = "30") int cooldownSeconds, | |
| @RequestBody @Valid CreateGroupV2Request request | |
| ) { | |
| CreateGroupV2Response response = groupV2Service.create(userDetails.getId(), request); | |
| CreateGroupV2Response response = groupV2Service.create(userDetails.getId(), request, cooldownSeconds); | |
| @Value("${group.create.cooldown-seconds:30}") | |
| private int cooldownSeconds; | |
| @PostMapping("/create") | |
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> create( | |
| @AuthenticationPrincipal CustomUserDetails userDetails, | |
| @RequestBody @Valid CreateGroupV2Request request | |
| ) { | |
| CreateGroupV2Response response = groupV2Service.create(userDetails.getId(), request, cooldownSeconds); |
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java
around lines 34 to 40, remove the client-controlled @RequestParam
cooldownSeconds and instead load the cooldown from server configuration (either
with @Value or a @ConfigurationProperties bean); update the controller method
signature to no longer accept cooldownSeconds from the request and pass the
server-side value into groupV2Service.create; update any affected service method
signature/usage to accept the configured cooldown (with a sensible
default/fallback) and add validation/logging as needed so clients cannot
override the cooldown.
There was a problem hiding this comment.
Pull request overview
This PR implements a new V2 image pre-upload flow using Redis as temporary storage and adds a group creation cooldown feature to prevent abuse. The changes refactor group creation to use image keys instead of direct URLs, improving security and separation of concerns.
- Introduces Redis-based pre-upload workflow where clients upload images before group creation
- Adds rate-limiting via cooldown mechanism with transaction rollback support
- Refactors group creation API to use imageKey references instead of direct image URLs
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| v2-group-create.http | Updated test cases to use V2 endpoints and imageKey instead of URLs |
| GroupV2Controller.java | Added cooldownSeconds parameter to create endpoint |
| GroupImageV2Controller.java | New controller for V2 image pre-upload endpoint |
| PreUploadedGroupImageRedisRepository.java | Redis repository for managing pre-uploaded images with 2-hour TTL |
| PreUploadedGroupImageRedisConfig.java | Redis configuration for pre-uploaded image storage |
| GroupImageV2.java | Added imageKey field with unique constraint to entity |
| GroupV2Service.java | Integrated cooldown service and image consumption from Redis |
| GroupImageV2PreUploadService.java | Service for pre-uploading and storing images in Redis |
| GroupCreateCooldownService.java | Service implementing cooldown logic with rollback support |
| ImageUploadService.java | Added new method for WebP upload with custom base name |
| PreUploadGroupImageV2Response.java | Response DTO for pre-upload operation |
| UpdateGroupImageItemV2Request.java | Request DTO for updating group images (new file) |
| CreateGroupImageV2Request.java | Refactored to use imageKey instead of URLs |
| PreUploadedGroupImage.java | Domain object for Redis-stored pre-uploaded image data |
| PreUploadGroupImageV2Item.java | Response item for pre-uploaded images |
| GroupErrorCode.java | Added error codes for cooldown and pre-upload validation |
| GlobalExceptionHandler.java | Added handlers for Redis and JSON exceptions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // create()는 @Transactional 안에서 호출되니까 보통 true | ||
| if (TransactionSynchronizationManager.isActualTransactionActive()) { | ||
| TransactionSynchronizationManager.registerSynchronization( | ||
| new TransactionSynchronization() { | ||
| @Override | ||
| public void afterCompletion(int status) { | ||
| if (status == STATUS_ROLLED_BACK) { | ||
| release(userId); | ||
| } | ||
| } | ||
| } | ||
| ); |
There was a problem hiding this comment.
The comment states "create()는 @transactional 안에서 호출되니까 보통 true" (usually true because create() is called within @transactional), but this makes a dangerous assumption. If the calling context changes or if this method is reused elsewhere without a transaction, the rollback synchronization won't be registered. Consider checking the transaction status and either throwing an exception or logging a warning if no transaction is active, to prevent silent failures in cooldown release.
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
The file ends with three blank lines. According to typical Java style conventions, files should end with a single newline. Consider removing the extra blank lines for consistency.
|
|
||
| @PostMapping("/create") | ||
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> createGroup( | ||
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> create( |
There was a problem hiding this comment.
The method name was changed from "createGroup" to just "create". While this may work, it's less descriptive and could be confusing when reading code or logs. The previous name "createGroup" was more explicit about what the method does. Consider reverting to the more descriptive name or document the reason for this change.
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> create( | |
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> createGroup( |
| @Column(name = "group_image_id", nullable = false, updatable = false) | ||
| private Long id; | ||
|
|
||
| @Column(name = "image_key", nullable = false, updatable = false, length = 36, unique = true) |
There was a problem hiding this comment.
The imageKey column is marked as unique, but there's a potential race condition. If the same pre-uploaded image is consumed multiple times (e.g., due to retries or concurrent requests), this will cause a unique constraint violation. Consider adding proper synchronization or handling this exception gracefully in the service layer.
| @Column(name = "image_key", nullable = false, updatable = false, length = 36, unique = true) | |
| @Column(name = "image_key", nullable = false, updatable = false, length = 36) |
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||
| } | ||
|
|
||
| groupCreateCooldownService.acquireOrThrowWithRollbackRelease(userId, cooldownSeconds); |
There was a problem hiding this comment.
The cooldown acquire operation occurs before the transaction starts. If the transaction processing takes a long time or if there are multiple retries, the cooldown lock may expire before the transaction completes. This could lead to inconsistent state where the cooldown is released but the group is still being created. Consider acquiring the cooldown lock after basic validation but ensuring it's held for the duration of the critical operations.
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
The file ends with three blank lines. According to typical Java style conventions, files should end with a single newline. Consider removing the extra blank lines for consistency.
| String imageKey = UUID.randomUUID().toString(); | ||
| String baseName = | ||
| LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) | ||
| + "_" + i + "_" + imageKey; |
There was a problem hiding this comment.
The imageKey is generated as a UUID but then included in the S3 key's baseName along with a timestamp and index. This could allow an attacker to predict the S3 key structure if they know the upload time. While the UUID provides randomness, consider whether exposing the timestamp pattern is acceptable for your security requirements, or use only the UUID for the S3 key to prevent any information leakage about upload timing.
| "sortOrder": 1, | ||
| "imageUrl440x240": "{{img1_main}}", | ||
| "imageUrl100x100": "{{img1_thumb}}" | ||
| "imageKey": "{{img1_key}}"ㄱ |
There was a problem hiding this comment.
There is a stray Korean character 'ㄱ' at the end of this line. This appears to be a typo that should be removed.
| "imageKey": "{{img1_key}}"ㄱ | |
| "imageKey": "{{img1_key}}" |
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> createGroup( | ||
| public ResponseEntity<ApiResponse<CreateGroupV2Response>> create( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @RequestParam(defaultValue = "30") int cooldownSeconds, |
There was a problem hiding this comment.
Using a request parameter for cooldownSeconds in a production endpoint is a security risk. This allows clients to bypass or reduce the cooldown protection by passing different values (e.g., cooldownSeconds=0 or cooldownSeconds=1). The cooldown duration should be configured server-side only, not client-controlled. Consider removing this parameter and using a fixed configuration value instead.
| PreUploadedGroupImage pre = preUploadedGroupImageRedisRepository.consume(imageKey) | ||
| .orElseThrow(() -> new GroupException( | ||
| GroupErrorCode.PRE_UPLOADED_IMAGE_NOT_FOUND, imageKey | ||
| )); |
There was a problem hiding this comment.
Using consume() removes the image from Redis, but if the transaction fails after consuming one or more images, those images are lost and cannot be used in a retry. This breaks idempotency and could frustrate users who encounter transaction failures. Consider using find() instead of consume() and only removing the images after successful transaction commit, or implement a mechanism to restore consumed images on transaction rollback.
사용자가 그룹을 너무 자주 생성하는 것을 방지하기 위해 쿨다운 기간을 구현합니다. 이 변경 사항은 그룹 생성 시도 후 30초의 고정 쿨다운 기간을 도입하여 시스템 안정성을 향상시키고 악용 가능성을 줄입니다. 쿨다운 시간은 이제 서비스 계층 내에서 상수로 관리됩니다. 또한 그룹 이미지의 정렬 순서가 음수가 아닌지 확인합니다.
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
Redis에 임시 저장소를 사용하는 새로운 이미지 사전 업로드 흐름을 도입하여 클라이언트가 그룹 생성 전에 이미지를 업로드할 수 있도록 합니다.
악용 방지를 위해 Redis를 사용한 그룹 생성 쿨다운 기능을 추가합니다. 이 기능에는 실패 시 쿨다운 잠금을 해제하는 트랜잭션 롤백 처리가 포함됩니다.
사전 업로드된 이미지와 이미지 키를 활용하도록 그룹 생성 프로세스를 재구성하여 보안과 효율성을 향상시킵니다.
🔍 관련 이슈
🧪 테스트
변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.
🚨 확인해야 할 사항 (Checklist)
PR을 제출하기 전에 아래 항목들을 확인해주세요.
🙋 기타 참고 사항
리뷰어가 참고하면 좋을 만한 추가 설명이 있다면 적어주세요.
Summary by CodeRabbit
신기능
향상된 오류 처리
✏️ Tip: You can customize this high-level summary in your review settings.