ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,40 @@
+package nextstep.security.context;
+
+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 java.io.IOException;
+
+public class SecurityContextHolderFilter implements Filter {
+
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
+        throws IOException, ServletException {
+
+        try {
+            SecurityContext securityContext = securityContextRepository.loadContext((HttpServletRequest) servletRequest);
+
+            if (securityContext != null) {
+                SecurityContextHolder.setContext(securityContext);
+            }
+
+            filterChain.doFilter(servletRequest, servletResponse);
+
+            SecurityContext afterSecurityContext = SecurityContextHolder.getContext();
+            securityContextRepository.saveContext(afterSecurityContext, (HttpServletRequest) servletRequest,
+                (HttpServletResponse) servletResponse);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: 053f96ece76768a3b187188eaf41110f9593b65e `finally`를 통해 최종적으로 `clear`를 할 수 있는 구조로 변경 해 보았습니다.
@@ -0,0 +1,27 @@
+package nextstep.security.filter.converter;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+
+import java.util.Map;
+
+public class LoginFormAuthenticationConverter implements AuthenticationConverter {
+
+    @Override
+    public Authentication convert(final HttpServletRequest request) {
+        final Map<String, String[]> parameterMap = request.getParameterMap();
+
+        String username;
+        String password;
+
+        try {
+            username = parameterMap.get("username")[0];
+            password = parameterMap.get("password")[0];
+        } catch (RuntimeException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+}

리뷰: ```suggestion
        try {
            String username = parameterMap.get("username")[0];
            String password = parameterMap.get("password")[0];
            return new UsernamePasswordAuthenticationToken(username, password); 
        } catch (RuntimeException e) {
            throw new IllegalArgumentException(e);
        }
```
가 조금 더 변수를 불변하게 사용하는 안정적인 코드가 될 수 있겠네요 :)
@@ -0,0 +1,33 @@
+package nextstep.security.filter.converter;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.app.util.Base64Convertor;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+
+public class BasicAuthenticationConverter implements AuthenticationConverter {
+
+    private static final String AUTHORIZATION = "Authorization";
+
+    @Override
+    public Authentication convert(HttpServletRequest request) {
+        final String authorization = request.getHeader(AUTHORIZATION);
+
+        final String username;
+        final String password;
+
+        try {
+            final String credentials = authorization.split("Basic ")[1];
+            final String decodedString = Base64Convertor.decode(credentials);
+
+            final String[] usernameAndPassword = decodedString.split(":");
+
+            username = usernameAndPassword[0];
+            password = usernameAndPassword[1];
+        } catch (RuntimeException e) {
+            throw new IllegalArgumentException();
+        }
+
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+}

리뷰: ```suggestion
            final String username = usernameAndPassword[0];
            final String password = usernameAndPassword[1];
            return new UsernamePasswordAuthenticationToken(username, password);
        } catch (RuntimeException e) {
            throw new IllegalArgumentException();
        }
```
위와 동일합니다.
@@ -0,0 +1,33 @@
+package nextstep.security.filter.converter;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.app.util.Base64Convertor;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+
+public class BasicAuthenticationConverter implements AuthenticationConverter {
+
+    private static final String AUTHORIZATION = "Authorization";
+
+    @Override
+    public Authentication convert(HttpServletRequest request) {
+        final String authorization = request.getHeader(AUTHORIZATION);
+
+        final String username;
+        final String password;
+
+        try {
+            final String credentials = authorization.split("Basic ")[1];
+            final String decodedString = Base64Convertor.decode(credentials);
+
+            final String[] usernameAndPassword = decodedString.split(":");
+
+            username = usernameAndPassword[0];
+            password = usernameAndPassword[1];
+        } catch (RuntimeException e) {
+            throw new IllegalArgumentException();
+        }
+
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+}

리뷰: `LoginFormAuthenticationConverter`와 ` BasicAuthenticationConveter` 모두 예외가 나는 상황을 추출하기 전에 처리해보면 좋을 것 같아요.

예를들면 현재 상황에서는 `Basic` split 하고 내부적으로 알아서 예외가 터지길 기대하기보다는, 헤더의 값 안에 `Basic`으로 패턴이 없으면 실패로 fast fail 하는 방법이 있겠네요.
@@ -0,0 +1,33 @@
+package nextstep.security.filter.converter;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.app.util.Base64Convertor;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+
+public class BasicAuthenticationConverter implements AuthenticationConverter {
+
+    private static final String AUTHORIZATION = "Authorization";
+
+    @Override
+    public Authentication convert(HttpServletRequest request) {
+        final String authorization = request.getHeader(AUTHORIZATION);
+
+        final String username;
+        final String password;
+
+        try {
+            final String credentials = authorization.split("Basic ")[1];
+            final String decodedString = Base64Convertor.decode(credentials);
+
+            final String[] usernameAndPassword = decodedString.split(":");
+
+            username = usernameAndPassword[0];
+            password = usernameAndPassword[1];
+        } catch (RuntimeException e) {
+            throw new IllegalArgumentException();
+        }
+
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+}

리뷰: 표준예외를 사용하는 것도 좋지만 `convert`로직에 들어온 이상 인증이 잘못되었다는 것을 명시하기 위해서 `AuthenticationException`를 던져주어도 좋을 것 같아요.

실제로는 `Basic`스킴이 잘못되었던 것 같은 케이스들을 세분화하여 `BadCredentialsException`를 날리고 있습니다.
@@ -0,0 +1,40 @@
+package nextstep.security.context;
+
+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 java.io.IOException;
+
+public class SecurityContextHolderFilter implements Filter {
+
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
+        throws IOException, ServletException {
+
+        try {
+            SecurityContext securityContext = securityContextRepository.loadContext((HttpServletRequest) servletRequest);
+
+            if (securityContext != null) {
+                SecurityContextHolder.setContext(securityContext);
+            }
+
+            filterChain.doFilter(servletRequest, servletResponse);
+
+            SecurityContext afterSecurityContext = SecurityContextHolder.getContext();
+            securityContextRepository.saveContext(afterSecurityContext, (HttpServletRequest) servletRequest,
+                (HttpServletResponse) servletResponse);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: `loadContext`는 try-finally 안에 들어갈 필요는 없어요. 단순히 load만 하고 있기 때문에 load에서 예외가 터져도 Context를 초기화할 것이 없습니다.
@@ -0,0 +1,50 @@
+package nextstep.app.config;
+
+import java.util.List;
+import nextstep.app.MemberService;
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.filter.BasicAuthenticationFilter;
+import nextstep.security.authentication.DaoAuthenticationProvider;
+import nextstep.security.filter.DefaultSecurityFilterChain;
+import nextstep.security.filter.FilterChainProxy;
+import nextstep.security.filter.LoginFormAuthenticationFilter;
+import nextstep.security.authentication.ProviderManager;
+import nextstep.security.filter.SecurityFilterChain;
+import nextstep.security.context.HttpSessionSecurityContextRepository;
+import nextstep.security.context.SecurityContextHolderFilter;
+import nextstep.security.filter.converter.BasicAuthenticationConverter;
+import nextstep.security.filter.converter.LoginFormAuthenticationConverter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.DelegatingFilterProxy;
+
+@Configuration
+class FilterConfig {
+
+    private final MemberService memberService;
+
+    public FilterConfig(MemberService memberService) {
+        this.memberService = memberService;
+    }
+
+    @Bean
+    public DelegatingFilterProxy delegatingFilterProxy() {
+        return new DelegatingFilterProxy(new FilterChainProxy(List.of(securityFilterChain())));
+    }
+
+    @Bean
+    public SecurityFilterChain securityFilterChain() {
+        return new DefaultSecurityFilterChain(
+            List.of(
+                new SecurityContextHolderFilter(new HttpSessionSecurityContextRepository())
+                , new BasicAuthenticationFilter(authenticationManager(), new BasicAuthenticationConverter())
+                , new LoginFormAuthenticationFilter(authenticationManager(), new LoginFormAuthenticationConverter())
+            )
+        );
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager() {
+        return new ProviderManager(new DaoAuthenticationProvider(memberService));
+    }
+}

리뷰: `authenticationManager()`를 호출해서 객체를 생성하기보다는 빈을 등록해두었다면 등록된 빈을 사용하도록 변경해주어야 할 것 같아요.
@@ -0,0 +1,27 @@
+package nextstep.security.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 SecurityContext createEmptyContext() {
+        return new SecurityContextImpl();
+    }
+
+    public static void setContext(SecurityContext context) {
+        contextHolder.set(context);
+    }
+
+    public static void clearContext() {
+        contextHolder.remove();
+    }
+}

리뷰: `SecurityContextHolder`를 그대로 사용하는 Filter들이 많은데 실제로 이렇게 구성하게 되는 경우 구현체에 의존하기 때문에 테스트하기 힘든 구조가 될 수 있어요. 프레임워크들에서는 testable한 구조를 가져가는 것이 확장성 있고, 본인들의 복잡한 코드들을 안정적으로 테스트코드를 작성하기 쉽게 만들어주기 때문에 대부분의 코드를 추상화해두는 편인데요.

spring security에서도 이를 위해서 `SecurityContextHolder`를 바로 쓰기보다는 한번 더 래핑한 추상화 클래스를 두고 사용하는 점 참고하여 넘어가주시면 좋을 것 같아요.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java
@@ -1,19 +1,23 @@
 package com.example.hrr_android
 
 import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
 import android.util.Log
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
 import androidx.appcompat.app.AppCompatActivity
 import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.ui.setupWithNavController
 import com.example.hrr_android.databinding.ActivityMainBinding
 import dagger.hilt.android.AndroidEntryPoint
-import com.example.hrr_android.message.ui.MessageFragment
 import com.google.android.material.bottomnavigation.BottomNavigationView
 
 @AndroidEntryPoint
 class MainActivity : AppCompatActivity() {
-    //뷰 바인딩
-    private lateinit var binding: ActivityMainBinding
+    private lateinit var binding: ActivityMainBinding      // 뷰 바인딩
+    private var backPressedOnce = false     // 뒤로가기 버튼 상태 저장 변수
+    private val handler = Handler(Looper.getMainLooper())   // 시간 초과 처리
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -24,6 +28,21 @@ class MainActivity : AppCompatActivity() {
         //바텀 네비게이션 세팅
         initBottomNavigation()
 
+        // 시스템 뒤로가기 버튼을 감지해서 두 번 눌렀을 때 종료 실행
+        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() {
+                if (backPressedOnce) {
+                    finish() // 앱 종료
+                } else {
+                    backPressedOnce = true
+                    Toast.makeText(this@MainActivity, "\"뒤로\" 버튼 한 번 더 누르시면 종료됩니다.", Toast.LENGTH_SHORT).show()
+
+                    // 2초 후 다시 false로 변경하여 재입력 요구
+                    handler.postDelayed({ backPressedOnce = false }, 2000)
+                }
+            }
+        })
+
     }
 
     private fun initBottomNavigation(){
@@ -41,8 +60,4 @@ class MainActivity : AppCompatActivity() {
 
     }
 
-    // 프래그먼트에서 접근할 수 있도록 추가
-    fun getBinding(): ActivityMainBinding {
-        return binding
-    }
 }
\ No newline at end of file

리뷰: 뒤로가기를 두 번 눌렀을 때 종료되게끔 추가하신 거 좋은 UX 개선인 것 같아요! handler로 타임아웃 처리까지 구현하신 거 굿입니다👍
@@ -10,22 +10,23 @@ import androidx.recyclerview.widget.LinearLayoutManager
 import com.example.hrr_android.databinding.FragmentProfileChallengeBinding
 
 class ProfileChallengeFragment : Fragment() {
-    private lateinit var binding: FragmentProfileChallengeBinding       //뷰 바인딩
+    private var _binding: FragmentProfileChallengeBinding? = null       //뷰 바인딩
+    private val binding get() = _binding!!
     private var participatingChallengeList = ArrayList<Challenge>()     //참가중인 챌린지 리스트
     private var completedChallengeList = ArrayList<Challenge>()         //최근 완주한 챌린지 리스트
 
     override fun onCreateView(
         inflater: LayoutInflater, container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View {
-        binding = FragmentProfileChallengeBinding.inflate(inflater, container, false)
+        _binding = FragmentProfileChallengeBinding.inflate(inflater, container, false)
 
         //참가중인 챌린지 더미 데이터 - 테스트 시 주석 해제 or 설정
-        participatingChallengeList.apply {
-            add(Challenge("토익 800점", R.drawable.img_english_book, isCertified = false))
-            add(Challenge("토익 900점 찍기. 쫄?", R.drawable.img_english_book, isCertified = true))
-            add(Challenge("열 자 제한 테스트", R.drawable.img_english_book, isCertified = true))
-        }
+//        participatingChallengeList.apply {
+//            add(Challenge("토익 800점", R.drawable.img_english_book, isCertified = false))
+//            add(Challenge("토익 900점 찍기. 쫄?", R.drawable.img_english_book, isCertified = true))
+//            add(Challenge("열 자 제한 테스트", R.drawable.img_english_book, isCertified = true))
+//        }
 
         //최근 완주한 챌린지 더미 데이터 - 테스트 시 주석 해제 or 설정
         completedChallengeList.apply {
@@ -58,7 +59,7 @@ class ProfileChallengeFragment : Fragment() {
         binding.rvProfileCompletedChallengeContent.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
 
         //더보기 버튼 클릭 처리
-        binding.llProfileCompletedChallengeMore.setOnClickListener {
+        binding.llProfileCompletedChallengeTitle.setOnClickListener {
             val intent = Intent(requireContext(), ProfileMoreActivity::class.java)
             intent.putExtra("type", "challenge")
             startActivity(intent)
@@ -67,4 +68,9 @@ class ProfileChallengeFragment : Fragment() {
         return binding.root
     }
 
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
 }
\ No newline at end of file

리뷰: `onDestroyView()` 메서드를 추가해 주셨네요! 이전에 PR 리뷰에서 **BaseFragment**에 대해 말씀드렸던 적 있는 것 같은데, 이번에 커밋 하신 거 보고 나니까.. 저희가 뷰 바인딩 초기화랑 해제 로직이 거의 모든 프래그먼트에서 반복되고 있잖아요?! 이런 공통 로직들을 BaseFragment로 추상화해서 썼으면 코드에 누락되는 일도 없고 더 간편했었을 거 같다는 생각이 드네요..🤔 이번에 프로젝트 진행하면서 깨달았으니 다음번부터는 이점 참고해서 작업해야겠습니다🤓
@@ -0,0 +1,62 @@
+package nextstep.app;
+
+import nextstep.app.domain.Member;
+import nextstep.app.domain.MemberRepository;
+import nextstep.security.DefaultSecurityFilterChain;
+import nextstep.security.FilterChainProxy;
+import nextstep.security.SecurityFilterChain;
+import nextstep.security.filter.BasicAuthenticationFilter;
+import nextstep.security.filter.FormLoginAuthenticationFilter;
+import nextstep.security.UserDetails;
+import nextstep.security.UserDetailsService;
+import nextstep.security.util.matcher.AnyRequestMatcher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.DelegatingFilterProxy;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class FilterConfig implements WebMvcConfigurer {
+
+    private final MemberRepository memberRepository;
+
+    public FilterConfig(MemberRepository memberRepository) {
+        this.memberRepository = memberRepository;
+    }
+
+    @Bean
+    public DelegatingFilterProxy delegatingFilterProxy() {
+        return new DelegatingFilterProxy(filterChainProxy());
+    }
+
+    private FilterChainProxy filterChainProxy() {
+        return new FilterChainProxy(securityFilterChain());
+    }
+
+    private SecurityFilterChain securityFilterChain() {
+        return new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE, List.of(
+                new BasicAuthenticationFilter(userDetailsService()),
+                new FormLoginAuthenticationFilter(userDetailsService())
+        ));
+    }
+
+    private UserDetailsService userDetailsService() {
+        return username -> {
+            Member member = memberRepository.findByEmail(username)
+                    .orElseThrow(() -> new IllegalArgumentException("해당하는 사용자를 찾을 수 없습니다."));
+            return new UserDetails() {
+                @Override
+                public String getUsername() {
+                    return member.getEmail();
+                }
+
+                @Override
+                public String getPassword() {
+                    return member.getPassword();
+                }
+            };
+        };
+    }
+}

리뷰: ```suggestion
        UserDetailsService userDetailsService = userDetailsService();
        return new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE, List.of(
                new BasicAuthenticationFilter(userDetailsService),
                new FormLoginAuthenticationFilter(userDetailsService)
        ));
```
객체를 두번 생성하지 않고 변수로 빼서 한번만 사용할 수 있도록 변경할 수 있을 것 같아요 :)
@@ -1,4 +1,4 @@
-package nextstep.app.ui;
+package nextstep.security;
 
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;

리뷰: 각 클래스는 단순히 security 패키지보다는 그 클래스들에 맞는 패키지를 고려해보면 좋을 것 같아요. spring security를 참고하셔도 좋고, 예를 들면 이 클래스는 인증관련된 예외 클래스이니 `authentication`이라는 패키지가 더 어울려보이네요.
@@ -0,0 +1,34 @@
+package nextstep.security;
+
+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 org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+import java.util.List;
+
+public class FilterChainProxy extends GenericFilterBean {
+
+    private final SecurityFilterChain securityFilterChain;
+
+    public FilterChainProxy(SecurityFilterChain securityFilterChain) {
+        this.securityFilterChain = securityFilterChain;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+        List<Filter> filters = securityFilterChain.getFilters();
+
+        var virtualFilterChain = new VirtualFilterChain(filters, filterChain);
+        virtualFilterChain.doFilter(request, response);
+    }
+}

리뷰: `FilterChainProxy`는 하나의 `FilterChain`만 가지고 있지 않습니다. 1단계 이미지에 나와있는 것처럼 `FilterChainProxy`가 여러 `SecurityFilterChain`을 가지고 있는 형태로 변경해주세요 :)
@@ -0,0 +1,31 @@
+package nextstep.security;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+import java.io.IOException;
+import java.util.List;
+
+public 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;
+    }
+
+    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+        if (this.currentPosition == filters.size()) {
+            this.originalChain.doFilter(request, response);
+        } else {
+            Filter nextFilter = filters.get(currentPosition++);
+            nextFilter.doFilter(request, response, this);
+        }
+    }
+}

리뷰: ```suggestion
        if (this.currentPosition == filters.size()) {
            this.originalChain.doFilter(request, response);
            return;
        }
        Filter nextFilter = filters.get(currentPosition++);
        nextFilter.doFilter(request, response, this);
```
else 문을 사용하지 않는 원칙 외에도 가시성을 위해서 종료시점에 return임을 명시해주는 것이 해당 코드의 가독성을 올리는데 도움을 줄 수 있을 것 같아요.
@@ -0,0 +1,13 @@
+package nextstep.security.util.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+public class AnyRequestMatcher implements RequestMatcher {
+
+    public static final AnyRequestMatcher INSTANCE = new AnyRequestMatcher();
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return true;
+    }
+}

리뷰: 개인적으로 싱글톤으로 만들 필요는 따로 없어보이고, 싱글톤이어야한다면 생성자를 private으로 막는 것이 좋아보이네요 😄