Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough이 PR은 기존의 resolver 기반 권한 검사 구현을 제거하고 Actor 기반으로 전환하는 대규모 구조 변경입니다. 구체적으로 FestivalIdsResolver, OrganizationIdResolver, PlaceIdsResolver 인터페이스와 다수의 구현체(Announcement*, Event*, Place*, Lineup*, 등)를 삭제했고 AuthorizationService 클래스와 관련 단위 테스트(다수)를 삭제했습니다. 대신 Actor 인터페이스와 ActorArgumentResolver를 추가하고 AccountDetails 및 구체 구현들(AdminDetails, OrganizerDetails, StaffDetails, PlaceAccessDetails)의 권한 관련 메서드 시그니처를 변경했으며, BaseEntity의 softDelete/restore/isActive 접근자를 public→protected로 낮췄습니다. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 |
Summary of ChangesHello @soeun2537, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 애플리케이션의 권한 시스템을 완전히 재구성합니다. 기존의 복잡한 권한 서비스와 여러 리졸버 인터페이스를 제거하고, Actor라는 새로운 추상화를 도입하여 사용자 역할을 더 유연하게 관리할 수 있도록 합니다. 이 변경으로 인해 인증된 사용자의 권한을 컨트롤러에서 직접 Actor 객체로 받아 처리할 수 있게 되어, 권한 로직의 가독성과 유지보수성이 크게 향상될 것입니다. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
권한 관리 로직을 Actor 기반의 새로운 모델로 리팩터링하는 좋은 시도입니다. 복잡했던 AuthorizationService를 제거하고 역할과 책임이 명확한 Actor를 도입하여 코드의 구조가 훨씬 깔끔해지고 유지보수성이 향상되었습니다. 특히 ActorArgumentResolver를 통해 컨트롤러에서 Actor를 직접 주입받아 사용하는 방식은 매우 인상적입니다.
다만, 이 과정에서 몇 가지 치명적인 오류가 확인되었습니다.
JpaActorFactory에서 반환하는 도메인 엔티티들이Actor인터페이스를 구현하지 않아 발생하는 컴파일 오류Admin액터의 권한이 잘못 구현되어 대부분의 기능에 접근할 수 없는 버그@EntityGraph의 속성 경로가 잘못 지정된 문제
또한, 대규모 리팩터링임에도 불구하고 관련 테스트 코드가 추가되거나 수정되지 않고 오히려 삭제된 점이 매우 아쉽습니다. 새로운 Actor 기반 권한 모델의 안정성을 보장하기 위해 단위 테스트와 통합 테스트를 반드시 보강해주시길 바랍니다.
자세한 내용은 각 코드 라인에 남긴 코멘트를 참고해주세요.
src/main/java/com/daedan/festabook/global/actor/JpaActorFactory.java
Outdated
Show resolved
Hide resolved
src/main/java/com/daedan/festabook/organization/domain/Organization.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java (1)
35-38: 인증 실패 시ForbiddenExceptionvsUnauthorizedException고려현재
authentication == null일 때ForbiddenException(403)을 던지고 있습니다.HTTP 의미론상:
- 401 Unauthorized: 인증되지 않은 경우 (로그인 필요)
- 403 Forbidden: 인증은 되었으나 권한이 없는 경우
인증 자체가 없는 상황이므로
UnauthorizedException(401)이 더 적절할 수 있습니다. 다만 프로젝트의 기존 예외 처리 전략에 따라 판단해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java` around lines 35 - 38, In ActorArgumentResolver, when SecurityContextHolder.getContext().getAuthentication() returns null you should throw UnauthorizedException (401) instead of ForbiddenException (403); update the null-check branch to throw the project's UnauthorizedException (or map to its equivalent) while leaving subsequent authorization checks unchanged so authenticated-but-forbidden cases still throw ForbiddenException.src/main/java/com/daedan/festabook/global/domain/Actor.java (1)
3-3: [Architecture] Sealed Interface 도입 고려Java 17+를 사용 중이라면
sealed interface를 고려해볼 수 있습니다:public sealed interface Actor permits Admin, Organizer, Staff, PlaceAccess {장점:
- Actor 구현체를 명시적으로 제한하여 예상치 못한 구현 방지
- 패턴 매칭 시 exhaustiveness 체크 가능
단점:
- 테스트에서 Mock Actor 생성이 어려워질 수 있음
- 모듈 경계를 넘어 확장이 필요한 경우 제약이 됨
현재 구조가 충분히 명확하므로 필수는 아니지만, 권한 모델의 엄격한 타입 제어가 필요하다면 검토해보세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/domain/Actor.java` at line 3, Consider converting the Actor interface to a sealed interface to restrict implementations: change the declaration of Actor to a sealed interface and add a permits clause listing the concrete implementors (e.g., Admin, Organizer, Staff, PlaceAccess) so the compiler enforces the allowed subclasses; if tests need mocks, either mark allowed test-friendly subclasses as non-sealed or adjust test setup to use concrete permitted types or Mockito inline to create mocks.src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java (1)
36-46: Accessor 스타일 일관성 검토
PlaceAccessDetails는@Getter어노테이션을 사용하고,StaffDetails는 수동으로 getter를 작성했습니다.이 차이가 의도적인 것인지(예:
festivalIds를 노출하지 않기 위해) 확인해 주세요. 의도적이라면 현재 방식이 적절하고, 그렇지 않다면@Getter사용으로 통일하는 것도 고려해볼 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java` around lines 36 - 46, StaffDetails currently defines manual getters (getUsername, getStaffId, getOrganizationId) while PlaceAccessDetails uses Lombok's `@Getter`; decide which approach to standardize: if the goal is to expose all fields similarly, replace the manual getters in class StaffDetails by adding `@Getter` to the class and remove the explicit getUsername/getStaffId/getOrganizationId methods; if the manual approach was intentional to restrict access to festivalIds, add a clarifying comment above StaffDetails explaining why those getters are omitted (or explicitly keep only the intended getters) so the difference from PlaceAccessDetails is documented; reference StaffDetails, getUsername, getStaffId, getOrganizationId, PlaceAccessDetails, and festivalIds when making the change.src/main/java/com/daedan/festabook/global/actor/JpaActorFactory.java (1)
31-47: [Architecture] 매 요청마다 DB 조회가 발생하는 구조에 대한 고려
ActorArgumentResolver가 컨트롤러 메서드 호출 시마다ActorResolver→JpaActorFactory를 통해 DB 조회를 수행합니다. 현재 구조에서는 다음과 같은 트레이드오프가 있습니다:현재 방식의 장점:
- 항상 최신 데이터(권한, 만료 시간 등)를 보장
- 구현이 단순하고 명확함
잠재적 고려사항:
- 요청당 1회의 추가 DB 쿼리 발생
- 고트래픽 환경에서 DB 부하 증가 가능성
선택지:
옵션 A (현재 유지): 권한 정보의 실시간 정확성이 중요하다면 현재 방식 유지. 대부분의 애플리케이션에서 허용 가능한 수준입니다.
옵션 B (Request-Scoped 캐싱): 동일 요청 내에서 중복 조회 방지가 필요하다면
RequestScope빈이나ThreadLocal활용 고려. 단, 복잡도가 증가합니다.현재 트래픽 수준과 권한 변경 빈도를 고려하여 판단해 주세요.
🤖 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/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java`:
- Around line 23-26: The supportsParameter logic is inverted: replace the
current check in ActorArgumentResolver.supportsParameter (which uses
parameter.getParameterType().isAssignableFrom(Actor.class)) with
Actor.class.isAssignableFrom(parameter.getParameterType()) so the resolver
returns true only when the controller parameter type is Actor or a subtype
(e.g., Admin, Organizer, Staff, PlaceAccess).
In `@src/main/java/com/daedan/festabook/global/domain/BaseEntity.java`:
- Around line 34-45: 현재 BaseEntity의 protected 메서드(softDelete, restore,
isActive)를 엔티티들이 직접 호출하는 구조는 도메인 의도 표현 기회를 놓치고 있으므로, 각 도메인 엔티티(Festival, Place
등)에 도메인어를 반영한 공개 메서드(예: markAsDeleted(), recover(), isActiveForBusiness() 등)를
추가하고, 해당 공개 메서드 내부에서 BaseEntity의 protected 메서드(softDelete(), restore(),
isActive())를 호출하도록 변경하세요; 새로운 엔티티부터 적용하고 기존 엔티티는 점진적으로 리팩토링하도록 백로그에 등록하세요.
In `@src/main/java/com/daedan/festabook/organization/domain/Organization.java`:
- Around line 41-49: In Organization.validateManagedBy, avoid tight
role-coupling and NPEs by first null-checking the actor and failing closed
(throw ForbiddenException if actor is null), then base authorization on
capabilities rather than specific roles: return when actor.isAdmin() ||
actor.hasOrganization(id); otherwise throw new ForbiddenException(); update the
method so it no longer relies on actor.isOrganizer() specifically, using
actor.hasOrganization(id) as the capability check (keep ForbiddenException as
the failure path).
In
`@src/main/java/com/daedan/festabook/organizer/infrastructure/OrganizerJpaRepository.java`:
- Around line 16-17: The annotations on OrganizerJpaRepository are out of the
team's prescribed order: swap the two so the shorter `@Query` appears above
`@EntityGraph`; locate the repository method or field annotated with
`@EntityGraph`(attributePaths = "organization") and `@Query`("SELECT o FROM
Organizer o WHERE o.id = :id") and reorder them to `@Query` then `@EntityGraph` to
follow the "shorter-first" annotation pyramid rule, or alternatively document an
explicit rule exception if you choose to keep the current order.
In
`@src/main/java/com/daedan/festabook/staff/infrastructure/StaffJpaRepository.java`:
- Around line 21-22: 현재 StaffJpaRepository의 메서드에 적용된 어노테이션 순서가 팀 컨벤션과 다릅니다:
`@EntityGraph`(attributePaths = "staffAuthorities")가 `@Query보다` 위에 위치해 있는데, 피라미드 구조
규칙에 따라 짧은 어노테이션(`@Query`)을 위로 옮겨야 합니다; StaffJpaRepository에서 해당 메서드의 어노테이션 순서를 바꿔
`@Query`("SELECT s FROM Staff s WHERE s.id = :id")를 먼저 두고 그 아래에
`@EntityGraph`(attributePaths = "staffAuthorities")가 오도록 재정렬하세요.
---
Nitpick comments:
In
`@src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java`:
- Around line 35-38: In ActorArgumentResolver, when
SecurityContextHolder.getContext().getAuthentication() returns null you should
throw UnauthorizedException (401) instead of ForbiddenException (403); update
the null-check branch to throw the project's UnauthorizedException (or map to
its equivalent) while leaving subsequent authorization checks unchanged so
authenticated-but-forbidden cases still throw ForbiddenException.
In `@src/main/java/com/daedan/festabook/global/domain/Actor.java`:
- Line 3: Consider converting the Actor interface to a sealed interface to
restrict implementations: change the declaration of Actor to a sealed interface
and add a permits clause listing the concrete implementors (e.g., Admin,
Organizer, Staff, PlaceAccess) so the compiler enforces the allowed subclasses;
if tests need mocks, either mark allowed test-friendly subclasses as non-sealed
or adjust test setup to use concrete permitted types or Mockito inline to create
mocks.
In
`@src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java`:
- Around line 36-46: StaffDetails currently defines manual getters (getUsername,
getStaffId, getOrganizationId) while PlaceAccessDetails uses Lombok's `@Getter`;
decide which approach to standardize: if the goal is to expose all fields
similarly, replace the manual getters in class StaffDetails by adding `@Getter` to
the class and remove the explicit getUsername/getStaffId/getOrganizationId
methods; if the manual approach was intentional to restrict access to
festivalIds, add a clarifying comment above StaffDetails explaining why those
getters are omitted (or explicitly keep only the intended getters) so the
difference from PlaceAccessDetails is documented; reference StaffDetails,
getUsername, getStaffId, getOrganizationId, PlaceAccessDetails, and festivalIds
when making the change.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (44)
src/main/java/com/daedan/festabook/announcement/service/authorization/AnnouncementFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/FestivalIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/OrganizationIdResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/PlaceIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/service/AuthorizationService.javasrc/main/java/com/daedan/festabook/event/service/authorization/EventDateFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/event/service/authorization/EventFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/festival/service/authorization/FestivalImageFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/global/actor/ActorFactory.javasrc/main/java/com/daedan/festabook/global/actor/ActorResolver.javasrc/main/java/com/daedan/festabook/global/actor/JpaActorFactory.javasrc/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.javasrc/main/java/com/daedan/festabook/global/config/WebMvcConfig.javasrc/main/java/com/daedan/festabook/global/domain/Actor.javasrc/main/java/com/daedan/festabook/global/domain/Admin.javasrc/main/java/com/daedan/festabook/global/domain/BaseEntity.javasrc/main/java/com/daedan/festabook/global/security/authorization/AccountDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.javasrc/main/java/com/daedan/festabook/lineup/service/authorization/LineupFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/lostitem/service/authorization/LostItemFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/organization/domain/Organization.javasrc/main/java/com/daedan/festabook/organizer/infrastructure/OrganizerJpaRepository.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceAnnouncementFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceImageFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceImagePlaceIdsResolver.javasrc/main/java/com/daedan/festabook/question/service/authorization/QuestionFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/staff/infrastructure/StaffJpaRepository.javasrc/main/java/com/daedan/festabook/staff/service/authorization/StaffOrganizationIdResolver.javasrc/main/java/com/daedan/festabook/timetag/service/authorization/TimeTagFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/AnotherDummyEntity.javasrc/test/java/com/daedan/festabook/authorization/service/AuthorizationServiceTest.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntity.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntityOrganizationIdResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntity.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntity.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityPlaceIdsResolver.java
💤 Files with no reviewable changes (28)
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceImageFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/announcement/service/authorization/AnnouncementFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/question/service/authorization/QuestionFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/AuthorizationServiceTest.java
- src/main/java/com/daedan/festabook/authorization/service/AuthorizationService.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceAnnouncementFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntity.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/lostitem/service/authorization/LostItemFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/event/service/authorization/EventDateFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/festival/service/authorization/FestivalImageFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceImagePlaceIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntity.java
- src/test/java/com/daedan/festabook/authorization/service/AnotherDummyEntity.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntityOrganizationIdResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/PlaceIdsResolver.java
- src/main/java/com/daedan/festabook/timetag/service/authorization/TimeTagFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/FestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntityFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/OrganizationIdResolver.java
- src/main/java/com/daedan/festabook/lineup/service/authorization/LineupFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntity.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntityFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/staff/service/authorization/StaffOrganizationIdResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityPlaceIdsResolver.java
- src/main/java/com/daedan/festabook/event/service/authorization/EventFestivalIdsResolver.java
src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java
Show resolved
Hide resolved
| protected void softDelete() { | ||
| this.deleted = true; | ||
| this.deletedAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public void restore() { | ||
| protected void restore() { | ||
| this.deleted = false; | ||
| this.deletedAt = null; | ||
| } | ||
|
|
||
| public boolean isActive() { | ||
| protected boolean isActive() { | ||
| return !this.deleted; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== [1] BaseEntity 클래스 조회 ==="
rg -nP --type=java 'class\s+BaseEntity' -A 5 -B 2 src/
echo "=== [2] 메서드 정의 확인 (기본 포맷) ==="
rg -nP --type=java '(softDelete|restore|isActive)\s*\(' \
'src/main/java/com/daedan/festabook/global/domain/BaseEntity.java' -B 1
echo "=== [3] BaseEntity 상속 클래스 목록 ==="
rg -nP --type=java 'class\s+\w+\s+extends\s+BaseEntity' src/ -l
echo "=== [4] softDelete 호출 위치 (BaseEntity 제외) ==="
rg -nP --type=java '\.softDelete\s*\(' src/ \
-g '!src/main/java/com/daedan/festabook/global/domain/BaseEntity.java' -C 2
echo "=== [5] restore 호출 위치 (BaseEntity 제외) ==="
rg -nP --type=java '\.restore\s*\(' src/ \
-g '!src/main/java/com/daedan/festabook/global/domain/BaseEntity.java' -C 2
echo "=== [6] isActive 호출 위치 (BaseEntity 제외) ==="
rg -nP --type=java '\.isActive\s*\(' src/ \
-g '!src/main/java/com/daedan/festabook/global/domain/BaseEntity.java' -C 2Repository: festabook/backend
Length of output: 7397
protected 접근 제어는 안전하게 적용되고 있으며, 이제 도메인 설계 관점에서의 의도 강화를 고려해보세요.
현재 코드 검증 결과, softDelete(), restore(), isActive()는 테스트 코드 내에서만 호출되고 프로덕션 코드에서는 직접 사용되지 않습니다. 따라서 protected 접근 제어 변경으로 인한 컴파일 에러나 우회 로직은 발생하지 않습니다.
다만, 진정한 설계적 개선은 한 단계 더 나아가는 데에 있습니다. 현재 구조에서는 각 엔티티가 BaseEntity의 보호된 메서드를 직접 호출하고 있는데, 이는 도메인 비즈니스 의도를 명시하는 기회를 놓치고 있습니다:
-
옵션 A: 각 엔티티(
Festival,Place등)에서 자신의 도메인 의도를 반영한 메서드(markAsDeleted(),recover()등)를 정의하고, 내부에서만protected메서드를 호출
→ 장점: 도메인 언어(Ubiquitous Language) 명확화, 각 엔티티별 비즈니스 규칙 추가 가능, 단일 책임 원칙 강화
→ 단점: 보일러플레이트 코드 증가 -
옵션 B: 현 상태 유지 (
protected공통 메서드)
→ 장점: 구조 단순함, 중복 제거
→ 단점: 도메인 불변식 미흡, 엔티티별 특수 요구사항 반영 어려움
점진적 마이그레이션을 고려하면, 새로운 엔티티부터 옵션 A를 적용하고 기존 코드는 리팩토링 백로그에 등재하는 것도 실용적입니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/daedan/festabook/global/domain/BaseEntity.java` around
lines 34 - 45, 현재 BaseEntity의 protected 메서드(softDelete, restore, isActive)를
엔티티들이 직접 호출하는 구조는 도메인 의도 표현 기회를 놓치고 있으므로, 각 도메인 엔티티(Festival, Place 등)에 도메인어를
반영한 공개 메서드(예: markAsDeleted(), recover(), isActiveForBusiness() 등)를 추가하고, 해당 공개
메서드 내부에서 BaseEntity의 protected 메서드(softDelete(), restore(), isActive())를 호출하도록
변경하세요; 새로운 엔티티부터 적용하고 기존 엔티티는 점진적으로 리팩토링하도록 백로그에 등록하세요.
src/main/java/com/daedan/festabook/organization/domain/Organization.java
Outdated
Show resolved
Hide resolved
src/main/java/com/daedan/festabook/organizer/infrastructure/OrganizerJpaRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/com/daedan/festabook/staff/infrastructure/StaffJpaRepository.java
Outdated
Show resolved
Hide resolved
68ae886 to
32ca4aa
Compare
32ca4aa to
474380f
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/java/com/daedan/festabook/global/domain/Actor.java (1)
5-39:default false전략은 안전하지만, 권한 구현 누락을 숨길 수 있습니다.현재 구조는 보안 측면에서 fail-closed라 장점이 있지만, 구현체가 override를 빠뜨려도 컴파일이 통과해 원인 파악이 늦어질 수 있습니다.
- 옵션 A (명시성/안전성 우선): 핵심 권한 메서드(
hasOrganizationAuthority등)는default를 제거해 구현 강제
- 장점: 누락을 컴파일 타임에 차단
- 단점: 구현 클래스 작성 부담 증가
- 옵션 B (유연성 우선):
default false유지 + Actor 구현체별 권한 계약 테스트 추가
- 장점: 확장 유연성 유지
- 단점: 테스트 누락 시 회귀 탐지가 늦어짐
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/domain/Actor.java` around lines 5 - 39, The current default-false methods hide missing implementations; remove the default implementations for the core authority checks so implementing classes are forced to override them — specifically change hasOrganizationAuthority(Long organizationId), hasFestivalAuthority(Long festivalId), hasOrganizerAuthority(Long organizerId), hasStaffAuthority(Long staffId), and hasPlaceAccessAuthority(Long placeId) (and optionally isAdmin(), isOrganizer(), isStaff(), isPlaceAccess()) from default methods to abstract/interface methods with no body so compile-time errors surface when an Actor implementation forgets to provide real logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/main/java/com/daedan/festabook/global/domain/Actor.java`:
- Around line 5-39: The current default-false methods hide missing
implementations; remove the default implementations for the core authority
checks so implementing classes are forced to override them — specifically change
hasOrganizationAuthority(Long organizationId), hasFestivalAuthority(Long
festivalId), hasOrganizerAuthority(Long organizerId), hasStaffAuthority(Long
staffId), and hasPlaceAccessAuthority(Long placeId) (and optionally isAdmin(),
isOrganizer(), isStaff(), isPlaceAccess()) from default methods to
abstract/interface methods with no body so compile-time errors surface when an
Actor implementation forgets to provide real logic.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (38)
src/main/java/com/daedan/festabook/announcement/service/authorization/AnnouncementFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/FestivalIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/OrganizationIdResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/PlaceIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/service/AuthorizationService.javasrc/main/java/com/daedan/festabook/event/service/authorization/EventDateFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/event/service/authorization/EventFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/festival/service/authorization/FestivalImageFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.javasrc/main/java/com/daedan/festabook/global/config/WebMvcConfig.javasrc/main/java/com/daedan/festabook/global/domain/Actor.javasrc/main/java/com/daedan/festabook/global/domain/BaseEntity.javasrc/main/java/com/daedan/festabook/global/security/authorization/AccountDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.javasrc/main/java/com/daedan/festabook/lineup/service/authorization/LineupFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/lostitem/service/authorization/LostItemFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/organization/domain/Organization.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceAnnouncementFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceImageFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceImagePlaceIdsResolver.javasrc/main/java/com/daedan/festabook/question/service/authorization/QuestionFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/staff/service/authorization/StaffOrganizationIdResolver.javasrc/main/java/com/daedan/festabook/timetag/service/authorization/TimeTagFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/AnotherDummyEntity.javasrc/test/java/com/daedan/festabook/authorization/service/AuthorizationServiceTest.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntity.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntityOrganizationIdResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntity.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntity.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityPlaceIdsResolver.java
💤 Files with no reviewable changes (28)
- src/test/java/com/daedan/festabook/authorization/service/DummyEntityFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/PlaceIdsResolver.java
- src/main/java/com/daedan/festabook/announcement/service/authorization/AnnouncementFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntityFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/lostitem/service/authorization/LostItemFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntityOrganizationIdResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntity.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/AnotherDummyEntity.java
- src/main/java/com/daedan/festabook/question/service/authorization/QuestionFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/OrganizationIdResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceImageFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/event/service/authorization/EventFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceImagePlaceIdsResolver.java
- src/main/java/com/daedan/festabook/lineup/service/authorization/LineupFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/service/AuthorizationService.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntity.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceAnnouncementFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntity.java
- src/main/java/com/daedan/festabook/staff/service/authorization/StaffOrganizationIdResolver.java
- src/main/java/com/daedan/festabook/timetag/service/authorization/TimeTagFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/festival/service/authorization/FestivalImageFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/FestivalIdsResolver.java
- src/main/java/com/daedan/festabook/event/service/authorization/EventDateFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/AuthorizationServiceTest.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityPlaceIdsResolver.java
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java
- src/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.java
474380f to
c937de3
Compare
c937de3 to
18c62de
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java (1)
53-59: nullable 가능 ID 접근자의 계약을 명확히 해주세요
staffId,organizationId가 null일 수 있는 구조라면get...()보다 계약을 분리하는 편이 안전합니다.
옵션 A: 생성자에서 non-null 보장 후get...()유지
옵션 B:find...()+Optional<Long>로 호출자 체크를 강제As per coding guidelines
메서드 접두사: 데이터를 확실하게 가져올 수 없고 호출자가 체크해야 한다면 'find' 접두사로 작성한다 (예: Optional 반환)and메서드 접두사: 데이터를 확실하게 가져올 수 있을 때 'get' 접두사로 작성한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java` around lines 53 - 59, The current StaffDetails accessor methods getStaffId() and getOrganizationId() have unclear null contracts; either ensure non-null at construction time (validate/require staffId and organizationId in the StaffDetails constructor and keep the getStaffId/getOrganizationId names) or change the API to express optionality (rename to findStaffId()/findOrganizationId() and return Optional<Long>), and update all callers of StaffDetails accordingly so they either rely on guaranteed non-null values or handle Optional results; locate these changes in the StaffDetails class and its constructor to implement the chosen option.src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java (1)
53-58: ID 접근자 계약(null 가능성)을 메서드 이름/타입으로 명확히 해주세요현재
organizerId,organizationId는 생성자에서 null 차단이 없어서 nullable일 수 있는데,get...()는 값이 항상 있다고 읽히기 쉽습니다.
옵션 A(명확성): 생성자에서Objects.requireNonNull로 불변식 강제 후get...()유지
옵션 B(안전성):find...()+Optional<Long>로 호출자 체크를 타입으로 강제As per coding guidelines
메서드 접두사: 데이터를 확실하게 가져올 수 없고 호출자가 체크해야 한다면 'find' 접두사로 작성한다 (예: Optional 반환)and메서드 접두사: 데이터를 확실하게 가져올 수 있을 때 'get' 접두사로 작성한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java` around lines 53 - 58, OrganizerDetails currently exposes getOrganizerId() and getOrganizationId() while the fields organizerId and organizationId can be nullable; change the contract to make nullability explicit by replacing these getters with findOrganizerId() and findOrganizationId() that return Optional<Long> (and update any callers), so callers are forced to handle absence; ensure the fields remain Long (nullable) and only the new methods wrap them with Optional.ofNullable(organizerId)/Optional.ofNullable(organizationId) and remove or deprecate the old get...() methods if present.src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java (1)
23-39: 현재 사용 패턴에서는 안전하지만, 구체 타입 파라미터 추가 시 유형 검증 부재로 인한 리스크가 있습니다지금은 컨트롤러에서 Actor 인터페이스 타입만 사용하고 있어 실제 문제가 없습니다. 다만 설계 관점에서 볼 점이 있습니다.
supportsParameter()는Actor.class.isAssignableFrom()으로 Actor 하위 타입도 허용하는데,resolveArgument()는principal instanceof Actor검증만 하고 파라미터 타입 일치를 확인하지 않습니다. 만약 향후 AccountDetails의 구체 구현체(AdminDetails, OrganizerDetails 등)를 파라미터 타입으로 쓰는 경우, principal의 실제 타입이 기대하는 구체 타입과 다르면 런타임에 타입 불일치가 발생할 수 있습니다.
- 옵션 A(방어적 설계): 지금부터
parameter.getParameterType().isInstance(principal)검증을 추가해 구체 타입 파라미터 추가 시 자동으로 보호받기- 옵션 B(현재 패턴 유지): Actor 인터페이스만 파라미터로 받도록 컨벤션을 정하고 그대로 진행하기
ForbiddenException은 이미 HttpStatus.FORBIDDEN(403)으로 올바르게 매핑되어 있으므로, 예외 처리 측면에서는 문제가 없습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java` around lines 23 - 39, The resolver currently checks that the security principal is an Actor but does not verify it matches the controller parameter's concrete type; update resolveArgument(...) to additionally verify parameter.getParameterType().isInstance(principal) and throw ForbiddenException() if it fails so that supportsParameter()'s isAssignableFrom behavior is enforced at runtime; reference the existing Authentication/SecurityContextHolder logic and keep the current ForbiddenException for failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java`:
- Around line 23-39: The resolver currently checks that the security principal
is an Actor but does not verify it matches the controller parameter's concrete
type; update resolveArgument(...) to additionally verify
parameter.getParameterType().isInstance(principal) and throw
ForbiddenException() if it fails so that supportsParameter()'s isAssignableFrom
behavior is enforced at runtime; reference the existing
Authentication/SecurityContextHolder logic and keep the current
ForbiddenException for failures.
In
`@src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java`:
- Around line 53-58: OrganizerDetails currently exposes getOrganizerId() and
getOrganizationId() while the fields organizerId and organizationId can be
nullable; change the contract to make nullability explicit by replacing these
getters with findOrganizerId() and findOrganizationId() that return
Optional<Long> (and update any callers), so callers are forced to handle
absence; ensure the fields remain Long (nullable) and only the new methods wrap
them with Optional.ofNullable(organizerId)/Optional.ofNullable(organizationId)
and remove or deprecate the old get...() methods if present.
In
`@src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java`:
- Around line 53-59: The current StaffDetails accessor methods getStaffId() and
getOrganizationId() have unclear null contracts; either ensure non-null at
construction time (validate/require staffId and organizationId in the
StaffDetails constructor and keep the getStaffId/getOrganizationId names) or
change the API to express optionality (rename to
findStaffId()/findOrganizationId() and return Optional<Long>), and update all
callers of StaffDetails accordingly so they either rely on guaranteed non-null
values or handle Optional results; locate these changes in the StaffDetails
class and its constructor to implement the chosen option.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (37)
src/main/java/com/daedan/festabook/announcement/service/authorization/AnnouncementFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/FestivalIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/OrganizationIdResolver.javasrc/main/java/com/daedan/festabook/authorization/domain/PlaceIdsResolver.javasrc/main/java/com/daedan/festabook/authorization/service/AuthorizationService.javasrc/main/java/com/daedan/festabook/event/service/authorization/EventDateFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/event/service/authorization/EventFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/festival/service/authorization/FestivalImageFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.javasrc/main/java/com/daedan/festabook/global/config/WebMvcConfig.javasrc/main/java/com/daedan/festabook/global/domain/Actor.javasrc/main/java/com/daedan/festabook/global/domain/BaseEntity.javasrc/main/java/com/daedan/festabook/global/security/authorization/AccountDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.javasrc/main/java/com/daedan/festabook/lineup/service/authorization/LineupFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/lostitem/service/authorization/LostItemFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceAnnouncementFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceImageFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/place/service/authorization/PlaceImagePlaceIdsResolver.javasrc/main/java/com/daedan/festabook/question/service/authorization/QuestionFestivalIdsResolver.javasrc/main/java/com/daedan/festabook/staff/service/authorization/StaffOrganizationIdResolver.javasrc/main/java/com/daedan/festabook/timetag/service/authorization/TimeTagFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/AnotherDummyEntity.javasrc/test/java/com/daedan/festabook/authorization/service/AuthorizationServiceTest.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntity.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyEntityOrganizationIdResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntity.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/DummyPlaceIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntity.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityFestivalIdsResolver.javasrc/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityPlaceIdsResolver.java
💤 Files with no reviewable changes (28)
- src/main/java/com/daedan/festabook/lineup/service/authorization/LineupFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceImageFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceImagePlaceIdsResolver.java
- src/main/java/com/daedan/festabook/event/service/authorization/EventDateFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/event/service/authorization/EventFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceAnnouncementFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntity.java
- src/main/java/com/daedan/festabook/staff/service/authorization/StaffOrganizationIdResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntityFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/lostitem/service/authorization/LostItemFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntityFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/question/service/authorization/QuestionFestivalIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/service/AuthorizationService.java
- src/main/java/com/daedan/festabook/place/service/authorization/PlaceFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceEntity.java
- src/test/java/com/daedan/festabook/authorization/service/AuthorizationServiceTest.java
- src/main/java/com/daedan/festabook/authorization/domain/FestivalIdsResolver.java
- src/main/java/com/daedan/festabook/festival/service/authorization/FestivalImageFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntityPlaceIdsResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/PlaceIdsResolver.java
- src/main/java/com/daedan/festabook/announcement/service/authorization/AnnouncementFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyPlaceIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/AnotherDummyEntity.java
- src/main/java/com/daedan/festabook/timetag/service/authorization/TimeTagFestivalIdsResolver.java
- src/test/java/com/daedan/festabook/authorization/service/DummyEntityOrganizationIdResolver.java
- src/main/java/com/daedan/festabook/authorization/domain/OrganizationIdResolver.java
- src/test/java/com/daedan/festabook/authorization/service/EmptyIdsEntity.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/daedan/festabook/global/domain/BaseEntity.java
src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java
Outdated
Show resolved
Hide resolved
| Object principal = authentication.getPrincipal(); | ||
| if (!(principal instanceof Actor actor)) { | ||
| throw new ForbiddenException(); | ||
| } |
There was a problem hiding this comment.
이것도 위에 isassignableFrom으로 Class 비교로 바꿀 수 있지 않을까요?
가능하면 위에랑 같이 메서드로 묶어도 좋을 것 같아요 잘 사용하지 않는 메서드다보니까 이해하기 어려운 것 같아서 메서드 네이밍으로 설명하면 좋을 것 같다고 생각했어요
!Actor.class.isAssignableFrom(principal.getClass())
src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java
Outdated
Show resolved
Hide resolved
src/main/java/com/daedan/festabook/global/config/WebMvcConfig.java
Outdated
Show resolved
Hide resolved
|
|
||
| public void softDelete() { | ||
| protected void softDelete() { | ||
| this.deleted = true; | ||
| this.deletedAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public void restore() { | ||
| protected void restore() { | ||
| this.deleted = false; | ||
| this.deletedAt = null; | ||
| } | ||
|
|
||
| public boolean isActive() { | ||
| protected boolean isActive() { | ||
| return !this.deleted; | ||
| } |
There was a problem hiding this comment.
이제 자동으로 나가지 않고 명시적으로 호출해야 되나요? 기억해야할 것 같아서요
그리고 만약, 명시적으로 호출해야한다면, 기존 JpaRepository 쿼리들에도 같이 포함되서 나가는게 맞을까요?
There was a problem hiding this comment.
이해를 못했어요. 원래도 그랬던 건데 외부에서 호출하는 걸 protected로 막아둔 것 뿐입니다!
src/main/java/com/daedan/festabook/global/security/authorization/AccountDetails.java
Show resolved
Hide resolved
src/main/java/com/daedan/festabook/global/security/authorization/Actor.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/test/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsTest.java (1)
15-16:@Nested클래스명을 실제 테스트 대상 메서드명과 맞춰주면 가독성이 더 좋아집니다.현재 호출은
hasX(...)인데@Nested이름은hasXAuthority로 남아 있어, 리팩터링 이후 테스트 의도 파악이 한 번 더 필요합니다.
- 옵션 A (권장):
@Nested클래스명을hasOrganization,hasFestival등으로 일괄 정렬
- 장점: 테스트 구조와 API가 1:1로 대응되어 탐색/유지보수 용이
- 단점: 파일 전반 rename으로 diff가 조금 커짐
- 옵션 B: 현재 이름 유지 + 클래스 주석으로 과거 명칭임을 명시
- 장점: 변경량 최소화
- 단점: 장기적으로 혼선이 누적됨
예시 변경안
- class hasOrganizationAuthority { + class hasOrganization { - class hasFestivalAuthority { + class hasFestival { - class hasOrganizerAuthority { + class hasOrganizer { - class hasStaffAuthority { + class hasStaff { - class hasPlaceAccessAuthority { + class hasPlaceAccess {As per coding guidelines "테스트 메서드 구조: 테스트하려는 메서드마다
@Nested를작성하고 클래스 명은 테스트하려는 메서드 명과 동일하게 작성한다."Also applies to: 35-36, 54-55, 73-74, 92-93, 27-27, 46-46, 65-65, 84-84, 103-103, 118-118, 133-133
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsTest.java` around lines 15 - 16, The `@Nested` test class names (e.g., hasOrganizationAuthority, hasFestivalAuthority, etc.) don’t match the actual tested method names (hasOrganization, hasFestival, etc.); rename each nested class in PlaceAccessDetailsTest (for example change hasOrganizationAuthority -> hasOrganization, hasFestivalAuthority -> hasFestival, etc.) so class names mirror the target method names per the guideline, or if you prefer minimal change, add a short class-level comment stating the legacy name; update all occurrences listed in the review (lines referenced) to keep naming consistent across the file.src/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.java (1)
17-25: 설계적 고민 포인트: Admin의 null 입력 처리 일관성현재
AdminDetails의hasOrganization(Long)과hasFestival(Long)은 항상true를 반환합니다. 반면,OrganizerDetails나StaffDetails에서는Objects.nonNull(id) && ...패턴으로 null 체크를 수행합니다.이로 인해 발생할 수 있는 상황:
organizerId가 실수로null인 경우, Admin은true를 반환하지만 다른 역할은false를 반환- 이는 잠재적인 버그를 마스킹할 수 있습니다
선택지:
- 옵션 A (현재 유지): Admin은 모든 권한을 가지므로 null 체크 불필요. 단순하고 직관적이지만, 잘못된 ID 전달을 감지하기 어려움
- 옵션 B (null 체크 추가):
Objects.nonNull(id)체크를 추가하여 일관성 확보. 단, Admin이 "모든 권한"을 가진다는 의미가 다소 희석됨현재 구현이 의도된 것이라면 이대로 진행해도 괜찮지만, 팀 내에서 이 동작에 대한 합의가 있으면 좋겠습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.java` around lines 17 - 25, The AdminDetails.hasOrganization(Long) and hasFestival(Long) methods always return true which is inconsistent with OrganizerDetails/StaffDetails null-safe checks; update both methods (hasOrganization and hasFestival) to mirror the others by returning Objects.nonNull(organizationId) (and Objects.nonNull(festivalId)) && <existing admin-permission-logic> — i.e., add a null-check first so they return false on null inputs while preserving Admin's permission behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@src/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.java`:
- Around line 17-25: The AdminDetails.hasOrganization(Long) and
hasFestival(Long) methods always return true which is inconsistent with
OrganizerDetails/StaffDetails null-safe checks; update both methods
(hasOrganization and hasFestival) to mirror the others by returning
Objects.nonNull(organizationId) (and Objects.nonNull(festivalId)) && <existing
admin-permission-logic> — i.e., add a null-check first so they return false on
null inputs while preserving Admin's permission behavior.
In
`@src/test/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsTest.java`:
- Around line 15-16: The `@Nested` test class names (e.g.,
hasOrganizationAuthority, hasFestivalAuthority, etc.) don’t match the actual
tested method names (hasOrganization, hasFestival, etc.); rename each nested
class in PlaceAccessDetailsTest (for example change hasOrganizationAuthority ->
hasOrganization, hasFestivalAuthority -> hasFestival, etc.) so class names
mirror the target method names per the guideline, or if you prefer minimal
change, add a short class-level comment stating the legacy name; update all
occurrences listed in the review (lines referenced) to keep naming consistent
across the file.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.javasrc/main/java/com/daedan/festabook/global/config/WebMvcConfig.javasrc/main/java/com/daedan/festabook/global/domain/BaseEntity.javasrc/main/java/com/daedan/festabook/global/security/authorization/AccountDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/Actor.javasrc/main/java/com/daedan/festabook/global/security/authorization/AdminDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetails.javasrc/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.javasrc/test/java/com/daedan/festabook/global/security/authorization/AccountDetailsExtractorTest.javasrc/test/java/com/daedan/festabook/global/security/authorization/AdminDetailsTest.javasrc/test/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategyTest.javasrc/test/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsTest.javasrc/test/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsTest.javasrc/test/java/com/daedan/festabook/global/security/authorization/StaffDetailsExtractStrategyTest.javasrc/test/java/com/daedan/festabook/global/security/authorization/StaffDetailsTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/daedan/festabook/global/config/WebMvcConfig.java
- src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java
|
@coderabbitai @gemini-code-assist 현재 올라간 주요 도메인들에 대해 논리적인 오류가 있는지 검토해 주세요. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 75548 🏁 Script executed: Length of output: 14275 🏁 Script executed: Length of output: 6774
중간 점검 PR에 대한 논리적 오류 검토 결과를 공유드립니다. 🚨 치명적인 논리적 오류1. Festival.validateCreatableBy() - return 누락위치: private void validateCreatableBy(Actor actor) {
if (Objects.isNull(actor)) {
throw new InternalServerException();
}
if (actor.isAdmin()) {
return;
}
if (actor.isOrganizer()) {
organization.validateOwnedBy(actor);
}
throw new ForbiddenException(); // ❌ 항상 실행됨!
}문제점: Organizer가 자신의 Organization에 속한 Festival을 생성하려고 할 때, 수정안: if (actor.isOrganizer()) {
organization.validateOwnedBy(actor);
return; // 추가 필요
}2. Staff.validateCreatableBy() - 동일한 문제위치: Festival과 정확히 동일한 패턴의 오류가 있습니다. Organizer가 자신의 Organization에 Staff를 생성할 수 없게 됩니다. 3. Staff.validateUpdatableBy() - 잘못된 논리 연산자위치: private void validateUpdatableBy(Actor actor) {
if (Objects.isNull(actor)) {
throw new InternalServerException();
}
if (actor.isAdmin()) {
return;
}
if (actor.isStaff() || actor.hasStaff(id)) { // ❌ OR 연산자 사용
return;
}
throw new ForbiddenException();
}문제점:
수정안: if (actor.isStaff() && actor.hasStaff(id)) { // AND 연산자로 변경
return;
}비교를 위해, if (actor.isOrganizer() && actor.hasOrganizer(id)) {
return;
}
|
There was a problem hiding this comment.
Code Review
권한 시스템을 중앙 집중식 서비스에서 도메인 주도 설계 방식으로 전면 리팩터링한 점이 매우 인상적입니다. Actor 인터페이스와 ArgumentResolver를 도입하여 컨트롤러를 간소화하고, 각 도메인 객체가 자신의 권한 검증 책임을 갖도록 변경한 것은 아키텍처를 크게 개선하는 훌륭한 변화입니다. 제 리뷰는 새로운 권한 검증 로직의 몇 가지 반복적인 논리 오류와, 리팩터링 과정에서 누락될 수 있는 권한 검증 부분을 중심으로 작성되었습니다. 제공된 규칙에 따라 검토한 결과, 모든 기존 코멘트는 유효하며 수정하거나 삭제할 필요가 없다고 판단됩니다. 이 부분들을 보완하면 더욱 견고하고 안정적인 시스템이 될 것입니다.
src/main/java/com/daedan/festabook/staff/domain/StaffAuthority.java
Outdated
Show resolved
Hide resolved
40a3cf1 to
9354e6a
Compare
|
@coderabbitai @gemini-code-assist 피드백을 반영했습니다. 현재 올라간 주요 도메인들에 대해 논리적인 오류가 있는지 검토해 주세요. |
src/main/java/com/daedan/festabook/organization/domain/Organization.java
Show resolved
Hide resolved
| // TODO: 이 검증이 필요한지 검토 | ||
| validateStaffAuthorityUpdateRequest(request, organizationId); |
There was a problem hiding this comment.
입력값에대한 Null 검증들은 service가 아니라 입력이 들어오는 interface, 즉, controller의 책임이 아닐까라는 생각이 갑자기 드네요
값에 대한 비즈니스 규칙들, 몇자 이하여야 된다. 뭐 이메일 형식이여야 한다 이런것들ㄹ은 도메인의 역할이지만, 어떻게 생각하시나요?
There was a problem hiding this comment.
저도 이거 DTO의 @NotNull 같은 어노테이션으로 검증하는 게 좋아보여요
이거랑 별개로 DDD 설계하면서 StaffService도 전면 리팩터링 필요해 보입니다..
| Set<Long> currentFestivalIds = authoritiesToKeep.stream() | ||
| .map(StaffAuthority::getFestival) | ||
| .map(Festival::getId) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| Map<Long, Festival> requestedFestivals = getRequestedFestivals(requestedFestivalIds); | ||
| List<StaffAuthority> authoritiesToCreate = requestedFestivalIds.stream() | ||
| .filter(festivalId -> !currentFestivalIds.contains(festivalId)) | ||
| .map(festivalId -> new StaffAuthority(staff, requestedFestivals.get(festivalId))) | ||
| .map(festivalId -> StaffAuthority.createBy(actor, staff, requestedFestivals.get(festivalId))) | ||
| .toList(); |
There was a problem hiding this comment.
읽으면서 느꼈는데 toKeep이라는 메서드 네이밍이랑 currentFestivalIds 2가지가 있어서 이해하기 조금 어려웟던 것 같은데
둘다 toKeep 혹은 current로 통일하는건 어떻게 생각하시나요?
There was a problem hiding this comment.
이거랑 별개로 DDD 설계하면서 StaffService도 전면 리팩터링 필요해 보입니다..
| @Test | ||
| void 예외_STAFF이면_ForbiddenException이_발생한다() { | ||
| // given | ||
| Actor actor = ActorFixture.staff(); | ||
| Organization organization = OrganizationFixture.create(); | ||
|
|
||
| // when & then | ||
| assertThatThrownBy(() -> Festival.createBy( | ||
| actor, | ||
| organization, | ||
| "축제 이름", | ||
| LocalDate.of(2025, 5, 1), | ||
| LocalDate.of(2025, 5, 3), | ||
| true, | ||
| 16, | ||
| new Coordinate(37.1234, 127.1234), | ||
| List.of( | ||
| new Coordinate(37.1234, 127.1234), | ||
| new Coordinate(37.1235, 127.1235), | ||
| new Coordinate(37.1236, 127.1236) | ||
| ), | ||
| "분실물 안내" | ||
| )) | ||
| .isInstanceOf(ForbiddenException.class); | ||
| } |
There was a problem hiding this comment.
이 부분을 다른 ParameterizedTest EnumSource로 다른 권한에 대한 예외를 테스트해도 좋을 것 같아요
There was a problem hiding this comment.
이제 도메인 로직 보시면.... 권한 검증이 굉장히 꼼꼼하게 많이 작성되어 있거든요?
그런데 이게 중복 코드가 아니라 그렇게 Parameterized 테스트로 작성하기 어려울 것 같은데, 혹시 아이디어 있을까요...?
|
@coderabbitai @gemini-code-assist DDD 구조로 권한 검증 로직을 전면 리팩터링했습니다. 이전과 비교했을 때 논리적으로 변경되었거나 오류가 있는지 꼼꼼하게 검토 부탁 드립니다. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 726 🏁 Script executed: Length of output: 1236 🏁 Script executed: Length of output: 2781 🏁 Script executed: Length of output: 10615 🏁 Script executed: Length of output: 5050 🏁 Script executed: Length of output: 5298 🏁 Script executed: Length of output: 6919 🏁 Script executed: Length of output: 5382
DDD 구조로의 권한 검증 리팩터링을 상세히 검토했습니다. 논리적 오류는 발견되지 않았으며, 이전 중앙집중식 AuthorizationService와 의미적으로 동등한 검증을 수행하고 있습니다. ✅ 리팩터링 검증 결과: 논리적 오류 없음1. 아키텍처 변경 비교
2. 권한 검증 로직 매핑이전: AuthorizationService.validateFestivalCommandAccess()// Resolver로 Entity ID → Festival ID 변환 후 권한 확인
if (hasNotFestivalAuthority(entityFestivalIds, accountDetails)) {
throw new ForbiddenException();
}현재: 도메인 체인을 통한 검증// Event 예시
private void validateWritableBy(Actor actor) {
if (actor.isAdmin()) return;
if (actor.isOrganizer() || actor.isStaff()) {
eventDate.validateOwnedBy(actor); // EventDate로 위임
return;
}
throw new ForbiddenException();
}
// EventDate가 다시 Festival로 위임
public void validateOwnedBy(Actor actor) {
if (actor.isAdmin()) return;
festival.validateOwnedBy(actor); // Festival로 위임
}
// Festival이 최종 검증
public void validateOwnedBy(Actor actor) {
if (actor.isAdmin()) return;
if ((actor.isOrganizer() || actor.isStaff()) && actor.hasFestivalId(id)) {
return;
}
throw new ForbiddenException();
}결과: Resolver 패턴 대신 도메인 간 위임 체인을 사용하여 동일한 검증 수행 ✅ 🎯 주요 변경사항 (의도된 설계)1. PlaceAccess 역할 추가 (새로운 기능)이전: PlaceAccess 역할 없음 현재: Place의 제한적 관리자 역할 추가 // PlaceImage, PlaceAnnouncement 검증
if (actor.isPlaceAccess() || actor.isStaff() || actor.isOrganizer()) {
place.validateOwnedBy(actor);
return;
}의도: Place 자체는 수정 불가하지만, 이미지/공지사항 같은 하위 리소스만 관리할 수 있는 제한적 권한 부여. 예를 들어, 부스 운영자가 자신의 부스 사진/공지만 업데이트하는 시나리오에 적합합니다. 검증:
2. Staff 읽기 권한 제한 (프라이버시 강화)현재 구현: // Staff.validateReadableBy()
if (actor.isStaff() && actor.hasStaffId(id)) { // 본인만 읽기 가능
return;
}
// Organizer도 Staff 정보 읽기 불가의도: Staff의 개인정보(username, password 등)는 본인만 조회 가능하도록 프라이버시 보호. Organizer는 Staff를 생성/삭제할 수 있지만, 개인정보는 열람 불가. 일관성 검증:
|
| 항목 | 평가 | 비고 |
|---|---|---|
| 논리적 동등성 | ✅ 완벽 | 이전 로직과 의미적으로 동일 |
| 도메인 책임 분리 | ✅ 우수 | 각 도메인이 자신의 권한 검증 담당 |
| 확장성 | ✅ 우수 | 새 도메인 추가 시 일관된 패턴 적용 가능 |
| 코드 응집도 | ✅ 우수 | 관련 로직이 도메인 내부에 집중 |
| 테스트 용이성 | ✅ 우수 | 도메인 단위 테스트 가능 |
| PlaceAccess 역할 | ✅ 합리적 | 제한적 권한 부여로 보안 강화 |
| Staff 프라이버시 | ✅ 합리적 | 개인정보 보호 강화 |
💡 추가 권장사항
1. AdminDetails에 주석 추가
/**
* 관리자 권한을 가진 사용자의 인증 정보.
* Admin은 모든 리소스에 대한 무제한 권한을 가지므로,
* hasOrganizationId(), hasFestivalId() 등의 메서드는 호출되지 않습니다.
*/
public class AdminDetails extends AccountDetails {
// ...
}2. 도메인 검증 컨벤션 문서화
리팩터링한 패턴이 매우 우수하므로, 문서화하여 팀 전체가 일관되게 적용할 수 있도록 권장합니다:
## 도메인 권한 검증 패턴
모든 도메인 엔티티는 다음 패턴을 따릅니다:
1. null 체크 → InternalServerException
2. Admin 체크 → 통과
3. 역할별 검증 (Organizer, Staff, PlaceAccess 등)
4. 상위 도메인 위임 (festival.validateOwnedBy() 등)
5. 실패 시 → ForbiddenException🏆 결론
이번 리팩터링은 매우 성공적입니다! 🎊
- ✅ 이전 AuthorizationService와 논리적으로 완전히 동등한 검증 수행
- ✅ DDD 원칙을 잘 따른 도메인 중심 설계
- ✅ 일관된 패턴으로 유지보수성 향상
- ✅ PlaceAccess 역할 추가로 보안 강화
- ✅ Staff 프라이버시 보호 강화
논리적 오류나 누락된 검증 없이, 이전 중앙집중식 구조를 성공적으로 도메인 주도 설계로 전환하셨습니다! 👏
추가로 확인이 필요한 부분이 있으시면 말씀해주세요! 😊
/)/)
( •ᴗ•)
/>🎯 완벽한 리팩터링입니다!
🧠 Learnings used
Learnt from: CR
Repo: festabook/backend PR: 0
File: code-style.md:0-0
Timestamp: 2026-01-06T04:11:44.306Z
Learning: 테스트 코드 대상: service, repository, 도메인 로직은 단위 테스트로 작성한다.
There was a problem hiding this comment.
Code Review
This pull request focuses on enhancing the code's security and adherence to coding standards by implementing actor-based authorization and updating code structures. It includes changes to the code style guide, domain models, controllers, services, and test conventions. The code changes enforce domain-driven design principles for authorization, ensuring that access control is handled within the domain layer using an Actor object passed from the controller to the service layer. The pull request also removes the AuthorizationService and related resolver patterns, and updates the testing guidelines to align with the new authorization approach. Additionally, the code changes include updates to method signatures in controllers and services to include the Actor parameter, and modifications to domain entities to enforce authorization rules.
|
DDD 구조로 권한 검증 로직을 전면 리팩터링했습니다. 이전과 비교했을 때 논리적으로 변경되었거나 오류가 있는지 꼼꼼하게 검토 부탁 드립니다. |
There was a problem hiding this comment.
Code Review
권한 시스템을 중앙 집중 방식에서 도메인 주도 설계에 기반한 분산 방식으로 전면 리팩터링한 인상적인 작업입니다. AuthorizationService를 제거하고 각 도메인 엔티티가 Actor를 통해 스스로 권한을 검증하도록 변경함으로써, 코드의 응집도를 높이고 권한 검증 누락의 위험을 크게 줄였습니다. 이러한 변경 사항이 컨트롤러, 서비스, 도메인, 테스트 코드 전반에 걸쳐 일관되게 적용된 점이 훌륭합니다.
리뷰에서는 주로 새로운 설계가 가져올 수 있는 성능상의 트레이드오프와, 일부 일관성이 부족해 보이는 부분에 대해 몇 가지 의견을 제시했습니다. 특히, 연관된 하위 엔티티 삭제 시 발생하는 N+1 쿼리 문제와 FestivalNotification의 삭제 로직에서 권한 검증이 누락된 것으로 보이는 지점을 중점적으로 살펴보았습니다. 전반적으로 이번 리팩터링은 시스템을 더욱 견고하고 유지보수하기 좋은 구조로 발전시키는 중요한 단계라고 생각합니다.
src/main/java/com/daedan/festabook/festival/domain/FestivalNotification.java
Show resolved
Hide resolved
| @Transactional | ||
| public void deleteEventDateByEventDateId(Long eventDateId) { | ||
| authorizationService.validateFestivalCommandAccess(EventDate.class, eventDateId); | ||
| public void deleteEventDateByEventDateId(Actor actor, Long eventDateId) { | ||
| EventDate eventDate = getEventDateById(eventDateId); | ||
| eventJpaRepository.deleteAllByEventDateId(eventDate.getId()); | ||
| eventDateJpaRepository.delete(eventDate); | ||
| List<Event> events = getAllEventsById(eventDateId); | ||
|
|
||
| events.forEach(event -> event.deleteBy(actor)); | ||
| eventDate.deleteBy(actor); | ||
| } |
There was a problem hiding this comment.
EventDate 삭제 시 하위 Event들을 순회하며 deleteBy(actor)를 호출하는 방식으로 변경하여 각 Event에 대한 권한 검증을 보장한 점이 인상적입니다. 새로운 아키텍처의 의도를 잘 반영한 훌륭한 리팩터링입니다.
다만, 이 방식의 트레이드오프에 대해 함께 고민해보면 좋을 것 같습니다.
-
[관점 A: 현재 방식 - 안정성]
각Event마다 개별적으로 권한을 검증하므로 매우 안전합니다. -
[관점 B: 성능]
하나의EventDate에 수백, 수천 개의Event가 연결될 경우, N+1 삭제 쿼리가 발생하여 성능 저하의 원인이 될 수 있습니다. (기존에는deleteAllByEventDateId로 한 번에 처리) -
[제안]
현재는Event의 개수가 많지 않아 문제가 되지 않을 수 있지만, 향후 시스템 확장성을 고려한다면 이러한 구조가 잠재적인 성능 병목 지점이 될 수 있다는 점을 인지하고 있으면 좋을 것 같습니다. 만약 성능 이슈가 발생한다면, 권한 검증을 통과한 하위 엔티티 ID 목록을 이용한 bulk delete를 고려해볼 수 있습니다.
| @Transactional | ||
| public void deleteByPlaceId(Actor actor, Long placeId) { | ||
| Place place = getPlaceById(placeId); | ||
| List<PlaceImage> placeImages = placeImageJpaRepository.findAllByPlaceId(placeId); | ||
| List<PlaceAnnouncement> placeAnnouncements = placeAnnouncementJpaRepository.findAllByPlaceId(placeId); | ||
|
|
||
| placeImages.forEach(placeImage -> placeImage.deleteBy(actor)); | ||
| placeAnnouncements.forEach(placeAnnouncement -> placeAnnouncement.deleteBy(actor)); | ||
| place.deleteBy(actor); |
There was a problem hiding this comment.
Place 삭제 시 하위 엔티티인 PlaceImage와 PlaceAnnouncement를 각각 순회하며 deleteBy(actor)를 호출하도록 변경하여 각 하위 엔티티에 대한 권한 검증을 누락 없이 수행할 수 있게 된 점이 좋습니다.
다만, 이 방식은 EventDate 삭제 시와 유사한 성능 트레이드오프를 가집니다. 만약 하나의 Place에 수십, 수백 개의 이미지나 공지가 연결될 경우, 루프를 돌며 삭제 쿼리가 여러 번 실행되어 성능에 영향을 줄 수 있습니다.
현재로서는 문제가 되지 않을 가능성이 높지만, 향후 대규모 트래픽을 대비하는 차원에서 이러한 구조가 잠재적인 성능 병목 지점이 될 수 있다는 점을 인지하고 있으면 좋을 것 같습니다.
| private void deletePlaceTimeTags(Actor actor, Long placeId, List<Long> deleteTimeTagIds) { | ||
| if (deleteTimeTagIds.isEmpty()) { | ||
| return; | ||
| } | ||
| List<PlaceTimeTag> deletePlaceTimeTags = placeTimeTagJpaRepository.findAllByPlaceIdAndTimeTagIdIn( | ||
| placeId, | ||
| deleteTimeTagIds | ||
| ); | ||
| deletePlaceTimeTags.forEach(placeTimeTag -> placeTimeTag.deleteBy(actor)); | ||
| } |
There was a problem hiding this comment.
changuii
left a comment
There was a problem hiding this comment.
확인부탁드립니다.
테스트에서 주로 아래와 같은 상황입니다.
- assertSoftly 누락
- create에서 값 검증 누락
- 이미 값 검증했는데 값 검증이 다른 권한 검사 테스트에 존재
src/test/java/com/daedan/festabook/announcement/domain/AnnouncementTest.java
Show resolved
Hide resolved
src/test/java/com/daedan/festabook/event/service/EventDateServiceTest.java
Outdated
Show resolved
Hide resolved
src/test/java/com/daedan/festabook/festival/domain/FestivalNotificationTest.java
Outdated
Show resolved
Hide resolved
src/test/java/com/daedan/festabook/festival/service/FestivalNotificationServiceTest.java
Show resolved
Hide resolved
|



#️⃣ 이슈 번호
#96
🛠️ 작업 내용
🙇🏻 중점 리뷰 요청
Summary by CodeRabbit
New Features
Refactor
Tests