ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,24 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.aop.support.AopUtils;
+
+import java.lang.reflect.Method;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+        Class<?> targetClass = AopUtils.getTargetClass(method.getClass());
+        Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+        Secured secured = targetMethod.getAnnotation(Secured.class);
+
+        if (authentication.getAuthorities().contains(secured.value())) {
+            return new AuthorizationDecision(true);
+        }
+
+        return new AuthorizationDecision(false);
+    }
+}

리뷰: Spring AOP 환경에서는 대상 객체가 프록시 객체로 감싸질 수 있기 때문에,  
`method.getAnnotation(Secured.class)`를 사용하면 실제 구현체의 메서드에서 선언된 `@Secured` 어노테이션을 찾지 못할 가능성이 있습니다.method.getAnnotation 와 AopUtils.getMostSpecificMethod() 어떤 차이가 있을까요? 😄
@@ -0,0 +1,11 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+
+public class AuthenticatedAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) {
+        return new AuthorizationDecision(authentication.isAuthenticated());
+    }
+}

리뷰: `AuthenticatedAuthorizationManager`는 인증된 사용자의 접근을 허용할지 여부를 결정하는 인가(Authorization) 역할을 하는데요,  
때문에 `request.setAttribute("username", authentication.getPrincipal());` 는 인가의 책임을 벗어난 동작으로 보여집니다. 개선해 보면 좋을 것 같아요. 😄
@@ -1,13 +1,20 @@
 package nextstep.app.ui;
 
+import jakarta.servlet.http.HttpServletRequest;
 import nextstep.app.domain.Member;
 import nextstep.app.domain.MemberRepository;
+import nextstep.app.exception.UserNotFoundException;
+import nextstep.security.authentication.Authentication;
 import nextstep.security.authorization.Secured;
+import nextstep.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
+import java.util.Optional;
 
 @RestController
 public class MemberController {
@@ -24,6 +31,17 @@ public ResponseEntity<List<Member>> list() {
         return ResponseEntity.ok(members);
     }
 
+    @GetMapping("/members/me")
+    public ResponseEntity<Member> get() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        String email = authentication.getPrincipal().toString();
+
+        Member member = memberRepository.findByEmail(email)
+                .orElseThrow(UserNotFoundException::new);
+
+        return ResponseEntity.ok(member);
+    }
+
     @Secured("ADMIN")
     @GetMapping("/search")
     public ResponseEntity<List<Member>> search() {

리뷰: `request.setAttribute`를 통해 값을 저장하고 다시 가져오는 과정을 거치지 않고,  
SecurityContext에서 직접 가져올 수 있을 것 같네요 😄
@@ -0,0 +1,20 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod method;
+    private final String uri;
+
+    public MvcRequestMatcher(HttpMethod method, String uri) {
+        this.method = method;
+        this.uri = uri;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return request.getMethod().equalsIgnoreCase(method.name()) && request.getRequestURI().equalsIgnoreCase(uri);
+    }
+}

리뷰: `matches()`는 정규식 매칭을 수행하는 메서드 이므로, 단순한 문자열 비교에는 적합하지 않을 것 같아요.  
HTTP 메서드(GET, POST 등)는 정규식이 필요하지 않으므로, `equalsIgnoreCase()`를 사용하면 성능 면에서도 더 효율적일 것 같네요 😊
@@ -0,0 +1,25 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+
+import java.util.List;
+
+public class RequestAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+
+    private final List<RequestMatcherEntry<AuthorizationManager>> requestMatcherEntries;
+
+    public RequestAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager>> requestMatcherEntries) {
+        this.requestMatcherEntries = requestMatcherEntries;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) {
+        return requestMatcherEntries.stream()
+                .filter(requestMatcherEntry -> requestMatcherEntry.matchRequest(request))
+                .findFirst()
+                .map(RequestMatcherEntry::getEntry)
+                .map(entry -> entry.check(authentication, request))
+                .orElseGet(() -> new AuthorizationDecision(false));
+    }
+}

리뷰: `orElseGet()` 와 `orElse()`는 어떤 차이가 있을까요? 😄
@@ -11,29 +11,29 @@
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
 
-import java.lang.reflect.Method;
-
 public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
 
     private final Pointcut pointcut;
+    private final AuthorizationManager<MethodInvocation> securedAuthorizationManager;
 
     public SecuredMethodInterceptor() {
         this.pointcut = new AnnotationMatchingPointcut(null, Secured.class);
+        this.securedAuthorizationManager = new SecuredAuthorizationManager();
     }
 
     @Override
     public Object invoke(MethodInvocation invocation) throws Throwable {
-        Method method = invocation.getMethod();
-        if (method.isAnnotationPresent(Secured.class)) {
-            Secured secured = method.getAnnotation(Secured.class);
-            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-            if (authentication == null) {
-                throw new AuthenticationException();
-            }
-            if (!authentication.getAuthorities().contains(secured.value())) {
-                throw new ForbiddenException();
-            }
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            throw new AuthenticationException();
         }
+
+        AuthorizationDecision authorizationDecision = securedAuthorizationManager.check(authentication, invocation);
+        if (!authorizationDecision.isGranted()) {
+            throw new ForbiddenException();
+        }
+
         return invocation.proceed();
     }
 

리뷰: `@Secured` 어노테이션이 존재하는지 다시 검사하고 있는데요,  
이미 `AnnotationMatchingPointcut`을 통해 `@Secured`이 붙은 메서드만 인터셉트하도록 설정되어 있기 때문에,  
`if (!method.isAnnotationPresent(Secured.class))` 부분은 제거해도 될 것 같아요. 😊
@@ -17,6 +17,7 @@
 import java.util.Set;
 
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 @SpringBootTest
@@ -48,7 +49,7 @@ void request_success_with_admin_user() throws Exception {
         );
 
         response.andExpect(status().isOk())
-                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2));
+                .andExpect(jsonPath("$.length()").value(2));
     }
 
     @DisplayName("일반 사용자가 요청할 경우 권한이 없어야 한다.")
@@ -89,4 +90,32 @@ void request_fail_invalid_password() throws Exception {
 
         response.andExpect(status().isUnauthorized());
     }
+
+    @DisplayName("인증된 사용자는 자신의 정보를 조회할 수 있다.")
+    @Test
+    void request_success_members_me() throws Exception {
+        String token = Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes());
+
+        ResultActions response = mockMvc.perform(get("/members/me")
+                .header("Authorization", "Basic " + token)
+                .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+        );
+
+        response.andExpect(status().isOk())
+                .andExpect(jsonPath("$.email").value("b@b.com"))
+                .andExpect(jsonPath("$.password").value("password"));
+    }
+
+    @DisplayName("인증되지 않은 사용자는 자신의 정보를 조회할 수 없다.")
+    @Test
+    void request_fail_members_me() throws Exception {
+        String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + "invalid").getBytes());
+
+        ResultActions response = mockMvc.perform(get("/members/me")
+                .header("Authorization", "Basic " + token)
+                .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+        );
+
+        response.andExpect(status().isUnauthorized());
+    }
 }

리뷰: 테스트 설명이 잘못되어 있네요 😄
@@ -11,29 +11,29 @@
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
 
-import java.lang.reflect.Method;
-
 public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
 
     private final Pointcut pointcut;
+    private final AuthorizationManager<MethodInvocation> securedAuthorizationManager;
 
     public SecuredMethodInterceptor() {
         this.pointcut = new AnnotationMatchingPointcut(null, Secured.class);
+        this.securedAuthorizationManager = new SecuredAuthorizationManager();
     }
 
     @Override
     public Object invoke(MethodInvocation invocation) throws Throwable {
-        Method method = invocation.getMethod();
-        if (method.isAnnotationPresent(Secured.class)) {
-            Secured secured = method.getAnnotation(Secured.class);
-            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-            if (authentication == null) {
-                throw new AuthenticationException();
-            }
-            if (!authentication.getAuthorities().contains(secured.value())) {
-                throw new ForbiddenException();
-            }
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            throw new AuthenticationException();
         }
+
+        AuthorizationDecision authorizationDecision = securedAuthorizationManager.check(authentication, invocation);
+        if (!authorizationDecision.isGranted()) {
+            throw new ForbiddenException();
+        }
+
         return invocation.proceed();
     }
 

리뷰: > 고민한 부분
> Q. securedAuthorizationManager 로직이 수행되는 시점이 SecuredMethodInterceptor 안에서 수행되는 것이 맞는지 궁금합니다.

A. AOP를 통해 메서드가 실행되기 전에 인가(Authorization) 로직을 수행하도록 잘 구현해 주셨네요! 👍  
혹시 고민했던 부분이 있으셨다면 남겨주시면 저도 고민해보고 답변 드리도록 하겠습니다 😄
@@ -0,0 +1,24 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.aop.support.AopUtils;
+
+import java.lang.reflect.Method;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+        Class<?> targetClass = AopUtils.getTargetClass(method.getClass());
+        Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+        Secured secured = targetMethod.getAnnotation(Secured.class);
+
+        if (authentication.getAuthorities().contains(secured.value())) {
+            return new AuthorizationDecision(true);
+        }
+
+        return new AuthorizationDecision(false);
+    }
+}

리뷰: AopUtils.getMostSpecificMethod() 로 실제 메서드의 정보를 가져올 수 있겠네요!
그래도 어노테이션 정보를 가져오려면 getAnnotation 메서드는 계속 사용해야 하는 거죠?
수정해봤는데 맞는 방식인지 확인 부탁드립니다!
@@ -0,0 +1,20 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod method;
+    private final String uri;
+
+    public MvcRequestMatcher(HttpMethod method, String uri) {
+        this.method = method;
+        this.uri = uri;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return request.getMethod().equalsIgnoreCase(method.name()) && request.getRequestURI().equalsIgnoreCase(uri);
+    }
+}

리뷰: 제가 왜 matches를 사용했는지 모르겠네요.. 꼼꼼한 리뷰 감사합니다 👍
@@ -11,29 +11,29 @@
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
 
-import java.lang.reflect.Method;
-
 public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
 
     private final Pointcut pointcut;
+    private final AuthorizationManager<MethodInvocation> securedAuthorizationManager;
 
     public SecuredMethodInterceptor() {
         this.pointcut = new AnnotationMatchingPointcut(null, Secured.class);
+        this.securedAuthorizationManager = new SecuredAuthorizationManager();
     }
 
     @Override
     public Object invoke(MethodInvocation invocation) throws Throwable {
-        Method method = invocation.getMethod();
-        if (method.isAnnotationPresent(Secured.class)) {
-            Secured secured = method.getAnnotation(Secured.class);
-            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-            if (authentication == null) {
-                throw new AuthenticationException();
-            }
-            if (!authentication.getAuthorities().contains(secured.value())) {
-                throw new ForbiddenException();
-            }
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            throw new AuthenticationException();
         }
+
+        AuthorizationDecision authorizationDecision = securedAuthorizationManager.check(authentication, invocation);
+        if (!authorizationDecision.isGranted()) {
+            throw new ForbiddenException();
+        }
+
         return invocation.proceed();
     }
 

리뷰: 다른 AuthorizationManager 들은 Filter에서 인가 로직이 수행되는데 이와 다르게 AOP를 통해 수행되는 로직이어서 맞는 방식인지 궁금했었습니다..! 답변 감사합니다 👍
@@ -0,0 +1,25 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+
+import java.util.List;
+
+public class RequestAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+
+    private final List<RequestMatcherEntry<AuthorizationManager>> requestMatcherEntries;
+
+    public RequestAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager>> requestMatcherEntries) {
+        this.requestMatcherEntries = requestMatcherEntries;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) {
+        return requestMatcherEntries.stream()
+                .filter(requestMatcherEntry -> requestMatcherEntry.matchRequest(request))
+                .findFirst()
+                .map(RequestMatcherEntry::getEntry)
+                .map(entry -> entry.check(authentication, request))
+                .orElseGet(() -> new AuthorizationDecision(false));
+    }
+}

리뷰: orElse로 구현하게 되면, null이 아니어도 AuthorizationDecision 객체 생성 로직은 수행될 것 같네요.
orElseGet으로 수정했습니다!
@@ -0,0 +1,24 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.aop.support.AopUtils;
+
+import java.lang.reflect.Method;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+        Class<?> targetClass = AopUtils.getTargetClass(method.getClass());
+        Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+        Secured secured = targetMethod.getAnnotation(Secured.class);
+
+        if (authentication.getAuthorities().contains(secured.value())) {
+            return new AuthorizationDecision(true);
+        }
+
+        return new AuthorizationDecision(false);
+    }
+}

리뷰: method.getClass()는 Method 클래스 자체의 타입을 반환하므로, 올바른 대상 클래스(Target Class)를 가져올 수 없습니다.
invocation.getThis()를 사용하여 실제 대상 객체(Target Object)의 클래스를 가져오는 것이 좋겠네요 😄 
`Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis());`
@@ -0,0 +1,24 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.aop.support.AopUtils;
+
+import java.lang.reflect.Method;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+        Class<?> targetClass = AopUtils.getTargetClass(method.getClass());
+        Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+        Secured secured = targetMethod.getAnnotation(Secured.class);
+
+        if (authentication.getAuthorities().contains(secured.value())) {
+            return new AuthorizationDecision(true);
+        }
+
+        return new AuthorizationDecision(false);
+    }
+}

리뷰: 네 맞습니다 
어노테이션 정보를 가져오는 것은 동일하게 getAnnotation(Secured.class)을 사용해야 합니다. 😄
@@ -11,29 +11,29 @@
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
 
-import java.lang.reflect.Method;
-
 public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
 
     private final Pointcut pointcut;
+    private final AuthorizationManager<MethodInvocation> securedAuthorizationManager;
 
     public SecuredMethodInterceptor() {
         this.pointcut = new AnnotationMatchingPointcut(null, Secured.class);
+        this.securedAuthorizationManager = new SecuredAuthorizationManager();
     }
 
     @Override
     public Object invoke(MethodInvocation invocation) throws Throwable {
-        Method method = invocation.getMethod();
-        if (method.isAnnotationPresent(Secured.class)) {
-            Secured secured = method.getAnnotation(Secured.class);
-            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-            if (authentication == null) {
-                throw new AuthenticationException();
-            }
-            if (!authentication.getAuthorities().contains(secured.value())) {
-                throw new ForbiddenException();
-            }
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            throw new AuthenticationException();
         }
+
+        AuthorizationDecision authorizationDecision = securedAuthorizationManager.check(authentication, invocation);
+        if (!authorizationDecision.isGranted()) {
+            throw new ForbiddenException();
+        }
+
         return invocation.proceed();
     }
 

리뷰: Filter 기반 인가는 HTTP 요청 단위에서 적용되지만, 
`@Secured`는 개별 메서드 호출 단위에서 보안 검사를 해야 해서 적절한 구현 방식입니다 😄
@@ -6,13 +6,14 @@ import ReviewContent from "./ReviewContent";
 import ArrowDown from "@/assets/images/icons/arrow_down.svg";
 import MenuCircle from "@/assets/images/icons/menu-circle.svg";
 import Dropdown from "@/components/common/Dropdown";
+import useReviewModals from "@/hooks/review/useReviewModals";
 import { type ReviewInfoProps } from "@/types/review";
 import { type RatingStyle } from "@/types/review";
 
 export default function Review({ reviewInfo }: ReviewInfoProps) {
   const router = useRouter();
+  const { handleDeleteClick } = useReviewModals();
   const [isOpen, setIsOpen] = useState(false);
-
   const reviewTextStyle: RatingStyle = {
     0: "text-purple-200",
     1: "text-blue-200",
@@ -37,7 +38,7 @@ export default function Review({ reviewInfo }: ReviewInfoProps) {
   };
 
   const handleClickDelete = () => {
-    alert("리뷰 삭제입니다.");
+    handleDeleteClick(reviewInfo.reviewId);
   };
 
   const handleClickDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -48,8 +49,7 @@ export default function Review({ reviewInfo }: ReviewInfoProps) {
   return (
     <div
       className='rounded-[12px] bg-gray-900 p-4'
-      onClick={handleClickReview}
-      aria-label={"routeMeet"}
+      onClick={reviewInfo.isMyReview ? handleClickReview : undefined}
     >
       <div className='relative'>
         <div className='flex justify-between'>

리뷰: @ITHPARK  리뷰 삭제 api 연결하면서 해당 컴포넌트 일부 수정되었습니다!
@@ -11,6 +11,8 @@ export interface ReviewInfo {
   isMyWritten?: boolean;
   eventId?: number;
   eventType?: string;
+  reviewId: number;
+  editable?: boolean;
 }
 
 export interface ReviewInfoProps {

리뷰: 리뷰 타입에 `reviewId`, `editable` 추가했습니다
@@ -2,6 +2,7 @@ spring.application.name=delivery
 
 spring.jpa.hibernate.ddl-auto=update
 
+
 spring.jpa.properties.hibernate.show_sql=true
 spring.jpa.properties.hibernate.format_sql=true
 spring.jpa.properties.hibernate.use_sql_comments=true

리뷰: 여기 DB 민감정보 .env로 빼주실 수 있나요?? 디스코드에 제가 사용한 .env는 올려두었습니다!
@@ -2,11 +2,24 @@
 
 import nextstep.security.authentication.Authentication;
 import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.GrantedAuthority;
+import nextstep.security.authorization.role.NullRoleHierarchy;
+import nextstep.security.authorization.role.RoleHierarchy;
 
 import java.util.Collection;
 
 public class AuthoritiesAuthorizationManager implements AuthorizationManager<Collection<String>> {
 
+    private final RoleHierarchy roleHierarchy;
+
+    public AuthoritiesAuthorizationManager() {
+        this(new NullRoleHierarchy());
+    }
+
+    public AuthoritiesAuthorizationManager(RoleHierarchy roleHierarchy) {
+        this.roleHierarchy = roleHierarchy;
+    }
+
     @Override
     public AuthorizationDecision check(Authentication authentication, Collection<String> authorities) {
 
@@ -19,12 +32,17 @@ private boolean isGranted(Authentication authentication, Collection<String> auth
     }
 
     private boolean hasAuthority(Authentication authentication, Collection<String> authorities) {
-        for (String authority : authentication.getAuthorities()) {
-            if (authorities.contains(authority)) {
+        Collection<GrantedAuthority> reachableAuthorities = getReachableAuthorities(authentication);
+        for (GrantedAuthority authority : reachableAuthorities) {
+            if (authorities.contains(authority.getAuthority())) {
                 return true;
             }
         }
 
         return false;
     }
+
+    private Collection<GrantedAuthority> getReachableAuthorities(Authentication authentication) {
+        return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
+    }
 }

리뷰: ```suggestion
        this(new NullRoleHierarchy());
```

소소하지만.. 주 생성자를 정해 하나의 생성자에서만 초기화를 담당하면 유지보수성을 높일 수 있습니다!