ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,68 @@
+package nextstep.authentication.filter;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.authentication.Authentication;
+import nextstep.authentication.AuthenticationManager;
+import nextstep.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.authentication.context.SecurityContextHolder;
+import nextstep.authentication.exception.AuthenticationException;
+import nextstep.authentication.util.matcher.MvcRequestMatcher;
+import org.springframework.http.HttpMethod;
+
+import java.io.IOException;
+
+public class FormLoginAuthenticationFilter implements Filter {
+
+    private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.POST, "/login");
+
+    private final AuthenticationManager authenticationManager;
+
+    public FormLoginAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) servletRequest)) {
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+        try {
+
+            Authentication authenticationResult = attemptAuthentication(request, response);
+            if (authenticationResult == null) {
+                filterChain.doFilter(servletRequest, servletResponse);
+                return;
+            }
+
+            SecurityContextHolder.getContext().setAuthentication(authenticationResult);
+
+        } catch (Exception e) {
+            SecurityContextHolder.clearContext();
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+
+            return;
+        }
+
+        filterChain.doFilter(servletRequest, servletResponse);
+    }
+
+    private Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+
+        String username = request.getParameter("username");
+        String password = request.getParameter("password");
+        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
+
+        return authenticationManager.authenticate(authRequest);
+    }
+}

리뷰: 인증이 null일 때 다음 필터로 이동할 수 있도록 `filterChain.doFilter(servletRequest, servletResponse);` 코드를 추가했습니다!
@@ -0,0 +1,10 @@
+package nextstep.authentication;
+
+public interface Authentication {
+
+    String getPrincipal();
+
+    String getCredentials();
+
+    boolean isAuthenticated();
+}

리뷰: 실제 패키지 구조는 `authentication`안에 모든 것이 있는 것이 아닌 웹에 대한 호출을 처리하는 `FilterChainProxy`와 같은 클래스들은 `web`패키지에, `UserDetails`와 같이 유저정보를 표현하는 클래스들은 별도로 `userdetails`에서 관리합니다.
@@ -0,0 +1,27 @@
+package nextstep.authentication.context;
+
+public class SecurityContextHolder {
+
+    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
+
+    public static SecurityContext getContext() {
+        SecurityContext context = contextHolder.get();
+        if (context == null) {
+            context = createEmptyContext();
+            contextHolder.set(context);
+        }
+        return context;
+    }
+
+    public static void setContext(SecurityContext context) {
+        contextHolder.set(context);
+    }
+
+    public static void clearContext() {
+        contextHolder.remove();
+    }
+
+    public static SecurityContext createEmptyContext() {
+        return new SecurityContextImpl();
+    }
+}

리뷰: `SecurityContextHolder`라는 구현체를 사용하게 되는 경우에는 이 클래스를 사용하는 다른 클래스에서 구현체에 의존하여 테스트하기 어려운 구조가 될 수 있어 실제로는 이를 래핑한 인터페이스를 사용하게끔 구성되어있습니다.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java
@@ -0,0 +1,19 @@
+package nextstep.authentication.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
+public class AuthenticationException extends RuntimeException {
+
+    public AuthenticationException() {
+    }
+
+    public AuthenticationException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+
+    public AuthenticationException(String msg) {
+        super(msg);
+    }
+}

리뷰: 사용하고 있지 않고 있는 것 같네요.
@@ -2,7 +2,10 @@
 
 import nextstep.app.domain.Member;
 import nextstep.app.domain.MemberRepository;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authorization.Secured;
+import nextstep.security.context.SecurityContextHolder;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -30,4 +33,15 @@ public ResponseEntity<List<Member>> search() {
         List<Member> members = memberRepository.findAll();
         return ResponseEntity.ok(members);
     }
+
+    @GetMapping("/members/me")
+    public ResponseEntity<Member> me() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        String email = authentication.getPrincipal().toString();
+        Member member = memberRepository.findByEmail(email)
+                .orElseThrow(RuntimeException::new);
+
+        return ResponseEntity.ok(member);
+    }
 }

리뷰: 이미 Filter에서 처리하고 있는데 불필요한 로직이 아닐까요?
@@ -0,0 +1,20 @@
+package nextstep.security.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod method;
+    private final String pattern;
+
+    public MvcRequestMatcher(HttpMethod method, String pattern) {
+        this.method = method;
+        this.pattern = pattern;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return this.method.equals(HttpMethod.valueOf(request.getMethod())) && pattern.equals(request.getRequestURI());
+    }
+}

리뷰: ```suggestion
        return this.method.equals(HttpMethod.valueOf(request.getMethod())) && pattern.equals(request.getRequestURI());
```
null-safe 관점에서는 파라미터로 들어오는 `request.getRequestURI()`는 null일 수 있기 때문에 별도로 null 체크를 하지 않는 이상 위와 같이 표현하는 것이 npe가 발생하지 않는 안전한 코드를 만들 수 있습니다.
@@ -0,0 +1,14 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.web.AuthorizationResult;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    @Deprecated
+    AuthorizationDecision check(Authentication authentication, T object);
+
+    default AuthorizationResult authorize(Authentication authentication, T object) {
+        return check(authentication, object);
+    }
+}

리뷰: 현재 spring security를 확인해보시면 `check`는 deprecated되었는데요. `AuthorizationDecision`이라는 클래스는 구현해주신 것처럼 구현체로 되어있고, 보통의 프레임워크들은 규모가 커질수록 만들어둔 구현체들을 추상화하는 형태로 개선해나갑니다.

`check`가 deprecated됨에 따라 해당 기능이 막힌 것은 아니고 이를 추상화한 `AuthorizationResult`를 반환하는 메소드 사용을 권장하고 있으니 참고해주시면 좋을 것 같아요 :)

https://github.com/franticticktick/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java

https://github.com/spring-projects/spring-security/pull/14712
https://github.com/spring-projects/spring-security/pull/14846
@@ -0,0 +1,14 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.web.AuthorizationResult;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    @Deprecated
+    AuthorizationDecision check(Authentication authentication, T object);
+
+    default AuthorizationResult authorize(Authentication authentication, T object) {
+        return check(authentication, object);
+    }
+}

리뷰: 준형님이 생각하시기에 선택사항으로 주어진 `verfiy`는 `check`와 비교하여 어떤 상황에서 사용하면 좋을 것 같으신가요?
@@ -0,0 +1,22 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authorization.web.AuthorizationResult;
+
+public class AuthorizationDecision implements AuthorizationResult {
+    public static final AuthorizationDecision ALLOW = new AuthorizationDecision(true);
+    public static final AuthorizationDecision DENY = new AuthorizationDecision(false);
+
+    private final boolean granted;
+
+    public AuthorizationDecision(boolean granted) {
+        this.granted = granted;
+    }
+
+    public boolean isGranted() {
+        return granted;
+    }
+
+    public static AuthorizationDecision of(boolean granted) {
+        return granted ? ALLOW : DENY;
+    }
+}

리뷰: true 혹은 false만 가지는 `AuthorizationDecision`이 자주 사용되는데 사용될때마다 인스턴스화하기보다는 불변임을 활용하여 미리 만들어준 상수를 사용하도록 유도할 수 있을 것 같네요.
@@ -0,0 +1,38 @@
+package nextstep.security.authorization.web;
+
+import java.util.Collection;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationException;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.AuthorizationManager;
+
+public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+
+    private final Collection<String> authorities;
+
+    public AuthorityAuthorizationManager(Collection<String> authorities) {
+        this.authorities = authorities;
+    }
+
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, T object) {
+        if (authentication == null) {
+            throw new AuthenticationException();
+        }
+
+        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)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

리뷰: 실제 spring security에서는 성능의 문제로 인해 stream 사용을 제한하고 있습니다.

https://github.com/spring-projects/spring-security/issues/7154
@@ -0,0 +1,28 @@
+package nextstep.security.authorization.web;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.util.RequestMatcher;
+import nextstep.security.util.RequestMatcherEntry;
+
+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) {
+        for (RequestMatcherEntry<AuthorizationManager> mapping : mappings) {
+            if (mapping.getRequestMatcher().matches(request)) {
+                return mapping.getEntry().check(authentication, request);
+            }
+        }
+        return AuthorizationDecision.DENY;
+    }
+}

리뷰: getter로 호출하여 일치 여부를 확인하는 것보다는 `mapping`에서 처리하도록 수정하면 책임이 명확하게 넘어갈 수 있겠네요 :)
@@ -0,0 +1,43 @@
+package nextstep.security.authorization.method;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+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 org.aopalliance.intercept.MethodInvocation;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private AuthorityAuthorizationManager<Collection<String>> authorityAuthorizationManager;
+
+    public void setAuthorityAuthorizationManager(Collection<String> authorities) {
+        authorityAuthorizationManager = new AuthorityAuthorizationManager<>(authorities);
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Collection<String> authorities = getAuthorities(invocation);
+
+        if (authorities.isEmpty()) {
+            return null;
+        }
+        setAuthorityAuthorizationManager(authorities);
+        return authorities.isEmpty() ? null : authorityAuthorizationManager.check(authentication, authorities);
+    }
+
+    private Collection<String> getAuthorities(MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+
+        if (!method.isAnnotationPresent(Secured.class)) {
+            return Collections.emptySet();
+        }
+
+        Secured secured = method.getAnnotation(Secured.class);
+        return Set.of(secured.value());
+    }
+}

리뷰: `AuthorityAuthorizationManager`의 로직과 함께보면
```java
boolean hasAuthority = authentication.getAuthorities().stream()
        .anyMatch(authorities::contains);

return new AuthorizationDecision(hasAuthority);
```
부분이 동일한 것을 확인해볼 수 있어요. 각 `AuthorizatinManager`는 단일에서 각각 본인의 것을 모두 구성하는 것이 아닌 서로 유기적으로 결합되어 사용하기도 하는데요.

즉, `SecuredAuthorizationManager`는 `@Secured`에 있는 정보를 가지고 처리하며, authority에 대한 권한체크는 `AuthorizatinManager`가 온전히 담당하는거죠.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java

실제로는 권한체계가 다양하기 때문에 `AuthoritiesAuthorizationManager`를 사용하고 있기는 하지만 관련하여 참고하시면 이해에 도움이 되실 것 같아요.
@@ -0,0 +1,18 @@
+package nextstep.security.fixture;
+
+import java.util.Base64;
+import java.util.Set;
+import nextstep.app.domain.Member;
+
+public class MemberTestFixture {
+    public static final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN"));
+    public static final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of("USER"));
+
+    public static String createAdminToken(){
+        return Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes());
+    }
+
+    public static String createMemberToken(){
+        return Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes());
+    }
+}

리뷰: (반영하지 않으셔도 됩니다.) 개인적으로는 enum에서 지원되는 메소드들을 활용하는 경우도 많아 특정 도메인의 fixture는 enum 으로 생성하는 편입니다 😄
@@ -2,7 +2,10 @@
 
 import nextstep.app.domain.Member;
 import nextstep.app.domain.MemberRepository;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authorization.Secured;
+import nextstep.security.context.SecurityContextHolder;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -30,4 +33,15 @@ public ResponseEntity<List<Member>> search() {
         List<Member> members = memberRepository.findAll();
         return ResponseEntity.ok(members);
     }
+
+    @GetMapping("/members/me")
+    public ResponseEntity<Member> me() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        String email = authentication.getPrincipal().toString();
+        Member member = memberRepository.findByEmail(email)
+                .orElseThrow(RuntimeException::new);
+
+        return ResponseEntity.ok(member);
+    }
 }

리뷰: 앗 해당 app패키지에 컨트롤러는
강의실에서 실습이후 체리픽 해온것이라 열어볼 생각을 못했네요.
수정해두겠습니다!
@@ -0,0 +1,20 @@
+package nextstep.security.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod method;
+    private final String pattern;
+
+    public MvcRequestMatcher(HttpMethod method, String pattern) {
+        this.method = method;
+        this.pattern = pattern;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return this.method.equals(HttpMethod.valueOf(request.getMethod())) && pattern.equals(request.getRequestURI());
+    }
+}

리뷰: 간과하고 있었네요. 수정해두겠습니다! 👍
@@ -0,0 +1,14 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.web.AuthorizationResult;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    @Deprecated
+    AuthorizationDecision check(Authentication authentication, T object);
+
+    default AuthorizationResult authorize(Authentication authentication, T object) {
+        return check(authentication, object);
+    }
+}

리뷰: 오.. 불과 4달전에 업데이트된 기능이군요!
피드백 주신 대로 반영해보면서 어떤 식으로 
오픈소스가 개선되어 나가는지 체험 해 볼 수 있었네요 감사합니다.
@@ -0,0 +1,22 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authorization.web.AuthorizationResult;
+
+public class AuthorizationDecision implements AuthorizationResult {
+    public static final AuthorizationDecision ALLOW = new AuthorizationDecision(true);
+    public static final AuthorizationDecision DENY = new AuthorizationDecision(false);
+
+    private final boolean granted;
+
+    public AuthorizationDecision(boolean granted) {
+        this.granted = granted;
+    }
+
+    public boolean isGranted() {
+        return granted;
+    }
+
+    public static AuthorizationDecision of(boolean granted) {
+        return granted ? ALLOW : DENY;
+    }
+}

리뷰: 현 상황에서 메모리 효율성을 더 높일 수 있겠네요!
@@ -0,0 +1,38 @@
+package nextstep.security.authorization.web;
+
+import java.util.Collection;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationException;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.AuthorizationManager;
+
+public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+
+    private final Collection<String> authorities;
+
+    public AuthorityAuthorizationManager(Collection<String> authorities) {
+        this.authorities = authorities;
+    }
+
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, T object) {
+        if (authentication == null) {
+            throw new AuthenticationException();
+        }
+
+        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)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

리뷰: 가독성 때문에 stream을 선호하는 편인데, spring security같은 라이브러리는 성능이 중요하다보니 그렇게 된거군요..
구현체 코드 보면서 가독성이 생각만큼은 아닌것 같았는데, 그 위화감이 여기서 기인한 것 같습니다.
@@ -0,0 +1,28 @@
+package nextstep.security.authorization.web;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.util.RequestMatcher;
+import nextstep.security.util.RequestMatcherEntry;
+
+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) {
+        for (RequestMatcherEntry<AuthorizationManager> mapping : mappings) {
+            if (mapping.getRequestMatcher().matches(request)) {
+                return mapping.getEntry().check(authentication, request);
+            }
+        }
+        return AuthorizationDecision.DENY;
+    }
+}

리뷰: 음.. 제 생각으로는 `mapping`인 `RequestMatcherEntry`는 `RequestMatcher`랑 `AuthorizationManager`를
단순히 묶어주는 역할을 수행한다고 생각해서, 
해당 객체에 `matches()`의 로직까지 있으면 오히려 책임이 과해지고,
원래 RequestMatcher의 역할이 흐려질것 같아 getter로 호출해서 일치 여부를 확인했습니다.

제가 제대로 이해한거라면 진영님 생각이 궁금합니다!
@@ -0,0 +1,18 @@
+package nextstep.security.fixture;
+
+import java.util.Base64;
+import java.util.Set;
+import nextstep.app.domain.Member;
+
+public class MemberTestFixture {
+    public static final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN"));
+    public static final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of("USER"));
+
+    public static String createAdminToken(){
+        return Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes());
+    }
+
+    public static String createMemberToken(){
+        return Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes());
+    }
+}

리뷰: 3단계 수행하면서 변경해 보겠습니다! 😄
@@ -0,0 +1,43 @@
+package nextstep.security.authorization.method;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+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 org.aopalliance.intercept.MethodInvocation;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private AuthorityAuthorizationManager<Collection<String>> authorityAuthorizationManager;
+
+    public void setAuthorityAuthorizationManager(Collection<String> authorities) {
+        authorityAuthorizationManager = new AuthorityAuthorizationManager<>(authorities);
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Collection<String> authorities = getAuthorities(invocation);
+
+        if (authorities.isEmpty()) {
+            return null;
+        }
+        setAuthorityAuthorizationManager(authorities);
+        return authorities.isEmpty() ? null : authorityAuthorizationManager.check(authentication, authorities);
+    }
+
+    private Collection<String> getAuthorities(MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+
+        if (!method.isAnnotationPresent(Secured.class)) {
+            return Collections.emptySet();
+        }
+
+        Secured secured = method.getAnnotation(Secured.class);
+        return Set.of(secured.value());
+    }
+}

리뷰: [83a4efd](https://github.com/next-step/spring-security-authorization/pull/15/commits/83a4efd95662b7e9d2590f72ee5a56173a16477d)
으로 반영해보았습니다!
각자의 Manager가 단일 책임이 아닌 결합해서 사용하기도 하는군요!
저도 작업하면서 같은 로직이 들어간다고 생각이 들었는데,
이런식으로도 역할과 책임 분배를 가져갈 수 있겠네요!

현재 말씀해주신 `AuthoritiesAuthorizationManager` 처럼 다양한 권한 체계는 없기 떄문에
기존 `SecuredAuthorizationManager`에서 `AuthorityAuthorizationManager`를 사용해보는 방향으로 변경해보았습니다.