Conversation
* [feat]: Club, ClubBookmark 엔티티 구현 * [feat]: club 및 club_bookmark 테이블 Flyway 마이그레이션 추가 * [feat]: ClubCategory enum 구현 * [feat]: ClubDivision enum 구현 * [test]: Club 도메인 테스트 구현 * [fix]: Club category/division 컬럼 NOT NULL 제약 반영 * [refactor]: Club 위치 필드명을 의미 명확화를 위해 lat, lon으로 수정 * [refactor]: description, qualifications 필드 @lob → @column(TEXT)로 변경 * [feat]: ClubSns 엔티티 및 ClubSnsType enum 추가, Club에 SNS 1:N 구조 적용 * [refactor]: 즐겨찾기 -> 구독으로 명칭 변경 * [refactor]: db club 테이블 변경사항 수정 * [refactor]: club 에러코드 네이밍 수정 --------- Co-authored-by: Han Ju Kim <[email protected]>
* feat : Club Equals & HashCode 오버라이딩 * feat : KuringFeatures 추가 * feat : Club MessageType 추가 * feat : 사용자 동아리 즐겨찾기 추가/삭제 API Adapter추가 * feat : 동아리 즐겨찾기 Request & Command 추가 * feat : 동아리 즐겨찾기 Usecase & Service 추가 * feat : Club & ClubSubscribe Query & CommandPort 추가 * feat : Club & ClubSubscribe Repository추가 * feat : ClubQueryRepository & Impl 추가 * feat : ClubPersistenceAdapter 추가 * feat : ClubSubscribe RootUser Mapping 수정 * feat : ClubNotificationUseCase & Service 추가 * feat : ClubNotificationScheduler 추가 * feat : Club ErrorCode & ResponseCodeAndMessages 추가 * feat : Club 구독 응답 DTO 추가 * feat : ClubSubscribe user -> rootUser 매핑 변경 * feat : 사용자 구독 추가/취소 인수테스트 추가 * feat : Club 도메인 DependencyRuleTests 추가 * feat : DatabaseConfigurator 동아리 init 추가 * feat : ClubPersistenceAdapterTest 추가 * feat : ClubNotificationServiceTest 추가 * feat : ClubNotificationSchedulerTest 추가 * feat : ClubCommandServiceTest 추가 * test : ClubPersistenceAdapterTest 테스트 간소화 * test : ClubServiceTest 리팩토링 * test : Club Service & Adapter Test assertAll 처리 * test: 미사용 메서드 제거 * remove: 미사용 Enum 제거 * fix : 동아리 즐겨찾기 삭제 API Request형식 수정 * fix : 동아리 즐겨찾기 추가/삭제 시 dev suffix추가하도록 수정 * test : ServerProperties Mock 추가 * remove : 미사용 메서드 제거(equals, hashcode) * fix : JWT static final String 으로 분리 * fix : club_subscribe 테이블 root_user_id로 컬럼 변경 시 기존 데이터 무시
Walkthrough동아리 구독 및 알림 기능을 추가하는 변경사항입니다. 동아리 도메인 엔티티, 포트 인터페이스, 퍼시스턴스 어댑터, 애플리케이션 서비스, REST API, 스케줄러, 데이터베이스 마이그레이션, 그리고 통합 테스트를 포함합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant API as UserClubSubscriptionApiV2
participant UseCase as ClubCommandService
participant QueryPort as ClubQueryPort
participant CommandPort as ClubSubscriptionCommandPort
participant QueryPort2 as ClubSubscriptionQueryPort
participant Device as DeviceSubscriptionPort
Client->>API: POST /api/v2/users/subscriptions/clubs<br/>(email, clubId)
activate API
API->>API: validateJwtAndGetEmail()
API->>UseCase: addSubscription(command)
activate UseCase
UseCase->>QueryPort: findClubById(clubId)
activate QueryPort
QueryPort-->>UseCase: Optional<Club>
deactivate QueryPort
alt Club not found
UseCase-->>API: InvalidStateException(CLUB_NOT_FOUND)
else
UseCase->>QueryPort2: existsSubscription(userId, clubId)
activate QueryPort2
QueryPort2-->>UseCase: boolean
deactivate QueryPort2
alt Already subscribed
UseCase-->>API: InvalidStateException(CLUB_ALREADY_SUBSCRIBED)
else
UseCase->>CommandPort: saveSubscription(rootUser, club)
activate CommandPort
CommandPort-->>UseCase: void
deactivate CommandPort
UseCase->>Device: subscribeToTopic(club.{clubId})
activate Device
Device-->>UseCase: void
deactivate Device
UseCase->>QueryPort2: countSubscriptions(userId)
activate QueryPort2
QueryPort2-->>UseCase: long
deactivate QueryPort2
UseCase-->>API: count
end
end
deactivate UseCase
API-->>Client: 200 OK<br/>{subscriptionCount, message}
deactivate API
sequenceDiagram
participant Scheduler as ClubNotificationScheduler
participant UseCase as ClubNotificationService
participant QueryPort as ClubQueryPort
participant Firebase as FirebaseMessagingPort
participant Config as ServerProperties
Scheduler->>Scheduler: Feature Flag Check<br/>(NOTIFY_CLUB_DEADLINE)
alt Feature Flag OFF
Scheduler->>Scheduler: Return early
else Feature Flag ON
Scheduler->>UseCase: sendDeadlineNotifications()
activate UseCase
UseCase->>QueryPort: findNextDayRecruitEndClubs(now)
activate QueryPort
QueryPort-->>UseCase: List<Club>
deactivate QueryPort
loop For each Club
UseCase->>Config: getEnvironmentSuffix()
activate Config
Config-->>UseCase: suffix
deactivate Config
UseCase->>UseCase: buildMessage()<br/>(club, topic, payload)
UseCase->>Firebase: send(message)
activate Firebase
alt Send succeeds
Firebase-->>UseCase: messageId
else Send fails
Firebase-->>UseCase: Exception
UseCase->>UseCase: Log error<br/>(continue)
end
deactivate Firebase
end
UseCase-->>Scheduler: void
deactivate UseCase
end
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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: 5
🧹 Nitpick comments (13)
src/test/java/com/kustacks/kuring/archunit/DependencyRuleTests.java (1)
176-178: 메서드명 및 DisplayName 네이밍 패턴 일관성기존 테스트들은
validateXxxArchitecture패턴과 "Xxx 아키텍처 검증" DisplayName을 사용합니다. 일관성을 위해 수정을 고려해 주세요.♻️ 네이밍 일관성 개선 제안
- `@DisplayName`("Club Domain 의존성 검증") + `@DisplayName`("Club 아키텍처 검증") `@Test` - void validateClubDomainDependencies() { + void validateClubArchitecture() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/kustacks/kuring/archunit/DependencyRuleTests.java` around lines 176 - 178, The test method validateClubDomainDependencies() and its `@DisplayName` currently break the project's naming pattern; rename the method to validateClubDomainArchitecture() and update the `@DisplayName` to match the convention (e.g., "Club 아키텍처 검증") so it follows the existing validateXxxArchitecture / "Xxx 아키텍처 검증" pattern, and update any references/imports/usages accordingly (search for validateClubDomainDependencies and the old display name) to keep tests compiling and consistent.src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java (1)
134-143:initClub()의 하드코딩된 enum 문자열을 상수화하여 타입 안전성을 높이세요.Line 139, Line 140의 리터럴(
"ACADEMIC","CENTRAL")을 enum의.name()메서드로 대체하면 리팩토링 시 자동으로 반영되어 테스트의 안정성이 향상됩니다. 현재 값은 JPA@Enumerated(EnumType.STRING)의 저장 형식과 일치하지만, enum 상수에서 직접 값을 가져오는 방식이 더 유지보수하기 좋습니다.♻️ 제안 diff
+import com.kustacks.kuring.club.domain.ClubCategory; +import com.kustacks.kuring.club.domain.ClubDivision; + private void initClub() { jdbcTemplate.update( "INSERT INTO club (name, summary, category, division, is_always) VALUES (?, ?, ?, ?, ?)", "테스트동아리1", "테스트 요약", - "ACADEMIC", - "CENTRAL", + ClubCategory.ACADEMIC.name(), + ClubDivision.CENTRAL.name(), false ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java` around lines 134 - 143, Replace the hardcoded enum string literals in initClub() with the corresponding enum .name() values to improve type-safety and maintainability: locate the jdbcTemplate.update call inside initClub() and change the category and division arguments from "ACADEMIC" and "CENTRAL" to the enum references (e.g., ClubCategory.ACADEMIC.name() and ClubDivision.CENTRAL.name()) so the persisted strings come directly from the enum constants.src/main/java/com/kustacks/kuring/message/application/port/out/dto/NoticeMessageDto.java (1)
53-53:String.valueOf()의 null 처리 동작 검토
notice.getId()가null을 반환하는 경우,String.valueOf(null)은 문자열"null"을 반환합니다. 이후 Line 33의Assert.notNull(id, ...)검증을 통과하게 되어 의도치 않은"null"문자열이 저장될 수 있습니다.
Notice엔티티의id가 항상 non-null임이 보장된다면 문제없지만, 방어적 코딩을 원한다면 명시적인 null 체크를 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/message/application/port/out/dto/NoticeMessageDto.java` at line 53, The current .id(String.valueOf(notice.getId())) call in NoticeMessageDto will convert a null id to the literal "null" and bypass the later Assert.notNull(id,...); fix by performing an explicit null check on notice.getId() before converting: either assert/throw early with Assert.notNull(notice.getId(), "...") (preferred if id must never be null) or set id with a null-aware conversion like notice.getId() != null ? String.valueOf(notice.getId()) : null so the subsequent Assert.notNull(id, ...) behaves correctly; update the builder call in NoticeMessageDto accordingly.src/main/java/com/kustacks/kuring/club/application/port/in/dto/ClubSubscriptionCommand.java (1)
3-7: 입력 검증 추가 고려Command 객체에 기본적인 입력 검증을 추가하면 잘못된 데이터가 서비스 레이어까지 전달되는 것을 조기에 방지할 수 있습니다.
♻️ 검증 추가 제안
package com.kustacks.kuring.club.application.port.in.dto; +import org.springframework.util.Assert; + public record ClubSubscriptionCommand( String email, Long clubId ) { + public ClubSubscriptionCommand { + Assert.hasText(email, "email must not be empty"); + Assert.notNull(clubId, "clubId must not be null"); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/club/application/port/in/dto/ClubSubscriptionCommand.java` around lines 3 - 7, The ClubSubscriptionCommand record lacks input validation; add a compact canonical constructor in ClubSubscriptionCommand to validate its components (email and clubId): ensure email is non-null, non-blank and matches a simple email pattern, and ensure clubId is non-null and > 0, throwing IllegalArgumentException (or NullPointerException for null) with clear messages on failure so invalid data is rejected before reaching the service layer.src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserClubSubscriptionRequest.java (1)
5-7: 필드 이름을clubId로 명확하게 변경하는 것을 권장합니다.현재
id필드명은 모호합니다. 이 DTO가 동아리 구독 요청에 사용되므로clubId로 명명하면 API 문서화와 코드 가독성이 향상됩니다.♻️ 제안된 수정
public record UserClubSubscriptionRequest( - `@NotNull` Long id + `@NotNull` Long clubId ) { }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserClubSubscriptionRequest.java` around lines 5 - 7, Rename the ambiguous record field id to clubId in UserClubSubscriptionRequest and update all call sites, tests, mappers, controllers and API docs to use the new name; specifically change the record declaration in UserClubSubscriptionRequest (and any constructor/parameter references) to use clubId (keeping `@NotNull`) and update any JSON serialization/deserialization annotations or consumers (e.g., controllers, service methods, DTO mappers, request binding) so they expect "clubId" instead of "id" to maintain compatibility with API documentation and improve readability.src/main/java/com/kustacks/kuring/club/domain/ClubDivision.java (1)
36-56:ClubCategory와 동일한 패턴 - 공통 추상화 고려
ClubCategory와 동일한NAME_MAP패턴 및fromName()구현을 사용하고 있습니다. 현재 구조도 동작하지만, 향후 유사한 enum이 추가될 경우 공통 인터페이스나 유틸리티 메서드로 추출하는 것을 고려해볼 수 있습니다.
ClubCategory에서 제안한 것과 동일하게Map<String, ClubDivision>으로 직접 enum을 저장하면 코드가 간결해집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/club/domain/ClubDivision.java` around lines 36 - 56, The NAME_MAP currently maps names to enum name strings and fromName(String) does an extra lookup with ClubDivision.valueOf; change NAME_MAP to Map<String, ClubDivision> (store the enum instance) by building it with Arrays.stream(values()).collect(Collectors.toMap(ClubDivision::getName, Function.identity())) and make it unmodifiable, then simplify fromName(String) to return Optional.ofNullable(NAME_MAP.get(name)).orElseThrow(...); also consider extracting this pattern into a shared utility or interface (as suggested for ClubCategory) to avoid duplication across enums like ClubCategory and ClubDivision.src/main/java/com/kustacks/kuring/club/application/service/ClubNotificationService.java (2)
25-25:CLUB_TOPIC_PREFIX상수가 중복 정의되어 있습니다
ClubCommandService에도 동일한 상수(CLUB_TOPIC_PREFIX = "club.")가 정의되어 있습니다. 중복을 제거하고 공통 상수 클래스나 도메인 객체로 추출하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/club/application/service/ClubNotificationService.java` at line 25, CLUB_TOPIC_PREFIX is duplicated in ClubNotificationService and ClubCommandService; extract it into a single shared constant (e.g., a new ClubConstants or TopicConstants) and replace the local definitions with references to that shared constant. Specifically, remove the private static final String CLUB_TOPIC_PREFIX from ClubNotificationService and ClubCommandService, add the new shared constant, and update all usages in methods/classes that referenced CLUB_TOPIC_PREFIX to import/reference the new constant (ensuring visibility is public/static/final as needed). Ensure tests and imports are updated accordingly.
49-51:LocalDateTime.now()직접 호출은 테스트 용이성을 저하시킵니다
findDeadlineClubs()메서드 내에서LocalDateTime.now()를 직접 호출하면 단위 테스트에서 시간을 제어하기 어렵습니다.Clock을 주입받거나, 현재 시간을 파라미터로 받는 방식을 고려해주세요.♻️ Clock 주입 방식 제안
+import java.time.Clock; + `@Slf4j` `@UseCase` `@RequiredArgsConstructor` public class ClubNotificationService implements ClubNotificationUseCase { + private final Clock clock; private final ClubQueryPort clubQueryPort; // ... private List<Club> findDeadlineClubs() { - return clubQueryPort.findNextDayRecruitEndClubs(LocalDateTime.now()); + return clubQueryPort.findNextDayRecruitEndClubs(LocalDateTime.now(clock)); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/club/application/service/ClubNotificationService.java` around lines 49 - 51, The findDeadlineClubs() method calls LocalDateTime.now() directly which makes tests time-dependent; refactor to obtain current time from an injectable Clock or by passing the current LocalDateTime as a parameter. Specifically, change findDeadlineClubs() to use a Clock field (inject via constructor) or add a parameter (e.g., LocalDateTime now) and call clubQueryPort.findNextDayRecruitEndClubs(now) instead of LocalDateTime.now(); update callers and unit tests to supply a fixed Clock or fixed time when testing.src/main/java/com/kustacks/kuring/club/domain/Club.java (1)
59-61: 위도/경도 필드의 유효성 검증 고려
lat과lon필드가Double타입으로 선언되어 있습니다. 도메인 레벨에서 유효 범위(-9090, -180180) 검증이 필요한지 검토해주세요. 현재는 데이터 무결성이 외부(API 또는 DB 제약)에 의존합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/club/domain/Club.java` around lines 59 - 61, Add domain-level range validation for the Club class's lat and lon fields: implement checks for lat in [-90, 90] and lon in [-180, 180] inside the Club class (e.g., in the constructor that sets coordinates, in setLat and setLon methods, or in a `@PrePersist/`@PreUpdate validateCoordinates method) and throw a clear IllegalArgumentException (or a javax.validation.ConstraintViolation) when out of range; alternatively add javax.validation annotations (`@DecimalMin/`@DecimalMax or a custom `@Latitude/`@Longitude constraint) on the lat and lon fields to enforce the same ranges so validation happens at the domain level rather than relying on external API/DB constraints.src/main/java/com/kustacks/kuring/club/domain/ClubCategory.java (1)
22-42:NAME_MAP구조 간소화 가능현재
NAME_MAP이name -> enum.name()매핑 후valueOf()를 호출하는 2단계 조회를 수행합니다.Map<String, ClubCategory>로 직접 enum을 저장하면 코드가 더 간결해집니다.♻️ 간소화 제안
- private static final Map<String, String> NAME_MAP; + private static final Map<String, ClubCategory> NAME_MAP; static { NAME_MAP = Collections.unmodifiableMap(Arrays.stream(ClubCategory.values()) - .collect(Collectors.toMap(ClubCategory::getName, ClubCategory::name)) + .collect(Collectors.toMap(ClubCategory::getName, c -> c)) ); } // ... public static ClubCategory fromName(String name) { - String findName = Optional.ofNullable(NAME_MAP.get(name)) + return Optional.ofNullable(NAME_MAP.get(name)) .orElseThrow(() -> new NotFoundException(CLUB_CATEGORY_NOT_SUPPORTED)); - return ClubCategory.valueOf(findName); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/kustacks/kuring/club/domain/ClubCategory.java` around lines 22 - 42, Replace the current NAME_MAP and two-step lookup with a Map<String, ClubCategory> to store the enum directly: change NAME_MAP to Map<String, ClubCategory>, initialize it by mapping ClubCategory::getName to Function.identity() (or v -> v) inside the static block, and update fromName(String name) to retrieve the ClubCategory directly from NAME_MAP (using Optional.ofNullable(...).orElseThrow(...)) instead of fetching a String and calling ClubCategory.valueOf; this simplifies the lookup and removes the extra valueOf call while keeping NotFoundException(CLUB_CATEGORY_NOT_SUPPORTED) behavior.src/test/java/com/kustacks/kuring/club/application/service/ClubNotificationServiceTest.java (2)
75-75: Unchecked cast 경고에 대한 억제 어노테이션을 고려하세요.
ReflectionTestUtils.getField는 Object를 반환하므로Map<String, String>으로의 캐스팅 시 unchecked cast 경고가 발생합니다. 테스트 코드이므로 심각하지 않지만,@SuppressWarnings("unchecked")를 메소드 레벨에 추가하면 경고를 명시적으로 처리할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/kustacks/kuring/club/application/service/ClubNotificationServiceTest.java` at line 75, Add a `@SuppressWarnings`("unchecked") annotation at the test method level in ClubNotificationServiceTest where ReflectionTestUtils.getField is cast to Map<String, String> (the line assigning to variable data) to explicitly suppress the unchecked cast warning from casting the Object returned by ReflectionTestUtils.getField to Map<String, String>.
72-80: 두 번째 메시지(club2)에 대한 검증이 누락되었습니다.첫 번째 메시지(club.1.dev)만 검증하고 있습니다. 두 번째 메시지(club.2.dev)도 올바른 topic과 data를 가지는지 검증하면 테스트 커버리지가 더 완전해집니다.
♻️ 두 번째 메시지 검증 추가 제안
Map<String, String> data = (Map<String, String>) ReflectionTestUtils.getField(sent, "data"); + Message sent2 = captor.getAllValues().get(1); // 2번 ID에 대한 Club 메시지 + String topic2 = (String) ReflectionTestUtils.getField(sent2, "topic"); + Map<String, String> data2 = (Map<String, String>) ReflectionTestUtils.getField(sent2, "data"); assertAll( () -> assertThat(topic).isEqualTo("club.1.dev"), () -> assertThat(data).containsEntry("clubId", "1"), - () -> assertThat(data).containsEntry("messageType", "club") + () -> assertThat(data).containsEntry("messageType", "club"), + () -> assertThat(topic2).isEqualTo("club.2.dev"), + () -> assertThat(data2).containsEntry("clubId", "2"), + () -> assertThat(data2).containsEntry("messageType", "club") );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/kustacks/kuring/club/application/service/ClubNotificationServiceTest.java` around lines 72 - 80, The test only asserts the first captured Message; add assertions for the second captured Message by retrieving captor.getAllValues().get(1) (or equivalent) into a second variable (e.g., sent2), extract its "topic" and "data" with ReflectionTestUtils (like topic2 and data2) and assert topic2 equals "club.2.dev" and data2 contains entries "clubId" -> "2" and "messageType" -> "club" to fully verify both messages.src/test/java/com/kustacks/kuring/club/application/service/ClubCommandServiceTest.java (1)
165-181: root user not found 케이스에 대한 테스트 추가를 고려하세요.존재하지 않는 동아리(CLUB_NOT_FOUND)에 대한 테스트는 있지만, 존재하지 않는 root user(ROOT_USER_NOT_FOUND)에 대한 테스트가 없습니다. 서비스에서 이 케이스를 처리한다면 해당 테스트도 추가하는 것이 좋습니다.
♻️ 제안하는 테스트 케이스
`@DisplayName`("존재하지 않는 사용자는 동아리를 구독할 수 없다") `@Test` void add_subscription_fail_when_root_user_not_found() { //given when(rootUserQueryPort.findRootUserByEmail("[email protected]")) .thenReturn(Optional.empty()); //when & then assertAll( () -> assertThatThrownBy(() -> service.addSubscription(new ClubSubscriptionCommand("[email protected]", 1L))) .isInstanceOf(InvalidStateException.class) .extracting(ex -> ((InvalidStateException) ex).getErrorCode()) .isEqualTo(ErrorCode.ROOT_USER_NOT_FOUND), () -> verify(userEventPort, never()).subscribeEvent(anyString(), anyString()) ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/kustacks/kuring/club/application/service/ClubCommandServiceTest.java` around lines 165 - 181, Add a test that verifies addSubscription rejects requests when the root user is missing: mock rootUserQueryPort.findRootUserByEmail("[email protected]") to return Optional.empty(), call service.addSubscription(new ClubSubscriptionCommand("[email protected]", 1L)), assert that it throws InvalidStateException with ErrorCode.ROOT_USER_NOT_FOUND, and verify that userEventPort.subscribeEvent(...) is never invoked; place this alongside the existing add_subscription_fail_when_club_not_found test to cover the ROOT_USER_NOT_FOUND branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java`:
- Around line 46-49: The methods currently call external subscription operations
(subscribeAllLoggedInDevices / unsubscribeAllLoggedInDevices) inside the DB
transaction alongside clubSubscriptionCommandPort.saveSubscription /
deleteSubscription and countSubscriptionsQueryPort.countSubscriptions, which
risks data vs external-state inconsistency if the transaction rolls back; move
these external calls out of the transactional boundary by either publishing a
domain event or registering an after-commit callback (e.g.,
TransactionSynchronizationManager.registerSynchronization) from the
transactional methods (those invoking
clubSubscriptionCommandPort.saveSubscription and deleteSubscription and
countSubscriptionsQueryPort.countSubscriptions) and perform
subscribeAllLoggedInDevices/unsubscribeAllLoggedInDevices with makeTopic(...)
only in the after-commit handler so external subscriptions run only after
successful commit.
- Around line 42-47: The pre-check using isAlreadySubscription before calling
clubSubscriptionCommandPort.saveSubscription (and the similar delete flow)
causes race conditions; instead make save/delete operations rely on a DB
uniqueness/constraint and map database exceptions to domain errors: remove the
existence check in the subscribe path, call
clubSubscriptionCommandPort.saveSubscription(rootUser, club) inside a
transaction, catch the persistence exception (e.g.
DataIntegrityViolationException or the specific repository exception) and throw
InvalidStateException(ErrorCode.CLUB_ALREADY_SUBSCRIBED) on duplicate-key; for
unsubscribe, perform the delete via
clubSubscriptionCommandPort.deleteSubscription(...) and if the delete reports
zero rows affected throw InvalidStateException(ErrorCode.CLUB_NOT_SUBSCRIBED);
keep subscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club)) only after a
confirmed successful save and ensure transactional boundaries around these
operations.
In `@src/main/java/com/kustacks/kuring/club/domain/Club.java`:
- Around line 47-48: The Club entity's homepageUrls List<ClubSns> (field name:
homepageUrls in class Club) lacks cascade/orphan removal settings, so deleting a
Club can leave ClubSns rows with a non-null club_id and cause FK violations;
update the `@OneToMany` on homepageUrls to manage ClubSns lifecycle by adding
cascade = CascadeType.REMOVE (or CascadeType.ALL as appropriate) and
orphanRemoval = true so child ClubSns are deleted when their parent Club is
removed.
In `@src/main/resources/db/migration/V260206__Create_club_table.sql`:
- Around line 36-39: 중복 인덱스가 존재합니다: CONSTRAINT uk_club_user 생성 시 (club_id,
user_id) 복합 인덱스가 생성되어 왼쪽 접두사로 club_id 단독 조회를 지원하므로 기존의 INDEX
idx_club_subscribe_club는 불필요합니다; SQL 마이그레이션(V260206__Create_club_table.sql)에서
INDEX idx_club_subscribe_club 항목을 제거하고 INDEX idx_club_subscribe_user 및
CONSTRAINT uk_club_user는 그대로 유지해 중복 인덱스를 제거하세요.
In
`@src/main/resources/db/migration/V260217__Alter_club_subscribe_to_root_user.sql`:
- Around line 1-3: The migration V260217__Alter_club_subscribe_to_root_user.sql
currently contains a DELETE FROM club_subscribe which will irreversibly delete
production data; instead, remove the unconditional DELETE, and either add a safe
data-migration step that maps existing user_id to root_user_id (e.g., an INSERT
... SELECT or UPDATE using a reliable user→root_user mapping) or add a guard
that aborts/backs up when club_subscribe contains rows (check COUNT(*) and fail
with clear message), ensuring the migration only deletes rows in dev where
confirmed empty; locate and update the SQL in
V260217__Alter_club_subscribe_to_root_user.sql and any helper scripts that
perform the user→root_user mapping to implement the safe path.
---
Nitpick comments:
In
`@src/main/java/com/kustacks/kuring/club/application/port/in/dto/ClubSubscriptionCommand.java`:
- Around line 3-7: The ClubSubscriptionCommand record lacks input validation;
add a compact canonical constructor in ClubSubscriptionCommand to validate its
components (email and clubId): ensure email is non-null, non-blank and matches a
simple email pattern, and ensure clubId is non-null and > 0, throwing
IllegalArgumentException (or NullPointerException for null) with clear messages
on failure so invalid data is rejected before reaching the service layer.
In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubNotificationService.java`:
- Line 25: CLUB_TOPIC_PREFIX is duplicated in ClubNotificationService and
ClubCommandService; extract it into a single shared constant (e.g., a new
ClubConstants or TopicConstants) and replace the local definitions with
references to that shared constant. Specifically, remove the private static
final String CLUB_TOPIC_PREFIX from ClubNotificationService and
ClubCommandService, add the new shared constant, and update all usages in
methods/classes that referenced CLUB_TOPIC_PREFIX to import/reference the new
constant (ensuring visibility is public/static/final as needed). Ensure tests
and imports are updated accordingly.
- Around line 49-51: The findDeadlineClubs() method calls LocalDateTime.now()
directly which makes tests time-dependent; refactor to obtain current time from
an injectable Clock or by passing the current LocalDateTime as a parameter.
Specifically, change findDeadlineClubs() to use a Clock field (inject via
constructor) or add a parameter (e.g., LocalDateTime now) and call
clubQueryPort.findNextDayRecruitEndClubs(now) instead of LocalDateTime.now();
update callers and unit tests to supply a fixed Clock or fixed time when
testing.
In `@src/main/java/com/kustacks/kuring/club/domain/Club.java`:
- Around line 59-61: Add domain-level range validation for the Club class's lat
and lon fields: implement checks for lat in [-90, 90] and lon in [-180, 180]
inside the Club class (e.g., in the constructor that sets coordinates, in setLat
and setLon methods, or in a `@PrePersist/`@PreUpdate validateCoordinates method)
and throw a clear IllegalArgumentException (or a
javax.validation.ConstraintViolation) when out of range; alternatively add
javax.validation annotations (`@DecimalMin/`@DecimalMax or a custom
`@Latitude/`@Longitude constraint) on the lat and lon fields to enforce the same
ranges so validation happens at the domain level rather than relying on external
API/DB constraints.
In `@src/main/java/com/kustacks/kuring/club/domain/ClubCategory.java`:
- Around line 22-42: Replace the current NAME_MAP and two-step lookup with a
Map<String, ClubCategory> to store the enum directly: change NAME_MAP to
Map<String, ClubCategory>, initialize it by mapping ClubCategory::getName to
Function.identity() (or v -> v) inside the static block, and update
fromName(String name) to retrieve the ClubCategory directly from NAME_MAP (using
Optional.ofNullable(...).orElseThrow(...)) instead of fetching a String and
calling ClubCategory.valueOf; this simplifies the lookup and removes the extra
valueOf call while keeping NotFoundException(CLUB_CATEGORY_NOT_SUPPORTED)
behavior.
In `@src/main/java/com/kustacks/kuring/club/domain/ClubDivision.java`:
- Around line 36-56: The NAME_MAP currently maps names to enum name strings and
fromName(String) does an extra lookup with ClubDivision.valueOf; change NAME_MAP
to Map<String, ClubDivision> (store the enum instance) by building it with
Arrays.stream(values()).collect(Collectors.toMap(ClubDivision::getName,
Function.identity())) and make it unmodifiable, then simplify fromName(String)
to return Optional.ofNullable(NAME_MAP.get(name)).orElseThrow(...); also
consider extracting this pattern into a shared utility or interface (as
suggested for ClubCategory) to avoid duplication across enums like ClubCategory
and ClubDivision.
In
`@src/main/java/com/kustacks/kuring/message/application/port/out/dto/NoticeMessageDto.java`:
- Line 53: The current .id(String.valueOf(notice.getId())) call in
NoticeMessageDto will convert a null id to the literal "null" and bypass the
later Assert.notNull(id,...); fix by performing an explicit null check on
notice.getId() before converting: either assert/throw early with
Assert.notNull(notice.getId(), "...") (preferred if id must never be null) or
set id with a null-aware conversion like notice.getId() != null ?
String.valueOf(notice.getId()) : null so the subsequent Assert.notNull(id, ...)
behaves correctly; update the builder call in NoticeMessageDto accordingly.
In
`@src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserClubSubscriptionRequest.java`:
- Around line 5-7: Rename the ambiguous record field id to clubId in
UserClubSubscriptionRequest and update all call sites, tests, mappers,
controllers and API docs to use the new name; specifically change the record
declaration in UserClubSubscriptionRequest (and any constructor/parameter
references) to use clubId (keeping `@NotNull`) and update any JSON
serialization/deserialization annotations or consumers (e.g., controllers,
service methods, DTO mappers, request binding) so they expect "clubId" instead
of "id" to maintain compatibility with API documentation and improve
readability.
In `@src/test/java/com/kustacks/kuring/archunit/DependencyRuleTests.java`:
- Around line 176-178: The test method validateClubDomainDependencies() and its
`@DisplayName` currently break the project's naming pattern; rename the method to
validateClubDomainArchitecture() and update the `@DisplayName` to match the
convention (e.g., "Club 아키텍처 검증") so it follows the existing
validateXxxArchitecture / "Xxx 아키텍처 검증" pattern, and update any
references/imports/usages accordingly (search for validateClubDomainDependencies
and the old display name) to keep tests compiling and consistent.
In
`@src/test/java/com/kustacks/kuring/club/application/service/ClubCommandServiceTest.java`:
- Around line 165-181: Add a test that verifies addSubscription rejects requests
when the root user is missing: mock
rootUserQueryPort.findRootUserByEmail("[email protected]") to return
Optional.empty(), call service.addSubscription(new
ClubSubscriptionCommand("[email protected]", 1L)), assert that it throws
InvalidStateException with ErrorCode.ROOT_USER_NOT_FOUND, and verify that
userEventPort.subscribeEvent(...) is never invoked; place this alongside the
existing add_subscription_fail_when_club_not_found test to cover the
ROOT_USER_NOT_FOUND branch.
In
`@src/test/java/com/kustacks/kuring/club/application/service/ClubNotificationServiceTest.java`:
- Line 75: Add a `@SuppressWarnings`("unchecked") annotation at the test method
level in ClubNotificationServiceTest where ReflectionTestUtils.getField is cast
to Map<String, String> (the line assigning to variable data) to explicitly
suppress the unchecked cast warning from casting the Object returned by
ReflectionTestUtils.getField to Map<String, String>.
- Around line 72-80: The test only asserts the first captured Message; add
assertions for the second captured Message by retrieving
captor.getAllValues().get(1) (or equivalent) into a second variable (e.g.,
sent2), extract its "topic" and "data" with ReflectionTestUtils (like topic2 and
data2) and assert topic2 equals "club.2.dev" and data2 contains entries "clubId"
-> "2" and "messageType" -> "club" to fully verify both messages.
In `@src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java`:
- Around line 134-143: Replace the hardcoded enum string literals in initClub()
with the corresponding enum .name() values to improve type-safety and
maintainability: locate the jdbcTemplate.update call inside initClub() and
change the category and division arguments from "ACADEMIC" and "CENTRAL" to the
enum references (e.g., ClubCategory.ACADEMIC.name() and
ClubDivision.CENTRAL.name()) so the persisted strings come directly from the
enum constants.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (40)
src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubPersistenceAdapter.javasrc/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubQueryRepository.javasrc/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubQueryRepositoryImpl.javasrc/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubRepository.javasrc/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubSubscribeRepository.javasrc/main/java/com/kustacks/kuring/club/application/port/in/ClubNotificationUseCase.javasrc/main/java/com/kustacks/kuring/club/application/port/in/ClubSubscriptionUseCase.javasrc/main/java/com/kustacks/kuring/club/application/port/in/dto/ClubSubscriptionCommand.javasrc/main/java/com/kustacks/kuring/club/application/port/out/ClubQueryPort.javasrc/main/java/com/kustacks/kuring/club/application/port/out/ClubSubscriptionCommandPort.javasrc/main/java/com/kustacks/kuring/club/application/port/out/ClubSubscriptionQueryPort.javasrc/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.javasrc/main/java/com/kustacks/kuring/club/application/service/ClubNotificationService.javasrc/main/java/com/kustacks/kuring/club/domain/Club.javasrc/main/java/com/kustacks/kuring/club/domain/ClubCategory.javasrc/main/java/com/kustacks/kuring/club/domain/ClubDivision.javasrc/main/java/com/kustacks/kuring/club/domain/ClubSns.javasrc/main/java/com/kustacks/kuring/club/domain/ClubSnsType.javasrc/main/java/com/kustacks/kuring/club/domain/ClubSubscribe.javasrc/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.javasrc/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.javasrc/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.javasrc/main/java/com/kustacks/kuring/message/application/port/out/dto/NoticeMessageDto.javasrc/main/java/com/kustacks/kuring/message/domain/MessageType.javasrc/main/java/com/kustacks/kuring/user/adapter/in/web/UserClubSubscriptionApiV2.javasrc/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserClubSubscriptionCountResponse.javasrc/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserClubSubscriptionRequest.javasrc/main/java/com/kustacks/kuring/worker/notification/ClubNotificationScheduler.javasrc/main/resources/db/migration/V260206__Create_club_table.sqlsrc/main/resources/db/migration/V260217__Alter_club_subscribe_to_root_user.sqlsrc/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.javasrc/test/java/com/kustacks/kuring/acceptance/UserStep.javasrc/test/java/com/kustacks/kuring/archunit/DependencyRuleTests.javasrc/test/java/com/kustacks/kuring/club/adapter/out/persistence/ClubPersistenceAdapterTest.javasrc/test/java/com/kustacks/kuring/club/application/service/ClubCommandServiceTest.javasrc/test/java/com/kustacks/kuring/club/application/service/ClubNotificationServiceTest.javasrc/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.javasrc/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.javasrc/test/java/com/kustacks/kuring/support/DatabaseConfigurator.javasrc/test/java/com/kustacks/kuring/worker/notification/ClubNotificationSchedulerTest.java
| if (isAlreadySubscription(rootUser, club)) { | ||
| throw new InvalidStateException(ErrorCode.CLUB_ALREADY_SUBSCRIBED); | ||
| } | ||
|
|
||
| clubSubscriptionCommandPort.saveSubscription(rootUser, club); | ||
| subscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club)); |
There was a problem hiding this comment.
구독 상태 체크 후 저장/삭제하는 패턴이 동시성 경쟁 상태를 만듭니다.
exists와 save/delete가 원자적으로 보장되지 않아 동시 요청에서 중복 저장 또는 의도와 다른 성공/실패 결과가 발생할 수 있습니다. 저장/삭제는 DB 제약 기반으로 처리하고 충돌 예외를 도메인 예외로 매핑하는 방식이 필요합니다.
Also applies to: 57-61
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java`
around lines 42 - 47, The pre-check using isAlreadySubscription before calling
clubSubscriptionCommandPort.saveSubscription (and the similar delete flow)
causes race conditions; instead make save/delete operations rely on a DB
uniqueness/constraint and map database exceptions to domain errors: remove the
existence check in the subscribe path, call
clubSubscriptionCommandPort.saveSubscription(rootUser, club) inside a
transaction, catch the persistence exception (e.g.
DataIntegrityViolationException or the specific repository exception) and throw
InvalidStateException(ErrorCode.CLUB_ALREADY_SUBSCRIBED) on duplicate-key; for
unsubscribe, perform the delete via
clubSubscriptionCommandPort.deleteSubscription(...) and if the delete reports
zero rows affected throw InvalidStateException(ErrorCode.CLUB_NOT_SUBSCRIBED);
keep subscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club)) only after a
confirmed successful save and ensure transactional boundaries around these
operations.
There was a problem hiding this comment.
이 부분은 낙관적으로 처리하는거에 대한 고민을 해봐야겠네
이 부분을 요약해서 이슈로 만들어줘
There was a problem hiding this comment.
🧩 Analysis chain
@rlagkswn00, 동시성 이슈를 정리해서 GitHub 이슈로 생성하겠습니다.
🏁 Script executed:
#!/bin/bash
# Create GitHub issue for the concurrency race condition problem
gh issue create \
--title "동아리 구독 기능의 동시성 경쟁 상태 개선" \
--body "## 문제 설명
동아리 구독/구독 취소 기능에서 구독 상태를 확인한 후 저장/삭제하는 패턴이 동시성 경쟁 상태(race condition)를 발생시킬 수 있습니다.
## 발생 위치
- 파일: \`src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java\`
- 메서드: \`addSubscription()\` 및 \`removeSubscription()\`
- 라인: 42-47, 57-61
## 현재 문제점
\`existsSubscription()\` 체크와 \`save()/delete()\` 작업이 원자적으로 보장되지 않아 다음과 같은 문제가 발생할 수 있습니다:
1. **구독 추가 시**: 동시에 여러 요청이 들어올 경우, 모두 존재하지 않는다고 판단하여 중복 저장이 시도될 수 있습니다.
2. **구독 취소 시**: 동시에 여러 삭제 요청이 들어올 경우, 모두 존재한다고 판단하여 의도와 다른 결과가 발생할 수 있습니다.
## 개선 방향 고려사항
낙관적 처리(optimistic) 방식을 포함한 다양한 동시성 제어 방안에 대한 검토가 필요합니다.
## 참고
- PR: https://github.com/ku-ring/ku-ring-backend-web/pull/349
- 코멘트: https://github.com/ku-ring/ku-ring-backend-web/pull/349#discussion_r2856267052
- 보고자: `@rlagkswn00`" \
--assignee rlagkswn00
Length of output: 57
@rlagkswn00, 동시성 경쟁 상태 이슈를 생성했습니다!
이슈 #351: 동아리 구독 기능의 동시성 경쟁 상태 개선
낙관적 처리 방식을 포함한 다양한 동시성 제어 방안을 검토하실 때 참고하시면 됩니다.
| clubSubscriptionCommandPort.saveSubscription(rootUser, club); | ||
| subscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club)); | ||
|
|
||
| return countSubscriptionsQueryPort.countSubscriptions(rootUser.getId()); |
There was a problem hiding this comment.
외부 토픽 구독/해제를 트랜잭션 내부에서 호출하면 데이터-외부상태 불일치가 발생할 수 있습니다.
DB 트랜잭션이 롤백되더라도 외부 구독 상태는 이미 변경될 수 있어 정합성이 깨질 수 있습니다. 외부 호출은 커밋 이후로 분리하는 게 안전합니다.
🔧 제안 수정안
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
@@
- clubSubscriptionCommandPort.saveSubscription(rootUser, club);
- subscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club));
+ clubSubscriptionCommandPort.saveSubscription(rootUser, club);
+ runAfterCommit(() -> subscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club)));
@@
- clubSubscriptionCommandPort.deleteSubscription(rootUser, club);
- unsubscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club));
+ clubSubscriptionCommandPort.deleteSubscription(rootUser, club);
+ runAfterCommit(() -> unsubscribeAllLoggedInDevices(rootUser.getId(), makeTopic(club)));
@@
+ private void runAfterCommit(Runnable action) {
+ if (!TransactionSynchronizationManager.isActualTransactionActive()) {
+ action.run();
+ return;
+ }
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+ `@Override`
+ public void afterCommit() {
+ action.run();
+ }
+ });
+ }Also applies to: 60-63, 80-92
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java`
around lines 46 - 49, The methods currently call external subscription
operations (subscribeAllLoggedInDevices / unsubscribeAllLoggedInDevices) inside
the DB transaction alongside clubSubscriptionCommandPort.saveSubscription /
deleteSubscription and countSubscriptionsQueryPort.countSubscriptions, which
risks data vs external-state inconsistency if the transaction rolls back; move
these external calls out of the transactional boundary by either publishing a
domain event or registering an after-commit callback (e.g.,
TransactionSynchronizationManager.registerSynchronization) from the
transactional methods (those invoking
clubSubscriptionCommandPort.saveSubscription and deleteSubscription and
countSubscriptionsQueryPort.countSubscriptions) and perform
subscribeAllLoggedInDevices/unsubscribeAllLoggedInDevices with makeTopic(...)
only in the after-commit handler so external subscriptions run only after
successful commit.
| @OneToMany(mappedBy = "club") | ||
| private List<ClubSns> homepageUrls = new ArrayList<>(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the ClubSns entity
find . -name "*.java" -type f | xargs grep -l "class ClubSns" | head -5Repository: ku-ring/ku-ring-backend-web
Length of output: 131
🏁 Script executed:
# Check for ClubSnsRepository
find . -name "*.java" -type f | xargs grep -l "ClubSnsRepository" | head -10Repository: ku-ring/ku-ring-backend-web
Length of output: 53
🏁 Script executed:
# Search for how ClubSns is saved/deleted
rg -n "clubSnsList|homepageUrls" -t java --context 3Repository: ku-ring/ku-ring-backend-web
Length of output: 704
🏁 Script executed:
# Check the Club entity relationships
rg -n "class Club|@OneToMany|@ManyToOne" src/main/java/com/kustacks/kuring/club/domain/Club.javaRepository: ku-ring/ku-ring-backend-web
Length of output: 130
🏁 Script executed:
cat -n ./src/main/java/com/kustacks/kuring/club/domain/ClubSns.javaRepository: ku-ring/ku-ring-backend-web
Length of output: 1355
🏁 Script executed:
# Search for ClubSnsRepository more broadly
rg "ClubSns" -t java --type-list | head -20
fd -e java -type f | xargs grep -l "ClubSns" | grep -i repositoryRepository: ku-ring/ku-ring-backend-web
Length of output: 631
🏁 Script executed:
# Check service layer for how ClubSns is managed
fd -e java -path "*/service/*" -type f | xargs grep -l "ClubSns" 2>/dev/nullRepository: ku-ring/ku-ring-backend-web
Length of output: 241
🏁 Script executed:
# Look for any usage patterns of homepageUrls or ClubSns operations
rg "homepageUrls|new ClubSns|\.setClubSns|\.addClubSns" -t java --context 2Repository: ku-ring/ku-ring-backend-web
Length of output: 500
🏁 Script executed:
# Find ClubRepository and Club service
find . -name "*ClubRepository.java" -o -name "*ClubService.java" | head -10Repository: ku-ring/ku-ring-backend-web
Length of output: 155
🏁 Script executed:
# Check for repository/service files
fd -e java -type f | xargs grep -l "repository\|service" | grep -i club | head -20Repository: ku-ring/ku-ring-backend-web
Length of output: 241
🏁 Script executed:
# Search for delete operations on Club entity
rg "delete|remove" -t java --context 2 | grep -i club | head -30Repository: ku-ring/ku-ring-backend-web
Length of output: 3978
🏁 Script executed:
# Check for any cascade type definitions in Club.java
cat src/main/java/com/kustacks/kuring/club/domain/Club.java | head -80Repository: ku-ring/ku-ring-backend-web
Length of output: 1965
🏁 Script executed:
# Check ClubRepository for delete operations
cat ./src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubRepository.javaRepository: ku-ring/ku-ring-backend-web
Length of output: 321
🏁 Script executed:
# Search for ClubSnsRepository
find . -name "*ClubSnsRepository.java" -o -name "*ClubSns*Repository.java"Repository: ku-ring/ku-ring-backend-web
Length of output: 53
🏁 Script executed:
# Check service layer for Club deletion logic
find . -name "ClubCommandService.java" -o -name "ClubService.java" | xargs cat 2>/dev/null | head -100Repository: ku-ring/ku-ring-backend-web
Length of output: 4277
🏁 Script executed:
# Search for any explicit ClubSns deletion before Club deletion
rg "delete.*ClubSns|remove.*ClubSns|ClubSns.*delete" -A 3 -B 3Repository: ku-ring/ku-ring-backend-web
Length of output: 53
🏁 Script executed:
# Comprehensive search for Club deletion operations
rg "\.delete|\.remove|repository\.delete" -A 2 -B 2 | grep -i "club\|homepageurl" | head -50Repository: ku-ring/ku-ring-backend-web
Length of output: 3625
🏁 Script executed:
# Check if there are any other service classes handling Club lifecycle
find . -name "*.java" -path "*/service/*" -o -name "*.java" -path "*/adapter/*" | xargs grep -l "Club" | grep -i "command\|delete\|remove" | head -10Repository: ku-ring/ku-ring-backend-web
Length of output: 244
🏁 Script executed:
# Final check: look for all usages of homepageUrls in the codebase
rg "homepageUrls|ClubSns" -B 3 -A 3Repository: ku-ring/ku-ring-backend-web
Length of output: 2336
@OneToMany 관계에 CascadeType 설정 필요 검토
homepageUrls 필드의 club_id 외래키는 NOT NULL 제약이 있습니다. Club 엔티티 삭제 시 적절한 cascade 옵션이 없으면 ClubSns 레코드가 고아 상태가 되어 외래키 제약 조건 위반이 발생합니다. Club이 ClubSns의 생명주기를 관리한다면 @OneToMany(mappedBy = "club", cascade = CascadeType.REMOVE, orphanRemoval = true) 추가를 고려하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/kustacks/kuring/club/domain/Club.java` around lines 47 -
48, The Club entity's homepageUrls List<ClubSns> (field name: homepageUrls in
class Club) lacks cascade/orphan removal settings, so deleting a Club can leave
ClubSns rows with a non-null club_id and cause FK violations; update the
`@OneToMany` on homepageUrls to manage ClubSns lifecycle by adding cascade =
CascadeType.REMOVE (or CascadeType.ALL as appropriate) and orphanRemoval = true
so child ClubSns are deleted when their parent Club is removed.
| -- root_user로 바꾸면 데이터 이관을 해야하나, 아직 데이터가 존재하지 않는 개발 단계이므로 데이터 삭제하고 진행. | ||
| -- 1) 기존 device(user) 기준 구독 데이터는 유지하지 않는다. | ||
| DELETE FROM club_subscribe; |
There was a problem hiding this comment.
프로덕션 환경에서 데이터 손실 위험이 있습니다.
주석에서 개발 단계이므로 데이터 삭제를 진행한다고 설명하고 있지만, Flyway 마이그레이션은 한 번 적용되면 프로덕션 환경에서도 동일하게 실행됩니다. 만약 프로덕션에 이미 club_subscribe 데이터가 존재한다면 모든 구독 데이터가 영구 삭제됩니다.
다음 사항을 확인해 주세요:
- 프로덕션 환경에 이 마이그레이션이 적용되기 전에 club_subscribe 테이블에 데이터가 없음을 확인
- 또는 데이터 이관 로직 추가 고려 (예: user_id → root_user_id 매핑이 가능한 경우)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/resources/db/migration/V260217__Alter_club_subscribe_to_root_user.sql`
around lines 1 - 3, The migration V260217__Alter_club_subscribe_to_root_user.sql
currently contains a DELETE FROM club_subscribe which will irreversibly delete
production data; instead, remove the unconditional DELETE, and either add a safe
data-migration step that maps existing user_id to root_user_id (e.g., an INSERT
... SELECT or UPDATE using a reliable user→root_user mapping) or add a guard
that aborts/backs up when club_subscribe contains rows (check COUNT(*) and fail
with clear message), ensuring the migration only deletes rows in dev where
confirmed empty; locate and update the SQL in
V260217__Alter_club_subscribe_to_root_user.sql and any helper scripts that
perform the user→root_user mapping to implement the safe path.
Summary by CodeRabbit
Release Notes
New Features
Database