🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [56]일차
🚀56일차에는 Thread, ThreadLocal과 함께 Filter의 동작흐름에 대해 학습하고 Spring Security 설정에 대해서도 학습하였다.
학습 목표 : Filter에 대한 전반적인 이해와 Spring Security를 사용하는 이유 공부
학습 과정 : 회고를 통해 작성
Filter
- Filter는 Bean으로 등록해주어야하므로 @Component로 등록
- 필터의 설정은 복잡해질 경우 application.yml 설정 이외로
@Configuration 클래스를 작성해주어도 설정 적용이 가능
Builder
- 빌더 사용의 목적
➡️ 메소드 체이닝을 통해 객체 생성을 편리하게 도와줌
➡️new 로 초기화해서 생성하는 것이 아닌 Builder를 통해서 객체 생성
Thread
- 애플리케이션 안에서의 하나의 작업 흐름
- Spring Boot Web Application에서는 하나의 요청을 하나의 스레드가 처리하게된다.
(= 클라이언트로부터 HTTP요청이 들어오면, 서블릿 컨테이너는 쓰레드 풀에서 쓰레드를 할당하여 요청을 처리) - Thread를 너무 많이 설정하면 메모리가 부족하거나 성능이 더 나빠질 수 있음
- 쓰레드가 생성되는 시간이 있어 서버 실행 후 요청이 많을 경우 처음 요청의 처리가 늦어질 수 있음
- ex. 최소 쓰레드 수가 20일 경우 갑자기 요청이 많아져 쓰레드를 MAX수만큼 증가하면서
처리가 늦어질 수 있기때문에 최대쓰레드 수와 최소 쓰레드 수를 같은 수로 저장하는 것이 좋음
@Slf4j
@RestController
public class UserController {
@GetMapping("/hello")
public String hello(){
log.info("UserController hello() 실행!" + Thread.currentThread().getName())
return "hello";
}
...
}
- currentThread() : 현재 스레드가 어떤 것인지 가져올 수 있음
- .getName() : 현재 사용중인 스레드의 이름을 가져옴
Spring Boot Web Application
- 기본적으로 내장된 서블릿 컨테이너 (ex. Tomcat, Jetty, Undertow)를 사용해 요청을 처리
- 각 요청은 해당 컨테이너의 쓰레드 풀 (Thread pool)에 의해 처리
- Spring Boot에서 기본적으로 사용하는 내장 Tomcat을 사용
내장 Tomcat에서 Thread 설정
- maxThreads : 최대 쓰레드 수는 200 (= 동시 요청을 처리할 수 있는 최대 쓰레드 수)
➡️동시에 요청할 수 있는 최대 스레드 수 - minSpareThreads : 최소 여유 쓰레드 수는 10 (= 초기화시 생성되는 기본 쓰레드 수)
➡️항상 유지할 수 있는 최소 대기 스레드 수
server:
tomcat:
max-threads: 2
min-spare-threads: 2
- max-threads와 min-spare-threads를 설정 ( application.yml)
- 서버의 성능에 따라 최대 쓰레드 수는 200개를 넘을 수도 있다.
▶️실습 - 각 필터들에 스레드 추가
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("JunFilter doFilter() 실행 전" + Thread.currentThread().getName());
filterChain.doFilter(servletRequest, servletResponse);
log.info("JunFilter doFilter() 실행 후" + Thread.currentThread().getName());
}
- 실행 후 localhost:8080/test/hi 에 접속하면 이처럼 각 요청마다 다른 스레드를 가지는 것을 확인
FilterConfig - 필터 설정 파일
@Configuration
public class FilterConfig{
@Bean
public FilterRegistrationBean<JunFilter> junFilter(){
FilterRegistrationBean<JunFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JunFilter());
registrationBean.addUrlPatterns("/test/*");
registrationBean.setOrder(1);
return registrationBean;
}
}
- registrationBean.addUrlPatterns("/test/*");
➡️URL에서 /test 밑에 필터가 요청되게끔 지정할 수 있다.
➡️특정 URL을 적어주면 그 URL에 대해서만 필터가 요청되게끔 설정가능 - 실행 흐름
스레드 1: http-nio-8080-exec-1
------------------------------------------------
[JunFilter] -> [UserFilter] -> [UserFilter] -> [JunFilter]
------------------------------------------------
스레드 2: http-nio-8080-exec-2
------------------------------------------------
[UserFilter] -> [UserFilter]
------------------------------------------------
- 두 개의 스레드가 존재하며, UserFilter가 두 번 실행되는 스레드가 따로 있음
만약 UrlPatterns 를 변경한다면
registrationBean.addUrlPatterns("/test/*"); 가 아닌
registrationBean.addUrlPatterns("/*"); 로 접속해서 테스트해보면
- 실행 흐름
스레드 1: http-nio-8080-exec-1
------------------------------------------------
[JunFilter] -> [UserFilter] -> [UserFilter] -> [JunFilter]
------------------------------------------------
- 모든 필터가 같은 스레드에서 실행
- /test/hi 요청과 다르게 새로운 스레드가 생성되지 않음
- 이처럼 URL 패턴을 "/*"로 설정하면 모든 요청이 하나의 스레드에서 처리
ex. http-nio-8080-exec-1
ThreadLocal
- 하나의 쓰레드에서만 접근 가능한 변수를 쉽게 사용할 수 있도록 변수를 관리하는 클래스
- ThreadLocal 클래스 : 각 쓰레드가 고유한 값을 가질 수 있고, 다른 스레드와 공유되지 않도록 함
- ThreadLocal에 값을 저장을 해놓으면 하나의 스레드가 동작하면서 값이 필요할때
요청하면 바로 반환해줄 수 있도록 함
(=따라서 쓰레드가 해당 변수를 읽거나 쓸때, ThreadLocal은 그 쓰레드만을 위한 고유한 인스턴스를 반환) - 하나의 요청(=스레드)에 대해서만 값이 저장되는 것
- ThreadLocal을 사용하면 특정 스레드가 동작하는 동안 값(ex. 사용자 정보, 트랜잭션 정보 등)을 유지 가능
- 요청이 끝나면 반드시 remove()를 호출하여 메모리 누수를 방지해야함
- ThreadLocal 사용의 장점
- 쓰레드 별로 데이터를 관리하기가 편하다는 것
- 각 쓰레드가 독립적으로 ThreadLocal 변수를 갖기때문에 동시성 문제를 피함
- 인증된 사용자 정보를 요청의 처음부터 끝까지 유지 가능
- 전역 변수를 사용하지 않고도 요청 스코프 데이터를 저장할 수 있게되어 코드간결화
1) ThreadLocal을 사용하지 않은 경우
[Thread-1] ───▶ [공유 변수: userId = 1] ◀─── [Thread-2]
Thread-1이 userId를 1로 설정했을때, Thread-2 또한 userId를 수정하고자하면 데이터가 덮어씌워지게됨
2) ThreadLocal을 사용한 경우
[Thread-1] ───▶ [ThreadLocal(userId) = 1]
[Thread-2] ───▶ [ThreadLocal(userId) = 2]
- 각 스레드가 고유한 값을 가짐 (다른 스레드의 값에 영향을 받지 않음)
반드시 remove() 호출
try {
threadLocalValue.set(123);
System.out.println("ThreadLocal 값: " + threadLocalValue.get());
} finally {
threadLocalValue.remove(); // 메모리 누수 방지
}
- ThreadLocal은 스레드가 종료되지 않는 한 값이 유지되므로, 제거하지 않으면 메모리 누수가 발생할 수 있음
Spring에서 ThreadLocal을 활용
- 사용자 인증 정보 저장 : 로그인한 사용자 정보를 요청별로 유지
- 트랜잭션 관리 : 같은 요청 내에서 동일한 트랜잭션을 유지
- 로깅(로그 추적 ID 관리) : 요청마다 고유한 ID를 부여하여 추적
▶️실습 - ThreadLocal에서 사용자 인증 정보 저장 흐름
- ThreadLocal을 활용하면 현재 로그인한 사용자 정보를 유지할 수 있음
- 요청이 들어올 때 사용자 정보를 저장하고, 서비스 계층에서 가져와서 사용 후 제거
1) UserContext : 사용자 정보 저장
- ThreadLocal을 이용하여 현재 요청(스레드)에서만 접근할 수 있는 User 객체를 저장하고 관리하는 기능
(=User 객체를 ThreadLocal을 통해 저장하고 조회) - 각 요청(스레드)마다 고유한 사용자 정보를 유지 가능
public class UserContext {
private static final ThreadLocal<String> currentUser = ThreadLocal.withInitial( () -> null);
// 현재 로그인한 사용자 ID 설정
public static void setUser(String userId) {
currentUser.set(userId);
}
// 현재 로그인한 사용자 ID 가져오기
public static String getUser() {
return currentUser.get();
}
// 메모리 누수 방지를 위한 제거
public static void clear() {
currentUser.remove();
}
}
- ThreadLocal에는 한 번에 하나 값이 들어가고, 넣고, 꺼내는 것을 확인하는 코드
- 스레드마다 원하는 데이터를 사용할 수 있게 지정가능
- clear() : 작업이 끝나면 (쓰레드의 작업이 끝나면) 받았던 값을 반납하는것이 clear()메소드에서 하는 일
- 일반적으로 사용자 인증 정보(로그인 정보)를 관리할 때 활용
- ThreadLocal.withInitial( () -> null);
➡️초기값을 설정하는 람다 표현식
만약 withInitial(() -> new User())로 설정하면, 각 스레드마다 기본 User 객체를 생성
➡️withInitial(() -> null)을 사용하여 기본값을 null로 설정
(=처음에는 아무 값도 없고, 요청(스레드)이 들어오면 setUser()를 통해 값을 저장)
정리하자면
각 스레드는 userThreadLocal을 가지고 있지만, 기본적으로 null 상태
이후 특정 스레드가 사용자 정보를 설정하면, 해당 스레드에서만 값이 유지 - return currentUser.get();
➡️동일한 요청(스레드) 내에서는 언제든지 UserContext.getUser()를 통해 "Kaoru" 객체를 얻을 수 있음
다른 요청(스레드)에서는 UserContext.getUser()를 호출해도 해당 값을 볼 수 없음
필터 내부에서 UserContext.getUser()를 호출하면 "Kaoru" 객체를 반환
컨트롤러에서 UserContext.getUser()를 호출하면 필터에서 저장한 사용자 정보("Kaoru")를 가져올 수 있음 - currentUser.remove()
➡️현재 스레드에서만 User 객체가 제거 (=다른 스레드에는 영향을 주지 않음)
즉 clear()을 반드시 호출해서 메모리 누수를 방지
2) UserFilter : 필터에서 요청이 들어오면 사용자 정보 설정
public class UserFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
String userId = "Jun";
// ThreadLocal에 사용자 정보 저장
UserContext.setUser(userId);
chain.doFilter(request, response);
} finally {
// 요청이 끝나면 반드시 제거 (메모리 누수 방지)
UserContext.clear();
}
}
}
- 요청이 들어올 때 UserContext.setUser(userId)로 사용자 정보를 저장하고
요청이 끝나면 clear()를 호출하여 정리 - UserContext.setUser(userId)
➡️현재 실행 중인 스레드에서 User 객체를 저장
➡️다른 스레드는 같은 userThreadLocal을 사용하더라도 각각의 고유한 User 객체
3) UserService : 사용자 정보를 사용
@Service
public class UserService {
public String getCurrentUser() {
return UserContext.getUser();
}
}
- UserContext.getUser()를 호출하면 어떠한 서비스 계층에서도 현재 요청의 사용자 정보를 가져올 수 있음
4) UserController : 사용자 정보 조회
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/loginUser")
public String getCurrentUser() {
return "현재 로그인한 사용자: " + userService.getCurrentUser();
}
}
❓요청마다 다른 스레드가 불러와지는 것인지
➡️사용자의 요청이 들어올 때마다 서버의 스레드 풀에서 사용 가능한 스레드를 하나 꺼내어 요청을 처리하게됨
각 요청은 새로운 스레드에서 실행될 수도 있고, 기존에 사용했던 스레드 재사용도 가능
정리하자면
요청이 많은 경우 여러 개의 스레드가 동작
요청이 적으면 기존 스레드를 재사용
▶️실습 - /hello 요청의 동작 흐름
[요청 시작] localhost:8080/hello
---------------------------------------------
[UserFilter] - UserContext.setUser(new User("Kaoru")) 실행
---------------------------------------------
[UserController] - UserContext.getUser() 실행 → "Kaoru" 반환
---------------------------------------------
[응답 반환] "helloKaoru"
[UserFilter] - UserContext.clear() 실행 (메모리 정리)
DelegatingFilterProxy
- Servlet Filter와 Spring의 Filter 빈을 연결해주는 역할을 하는 프록시(Proxy) 객체
- DelegatingFilterProxy는 서블릿 컨테이너(Tomcat 등)에 등록된 필터지만,
내부적으로 Spring의 Filter 빈을 위임(delegate)하여 실행
(= Spring에서 관리하는 필터(Spring Bean)를 서블릿 컨테이너에서 사용할 수 있도록 연결)
- Spring Security가 동작하려면 반드시 필요
(Spring Security의 FilterChainProxy가 DelegatingFilterProxy를 통해 동작하기때문)
DelegatingFilterProxy 동작 흐름
1) 클라이언트가 요청을 보냄 (/login 요청)
2) WAS(Tomcat)가 DelegatingFilterProxy를 실행
3) DelegatingFilterProxy가 Spring의 FilterChainProxy를 실행
4) FilterChainProxy가 여러 개의 Spring Security 필터를 실행
5) 최종적으로 요청이 컨트롤러(@RestController)로 전달됨
[클라이언트 요청]
▼
[Tomcat(WAS)]
▼
[DelegatingFilterProxy]
▼
[FilterChainProxy]
▼
[Spring Security 필터]
▼
[컨트롤러]
Context
- Context는 Spring이 관리하는 모든 객체(Bean)들의 환경을 담고 있는 컨테이너
DelegatingFilterProxy와 ContextLoaderListener
- DelegatingFilterProxy : ContextLoaderListener를 통해 Spring의 Filter 빈을 찾아서 실행
➡️WAS가 실행하는 필터를 Spring이 관리할 수 있도록 연결
➡️실제 필터를 감싸는 "필터 안의 필터(프록시 필터)"
ex. DelegatingFilterProxy가 실행되면,
내부적으로 Spring이 관리하는 FilterChainProxy를 실행하고,
FilterChainProxy는 다시 여러 개의 Spring Security 필터를 실행 - ContextLoaderListener : Spring의 WebApplicationContext를 초기화하는 리스너
(= 서블릿 컨테이너가 시작될 때 Spring 컨텍스트를 로드하고 초기화하는 역할)
➡️WAS가 Spring의 ApplicationContext를 자동으로 로드하도록 도와줌
따라서 ContextLoaderListener가 없다면 개발자가 직접 Filter를 등록해야함
(=WAS(Tomcat 등)에서 실행할 모든 필터를 직접 등록해야함)
정리하자면 이 개념들을 통해 개발자가 직접 필터를 WAS에 등록할 필요 없이,
Spring Security 같은 필터 기반 기능을 쉽게 적용할 수 있음
[WAS + DelegatingFilterProxy + Context]의 동작 흐름
[클라이언트 요청]
↓
[WAS (Tomcat)]
↓
[DelegatingFilterProxy (WAS 필터)]
↓
[Spring Context (ContextLoaderListener)]
↓
[FilterChainProxy (Spring Security 필터)]
↓
[인증 및 권한 검사]
↓
[Spring 컨트롤러 실행]
Spring Boot Security
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security 적용
- 의존성 추가 후 localhost:8080/hello 에 접속하면 "로그인 폼"이 출력됨
- log창의 생성된 비밀번호(매번 서버 실행시마다 다른 패스워드 발급)와 user를 입력 후 접속 성공
Spring Security 설정 실습
logging:
level:
org:
springframework:
security: TRACE
- security: TRACE
security의 레벨 설정 추가
springframework 패키지 안에 있는 security의 레벨을 TRACE로 설정
- 실행후 URL에 접속시켜보면 16개의 필터가 각 종류별로 실행되고 있는 것 확인 가능
SecurityConfig
- 설정을 담당할 클래스
- Security의 기본 설정, 인증을 설정하는 것
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests( auth -> auth
.anyRequest().authenticated()
// 모든 요청에 대해서 인증을 요구
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
- @Configuration
➡️Spring Boot가 실행될 때 이 클래스를 읽어서 보안 설정을 적용 - @EnableWebSecurity
➡️Spring Security를 활성화하는 어노테이션
내부적으로 SecurityFilterChain을 Spring의 필터 체인(DelegatingFilterProxy)에 자동으로 등록해줌 - SecurityFilterChain
➡️Spring Security에서 필터들을 하나의 체인으로 연결하는 객체
여러 개의 필터를 순차적으로 실행하여 보안 로직을 적용 - anyRequest().authenticated()
➡️모든 요청(anyRequest())에 대해 인증된 사용자만 접근 가능하도록 설정
(=로그인하지 않으면 어떤 페이지도 접근 불가)
▶️실습 - 특정 URL에 대해서는 인증 과정없이 접속, 로그인 페이지 추가
// 2. 인증없이 접근 가능한 URL 지정 및 (.requestMatchers())
// 로그인 페이지와 로그인 성공 시 이동할 페이지 설정 (.formLogin())
http
.authorizeHttpRequests( auth -> auth
.requestMatchers("/hello","/loginForm").permitAll()
.anyRequest().authenticated()
// 모든 요청에 대해서 인증을 요구
)
.formLogin(formLogin -> formLogin
.loginPage("/loginForm")
.defaultSuccessUrl("/success")
.failureUrl("/fail")
.usernameParameter("userId")
.passwordParameter("password")
);
return http.build();
- .requestMatchers(”/hello”, “loginForm”).permitAll()
hello와 loginForm URL로 접속하면 인증없이 접속 가능 - .loginPage("/loginForm") : 원하는 로그인 페이지 설정
- .defaultSuccessUrl("/success") : 인증에 성공하면 가고싶은 페이지 설정
- .failureUrl("/fail") : 인증에 실패하면 가고싶은 페이지 설정
- .usernameParameter("userId") : 로그인 폼에서의 Input 상자의 ID부분과 일치해야함
- .passwordParameter("password") : 로그인 폼에서의 Input 상자의 PASSWORD부분과 일치해야함
- 인증되지 않은 페이지 (ex. info)같은 경우 로그인 폼이 출력되고
- 인증된 페이지 (ex. hello, loginForm)같은 경우 로그인 폼 없이 바로 출력
🚀회고 결과 :
이번 회고에서는 인증과 인가된 페이지를 관리할 수 있는 SecurityConfig에 대해서 실습해볼 수 있었다.
- ThreadLocal 을 통한 Thread 실습
- 필터를 적용하여 인증 거치기
느낀 점 :
확실히 Spring Security 설정 부분에서 어려움을 많이 느꼈다. 디테일한 메소드들이 많았고 설정해주기 위해 배워야할 것이 많았다. 익숙해지기 위해 노력해야할 것 같다.
향후 계획 :
- Spring Security 메소드의 기능과 개념 공부
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_58일차_"Spring Security와 DB연결" (0) | 2025.03.04 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_57일차_"Spring Security - 권한, 쿠키" (0) | 2025.02.28 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_55일차_"DTO, Security" (0) | 2025.02.26 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_54일차_"@RestController" (0) | 2025.02.25 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_53일차_"CURL" (0) | 2025.02.25 |