refactor: 슬롯 확장 수정, 유저별 텃밭 조회 기능#135
Hidden character warning
Conversation
Walkthrough단계(위시트리 포인트 기반)별 텃밭 슬롯 개방 로직으로 전환하고, 기본 아바타/배경을 사용한 새 텃밭 생성 절차를 GardenService에 구현. 관련 에러 코드 추가/재배치. 현재 사용자 텃밭 ID 목록 조회 API 추가. 로컬 프로필 JPA DDL 전략을 update로 변경. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant API as GardenController
participant S as GardenService
participant UR as UserRepository
participant WR as WishTreeRepository
participant SR as Stage (WishTreeStage)
participant GR as GardenRepository
participant AR as AvatarRepository
participant BR as GardenBackgroundRepository
U->>API: POST /gardens/unlock
API->>S: unlockNewGardenSlot(userId)
S->>UR: findById(userId)
UR-->>S: User or null
alt user not found
S-->>API: throw USER_NOT_FOUND
API-->>U: 404 USER_NOT_FOUND
end
S->>WR: findByUserId(userId)
WR-->>S: WishTree or null
alt wish tree missing
S-->>API: throw WISH_TREE_NOT_FOUND
API-->>U: 404 WISH_TREE_NOT_FOUND
end
S->>SR: getStageForPoints(points)
SR-->>S: WishTreeStage (maxGardens)
S->>S: compare currentGardens vs maxGardens
alt exceeds limit
S-->>API: throw GARDEN_SLOT_LOCKED
API-->>U: 400 GARDEN_SLOT_LOCKED
else can create
S->>BR: findById(1) defaultBackground
BR-->>S: Background or null
S->>AR: findById(1) defaultAvatar
AR-->>S: Avatar or null
alt missing default resources
S-->>API: throw DEFAULT_RESOURCE_NOT_FOUND
API-->>U: 404 DEFAULT_RESOURCE_NOT_FOUND
else
S->>GR: save(new Garden with slotNumber = count+1)
GR-->>S: Garden
S->>S: user.addGarden(garden)
S-->>API: success
API-->>U: 200 OK
end
end
sequenceDiagram
autonumber
actor U as User
participant API as UserController
participant Sec as SecurityContext
U->>API: GET /api/v1/users/me/gardens
API->>Sec: @AuthenticationPrincipal User
Sec-->>API: User
API->>API: user.getGardens() -> sort by slotNumber -> map to id
API-->>U: 200 [gardenId...]
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/resources/application.yml (1)
88-93: 프로덕션에서 SQL 파라미터 TRACE 로그는 PII 노출 위험
org.hibernate.type.descriptor.sql.BasicBinder: TRACE는 바인딩된 파라미터(개인정보 포함 가능)를 그대로 로그에 남깁니다. prod 프로필에서 제거하거나 최소 INFO 이하로 낮추는 것이 안전합니다.적용 예:
-logging: - level: - org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE +logging: + level: + org.hibernate.SQL: INFO + # prod에서는 파라미터 로그 비활성화(필요 시 local/dev 전용 문서에만 유지) + # org.hibernate.type.descriptor.sql.BasicBinder: OFFsrc/main/java/com/example/cp_main_be/domain/member/user/presentation/UserController.java (1)
49-58: PathVariable/RequestParam 사용 불일치 버그
@PatchMapping("/me/{avatarId}")인데 파라미터는@RequestParam("avatarId")로 선언되어 매핑 실패합니다.@PathVariable로 교체하세요.- @PatchMapping("/me/{avatarId}") + @PatchMapping("/me/{avatarId}") public ResponseEntity<ApiResponse<Void>> updateAvatar( @AuthenticationPrincipal User user, @RequestBody @Valid AvatarChangeRequest request, - @RequestParam("avatarId") Long avatarId) { + @PathVariable("avatarId") Long avatarId) {
🧹 Nitpick comments (6)
src/main/resources/application.yml (1)
58-58: 로컬 DDL 전략을 update로 전환 — 테스트/초기화 영향 점검 필요H2(in‑mem) 환경에서도
update는 스키마 차이를 누적시키므로, 통합테스트가 “완전 초기화”를 전제로 하면 불안정할 수 있습니다. 테스트 전용 프로필(test)에create-drop을 유지하거나, 마이그레이션 스크립트만으로 부트스트랩하도록 합의가 필요합니다.권장: test 프로필을 추가해 테스트 시에는 항상 깨끗한 스키마를 보장.
+--- +spring: + config: + activate: + on-profile: test + jpa: + hibernate: + ddl-auto: create-drop + datasource: + url: jdbc:h2:mem:testdb;MODE=PostgreSQL + username: sa + password:src/main/java/com/example/cp_main_be/global/common/ErrorCode.java (1)
18-20: GARDEN_SLOT_MAXED_OUT 주석 최신화현재 슬롯 확장 로직이 단계(위시트리) 기반으로 전환되었습니다. “최종 3개 도달 시 사용 가능” 주석은 혼동 소지가 있어 최신 정책으로 정리하거나 Deprecated 표기를 권장합니다.
- GARDEN_SLOT_MAXED_OUT( - HttpStatus.BAD_REQUEST, "E40006", "더 이상 텃밭을 추가할 수 없습니다."), // 최종 3개 도달 시 사용 가능 + GARDEN_SLOT_MAXED_OUT( + HttpStatus.BAD_REQUEST, "E40006", "더 이상 텃밭을 추가할 수 없습니다."), // 단계 기반 슬롯 정책으로 대체(legacy)src/main/java/com/example/cp_main_be/domain/member/user/presentation/UserController.java (1)
91-100: 응답 스키마의 확장성ID만 노출하는 것은 간결하지만, 추후 슬롯 번호나 라벨 추가 시 API 변경이 필요합니다.
GardenSummaryResponse{id, slotNumber}같은 DTO로의 확장을 고려하세요.src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (3)
38-38: 미사용 상수 제거단계 기반 슬롯 정책으로 전환되어
MAX_GARDEN_COUNT는 사용되지 않습니다. 혼동 방지를 위해 제거하세요.- private static final int MAX_GARDEN_COUNT = 3;
195-210: 기본 리소스 ID 하드코딩(1L) 제거 권장데이터 의존성이 높고 이식성이 낮습니다. 설정 값 또는 “기본 여부 플래그” 조회로 치환하세요.
환경설정/서비스 예시:
+import org.springframework.beans.factory.annotation.Value; ... + @Value("${garden.default.background-id:1}") + private Long defaultBackgroundId; + @Value("${avatar.default.id:1}") + private Long defaultAvatarId; ... - gardenBackgroundRepository.findById(1L) + gardenBackgroundRepository.findById(defaultBackgroundId) ... - avatarRepository.findById(1L) + avatarRepository.findById(defaultAvatarId)application.yml(프로필별 오버라이드 가능):
+garden: + default: + background-id: 1 +avatar: + default: + id: 1필요하시면 리포지토리에
findDefault()API 형태로의 전환도 도와드리겠습니다.
212-219: 양방향 연관관계 일관성 유지 방식 통일
Garden.builder().user(user)와user.addGarden(newGarden)가 동시에 호출됩니다. 도메인 규칙상 한쪽만 진입점으로 삼아 양쪽 세팅을 내부에서 보장하도록 통일하는 것이 안전합니다(중복 추가/누락 방지).- Garden newGarden = Garden.builder() - .user(user) - ... - .build(); - gardenRepository.save(newGarden); - user.addGarden(newGarden); + Garden newGarden = Garden.builder() + ... + .build(); + user.addGarden(newGarden); // addGarden 내부에서 newGarden.setUser(this) 수행 + gardenRepository.save(newGarden);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java(4 hunks)src/main/java/com/example/cp_main_be/domain/member/user/presentation/UserController.java(3 hunks)src/main/java/com/example/cp_main_be/global/common/ErrorCode.java(2 hunks)src/main/resources/application.yml(1 hunks)
🔇 Additional comments (4)
src/main/java/com/example/cp_main_be/global/common/ErrorCode.java (2)
41-42: 새 404 코드 추가 적합
WISH_TREE_NOT_FOUND,DEFAULT_RESOURCE_NOT_FOUND추가는 서비스 로직과 일치하며 의미도 명확합니다. 별도 우려 없습니다.
21-27: E40008~E40011 에러 코드 변경에 따른 하드코딩 의존성 없음 확인 & 외부 소비자 검증 요청레포 전역 검색 결과, “E40008”~“E40011” 리터럴은 enum 정의부(src/main/java/com/example/cp_main_be/global/common/ErrorCode.java) 외에는 발견되지 않았습니다. Java 코드에서는 모두 enum 상수(ErrorCode.GARDEN_SLOT_LOCKED, FILE_SIZE_EXCEEDED, INVALID_MISSION_TYPE_FOR_QUIZ_OPTION, INVALID_REQUEST)로만 참조 중이므로 내부 로직에는 추가 수정이 필요 없습니다.
아래 영역에 대해서만 추가 검증을 권장드립니다:
- API 명세(Swagger/OpenAPI) 및 README 등 문서
- 통합/단위 테스트(응답 JSON 내 errorCode 비교)
- 프론트엔드 코드(문자열 기반 error code 매핑)
- 외부 클라이언트(Postman 컬렉션, SDK 등)
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (2)
170-176: 사용자/위시트리 존재 검증 전환 적절
USER_NOT_FOUND,WISH_TREE_NOT_FOUND로 명확하게 분기하는 변경은 의미적으로 타당합니다.
178-186: 단계 기반 슬롯 한도 계산 로직 적합포인트→단계→최대 텃밭 수 파이프라인이 명확합니다. 도메인 규칙과 정합적입니다.
| if (currentGardenCount >= maxGardensForStage) { | ||
| // 현재 단계에서는 더 이상 텃밭을 생성할 수 없음 | ||
| // ErrorCode에 GARDEN_SLOT_LOCKED 추가가 필요할 수 있습니다. | ||
| throw new CustomApiException( | ||
| ErrorCode.GARDEN_SLOT_LOCKED, "현재 단계에서는 더 이상 텃밭을 만들 수 없습니다. 소원나무를 성장시켜주세요."); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
슬롯 확장 동시성 취약 — 중복 슬롯/초과 생성 가능성
동시 요청에서 currentGardenCount 기반 계산은 경쟁 조건을 유발합니다. 사용자별 슬롯 행 잠금 또는 고유 제약 + 비관적 잠금으로 방어하세요.
DB/리포지토리/서비스 제안:
-- DB: 사용자별 슬롯번호 유니크 제약
ALTER TABLE garden ADD CONSTRAINT uq_garden_user_slot UNIQUE (user_id, slot_number);// GardenRepository
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select coalesce(max(g.slotNumber),0) from Garden g where g.user.id = :userId")
int findMaxSlotNumberByUserIdForUpdate(Long userId);- int currentGardenCount = user.getGardens().size();
+ int currentMaxSlot = gardenRepository.findMaxSlotNumberByUserIdForUpdate(userId);
...
- if (currentGardenCount >= maxGardensForStage) {
+ if (currentMaxSlot >= maxGardensForStage) {
throw new CustomApiException(
ErrorCode.GARDEN_SLOT_LOCKED, "현재 단계에서는 더 이상 텃밭을 만들 수 없습니다. 소원나무를 성장시켜주세요.");
}
...
- .slotNumber(currentGardenCount + 1)
+ .slotNumber(currentMaxSlot + 1)🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java
around lines 188 to 193, the current check using currentGardenCount can race
under concurrent requests allowing duplicate/over-limit garden creation; fix by
enforcing a DB-level unique constraint on (user_id, slot_number) and performing
the count/slot selection inside a transactional method with a pessimistic lock
query (or select max(slot_number) FOR UPDATE) on the user's garden rows so
concurrent threads serialize; adjust GardenRepository to provide a
PESSIMISTIC_WRITE locked accessor and wrap creation in a single @Transactional
unit that rechecks limits and handles unique-constraint violation to return a
proper ErrorCode.GARDEN_SLOT_LOCKED response.
| @Operation( | ||
| summary = "내 텃밭 ID 목록 조회", | ||
| description = "현재 로그인한 유저와 연결된 모든 텃밭의 ID 목록을 조회합니다. 텃밭 슬롯 번호 순으로 정렬됩니다.") | ||
| @GetMapping("/me/gardens") | ||
| public ResponseEntity<ApiResponse<List<Long>>> getMyGardenIds( | ||
| @AuthenticationPrincipal User user) { | ||
| List<Long> gardenIds = | ||
| user.getGardens().stream() | ||
| .sorted(Comparator.comparing(Garden::getSlotNumber)) | ||
| .map(Garden::getId) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(gardenIds)); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
컨트롤러에서 LAZY 컬렉션 접근 위험 및 N+1 가능성
user.getGardens()가 LAZY면 트랜잭션 밖에서 LazyInitializationException이 날 수 있습니다. 또한 정렬/매핑을 애그리거트 외부에서 수행 중입니다. 서비스 계층에서 fetch join으로 ID만 조회하는 리포지토리 메서드를 도입하세요.
권장 리포지토리/서비스 예시:
// GardenRepository
@Query("select g.id from Garden g where g.user.id = :userId order by g.slotNumber asc")
List<Long> findIdsByUserIdOrderBySlot(Long userId);-List<Long> gardenIds =
- user.getGardens().stream()
- .sorted(Comparator.comparing(Garden::getSlotNumber))
- .map(Garden::getId)
- .collect(Collectors.toList());
+List<Long> gardenIds = gardenService.findMyGardenIds(user.getId());검증 스크립트(매핑 확인):
🏁 Script executed:
#!/bin/bash
# User 엔티티에서 gardens 컬렉션 fetch 타입/매핑 확인
rg -n -C3 'class\s+User\b'
rg -n -C2 '@OneToMany[^\n]*gardens'Length of output: 750
이전 스크립트가 @OneToMany 어노테이션과 gardens 필드를 정확히 찾지 못해 매핑 정보를 확인할 수 없습니다. User 엔티티에서 해당 필드 선언과 페치 전략을 명확히 보기 위해 아래 스크립트를 실행해주세요.
🏁 Script executed:
#!/bin/bash
# User 엔티티에서 @OneToMany 어노테이션 위치와 속성 확인
rg -n -C3 '@OneToMany' src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java
# User 엔티티에서 gardens 필드 선언부 확인
rg -n -C3 'gardens' src/main/java/com/example/cp_main_be/domain/member/user/domain/User.javaLength of output: 1488
컨트롤러에서 LAZY 컬렉션 직접 접근 위험 – 서비스 계층으로 조회 로직 이동 필요
현재 User 엔티티의 gardens 컬렉션은 @OneToMany(mappedBy="user", cascade=ALL, orphanRemoval=true)로 선언되어 있어 JPA 기본 페치 전략인 LAZY를 사용합니다. 컨트롤러(getMyGardenIds)에는 트랜잭션이 걸려 있지 않으므로, 요청 처리 시점에 세션이 닫혀 LazyInitializationException이 발생할 수 있습니다. 또한, 정렬·매핑 로직이 프레젠테이션 계층에 흩어져 있어 책임 분리가 미흡합니다.
-
User 엔티티 매핑 확인
@OneToMany(mappedBy="user", …)에서fetch속성 미설정 → 기본 LAZY 페치private List<Garden> gardens = new ArrayList<>();
-
컨트롤러 문제
@GetMapping메서드에@Transactional미적용user.getGardens()호출 시점에 트랜잭션/세션 미존재 →LazyInitializationException리스크- 프레젠테이션 계층에서 정렬·매핑 로직 수행 → SRP 위반
제안하는 리팩터링
- Repository에 사용자별 텃밭 ID만 조회하는 JPQL 메서드 추가
public interface GardenRepository extends JpaRepository<Garden, Long> { @Query("select g.id from Garden g where g.user.id = :userId order by g.slotNumber asc") List<Long> findIdsByUserIdOrderBySlot(Long userId); }
- Service 계층에 트랜잭셔널 메서드 구현
@Service public class GardenService { private final GardenRepository gardenRepo; @Transactional(readOnly = true) public List<Long> getMyGardenIds(Long userId) { return gardenRepo.findIdsByUserIdOrderBySlot(userId); } }
- 컨트롤러에서 직접 엔티티 컬렉션 대신 서비스 호출로 교체
- List<Long> gardenIds = - user.getGardens().stream() - .sorted(Comparator.comparing(Garden::getSlotNumber)) - .map(Garden::getId) - .collect(Collectors.toList()); + List<Long> gardenIds = gardenService.getMyGardenIds(user.getId());
이렇게 하면
- 불필요한 엔티티 그래프 로딩 방지
- 트랜잭션 경계 내에서 안전하게 쿼리 수행
- 프레젠테이션 계층의 역할이 명확해집니다.
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/member/user/presentation/UserController.java
lines 87-100, the controller directly accesses the LAZY User.gardens collection
(user.getGardens()) which can trigger LazyInitializationException and mixes
presentation logic with data access; replace this by adding a GardenRepository
method to select garden IDs by userId ordered by slot, implement a @Service
GardenService with a @Transactional(readOnly=true) method getMyGardenIds(Long
userId) that calls the repository query, and update the controller to call that
service method (passing the authenticated user's id) and return the result
instead of iterating over user.getGardens(). Ensure no direct traversal of the
LAZY collection in the controller.
📝 개요
이번 PR의 핵심 내용을 한 줄로 요약해 주세요.
💻 작업 내용
이번 PR에서 작업한 내용을 상세히 설명해 주세요.
작업 내용 1
작업 내용 2
...
✅ PR 체크리스트
PR을 보내기 전에 아래 체크리스트를 확인해 주세요.
커밋 메시지는 포맷에 맞게 작성했나요?
스스로 코드를 다시 한번 검토했나요?
관련 이슈를 연결했나요?
빌드 및 테스트가 로컬에서 성공했나요?
🔗 관련 이슈
이번 PR과 관련된 이슈 번호를 기재해 주세요. 예: Closes #131
스크린샷 (선택)
UI 변경 사항이 있다면 스크린샷을 첨부해 주세요.
Summary by CodeRabbit