ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,24 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.matcher.RequestMatcherEntry;
+
+import java.util.List;
+
+public class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+    private final List<RequestMatcherEntry<AuthorizationManager>> mappings;
+
+    public RequestMatcherDelegatingAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager>> mappings) {
+        this.mappings = mappings;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) {
+        return mappings.stream()
+                .filter((it) -> it.getRequestMatcher().matches(request))
+                .map((it) -> it.getEntry().check(authentication, request))
+                .findFirst()
+                .orElse(AuthorizationDecision.deny());
+    }
+}

리뷰: RequestAuthorizationContext는 AuthorizationManager 인가에 사용될 VO 군요!
@@ -1,34 +1,30 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
 public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+
+    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+
+    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
+        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
 
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
+        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+
+        if (authorizationDecision.isDeny()) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return;
         }

리뷰: 요청당 한번에 필터를 수행하기 위함입니다.
인가는 요청당 한번만 수행하는게 합리적일꺼 같아서 OncePerRequestFilter를 사용하였습니다
@@ -1,34 +1,30 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
 public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+
+    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+
+    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
+        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
 
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
+        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+
+        if (authorizationDecision.isDeny()) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return;
         }

리뷰: 인터페이스를 사용하면 구현체만 바꿔서 주입하면 로직변경 없이 확장에 열려있게 됩니다.
현 필터에서는 RequestMatcherDelegatingAuthorizationManager의 특화된 로직이 없다보니 좀더 확장성있게 

`private final AuthorizationManager<HttpServletRequest> authorizationManager` 인터페이스로 의존해도 되겠네요!
@@ -0,0 +1,15 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    AuthorizationDecision check(Authentication authentication, T object);
+
+    default void verify(Authentication authentication, T object) throws ForbiddenException{
+        if (check(authentication, object).isDeny()) {
+            throw new ForbiddenException();
+        }
+    }
+}

리뷰: 앗 필터에서는 아무런 처리가 없군요. ..

다음 단계에 수정하겠습니다
@@ -1,34 +1,30 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
 public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+
+    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+
+    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
+        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
 
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
+        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+
+        if (authorizationDecision.isDeny()) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return;
         }

리뷰: AuthorizationFilter는 리디렉션이나 내부 요청(FORWARD, INCLUDE 등)이 발생할 수 있습니다.
따라서 요청당 한 번만 실행되면 필요한 권한 검사가 누락될 위험이 있습니다. 

그리고 하나의 요청이 여러 경로를 거칠 수 있으며, 각 경로에서 개별적인 권한 검사가 필요할 수도 있어서 GenericFilterBean을 사용하여 구현하는 것을 권장합니다!

자세한 내용을 찾아보고 싶으시다면 OncePerRequestFilter에서 ALREADY_FILTERED_SUFFIX를 활용하여 어떻게 중복 요청을 회피하는지 그리고 어떤 경우에 여러 경로를 거치는 지 확인해보시면 좋습니다 :)
@@ -1,34 +1,30 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
 public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+
+    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+
+    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
+        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
 
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
+        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+
+        if (authorizationDecision.isDeny()) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return;
         }

리뷰: > AuthorizationFilter는 리디렉션이나 내부 요청(FORWARD, INCLUDE 등)이 발생할 수 있습니다.

앗 이부분은 생각하지 못했었네요!..
@@ -0,0 +1,34 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private final AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager;
+
+    public SecuredAuthorizationManager(AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager) {
+        this.authorityAuthorizationManager = authorityAuthorizationManager;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation methodInvocation) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        Method method = methodInvocation.getMethod();
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = method.getAnnotation(Secured.class);
+            return authorityAuthorizationManager.check(authentication, Set.of(secured.value()));
+        }
+
+        return AuthorizationDecision.granted();
+    }
+}

리뷰: 조금 헷갈리실 수 있을 법한 부분이라고 생각해요. 두가지를 분리해놓고 생각해보면,

`SecuredAuthorizationManager`는 `@Secured` 어노테이션을 보고 인가 과정을 체크하는 것이고, `AuthorityAuthorizationManager `는 명확하게 인가 처리를 직접 처리하는 객체에요.

그래서 사실 어떻게보면 `SecuredAuthorizationManager`와 `AuthorityAuthorizationManager`는 분리되어있다기보다는 실제로는 `SecuredAuthorizationManager`가 `AuthorityAuthorizationManager`를 가지고 있고, `@Secured`에서 꺼낸 것을 가지고 `AuthorityAuthorizationManager`로 다시 검증을 진행하고 있다고 볼 수 있어요. 명확하게 용도 자체가 분리되어있고, 공통화된 인가에 대한 처리를 `SecuredAuthorizationManager`가 사용하는거죠. (실제로는 단일 권한만 검증하기 보다는 `Authorities`로 다중 권한을 가지고 체크합니다.)


https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java
@@ -1 +1,39 @@
 # spring-security-authorization
+
+## 실습
+
+1. [x] GET /members/me 엔드포인트 구현 및 테스트 작성
+2. [x] 권한 검증 로직을 AuthorizationFilter로 리팩터링
+
+## 🚀 1단계 - AuthorizationManager를 활용
+
+요구사항
+
+- [x] AuthorizationManager 를 활용하여 인가 과정 추상화
+- [x] 인가를 처리해줄 AuthorizationManager 생성
+- [x] RequestMatcherDelegatingAuthorizationManager 를 통한 AuthorizationManager 한번에 관리?
+- [x] 인가 과정을 추상화한 AuthorizationManager 를 작성한다. 이 때 필요한 AuthorizationDecision도 함께 작성한다. (실제 AuthorizationManager에는
+  verify도 있는데 이 부분에 대한 구현은 선택)
+- [x] SecuredMethodInterceptor와 Authorization Filter에서 작성된 인가 로직을 AuthorizationManager로 리팩터링 한다.
+
+## 🚀 2단계 - 요청별 권한 검증 정보 분리
+
+요구사항
+
+- [x] 요청별 권한 검증 정보를 별도의 객체로 분리하여 관리
+- [x] RequestMatcherRegistry와 RequestMatcher를 작성하고, RequestMatcher의 구현체를 작성한다.
+- [x] AnyRequestMatcher: 모든 경우 true를 리턴한다.
+- [x] MvcRequestMatcher: method와 pattern(uri)가 같은지 비교하여 리턴한다.
+- [x] RequestMatcherEntry의 T entry는 아래에 해당되는 각 요청별 인가 로직을 담당하는 AuthorizationManager가 된다.
+- [x] /login은 모든 요청을 받을 수 있도록 PermitAllAuthorizationManager로 처리
+- [x] /members/me는 인증된 사용자만에게만 권한을 부여하기 위해 AuthenticatedAuthorizationManager로 처리
+- [x] /members는 "ADMIN" 사용자만에게만 권한을 부여하기 위해 HasAuthorityAuthorizationManager로 처리
+- [x] 그 외 모든 요청은 권한을 제한하기 위해 DenyAllAuthorizationManager로 처리
+
+아래 객체와 시큐리티 코드 확인
+// SpEL
+// Role Authority
+// Role Hierarchy
+// AuthoritiesAuthorizationManager
+// SecureMethodSecurityConfiguration
+// SecuredAuthorizationManager

리뷰: 필요 요구사항 잘 정리해주셨네요 👍
@@ -0,0 +1,25 @@
+package nextstep.security.authorization;
+
+public class AuthorizationDecision {
+
+    private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
+    private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);
+
+    private final boolean isGranted;
+
+    public static AuthorizationDecision granted() {
+        return GRANTED;
+    }
+
+    public static AuthorizationDecision denied() {
+        return DENIED;
+    }
+
+    protected AuthorizationDecision(final boolean isGranted) {
+        this.isGranted = isGranted;
+    }
+
+    public boolean isDenied() {
+        return !isGranted;
+    }
+}

리뷰: 메소드로 정의할 수도 있지만 결국 동일한 객체를 여러번 생성하는 것이기 때문에 불변한 객체를 만들 수 있는 현재 상황이라면 단순히 상수로 만들고 재사용하는 것도 방법이 되겠네요 :)
@@ -1,38 +1,28 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.manager.RequestMatcherDelegatingAuthorizationManager;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
-public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+public class AuthorizationFilter extends OncePerRequestFilter {
+
+    private final RequestMatcherDelegatingAuthorizationManager authorizationManager;
+
+    public AuthorizationFilter(RequestMatcherDelegatingAuthorizationManager authorizationManager) {
+        this.authorizationManager = authorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
-
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
-            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
-            return;
-        }
-
+        authorizationManager.verifyInFilter(request, authentication);
         filterChain.doFilter(request, response);
     }
 }

리뷰: 인가권한이 거절되었는데 `AuthenticationException`인 인증 에러가 발생하는 것이 맞을까요?
@@ -0,0 +1,31 @@
+package nextstep.security.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod httpMethod;
+    private final String[] requestURIs;
+
+    public MvcRequestMatcher(HttpMethod httpMethod, String... requestURIs) {
+        this.httpMethod = httpMethod;
+        this.requestURIs = requestURIs;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        boolean matchMethod = httpMethod.name().equalsIgnoreCase(request.getMethod());
+
+        boolean matchURI = false;
+        final String requestURI = request.getRequestURI();
+        for (String uri : requestURIs) {
+            if (uri.equalsIgnoreCase(requestURI)) {
+                matchURI = true;
+                break;
+            }
+        }
+
+        return matchMethod && matchURI;
+    }
+}

리뷰: 실제 spring security에서는 성능의 문제로 인해 stream 사용을 제한하고 있습니다.

https://github.com/spring-projects/spring-security/issues/7154
@@ -0,0 +1,34 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private final AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager;
+
+    public SecuredAuthorizationManager(AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager) {
+        this.authorityAuthorizationManager = authorityAuthorizationManager;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation methodInvocation) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        Method method = methodInvocation.getMethod();
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = method.getAnnotation(Secured.class);
+            return authorityAuthorizationManager.check(authentication, Set.of(secured.value()));
+        }
+
+        return AuthorizationDecision.granted();
+    }
+}

리뷰: 질문주신 내용에 답변 달아두었던 것처럼 `AuthoritiesAuthorizationManager`로 구현하는 것까지는 아니어도 인가부분에 대해서 `AuthorityAuthorizationManager`를 사용하는 것으로 변경해보면 좋을 것 같아요.
@@ -0,0 +1,65 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.ForbiddenException;
+
+import java.util.Collection;
+import java.util.Set;
+
+public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, T authorities) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        boolean hasNotRole = false;
+
+        if (authorities instanceof Collection<?> collection) {
+            hasNotRole = roleCheckByCollection(authentication, collection);
+        } else if (authorities instanceof String authority) {
+            hasNotRole = roleCheckByString(authentication, authority);
+        }
+
+        if (hasNotRole) {
+            throw new ForbiddenException();
+        }
+
+        return AuthorizationDecision.granted();
+    }
+
+    private boolean roleCheckByCollection(Authentication authentication, Collection<?> requiredAuthorities) {
+        if (requiredAuthorities == null || requiredAuthorities.isEmpty()) {
+            return false;
+        }
+
+        Set<String> userAuthorities = authentication.getAuthorities();
+        if (userAuthorities == null || userAuthorities.isEmpty()) {
+            return true;
+        }
+
+        if (requiredAuthorities.iterator().next() instanceof String) {
+
+            @SuppressWarnings("unchecked")
+            Collection<String> requires = (Collection<String>) requiredAuthorities;
+            for (String requiredAuthority : requires) {
+                if (userAuthorities.contains(requiredAuthority)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    private boolean roleCheckByString(Authentication authentication, String requiredAuthority) {
+        Set<String> userAuthorities = authentication.getAuthorities();
+        if (userAuthorities == null || userAuthorities.isEmpty()) {
+            return true;
+        }
+
+        return !userAuthorities.contains(requiredAuthority);
+    }
+}

리뷰: ```suggestion
public class AuthorityAuthorizationManager implements AuthorizationManager<T> {
```
위에서 나열한 이유에서 사실 `HttpServletRequest`보다는 제네릭 타입을 활용하여 다른 곳에서 활용할 수 있게끔 하는 것이 좋겠죠.
@@ -0,0 +1,18 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    default void verify(Authentication authentication, T object) {
+        AuthorizationDecision decision = check(authentication, object);
+        if (decision != null && decision.isDenied()) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    AuthorizationDecision check(Authentication authentication, T object);
+}

리뷰: 선택사항에 나와있는 verify는 구현하지 않으셔도 되긴 하지만, check와 verify는 각각 어떤 상황에서 사용할 해야 좋은지 세준님의 의견은 어떠신가요?