-
Notifications
You must be signed in to change notification settings - Fork 0
3주차 금요일 그룹 5
스레드 로컬이 어떤 방식으로 가능한지 이야기하는 시간을 가졌습니다. 핵심은 다음과 같습니다.
-
ThreadLocal 에서 static class로 ThreadLocalMap을 관리한다.
-
ThreadLocalMap은 말 그대로 Map이다. 즉, Entry[] 배열로 관리된다.
-
특정 Thread → Entry 배열의 index 로 유니크하게 매핑하는 과정이 필요한다. (index = threadLocalHashCode & (length - 1))
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
스레드에서만 유효한 전역 변수를 만들어서, 매 로그인 시도마다 세션 매니저 호출하는 부담스러운 일을 회피
스프링 시큐리티 + 스레드 로컬을 활용하는 과정에서 다른 사람의 정보가 조회되었던 사례를 공유받았습니다.
톰캣은 워커 스레드를 스레드 풀로 관리 및 재사용하므로, 요청에 대한 응답을 마무리하면 스레드 로컬 초기화를 반드시 해줘야하겠습니다.
ConcurrentHashMap을 사용할 때, get / put 시점이 원자적이지 않음으로 인해 이슈가 발생할 수 있음을 알게 되었습니다.
이 때는 putIfAbsent
메서드를 활용하면 되는 것을 배웠습니다.
FilterChain의 구현 방법에 대해 학습할 수 있었습니다. 전반적인 큰 흐름은 다음과 같습니다.
- List로 Filter를 관리한다.
- 각 Filter에는 순서(order)가 있다.
- 순서에 맞는 Filter의 doFilter 메서드를 호출하며 필터 로직을 진행한다.
- 서버가 요청을 준다.
- 요청으로 부터
Request
객체를 만든다. 또한, 빈Response
객체를 만든다. -
FilterChain
에서 순차적으로Filter
를 거치면서 요청을 처리한다. - 이후
Filter
를 거치고Filter
를 모두 소진하면Handler Mapper
로 넘어간다. - 요청에
Handler
를 가져오고 맞는Handler
에서 요청을 처리한다. -
ResponseValueWriter
에서 응답 내용을 서술한다.
- 뭔가 남들이 Bean을 구현하는 것을 보고 나도 Bean을 구현해야하나... 생각이 들고 있습니다... 진짜 저만 못하는거 같아요
- 구조는 크게 변한 것이 없고, 대신 추후에 있을 복수의
Set-Cookie
를 지원하기 위해 헤더구조를 변경했습니다. - 아직까지는 제 구조가 기능 추가를 견딜 수 있지만, 추후 기능 추가는 좀 버거울 것 같습니다.
- 또한, 탬플릿 엔진을 보면서 문자열 대체를 하는 방식을 좀 다르게 해야하나 생각중입니다.
sequenceDiagram
participant Client
participant WebServer
participant FilterChain
participant StaticResourceHandler
participant DispatcherServlet
participant HtmlHandler
participant HandlerAdapter
participant ViewResolver
participant TemplateView
participant TemplateEngine
participant HttpResponseBuilder
Client->>WebServer: HTTP Request
WebServer->>FilterChain: doFilter(HttpRequest)
FilterChain->>StaticResourceHandler: handle(HttpRequest)
alt is not HTML file
StaticResourceHandler->>HttpResponseBuilder: build static response
HttpResponseBuilder-->>StaticResourceHandler: HttpResponse
StaticResourceHandler-->>FilterChain: HttpResponse
FilterChain-->>WebServer: HttpResponse
WebServer-->>Client: HTTP Response
else is HTML file or dynamic content
StaticResourceHandler-->>FilterChain: null (not handled)
FilterChain->>DispatcherServlet: service(HttpRequest)
DispatcherServlet->>HtmlHandler: handle(HttpRequest)
HtmlHandler->>HandlerAdapter: handle(HttpRequest, Handler)
HandlerAdapter-->>DispatcherServlet: ModelAndView
DispatcherServlet->>ViewResolver: resolveViewName(String viewName)
ViewResolver-->>DispatcherServlet: TemplateView
DispatcherServlet->>TemplateView: render(Map<String, Object> model)
TemplateView->>TemplateEngine: render(String template, Map<String, Object> model)
TemplateEngine-->>TemplateView: Rendered content
TemplateView->>HttpResponseBuilder: build response
HttpResponseBuilder-->>TemplateView: HttpResponse
TemplateView-->>DispatcherServlet: HttpResponse
DispatcherServlet-->>FilterChain: HttpResponse
FilterChain-->>WebServer: HttpResponse
WebServer-->>Client: HTTP Response
end
다들 쓰레드에 대한 고민이 많으신 것 같습니다. 특히 Thread Local이나 동시성 이슈들에 대한 논의를 나눈게 좋았습니다. 특히 문자열 코딩을 UTF8로 당연스럽게 하고 있었는데 이 부분에 대해 지적을 받고 고민의 여지가 있는 부분이라고 느꼈습니다.
- 세션 max size와 timeout을 기반으로 invalidate 처리 및 세션풀이 다 찬 경우 invalidated된 세션을 삭제하는 로직 구현
- 추후 비동기 스케줄링 스레드를 만들어 만료된 세션 삭제 예정
- 값 치환, if, for문 처리가 가능한 템플릿 엔진 구현
- if 문 표현식 계산을 위해 ==, != 연산이 가능한 AST Node 구현
- 필요 시 >, < 등 다른 연산도 추가 가능
- 각 api마다 인증은 아직 api 내부에서 구현
- 요청 전 인증 여부를 검사하는 로직으로 중복 제거 예정
-
ConcurrentHashMap
을 사용하는 조회 - 수정 로직에서 동시성 이슈가 발생할 수 있어 원자적 처리가 필요 -
ThreadLocal
사용 시 clear 해주지 않으면 스레드 재사용 시ThreadLocal
leak 가능 - View 다형성을 통해 Template, Redirect 등을 유연하게 적용 가능
-
ScheduledExecutorService
사용하여 스케줄링 비동기 스레드를 다룰 수 있음
- 코드 설명할 때 리팩토링되지 않은 부분이 많이 보여서 조금... 부끄러웠습니다.
- 다른 분들이 정말 많은 고민을 했겠구나, 생각이 들어 배우는 것이 많았습니다!
- 지금의 구조로는
RequestHandler
에서 응답을 완벽하게 만들어줘야 하는데, 오히려RequestHandler
의 책임이 굉장히 무거운 것 같습니다. 다른 분들 구조를 참고하니RequestHandler
에서는 요청에 대한 처리만, 페이지 렌더링은 다른 클래스로 빼고 그 후에 실행되도록 하는 것이 좋을 것 같습니다. 이 기반으로 수정해보겠습니다! - 다른 분들 코드에서 구조 아이디어를 얻어가는 면이 있어 참 좋습니다.
- 구조 그림을 보고 코드를 보니 이해가 더 쉬웠습니다. 시각적 자료가 이렇게 중요하구나, 깨달을 수 있었습니다.
- ThreadPool 재사용하는 경우 메모리 누수 문제가 발생할 수 있어 반드시 remove 메소드를 써줘야함
- JVM 안에서 ThreadLocal 은 어디에 저장되는가?
- Thread 는 각자의 스택 영역에 존재함
- ThreadLocal 은 본인 Thread 에 해당하는 ThreadLocalMap 를 사용함
- 결론적으로 모두 참조 주소에 관한 것은 모두 스택 영역, 실제 인스턴스는 힙 영역에 존재함
public class Thread implements Runnable {
//...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
//...
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
//ThreadLocalMap
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
참고
민지님께서 공유해주신 테스트 코드를 토대로 공부를 해보았습니다.
String koreanString = "안녕하세요"; // 한국어 문자열
char[] chars = koreanString.toCharArray();
System.out.println(new String(chars)); //okay
char 은 유니코드 코드를 저장하며 각 코드는 2byte이다.
String koreanString = "안녕하세요"; // 한국어 문자열
char[] chars = koreanString.toCharArray();
System.out.println(new String(chars)); //okay
char 은 2byte 로 UTF-16 인코딩하여 문자열을 저장한다.
String koreanString = "안녕하세요"; // 한국어 문자열
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(koreanString.getBytes());
byte[] bytes = byteArrayInputStream.readNBytes(뭘까요~?);
System.out.println(new String(bytes, "UTF-8"));
정확히 몇 바이트를 읽어와야 UTF-8 로 인코딩 했을 때도 안녕하세요가 나올까?
물론 koreanString.getBytes().length
를 하면 되긴 하지만 직접적인 숫자를 계산해보자
str.getBytes()는 UTF-8 인코딩된 바이트 배열를 반환합니다. 한국어는 UTF-8 인코딩을 하면 3byte 로 인코딩된다. 그렇기 때문에 5글자가 각각 3byte 만큼 차지하기 때문에 전체는 15byte 가 된다.
geyBytes() 를 하는 순간 UTF-8 인코딩이 되어 3 byte 가 되고 char[] 에 저장될 때는 UTF-16 인코딩 되어 2byte 이다.
-
ByteArrayInputStream
: 바이트 배열을 입력 스트림으로 변환하여 바이트 단위로 데이터를 읽을 수 있습니다. -
InputStreamReader
: 바이트 입력 스트림을 문자 입력 스트림으로 변환하여 문자 단위로 데이터를 읽을 수 있습니다. -
결합 사용: 바이트 배열을
ByteArrayInputStream
으로 변환하고, 이를InputStreamReader
로 변환하여 문자를 읽을 수 있습니다. 이를 통해 다양한 인코딩 방식을 지원할 수 있습니다.