ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,17 @@
+package com.fourseason.delivery.domain.review.repository;
+
+import com.fourseason.delivery.domain.order.entity.Order;
+import com.fourseason.delivery.domain.review.entity.Review;
+import com.fourseason.delivery.domain.shop.entity.Shop;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface ReviewRepository extends JpaRepository<Review, UUID> {
+    Optional<Review> findByIdAndOrderId(UUID reviewId, UUID orderId);
+
+    List<Review> findByShop(Shop shop);
+
+}

리뷰: 소프트 딜리트에 대해 찾아보니까 JPA에서 `@SQLDelte` 나 `@Where`를 엔티티 클래스에 붙임으로써, 쉽게 구현할 수 있는 것 같아요. 같이 논의해보면 좋을 것 같습니다.

모든 삭제 쿼리메서드를 update로 하거나, 조회 시 `DeleteAtIsNotNull` 을 접미사로 붙인다면, 나중에 정책이 바뀌었을 때, 코드 수정이 번거로울 것 같아요.

어느 한 곳에서 삭제 정책에 따라 공통으로 처리되면 좋을 것 같다는 생각이 들었습니다.
@@ -41,7 +41,7 @@ public Object checkReviewGroupAccess(ProceedingJoinPoint joinPoint,
 
     private boolean canMemberAccess(ReviewGroup reviewGroup, HttpServletRequest request) {
         GitHubMember gitHubMember = sessionManager.getGitHubMember(request);
-        return gitHubMember != null && reviewGroup.getMemberId() == gitHubMember.getMemberId();
+        return gitHubMember != null && reviewGroup.isMadeByMember(gitHubMember.getMemberId());
     }
 
     private boolean canGuestAccess(ReviewGroup reviewGroup, HttpServletRequest request) {

리뷰: **문제가 되는 부분은 이 부분이었습니다.**
회원이 접근하려는 리뷰 그룹이 "비회원이 만든 리뷰그룹"인 경우, 
reviewGroup.getMemberId() 가 null 이기 때문에
long인 gitHubMember.getMemberId() 와의 == 연산을 못하므로 NPE가 발생합니다. 

이런 일이 생기지 않게하려면 1차원적으로
`gitHubMember != null && (reviewGroup.getMemberId() != null && reviewGroup.getMemberId() == gitHubMember.getMemberId())` 를 할수도 있지만, 너무 가독성이 떨어져서 편의 메서드 isMadeByMember를 추가해줬습니다.
@@ -57,6 +57,13 @@ void setUp() {
         RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
     }
 
+    @Test
+    void 존재하지_않는_리뷰에_접근하면_NotFound_예외가_발생한다() {
+        // when & then
+        assertThatCode(() -> aopTestClass.testReviewMethod(1L))
+                .isInstanceOf(ReviewNotFoundException.class);
+    }
+
     @Nested
     class 성공적으로_리뷰에_접근할_수_있다 {
 
@@ -90,7 +97,7 @@ class 성공적으로_리뷰에_접근할_수_있다 {
         }
 
         @Test
-        void 비회원은_자신이_만든_리뷰_그룹에_작성된_리뷰에_접근할_수_있다() {
+        void 리뷰_그룹을_인증한_비회원은_리뷰_그룹에_작성된_리뷰에_접근할_수_있다() {
             // given
             ReviewGroup reviewGroup = reviewGroupRepository.save(비회원_리뷰_그룹());
             Review review = reviewRepository.save(비회원_작성_리뷰(1L, reviewGroup.getId(), List.of()));
@@ -102,13 +109,6 @@ class 성공적으로_리뷰에_접근할_수_있다 {
         }
     }
 
-    @Test
-    void 존재하지_않는_리뷰에_접근하면_NotFound_예외가_발생한다() {
-        // when & then
-        assertThatCode(() -> aopTestClass.testReviewMethod(1L))
-                .isInstanceOf(ReviewNotFoundException.class);
-    }
-
     @Nested
     class 유효하지_않은_세션으로_접근하면_예외가_발생한다 {
 

리뷰: 소소하게 테스트 코드 리팩터링했습니다.

생각해보면 비회원은 자기가 만들지 않았더라도 비밀번호가 일치하면 통과되는거라 "리뷰 그룹을 인증한" 으로 명확한 의미를 전달하려 했씁니다~
@@ -0,0 +1,73 @@
+package com.eureka.spartaonetoone.review.application;
+
+import com.eureka.spartaonetoone.review.application.dtos.request.ReviewRequestDto;
+import com.eureka.spartaonetoone.review.application.dtos.response.ReviewResponseDto;
+import com.eureka.spartaonetoone.review.application.exception.ReviewException;
+import com.eureka.spartaonetoone.review.domain.Review;
+import com.eureka.spartaonetoone.review.domain.repository.ReviewRepository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import jakarta.validation.Valid;
+
+@Service
+@Validated
+public class ReviewService {
+
+	private final ReviewRepository reviewRepository;
+
+	public ReviewService(ReviewRepository reviewRepository) {
+		this.reviewRepository = reviewRepository;
+	}
+
+	// 리뷰 등록 - DTO를 엔티티로 변환한 후 저장하고, 저장된 엔티티를 DTO로 변환하여 반환
+	public ReviewResponseDto createReview(@Valid ReviewRequestDto dto) {
+		Review review = dto.createReview();
+		Review savedReview = reviewRepository.save(review);
+		return ReviewResponseDto.from(savedReview);
+	}
+
+	// 특정 리뷰 조회 - 주어진 reviewId로 Review 엔티티를 조회하며, 존재하지 않으면 CustomException을 발생
+	public ReviewResponseDto getReviewById(UUID reviewId) {
+		Review review = reviewRepository.findById(reviewId)
+			.orElseThrow(ReviewException.ReviewNotFoundException::new);
+		return ReviewResponseDto.from(review);
+	}
+
+	// 특정 주문(orderId)에 속한 리뷰 목록 조회
+	// ReviewRepository의 findByOrderId()를 사용하여 해당 주문의 모든 리뷰를 조회
+	// 리스트를 Pageable로 감싸서 반환
+	public Page<ReviewResponseDto> getReviewsByOrderId(UUID orderId, Pageable pageable) {
+		List<Review> reviews = reviewRepository.findByOrderId(orderId);
+		List<ReviewResponseDto> dtoList = reviews.stream()
+			.map(ReviewResponseDto::from)
+			.collect(Collectors.toList());
+		return new PageImpl<>(dtoList, pageable, dtoList.size());
+	}
+
+	// 리뷰 수정 - 특정 reviewId에 해당하는 Review 엔티티를 조회한 후, DTO의 값으로 업데이트하고 저장
+	public ReviewResponseDto updateReview(UUID reviewId, @Valid ReviewRequestDto dto) {
+		Review review = reviewRepository.findById(reviewId)
+			.orElseThrow(ReviewException.ReviewNotFoundException::new);
+		review.update(dto.getContent(), dto.getRating(), dto.getImage());
+		Review updatedReview = reviewRepository.save(review);
+		return ReviewResponseDto.from(updatedReview);
+	}
+
+
+	// 리뷰 삭제 - 특정 reviewId로 Review 엔티티를 조회한 후, markDeleted()를 호출
+	public void deleteReview(UUID reviewId) {
+		Review review = reviewRepository.findById(reviewId)
+			.orElseThrow(ReviewException.ReviewNotFoundException::new);
+		review.markDeleted();
+		reviewRepository.save(review);
+	}
+}
\ No newline at end of file

리뷰: `@Transactional`이 빠진 것 같습니다!
@@ -0,0 +1,73 @@
+package com.eureka.spartaonetoone.review.application;
+
+import com.eureka.spartaonetoone.review.application.dtos.request.ReviewRequestDto;
+import com.eureka.spartaonetoone.review.application.dtos.response.ReviewResponseDto;
+import com.eureka.spartaonetoone.review.application.exception.ReviewException;
+import com.eureka.spartaonetoone.review.domain.Review;
+import com.eureka.spartaonetoone.review.domain.repository.ReviewRepository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import jakarta.validation.Valid;
+
+@Service
+@Validated
+public class ReviewService {
+
+	private final ReviewRepository reviewRepository;
+
+	public ReviewService(ReviewRepository reviewRepository) {
+		this.reviewRepository = reviewRepository;
+	}
+
+	// 리뷰 등록 - DTO를 엔티티로 변환한 후 저장하고, 저장된 엔티티를 DTO로 변환하여 반환
+	public ReviewResponseDto createReview(@Valid ReviewRequestDto dto) {
+		Review review = dto.createReview();
+		Review savedReview = reviewRepository.save(review);
+		return ReviewResponseDto.from(savedReview);
+	}
+
+	// 특정 리뷰 조회 - 주어진 reviewId로 Review 엔티티를 조회하며, 존재하지 않으면 CustomException을 발생
+	public ReviewResponseDto getReviewById(UUID reviewId) {
+		Review review = reviewRepository.findById(reviewId)
+			.orElseThrow(ReviewException.ReviewNotFoundException::new);
+		return ReviewResponseDto.from(review);
+	}
+
+	// 특정 주문(orderId)에 속한 리뷰 목록 조회
+	// ReviewRepository의 findByOrderId()를 사용하여 해당 주문의 모든 리뷰를 조회
+	// 리스트를 Pageable로 감싸서 반환
+	public Page<ReviewResponseDto> getReviewsByOrderId(UUID orderId, Pageable pageable) {
+		List<Review> reviews = reviewRepository.findByOrderId(orderId);
+		List<ReviewResponseDto> dtoList = reviews.stream()
+			.map(ReviewResponseDto::from)
+			.collect(Collectors.toList());
+		return new PageImpl<>(dtoList, pageable, dtoList.size());
+	}
+
+	// 리뷰 수정 - 특정 reviewId에 해당하는 Review 엔티티를 조회한 후, DTO의 값으로 업데이트하고 저장
+	public ReviewResponseDto updateReview(UUID reviewId, @Valid ReviewRequestDto dto) {
+		Review review = reviewRepository.findById(reviewId)
+			.orElseThrow(ReviewException.ReviewNotFoundException::new);
+		review.update(dto.getContent(), dto.getRating(), dto.getImage());
+		Review updatedReview = reviewRepository.save(review);
+		return ReviewResponseDto.from(updatedReview);
+	}
+
+
+	// 리뷰 삭제 - 특정 reviewId로 Review 엔티티를 조회한 후, markDeleted()를 호출
+	public void deleteReview(UUID reviewId) {
+		Review review = reviewRepository.findById(reviewId)
+			.orElseThrow(ReviewException.ReviewNotFoundException::new);
+		review.markDeleted();
+		reviewRepository.save(review);
+	}
+}
\ No newline at end of file

리뷰: 이 부분이 정적팩토리 메소드를 사용해서 아마 `Review.createReview(dto)`가 될 것 같아요!
@@ -0,0 +1,59 @@
+package com.eureka.spartaonetoone.review.presentation;
+
+import com.eureka.spartaonetoone.review.application.ReviewService;
+import com.eureka.spartaonetoone.review.application.dtos.request.ReviewRequestDto;
+import com.eureka.spartaonetoone.review.application.dtos.response.ReviewResponseDto;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/reviews")
+public class ReviewController {
+
+	private final ReviewService reviewService;
+
+	public ReviewController(ReviewService reviewService) {
+		this.reviewService = reviewService;
+	}
+
+	// 리뷰 생성
+	@PostMapping
+	public ResponseEntity<ReviewResponseDto> createReview(@RequestBody ReviewRequestDto reviewRequestDto) {
+		ReviewResponseDto responseDto = reviewService.createReview(reviewRequestDto);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 특정 리뷰 조회
+	@GetMapping("/{review_id}")
+	public ResponseEntity<ReviewResponseDto> getReview(@PathVariable("review_id") UUID reviewId) {
+		ReviewResponseDto responseDto = reviewService.getReviewById(reviewId);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 특정주문(orderId)에 속한 리뷰 전체 목록 조회
+	@GetMapping
+	public ResponseEntity<Page<ReviewResponseDto>> getReviewsByOrderId(@RequestParam("order_id") UUID orderId, Pageable pageable) {
+		Page<ReviewResponseDto> responseDtos = reviewService.getReviewsByOrderId(orderId, pageable);
+		return ResponseEntity.ok(responseDtos);
+	}
+
+	// 리뷰 수정
+	@PutMapping("/{review_id}")
+	public ResponseEntity<ReviewResponseDto> updateReview(@PathVariable("review_id") UUID reviewId,
+		@RequestBody ReviewRequestDto reviewRequestDto) {
+		ReviewResponseDto responseDto = reviewService.updateReview(reviewId, reviewRequestDto);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 리뷰 삭제
+	@DeleteMapping("/{review_id}")
+	public ResponseEntity<Void> deleteReview(@PathVariable("review_id") UUID reviewId) {
+		reviewService.deleteReview(reviewId);
+		return ResponseEntity.noContent().build();
+	}
+}

리뷰: dto 앞에 `@Valid` 추가해주시면 감사하겠습니다!
@@ -0,0 +1,59 @@
+package com.eureka.spartaonetoone.review.presentation;
+
+import com.eureka.spartaonetoone.review.application.ReviewService;
+import com.eureka.spartaonetoone.review.application.dtos.request.ReviewRequestDto;
+import com.eureka.spartaonetoone.review.application.dtos.response.ReviewResponseDto;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/reviews")
+public class ReviewController {
+
+	private final ReviewService reviewService;
+
+	public ReviewController(ReviewService reviewService) {
+		this.reviewService = reviewService;
+	}
+
+	// 리뷰 생성
+	@PostMapping
+	public ResponseEntity<ReviewResponseDto> createReview(@RequestBody ReviewRequestDto reviewRequestDto) {
+		ReviewResponseDto responseDto = reviewService.createReview(reviewRequestDto);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 특정 리뷰 조회
+	@GetMapping("/{review_id}")
+	public ResponseEntity<ReviewResponseDto> getReview(@PathVariable("review_id") UUID reviewId) {
+		ReviewResponseDto responseDto = reviewService.getReviewById(reviewId);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 특정주문(orderId)에 속한 리뷰 전체 목록 조회
+	@GetMapping
+	public ResponseEntity<Page<ReviewResponseDto>> getReviewsByOrderId(@RequestParam("order_id") UUID orderId, Pageable pageable) {
+		Page<ReviewResponseDto> responseDtos = reviewService.getReviewsByOrderId(orderId, pageable);
+		return ResponseEntity.ok(responseDtos);
+	}
+
+	// 리뷰 수정
+	@PutMapping("/{review_id}")
+	public ResponseEntity<ReviewResponseDto> updateReview(@PathVariable("review_id") UUID reviewId,
+		@RequestBody ReviewRequestDto reviewRequestDto) {
+		ReviewResponseDto responseDto = reviewService.updateReview(reviewId, reviewRequestDto);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 리뷰 삭제
+	@DeleteMapping("/{review_id}")
+	public ResponseEntity<Void> deleteReview(@PathVariable("review_id") UUID reviewId) {
+		reviewService.deleteReview(reviewId);
+		return ResponseEntity.noContent().build();
+	}
+}

리뷰: 여기도 dto 앞에 `@Valid` 추가해주시면 좋을 것 같아요!
@@ -0,0 +1,62 @@
+package com.eureka.spartaonetoone.store.presentation;
+
+import com.eureka.spartaonetoone.store.application.StoreService;
+import com.eureka.spartaonetoone.store.application.dtos.request.StoreRequestDto;	//
+import com.eureka.spartaonetoone.store.application.dtos.response.StoreResponseDto;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/stores")
+public class StoreController {
+
+	private final StoreService storeService;
+
+	// 생성자 주입
+	public StoreController(StoreService storeService) {
+		this.storeService = storeService;
+	}
+
+	// 가게 등록
+	@PostMapping
+	public ResponseEntity<StoreResponseDto> createStore(@RequestBody StoreRequestDto storeRequestDto) {	//
+		StoreResponseDto responseDto = storeService.createStore(storeRequestDto);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 특정 가게 조회
+	@GetMapping("/{store_id}")
+	public ResponseEntity<StoreResponseDto> getStore(@PathVariable(name = "store_id") UUID storeId) {
+		StoreResponseDto responseDto = storeService.getStoreById(storeId);
+		return ResponseEntity.ok(responseDto);
+	}		// getStore(@PathVariable(name = "id") UUID id) { // 이렇게 수정 - 스프링이 인식할 수 있도록 / requesst패럴? 찾아보기 / 링크 참고
+	// https://teamsparta.notion.site/RequestParam-PathVariable-Autowired-335787e51fac41418598434fa7f7ed51
+
+
+	// 전체 가게 목록 조회
+	@GetMapping
+	public ResponseEntity<Page<StoreResponseDto>> getAllStores(Pageable pageable) {
+		Page<StoreResponseDto> responseDtos = storeService.getAllStores(pageable);
+		return ResponseEntity.ok(responseDtos);
+	}
+
+	// 가게 수정
+	@PutMapping("/{store_id}")
+	public ResponseEntity<StoreResponseDto> updateStore(@PathVariable(name = "store_id") UUID storeId,
+		@RequestBody StoreRequestDto storeRequestDto) {
+		StoreResponseDto responseDto = storeService.updateStore(storeId, storeRequestDto);
+		return ResponseEntity.ok(responseDto);
+	}
+
+	// 가게 삭제
+	@DeleteMapping("/{store_id}")
+	public ResponseEntity<Void> deleteStore(@PathVariable(name = "store_id") UUID storeId) {
+		storeService.deleteStore(storeId);
+		return ResponseEntity.noContent().build();
+	}
+}

리뷰: 주석 부분만 나중에 제거해주셔도 될 것 같아요
@@ -0,0 +1,28 @@
+package com.eureka.spartaonetoone.common.client;
+
+import com.eureka.spartaonetoone.common.client.dto.ReviewAggregateRequest;
+import com.eureka.spartaonetoone.common.client.dto.ReviewAggregateResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+@RequiredArgsConstructor
+public class ReviewAggregateClient { // 어그리 빼자.
+
+	private final WebClient webClient;
+
+	// 예: http://review-service/api/v1/reviews/_aggregate
+	@Value("${review.aggregate.url}")
+	private String reviewAggregateUrl;
+
+	public ReviewAggregateResponse getReviewAggregate(ReviewAggregateRequest request) {
+		return webClient.post()
+			.uri(reviewAggregateUrl)
+			.bodyValue(request)
+			.retrieve()
+			.bodyToMono(ReviewAggregateResponse.class)
+			.block(); // 동기 호출
+	}
+}

리뷰: 주석 확인하였습니다 ~! :)
@@ -0,0 +1,28 @@
+package com.eureka.spartaonetoone.common.client;
+
+import com.eureka.spartaonetoone.common.client.dto.ReviewAggregateRequest;
+import com.eureka.spartaonetoone.common.client.dto.ReviewAggregateResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+@RequiredArgsConstructor
+public class ReviewAggregateClient { // 어그리 빼자.
+
+	private final WebClient webClient;
+
+	// 예: http://review-service/api/v1/reviews/_aggregate
+	@Value("${review.aggregate.url}")
+	private String reviewAggregateUrl;
+
+	public ReviewAggregateResponse getReviewAggregate(ReviewAggregateRequest request) {
+		return webClient.post()
+			.uri(reviewAggregateUrl)
+			.bodyValue(request)
+			.retrieve()
+			.bodyToMono(ReviewAggregateResponse.class)
+			.block(); // 동기 호출
+	}
+}

리뷰: 요 부분에서도 Aggregate 제거해주세요 ~~!
@@ -0,0 +1,28 @@
+package com.eureka.spartaonetoone.common.client;
+
+import com.eureka.spartaonetoone.common.client.dto.ReviewAggregateRequest;
+import com.eureka.spartaonetoone.common.client.dto.ReviewAggregateResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+@RequiredArgsConstructor
+public class ReviewAggregateClient { // 어그리 빼자.
+
+	private final WebClient webClient;
+
+	// 예: http://review-service/api/v1/reviews/_aggregate
+	@Value("${review.aggregate.url}")
+	private String reviewAggregateUrl;
+
+	public ReviewAggregateResponse getReviewAggregate(ReviewAggregateRequest request) {
+		return webClient.post()
+			.uri(reviewAggregateUrl)
+			.bodyValue(request)
+			.retrieve()
+			.bodyToMono(ReviewAggregateResponse.class)
+			.block(); // 동기 호출
+	}
+}

리뷰: 요 부분에서도 Aggregate 제거해주세요 :)
@@ -0,0 +1,15 @@
+package com.eureka.spartaonetoone.common.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReviewAggregateRequest {
+	// 해당 가게의 식별자
+	private UUID storeId;
+}

리뷰: 요 부분에서도 Aggregate 제거해주세요 ~~!
@@ -0,0 +1,13 @@
+package nextstep.security.access;
+
+import java.util.Collection;
+import nextstep.security.core.GrantedAuthority;
+
+public final class NullRoleHierarchy implements RoleHierarchy {
+
+    @Override
+    public Collection<GrantedAuthority> getReachableGrantedAuthorities(
+            Collection<GrantedAuthority> authorities) {
+        return authorities;
+    }
+}

리뷰: `<? extends GrantedAuthority>`로 지정되어있을 이유가 어떻게 될까요?
@@ -4,19 +4,22 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
+import nextstep.security.access.RoleHierarchy;
 import nextstep.security.authentication.Authentication;
 import nextstep.security.authorization.AuthorizationDecision;
 import nextstep.security.authorization.AuthorizationManager;
 import nextstep.security.authorization.Secured;
 import nextstep.security.authorization.web.AuthorityAuthorizationManager;
+import nextstep.security.core.GrantedAuthority;
 import org.aopalliance.intercept.MethodInvocation;
 
 public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
 
-    private AuthorityAuthorizationManager<Collection<String>> authorityAuthorizationManager;
+    private final AuthorityAuthorizationManager<Collection<String>> authorityAuthorizationManager;
 
-    public void setAuthorityAuthorizationManager(Collection<String> authorities) {
-        authorityAuthorizationManager = new AuthorityAuthorizationManager<>(authorities);
+    public SecuredAuthorizationManager(
+            AuthorityAuthorizationManager<Collection<String>> authorityAuthorizationManager) {
+        this.authorityAuthorizationManager = authorityAuthorizationManager;
     }
 
     @Override
@@ -26,8 +29,8 @@ public AuthorizationDecision check(Authentication authentication, MethodInvocati
         if (authorities.isEmpty()) {
             return null;
         }
-        setAuthorityAuthorizationManager(authorities);
-        return authorities.isEmpty() ? null : authorityAuthorizationManager.check(authentication, authorities);
+
+        return authorityAuthorizationManager.check(authentication, authorities);
     }
 
     private Collection<String> getAuthorities(MethodInvocation invocation) {

리뷰: `RoleHierarchy`를 따로 둘 필요가 없을 것 같아요.

`authorityAuthorizationManager`에서 처리하게 두면 될텐데 `SecuredAuthorizationManager`가 가질 필요가 있을까요?
@@ -1,38 +1,53 @@
 package nextstep.security.authorization.web;
 
 import java.util.Collection;
+import java.util.Set;
+import nextstep.security.access.RoleHierarchy;
 import nextstep.security.authentication.Authentication;
 import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authorization.AuthorizationDecision;
 import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.core.GrantedAuthority;
 
 public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+    private final RoleHierarchy roleHierarchy;
+    private Collection<String> authorities;
 
-    private final Collection<String> authorities;
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy) {
+        this.roleHierarchy = roleHierarchy;
+        this.authorities = Set.of();
+    }
 
-    public AuthorityAuthorizationManager(Collection<String> authorities) {
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, Collection<String> authorities) {
+        this.roleHierarchy = roleHierarchy;
         this.authorities = authorities;
     }
 
-
     @Override
     public AuthorizationDecision check(Authentication authentication, T object) {
         if (authentication == null) {
             throw new AuthenticationException();
         }
 
+        if(object instanceof Collection<?>) {
+            this.authorities = (Collection<String>) object;
+        }
+
         boolean hasAuthority = isAuthorized(authentication, authorities);
 
         return AuthorizationDecision.of(hasAuthority);
     }
 
-
     private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
-        for (String authority : authentication.getAuthorities()) {
-            if (authorities.contains(authority)) {
+        for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
+            if (authorities.contains(grantedAuthority.getAuthority())) {
                 return true;
             }
         }
         return false;
     }
+
+    private Collection<GrantedAuthority> getGrantedAuthorities(Authentication authentication) {
+        return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
+    }
 }

리뷰: ```suggestion
    private final RoleHierarchy roleHierarchy = new NullRoleHierarchy();
```
불변이면 좋을 것 같네요 :)