ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,47 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.*;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+import java.util.List;
+
+public class FilterChainProxy extends GenericFilterBean {
+
+    private final List<SecurityFilterChain> filterChains;
+
+    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
+        this.filterChains = filterChains;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+        for (SecurityFilterChain filterChain : filterChains) {
+            List<Filter> filters = filterChain.filters();
+            VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filters, chain);
+            virtualFilterChain.doFilter(servletRequest, servletResponse);
+        }
+    }
+
+    public static class VirtualFilterChain implements FilterChain {
+
+        private final List<Filter> filters;
+        private final FilterChain originalChain;
+        private int currentPosition = 0;
+
+        public VirtualFilterChain(List<Filter> filters, FilterChain originalChain) {
+            this.filters = filters;
+            this.originalChain = originalChain;
+        }
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+            if (currentPosition >= filters.size()) {
+                originalChain.doFilter(request, response);
+            } else {
+                Filter filter = filters.get(currentPosition++);
+                filter.doFilter(request, response, this);
+            }
+        }
+    }
+}

리뷰: 지금은 모든 체인의 필터를 실행하고 있는데요. 지금 프로젝트에선 하나의 필터 체인만 사용하고 있지만, 여러 필터 체인을 사용할 수 경우 문제가 생길 수 있을 것 같은데 어떻게 생각하시나요?
@@ -0,0 +1,25 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class ExceptionHandlerFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
+        try {
+            chain.doFilter(request, response);
+        } catch (AuthenticationException e) {
+            SecurityContextHolder.clearContext();
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        }
+    }
+}

리뷰: > 예외 발생 시 clearContext는 ExceptionHandlerFilter에서 수행하도록 구현했고, 요청 완료 후 clearContext는 SecurityContextHolderFilter에서 수행하도록 구현했습니다. 이게 맞는 방식인지 궁금합니다.

Filter의 역할을 잘 나눠주셨어요 👍 

조금 더 코드를 발전시켜 본다면 예외 상황에 따른 예외 처리가 추가되고, SecurityContextHolder와 관련된 모든 역할은 SecurityContextHolderFilter에서 담당하도록 해보면 좋을 것 같습니다!
@@ -0,0 +1,9 @@
+package nextstep.security;
+
+public interface Authentication {
+    Object getPrincipal();
+
+    Object getCredentials();
+
+    boolean isAuthenticated();
+}

리뷰: > security 패키지 하위의 패키지를 어떻게 설정 (네이밍) 해야 할지 고민이 되었습니다.

filter를 패키지로 만들어주신 것처럼, 동일한 맥락을 가진 구성 요소들을 패키지로 묶고 공통된 특징을 기준으로 네이밍 해보면 좋을 것 같아요.
context와 authentication에 대한 부분만 모아보면 어떨까요?
@@ -0,0 +1,35 @@
+package nextstep.security;
+
+import nextstep.security.domain.MemberDetail;
+import nextstep.security.domain.MemberDetailService;
+import nextstep.security.exception.AuthenticationException;
+
+public class DaoAuthenticationProvider implements AuthenticationProvider {
+
+    private final MemberDetailService memberDetailService;
+
+    public DaoAuthenticationProvider(MemberDetailService memberDetailService) {
+        this.memberDetailService = memberDetailService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) {
+        if (authentication.isAuthenticated()) {
+            return authentication;
+        }
+
+        MemberDetail member = memberDetailService.findByUsername((String) authentication.getPrincipal());
+        if (!member.isCorrectPassword((String) authentication.getCredentials())) {
+            throw new AuthenticationException();
+        }
+
+        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(member, null);
+        authenticationToken.setAuthenticated(true);
+        return authenticationToken;
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
+    }
+}

리뷰: > 인증 로직을 AuthenticationProvider에서 수행하도록 구현했는데, username, password를 파싱하는 로직도 인증 로직에 포함이 되어야 하는지 고민이 되었습니다. (Provider에서 파싱을 수행한다면 Provider 구현체를 2개 만들어야 한다고 생각했습니다. 일단 파싱은 Filter에서 수행하도록 구현했습니다.)

좋은 고민을 하셨네요 👍 일반적으로 구현해주신 것처럼 파싱의 역할은 Filter에서 하는 것이 일반적인 설계입니다.
Filter는 HTTP 요청을 처리하는 역할을 가지고 있고, AuthenticationProvider는 인증을 담당한다고 봤을 때 구현을 잘 해주셨네요 ㅎㅎ
@@ -0,0 +1,51 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.Authentication;
+import nextstep.security.AuthenticationManager;
+import nextstep.security.SecurityContextHolder;
+import nextstep.security.UsernamePasswordAuthenticationToken;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class LoginAuthenticationFilter extends GenericFilterBean {
+
+    public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
+    private final AuthenticationManager authenticationManager;
+
+    public LoginAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        if (notTarget(httpRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        Authentication authentication = authenticationManager.authenticate(getAuthentication(httpRequest));
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        httpRequest.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
+
+        chain.doFilter(request, response);
+    }
+
+    private static boolean notTarget(HttpServletRequest httpRequest) {
+        return !httpRequest.getRequestURI().startsWith("/login");
+    }
+
+    private static UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest httpRequest) {
+        Map<String, String[]> parameterMap = httpRequest.getParameterMap();
+        String username = parameterMap.get("username")[0];
+        String password = parameterMap.get("password")[0];
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+}

리뷰: SPRING_SECURITY_CONTEXT_KEY를 기반으로 처리하는 역할은 SecurityContextRepository에게 넘겨주면 좋겠네요!
@@ -0,0 +1,40 @@
+package nextstep.security.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.security.SecurityContext;
+import nextstep.security.SecurityContextHolder;
+import nextstep.security.SecurityContextRepository;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class SecurityContextHolderFilter extends GenericFilterBean {
+
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        SecurityContext securityContext = securityContextRepository.loadContext(httpRequest);
+        SecurityContextHolder.setContext(securityContext);
+
+        try {
+            chain.doFilter(httpRequest, response);
+        } finally {
+            SecurityContext context = SecurityContextHolder.getContext();
+            securityContextRepository.saveContext(context, httpRequest, httpResponse);
+//            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: > 4단계 SecurityContextHolderFilter 구현 중 clearContext를 SecurityContextHolderFilter에서 수행하는 게 맞다고 생각했는데, clearContext를 수행하면 테스트가 실패합니다. 일단 제출을 위해 주석처리했는데, 테스트가 실패하는 이유가 궁금합니다.

```suggestion
            securityContextRepository.saveContext(securityContext, httpRequest, httpResponse);
            SecurityContextHolder.clearContext();
```

위와 같이 수정하면 잘 동작할 것 같은데 왜 그럴지 고민해보아요 :)
@@ -1 +1,60 @@
 # spring-security-authentication
+
+## 1단계 - SecurityFilterChain 적용
+
+인터셉터로 구현한 인증 로직을 필터 형태로 변형한다.
+인증 로직이 담긴 필터를 바로 서블릿 필터로 등록하지 않고 아래 이미지를 참고하여 별도의 필터 체인을 구축하여 구현한다.
+주요 클래스: DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain, DefaultSecurityFilterChain, VirtualFilterChain(FilterChainProxy의 내부 클래스)
+
+![SecurityFilterChain.png](docs-img/SecurityFilterChain.png)
+
+### 요구 사항 
+- [X]  기존 Interceptor 인증 로직 변환
+    - 제공된 Interceptor 기반 인증 로직을 Filter로 변환
+    - 변환된 Filter는 HttpServletRequest와 HttpServletResponse를 처리하며, 인증 여부를 결정
+    - 요청이 인증되지 않은 경우, 적절한 HTTP 상태 코드(예: 401 Unauthorized) 반환
+    - 인증된 사용자 정보는 요청 객체에 추가하여 이후 필터에서 접근 가능하도록 설정
+- [X]  DelegatingFilterProxy 설정
+    - Spring Bean으로 DelegatingFilterProxy를 등록하고 이를 통해 FilterChainProxy를 호출하도록 설정
+    - DelegatingFilterProxy는 Servlet 컨테이너의 Filter와 Spring 컨텍스트를 연결
+- [X]  FilterChainProxy 구현
+    - FilterChainProxy는 요청 URI에 따라 적절한 SecurityFilterChain을 선택하여 실행
+    - SecurityFilterChain은 Filter 리스트를 포함하며 요청을 처리
+- [X]  SecurityFilterChain 리팩터링
+    - 각 SecurityFilterChain은 역할에 따라 다른 필터 세트를 관리
+    - 인증(Authentication) 필터와 권한(Authorization) 필터를 포함하도록 구성
+
+## 2단계 -  AuthenticationManager 적용
+
+다양한 인증 방식을 처리하기 위해 AuthenticationManager를 활용하여 인증 과정을 추상화한다.
+기존의 인증 로직을 분리하고, 인증 방식을 유연하게 확장 가능하도록 구조화한다.
+주요 클래스: AuthenticationManager, ProviderManager, AuthenticationProvider, DaoAuthenticationProvider
+
+![AuthenticationManager.png](docs-img/AuthenticationManager.png)
+
+### 요구 사항
+
+- [X] Authentication와 UsernamePasswordAuthenticationToken 구현
+- [X] 제공된 AuthenticationManager를 기반으로 ProviderManager 구현
+- [X] 제공된 AuthenticationProvider를 기반으로 DaoAuthenticationProvider 구현
+- [X] 기존 인증 필터에서 인증 로직 분리 및 AuthenticationManager로 통합
+
+## 3단계 - SecurityContextHolder 적용
+- 인증 성공 후 생성된 Authentication 객체를 기존의 세션 방식 대신 스레드 로컬(Thread Local)에 보관하도록 변경한다.
+- 각 필터에서 스레드 로컬에 보관된 인증 정보를 접근할 수 있도록 SecurityContextHolder와 SecurityContext 구조를 구현한다.
+
+![SecurityContextHolder.png](docs-img/SecurityContextHolder.png)
+
+### 요구사항 
+-[X] SecurityContext 및 SecurityContextHolder 작성
+-[X] BasicAuthenticationFilter에서 SecurityContextHolder 활용
+-[X] 기존 세션 방식에서 스레드 로컬 방식으로 인증 정보 관리 변경
+
+## 4단계 - SecurityContextHolderFilter 구현
+* 세션에 보관된 인증 정보를 SecurityContextHolder로 옮기기 위한 SecurityContextHolderFilter를 구현한다.
+
+### 요구 사항
+- [X] SecurityContextRepository 인터페이스를 기반으로 HttpSessionSecurityContextRepository 구현
+- [X] SecurityContextHolderFilter 작성 및 필터 체인에 등록
+- [X] login_after_members 테스트로 동작 검증
+

리뷰: 요구사항을 아주 꼼꼼하게 작성해 주셨네요! 👍
@@ -0,0 +1,34 @@
+package nextstep.security.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.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.context.SecurityContextRepository;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class SecurityContextHolderFilter extends GenericFilterBean {
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        SecurityContext securityContext = securityContextRepository.loadContext((HttpServletRequest) request);
+        SecurityContextHolder.setContext(securityContext);
+
+        chain.doFilter(request,response);
+
+        SecurityContext context = SecurityContextHolder.getContext();
+        securityContextRepository.saveContext(context, (HttpServletRequest) request, (HttpServletResponse) response);
+        SecurityContextHolder.clearContext();
+    }
+}

리뷰: 현재 SecurityContextHolderFilter에서 securityContextRepository.saveContext(context, request, response);를 호출하는 방식은 Spring Security의 일반적인 흐름과 맞지 않을 것 같아요 😢
인증이 이루어지지 않았음에도 불구하고 SecurityContext를 저장할 가능성이 있을 것 같은데 개선해보면 어떨까요? 😄
@@ -0,0 +1,46 @@
+package nextstep.security.authentication;
+
+import nextstep.security.UserDetails;
+import nextstep.security.UserDetailsService;
+
+import java.util.Objects;
+
+public class DaoAuthenticationProvider implements AuthenticationProvider {
+    private final UserDetailsService userDetailsService;
+
+    public DaoAuthenticationProvider(UserDetailsService userDetailsService) {
+        this.userDetailsService = userDetailsService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        check(authentication);
+
+        final String username = authentication.getPrincipal().toString();
+        final String password = authentication.getCredentials().toString();
+        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+
+        if (Objects.equals(password, userDetails.getPassword())) {
+            return UsernamePasswordAuthenticationToken.ofAuthenticated(username, password);
+        }
+
+        throw new AuthenticationException();
+    }
+
+    private static void check(Authentication authentication) {
+        if (authentication == null) {
+            throw new AuthenticationException();
+        }
+        if (authentication.getPrincipal() == null) {
+            throw new AuthenticationException();
+        }
+        if (authentication.getCredentials() == null) {
+            throw new AuthenticationException();
+        }
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return authentication == UsernamePasswordAuthenticationToken.class;
+    }
+}

리뷰: 해당 메서드에 static을 사용하신 이유가 있나요?
@@ -0,0 +1,46 @@
+package nextstep.security.authentication;
+
+import nextstep.security.UserDetails;
+import nextstep.security.UserDetailsService;
+
+import java.util.Objects;
+
+public class DaoAuthenticationProvider implements AuthenticationProvider {
+    private final UserDetailsService userDetailsService;
+
+    public DaoAuthenticationProvider(UserDetailsService userDetailsService) {
+        this.userDetailsService = userDetailsService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        check(authentication);
+
+        final String username = authentication.getPrincipal().toString();
+        final String password = authentication.getCredentials().toString();
+        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+
+        if (Objects.equals(password, userDetails.getPassword())) {
+            return UsernamePasswordAuthenticationToken.ofAuthenticated(username, password);
+        }
+
+        throw new AuthenticationException();
+    }
+
+    private static void check(Authentication authentication) {
+        if (authentication == null) {
+            throw new AuthenticationException();
+        }
+        if (authentication.getPrincipal() == null) {
+            throw new AuthenticationException();
+        }
+        if (authentication.getCredentials() == null) {
+            throw new AuthenticationException();
+        }
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return authentication == UsernamePasswordAuthenticationToken.class;
+    }
+}

리뷰: == 연산자를 사용하여 UsernamePasswordAuthenticationToken과 직접 비교하고 있는데요,
isAssignableFrom() 로 비교하는 것과 어떠한 차이가 있을까요? 😄
@@ -0,0 +1,21 @@
+package nextstep.security.context;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
+    private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
+
+    @Override
+    public SecurityContext loadContext(HttpServletRequest request) {
+        HttpSession session = request.getSession();
+        return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
+    }
+
+    @Override
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+        HttpSession session = request.getSession();
+        session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
+    }
+}

리뷰: 현재 request.getSession()을 호출하면 세션이 존재하지 않을 경우 자동으로 새 세션을 생성하게 됩니다.
세션이 필요하지 않은 요청에도 불필요한 세션이 생성될 수 있을 것 같은데, getSession(false)를 사용하면 어떤 차이가 있을까요? 🤔
기존 세션이 있는 경우에만 SecurityContext를 로드하도록 개선해보면 어떨까요? 😄
@@ -0,0 +1,60 @@
+package nextstep.security.filter;
+
+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.authentication.AuthenticationManager;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.util.Base64Convertor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class BasicAuthenticationFilter extends OncePerRequestFilter {
+    private static final String BASIC_PREFIX = "basic ";
+
+    private final AuthenticationManager authenticationManager;
+
+    public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
+        if (!isNotBasicAuth(authorization)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        try {
+            String[] usernameAndPassword = extractUsernameAndPassword(authorization);
+            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(usernameAndPassword[0], usernameAndPassword[1]);
+            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
+
+            SecurityContextHolder.getContext().setAuthentication(authenticate);
+
+            filterChain.doFilter(request, response);
+        } catch (Exception e) {
+            SecurityContextHolder.clearContext();
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        }
+    }
+
+    private String[] extractUsernameAndPassword(String authorization) {
+        String base64Credentials = authorization.substring(BASIC_PREFIX.length());
+        String decodedString = Base64Convertor.decode(base64Credentials);
+        return decodedString.split(":");
+    }
+
+    private boolean isNotBasicAuth(String authorization) {
+        if (authorization == null) {
+            return false;
+        }
+        return authorization.toLowerCase().startsWith(BASIC_PREFIX);
+    }
+}

리뷰: isNotBasicAuth() 메서드의 네이밍을 보면
Basic 인증이 아닐 경우 true를 반환해야 할 것 같지만, 
현재 구현에서는 Basic 인증일 때 true를 반환하고 있네요 😄
@@ -0,0 +1,19 @@
+package nextstep.security.authentication;
+
+import nextstep.security.role.GrantedAuthority;
+
+import java.util.List;
+
+public interface Authentication {
+    String getPrincipal();
+
+    String getCredentials();
+
+    boolean isAuthenticated();
+
+    List<GrantedAuthority> getAuthorities();
+
+    void addAuthority(GrantedAuthority grantedAuthority);
+
+    public boolean isNoPermission();
+}

리뷰: app 패키지는 일반적으로 구현하는 애플리케이션이고, security 패키지는 프레임워크의 설계를 가지게 되는데요.
그런 관점에서 지금의 Role은 NORMAL, ADMIN만 지원하고 있는데, 프레임워크를 사용하는 입장에서는 두 타입 이외에 많은 케이스가 존재할 것 같아요.
프레임워크라는 관점에서 어떻게 확장 포인트를 가져갈 수 있을까요?
@@ -0,0 +1,30 @@
+package nextstep.app.domain;
+
+import nextstep.security.core.uesrdetails.UserDetails;
+import nextstep.security.core.uesrdetails.UserDetailsService;
+import nextstep.security.exception.AuthenticationException;
+import nextstep.security.role.Role;
+
+public class CustomUserDetailsService implements UserDetailsService {
+    private final MemberRepository memberRepository;
+
+    public CustomUserDetailsService(final MemberRepository memberRepository) {
+        this.memberRepository = memberRepository;
+    }
+
+    @Override
+    public UserDetails loadUserByUsername(final String username) {
+        final CustomMember customMember = fetchUser(username);
+
+        return customMember;
+    }
+
+    private CustomMember fetchUser(final String username) {
+        final CustomMember customMember = memberRepository.findByEmail(username)
+                .map(CustomMember::new)
+                .orElseThrow(() -> new AuthenticationException());
+
+        customMember.addAuthority(new Role("NORMAL"));
+        return customMember;
+    }
+}

리뷰: ```suggestion
        return memberRepository.findByEmail(username)
                .map(CustomMember::new)
                .orElseThrow(() -> new IllegalArgumentException("해당하는 사용자를 찾을 수 없습니다."));
```

소소한 가독성 상승을 해볼 수 있겠네요~