Skip to content

v20260619.0#91

Merged
hongjm0912 merged 8 commits into
masterfrom
develop
Jun 19, 2026
Merged

v20260619.0#91
hongjm0912 merged 8 commits into
masterfrom
develop

Conversation

@hongjm0912

Copy link
Copy Markdown
Collaborator

#89 ~ #90 까지의 내용을 master 브랜치에 머지합니다.

@hongjm0912 hongjm0912 requested a review from ZaMan0806 June 19, 2026 04:33
@hongjm0912 hongjm0912 self-assigned this Jun 19, 2026
@hongjm0912 hongjm0912 merged commit 6cde445 into master Jun 19, 2026
1 check passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces infrastructure changes to ignore EC2 AMI and root block device updates, adds a promotion mechanism for reserved applications when an application is canceled or deleted, and restricts application registration based on dates for regular users. The review feedback highlights a critical concurrency issue (race condition) in both CancelApplicationService and DeleteApplicationService where deleting or canceling an application and promoting a reserve applicant does not acquire a pessimistic lock on the associated Activity, potentially leading to inconsistent states when concurrent applications or cancellations occur.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +19 to 28
ApplicationJpaEntity application = applicationRepository.findByActivity_IdAndUser_Id(activityId, userId)
.orElseThrow(() -> new ExpectedException("신청 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND));

boolean wasConfirmed = !application.isReserve();
applicationRepository.delete(application);

if (wasConfirmed) {
applicationRepository.findFirstByActivity_IdAndIsReserveTrueOrderByCreatedAtAscIdAsc(activityId)
.ifPresent(ApplicationJpaEntity::promote);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

신청 취소(CancelApplicationService)와 신청(ApplyActivityService)이 동시에 발생할 때 동시성 이슈(Race Condition)가 발생할 수 있습니다. ApplyActivityService에서는 activityRepository.findByIdWithLock(activityId)를 통해 비관적 락(Pessimistic Lock)을 획득하여 동시 신청을 제어하고 있지만, CancelApplicationService에서는 Activity에 대한 락을 획득하지 않고 삭제 및 대기자 승격(Promote) 로직을 수행합니다.

이로 인해 동시에 여러 취소/신청이 일어날 때 isReserve 여부 판단 및 승격 로직이 꼬여 확정 인원이 정원을 초과하거나 미달될 수 있습니다.

따라서 CancelApplicationService에서도 로직 시작 시점에 activityRepository.findByIdWithLock(activityId)를 호출하여 동일한 Activity에 대해 락을 획득한 후 진행해야 안전합니다. 이를 위해 ActivityRepository 의존성을 주입받아야 합니다.

        activityRepository.findByIdWithLock(activityId)
                .orElseThrow(() -> new ExpectedException("활동을 찾을 수 없습니다.", HttpStatus.NOT_FOUND));

        ApplicationJpaEntity application = applicationRepository.findByActivity_IdAndUser_Id(activityId, userId)
                .orElseThrow(() -> new ExpectedException("신청 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND));

        boolean wasConfirmed = !application.isReserve();
        applicationRepository.delete(application);

        if (wasConfirmed) {
            applicationRepository.findFirstByActivity_IdAndIsReserveTrueOrderByCreatedAtAscIdAsc(activityId)
                    .ifPresent(ApplicationJpaEntity::promote);
        }

Comment on lines +19 to 29
ApplicationJpaEntity application = applicationRepository.findById(applicationId)
.orElseThrow(() -> new ExpectedException("신청 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND));

boolean wasConfirmed = !application.isReserve();
Long activityId = application.getActivity().getId();
applicationRepository.delete(application);

if (wasConfirmed) {
applicationRepository.findFirstByActivity_IdAndIsReserveTrueOrderByCreatedAtAscIdAsc(activityId)
.ifPresent(ApplicationJpaEntity::promote);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

신청 내역 삭제(DeleteApplicationService)와 신청(ApplyActivityService)이 동시에 발생할 때 동시성 이슈(Race Condition)가 발생할 수 있습니다. ApplyActivityService에서는 activityRepository.findByIdWithLock(activityId)를 통해 비관적 락(Pessimistic Lock)을 획득하여 동시 신청을 제어하고 있지만, DeleteApplicationService에서는 Activity에 대한 락을 획득하지 않고 삭제 및 대기자 승격(Promote) 로직을 수행합니다.

따라서 DeleteApplicationService에서도 application 조회 후 해당 activityIdactivityRepository.findByIdWithLock(activityId)를 호출하여 동일한 Activity에 대해 락을 획득한 후 진행해야 안전합니다. 이를 위해 ActivityRepository 의존성을 주입받아야 합니다.

        ApplicationJpaEntity application = applicationRepository.findById(applicationId)
                .orElseThrow(() -> new ExpectedException("신청 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND));

        Long activityId = application.getActivity().getId();
        activityRepository.findByIdWithLock(activityId)
                .orElseThrow(() -> new ExpectedException("활동을 찾을 수 없습니다.", HttpStatus.NOT_FOUND));

        boolean wasConfirmed = !application.isReserve();
        applicationRepository.delete(application);

        if (wasConfirmed) {
            applicationRepository.findFirstByActivity_IdAndIsReserveTrueOrderByCreatedAtAscIdAsc(activityId)
                    .ifPresent(ApplicationJpaEntity::promote);
        }

@hongjm0912 hongjm0912 changed the title 20260619.0 v20260619.0 Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants