로그인 기능을 구현하기 전, 사용자 정보를 확인하기 위해 DTO 또는 쿼리 파라미터로 userId를 입력 받았습니다. 로그인은 OAuth Provider에서 사용자 정보를 받고 세션을 생성하는 방식으로 구현했습니다. 자세한 내용은 해당 링크를 참고해주시면 감사하겠습니다.
이 게시물은 사용자 정보를 직접 클라이언트로부터 받아오지 않고, 대신 Spring Security의 SecurityContextHolder를 활용하여 코드를 개선하면서 얻은 경험과 내용을 담고 있습니다.
Spring Security에서 사용자 인증 및 보안 정보를 처리하는 핵심 구성 요소는 SecurityContextHolder, SecurityContext, 그리고 Authentication 입니다.
SecurityContextHolder 가 사용자 정보를 관리하는 방식에 대해 관여하지 않습니다.SecurityContextHolder에 값이 있으면 로그인 된 사용자이고, 값이 없다면 로그인 되지 않은 사용자로 구분합니다.SecurityContextHolder는 SecurityContext 정보를 가지고 있습니다.SecurityContextHolder.getContext()를 호출하여 현재 스레드의 SecurityContext를 가져올 수 있습니다.SecurityContextHolder는 사용자 정보를 얻는 데 사용되며, 현재 스레드의 SecurityContext를 관리합니다.SecurityContextHolder에 의해 관리되며, 현재 스레드의 SecurityContext에는 현재 사용자의 Authentication 객체와 다른 관련 정보가 포함됩니다.SecurityContext는 현재 사용자 정보 및 인증 상태를 저장하는 컨테이너입니다.SecurityContext를 통해 현재 사용자의 인증 정보 및 권한을 가져올 수 있습니다.Authentication은 사용자의 인증 정보를 나타내는 인터페이스입니다.Authentication 객체로 만들어야 합니다.Authentication 구현체는 여러 가지가 있으며, 주로 UsernamePasswordAuthenticationToken를 사용합니다.SecurityContext에 저장된 Authentication 객체를 통해 현재 사용자의 식별 및 권한을 확인할 수 있습니다.SecurityContext context = SecurityContextHolder.getContext(); // 1
Authentication authentication = context.getAuthentication(); // 2
String username = authentication.getName(); // 3
Object principal = authentication.getPrincipal(); // 4
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 5
authentication)를 가져옵니다.authentication.getName()을 통해 현재 사용자의 이름을 가져올 수 있습니다.authentication.getPrincipal()을 통해 현재 사용자의 주요 정보(principal)를 가져올 수 있습니다.authentication.getAuthorities()를 사용하여 현재 사용자의 권한 정보를 가져옵니다.@PostMapping
public CartResponseDto postCart(@RequestBody CartDto cartDto) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
log.info("name: {}", authentication.getName());
log.info("principal: {}", authentication.getPrincipal());
log.info("authorities: {}", authentication.getAuthorities());
// 비즈니스 로직 호출
}
/*
[~.CartController.postCart:39] - name: 105537701744188388829
[~postCart:40] - principal: Name: [105537701744188388829], Granted Authorities: [[ROLE_USER]], User Attributes: [{sub=105537701744188388829, name=김태현, given_name=태현, family_name=김, picture=https://lh3.googleusercontent.com/a/ACg8ocKJan8dJyE_w8D9ddHs8830BZbnD5S0XHRE2QArPw98lGY=s96-c, email=thk98k@gmail.com, email_verified=true, locale=ko}]
[~.postCart:41] - authorities: [ROLE_USER]
*/
기본적으로 ThreadLocal은 자바 클래스입니다. ThreadLocal을 이용하면 쓰레드 영역에 변수를 설정할 수 있기 때문에 특정 쓰레드가 실행하는 모든 코드에서 그 쓰레드에 설정된 변수 값을 사용하게 됩니다.
쉽게 말하면 ThreadLocal은 Thread-A에서 설정한 값을 Thread-B에서 읽을 수 없도록 보장하는 개념입니다. ThreadLocal을 사용하면 각 쓰레드는 고유한 데이터 저장소를 가지며 한 쓰레드에서 설정한 값은 해당 쓰레드 내에서만 접근이 가능합니다. 다른 쓰레드에서 동일한 ThreadLocal 변수에 대한 접근이 격리되어 있기 때문에 Thread-A에서 설정한 값을 Thread-B에서 읽을 수 없는 것입니다.
ThreadLocal은 멀티쓰레드 환경에서 각 쓰레드 간 데이터를 분리하고 격리하기 위해 사용됩니다. 이는 쓰레드 간 데이터 공유와 동기화 문제를 방지하고 각 쓰레드가 자체 데이터를 유지할 수 있게됩니다.
결과적으로 Thread-A에서 설정한 ThreadLocal 변수에 저장된 값은 오로지 Thread-A에서만 접근 가능하며, Thread-B 등 다른 쓰레드에서 이 값을 읽을 수 없습니다.
쓰레드 로컬 변수를 사용한 후 remove() 메서드를 호출하여 변수를 메모리에서 명시적으로 제거해야합니다. 그렇지 않으면 해당 쓰레드의 메모리에서 데이터가 계속 유지되어 메모리 누수 문제가 발생할 수 있습니다.
특히 쓰레드 풀을 사용하는 환경에서 쓰레드가 반복적으로 재사용 되기 때문에 변수 값이 유지될 수 있습니다.
SpringSecurity는ThreadLocal변수를 자동으로 메모리에서 해제하는 기능을 제공합니다.
기본적으로 SecurityContextHolder는 ThreadLocal을 사용하여 현재 사용자 정보를 저장하며, 이렇게 하면 SecurityContext가 동일한 쓰레드 내의 메서드에서 항상 사용 가능하게 됩니다. 쉽게말하면, 한 쓰레드에서 사용자 정보를 설정하면 이 정보는 해당 쓰레드 내에서 어디서든지 언제든지 조회할 수 있다는 것입니다. 이것은 현재 스레드의 생명주기 동안 사용자 정보에 쉽게 액세스할 수 있게 만들어주는 중요한 기능 중 하나입니다.
Spring Security의 FilterChainProxy는 쓰레드 사용이 완료되면 SecurityContext가 안전하게 지워지도록 보장합니다. 즉 Spring Security를 사용한다면 개발자가 ThreadLocal의 메모리 누수 문제를 신경쓰지 않을 수 있게 되는 것입니다.
학습 내용을 바탕으로 적용한 코드는 아래 링크를 확인해주시면 감사하겠습니다.
https://github.com/f-lab-edu/commerce-market/pull/77