Skip to content

3주차 금요일 그룹 5

June edited this page Jul 16, 2024 · 14 revisions

김준기

그룹 리뷰를 통해 배운점을 정리합니다.

스레드 로컬

스레드 로컬의 동작 원리

스레드 로컬이 어떤 방식으로 가능한지 이야기하는 시간을 가졌습니다. 핵심은 다음과 같습니다.

  1. ThreadLocal 에서 static class로 ThreadLocalMap을 관리한다.

  2. ThreadLocalMap은 말 그대로 Map이다. 즉, Entry[] 배열로 관리된다.

  3. 특정 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

ConcurrentHashMap을 사용할 때, get / put 시점이 원자적이지 않음으로 인해 이슈가 발생할 수 있음을 알게 되었습니다.

이 때는 putIfAbsent 메서드를 활용하면 되는 것을 배웠습니다.


Filter, Filter Chain

FilterChain의 구현 방법에 대해 학습할 수 있었습니다. 전반적인 큰 흐름은 다음과 같습니다.

  • List로 Filter를 관리한다.
  • 각 Filter에는 순서(order)가 있다.
  • 순서에 맞는 Filter의 doFilter 메서드를 호출하며 필터 로직을 진행한다.

김현우

CleanShot 2024-07-10 at 21 32 04

나의 구현

  • 서버가 요청을 준다.
  • 요청으로 부터 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
Loading

다들 쓰레드에 대한 고민이 많으신 것 같습니다. 특히 Thread Local이나 동시성 이슈들에 대한 논의를 나눈게 좋았습니다. 특히 문자열 코딩을 UTF8로 당연스럽게 하고 있었는데 이 부분에 대해 지적을 받고 고민의 여지가 있는 부분이라고 느꼈습니다.

박민지

▶️ 구조

image

▶️ 구현 내용

  • 세션 max size와 timeout을 기반으로 invalidate 처리 및 세션풀이 다 찬 경우 invalidated된 세션을 삭제하는 로직 구현
    • 추후 비동기 스케줄링 스레드를 만들어 만료된 세션 삭제 예정
  • 값 치환, if, for문 처리가 가능한 템플릿 엔진 구현
    • if 문 표현식 계산을 위해 ==, != 연산이 가능한 AST Node 구현
    • 필요 시 >, < 등 다른 연산도 추가 가능
  • 각 api마다 인증은 아직 api 내부에서 구현
    • 요청 전 인증 여부를 검사하는 로직으로 중복 제거 예정

▶️ 배운 점

  • ConcurrentHashMap을 사용하는 조회 - 수정 로직에서 동시성 이슈가 발생할 수 있어 원자적 처리가 필요
  • ThreadLocal 사용 시 clear 해주지 않으면 스레드 재사용 시 ThreadLocal leak 가능
  • View 다형성을 통해 Template, Redirect 등을 유연하게 적용 가능
  • ScheduledExecutorService 사용하여 스케줄링 비동기 스레드를 다룰 수 있음

▶️ 후기

  • 코드 설명할 때 리팩토링되지 않은 부분이 많이 보여서 조금... 부끄러웠습니다.
  • 다른 분들이 정말 많은 고민을 했겠구나, 생각이 들어 배우는 것이 많았습니다!
  • 지금의 구조로는 RequestHandler에서 응답을 완벽하게 만들어줘야 하는데, 오히려 RequestHandler의 책임이 굉장히 무거운 것 같습니다. 다른 분들 구조를 참고하니 RequestHandler에서는 요청에 대한 처리만, 페이지 렌더링은 다른 클래스로 빼고 그 후에 실행되도록 하는 것이 좋을 것 같습니다. 이 기반으로 수정해보겠습니다!
  • 다른 분들 코드에서 구조 아이디어를 얻어가는 면이 있어 참 좋습니다.
  • 구조 그림을 보고 코드를 보니 이해가 더 쉬웠습니다. 시각적 자료가 이렇게 중요하구나, 깨달을 수 있었습니다.

김규원

ThreadLocal

  • 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);
        }

image 참고

인코딩 방식

민지님께서 공유해주신 테스트 코드를 토대로 공부를 해보았습니다.

CASE1. char[] -> String

String koreanString = "안녕하세요";  // 한국어 문자열
char[] chars = koreanString.toCharArray();

System.out.println(new String(chars)); //okay

char 은 유니코드 코드를 저장하며 각 코드는 2byte이다.

CASE2. byte[] -> String(UTF-8)

String koreanString = "안녕하세요";  // 한국어 문자열
char[] chars = koreanString.toCharArray();

System.out.println(new String(chars)); //okay

char 은 2byte 로 UTF-16 인코딩하여 문자열을 저장한다.

CASE3. byte[] -> String(UTF-8)

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 이다.

ByteArrayStream과 InputStreamReader

  • ByteArrayInputStream: 바이트 배열을 입력 스트림으로 변환하여 바이트 단위로 데이터를 읽을 수 있습니다.
  • InputStreamReader: 바이트 입력 스트림을 문자 입력 스트림으로 변환하여 문자 단위로 데이터를 읽을 수 있습니다.
  • 결합 사용: 바이트 배열을 ByteArrayInputStream으로 변환하고, 이를 InputStreamReader로 변환하여 문자를 읽을 수 있습니다. 이를 통해 다양한 인코딩 방식을 지원할 수 있습니다.

👼 개인 활동을 기록합시다.

개인 활동 페이지

🧑‍🧑‍🧒‍🧒 그룹 활동을 기록합시다.

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally