Skip to content

김현준 2주차 WAS 학습 일지

김현준 edited this page Jul 7, 2024 · 1 revision

try with resource with Stream

try with resource

파일을 사용하기 위해 디스크에 저장되어 있는 파일을 메모리에 올려야 하는데 자바에서는 JVM이 어플리케이션에 필요한 모든 파일의 자원을 관리한다.

BufferedInputStream을 사용하여 파일을 한 줄 한 줄 읽을 때마다 메모리 내에 각 줄의 내용이 저장된다.

따라서 메모리에 파일의 자원이 쌓이고 결국 OOM이 발생할 수 있는데 처리가 완료된 데이터는 반드시 자원 해제를 해줘야 한다.

Java 7 이전에는 close를 try-catch-finally을 이용해서 Null 검사 후 직접 호출해야 했는데 AutoCloseable과 try-with-resource가 나오면서 자동으로 자원을 반납해준다.

try-with-resource를 사용하면 좋은 이유는 예외 발생 시 finally 구문의 예외 스택 트레이스가 어플리케이션 예외 스택 트레이스를 덮는다.

또한 finally로 자원 2개를 순차적으로 반납 시에 앞에 있는 자원에서 예외가 터지면 뒤에 자원은 반환되지 않아 한번 더 try-catch-finally 문으로 감싸야 하는 수고로움이 있는데 try-with-resource는 이를 방지해준다. Java 파일이 Class 파일로 컴파일 될 때 try-with-resources에서 누락없이 모든 경우를 try-catch-finally로 변환해주기 때문이다.

FileInputStream r1 = null;
FileInputStream r2 = null;

try {
	r1 = new FileInputStream("");
	r2 = new FileInputStream("");
} finally {
	if(r1 != null) {
		r1.close(); -> 예외 발생!
	}
	
	if(r2 != null) {
		r2.close(); -> 반환 x
	}
}

출처 - https://mangkyu.tistory.com/217

소켓 프로그래밍

소켓의 개념(RFC 793)

  • 인터넷 상에서 존재하는 각 port를 유니크하게 식별하기 위한 주소 각 Socket은 인터넷 상에서 유니크하다.
  • inet address + port number

실제 동작

애플리케이션이 시스템의 기능을 함부로 쓸 순 없기 때문에 시스템은 애플리케이션이 네트워크 기능을 사용할 수 있도록 프로그래밍 인터페이스를 제공한다. 애플리케이션은 socket을 통해 데이터를 주고 받는다. 개발자는 socket programing을 통해 네트워크 상의 다른 프로세스와 데이터를 주고 받을 수 있도록 구현한다.

실제 동작 관점에서 socket은 <protocol, IP address, port number>로 정의 된다. 그러면 실제 프로토콜 표준에서 정의한 것 처럼 유니크하게 식별될까?

UDP 기준으론 맞고, TCP 기준으로는 아니다.

만약 서버의 여러개 클라이언트 기준으로 TCP 접속 시엔 TCP 소켓이 모든 클라이언트 기준으로 동일하다. 따라서 실제 TCP로 서버에 connection 연결 요청이 들어오면 listening socket으로 받고 connection이 성립되면 추가로 소켓을 만드는데 이때 각 소켓의 <IP address, port number>가 모두 동일하다. 따라서 각각의 소켓이 어떤 데이터를 수신할지 모르게된다. 따라서 TCP에선 소켓을 식별할 땐 실제로 <src IP, src port, dest IP, dest port>로 socket을 식별한다. 또한 클라이언트 쪽에서도 같은 IP와 port를 가지는 다른 TCP 소켓이 생길 수 있다.

UDP는 어떨까? UDP는 연결지향성이 아니기 때문에 connection 개념이 없다. 따라서 UDP는 소켓에서 데이터를 보낼 때 어느 UDP 소켓으로 보낼지 지정할 수 있다. 받는 쪽도 어느 UDP socket으로부터 왔는지 알 수 있어서 하나의 소켓으로 해결한다.

자바 소켓 프로그래밍

public static void main(String[] args) throws IOException {
        logger.info("Server is starting...");

        try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
            logger.info("Listening for connection on port 8080 ....");

            ExecutorService executorService = ExecutorServiceConfiguration.getExecutorService();

            while (true) { // 무한 루프를 돌며 클라이언트의 연결을 기다립니다.
                Socket clientSocket = serverSocket.accept(); // 클라이언트 연결을 수락합니다.
                executorService.submit(new HttpProcessor(clientSocket)); // 클라이언트 요청을 병렬로 처리합니다.
            }
        } // 8080 포트에서 서버를 엽니다.
    }

자바는 위처럼 소켓 프로그래밍을 지원한다.

ServerSocket

서버 소켓의 생성자는 3개를 가질 수 있다.

public ServerSocket(int port)
             throws IOException
들어오는 연결 표시(연결 요청)에 대한 최대 큐 길이는 50으로 설정됩니다. 
큐 꽉 찼을 때 연결 표시 도착하면 연결이 거부됩니다.
             
public ServerSocket(int port,
                    int backlog)
             throws IOException
들어오는 연결 표시(연결 요청)의 최대 대기열 길이는 backlog 매개변수로 설정됩니다. 
대기열이 꽉 찼을 때 연결 표시 도착하면 연결이 거부됩니다.
             
public ServerSocket(int port,
                    int backlog,
                    InetAddress bindAddr)
             throws IOException

backlog는 큐의 길이인데 작성하지 않으면 디폴트 값으로 50이 설정된다.

serverSocket.accept()

새로운 소켓이 생성되고 이것을 클라이언트 소켓으로 사용한다. 즉,위에서 봤던

실제 TCP로 서버에 connection 연결 요청이 들어오면 listening socket으로 받고 connection이 성립되면 추가로 소켓을 만드는데..

이부분이 이루어진다.

그 후 사용은 Socket에서 InputStream과 OutputStream을 사용해 통신한다. 그 후 try with resource를 통해 통신을 닫는다.

멀티 쓰레드

자바 쓰레드

스레드에는 **사용자 수준 스레드(User Level Threads)**와 커널 수준 스레드(Kernel Level Threads) 두 가지 유형이 있다.

사용자 수준 스레드는 사용자 라이브러리를 통해 사용자가 만든 스레드로, 스레드가 생성 된 프로세스의 주소 공간에서 해당 프로세스에 의해 실행되고 관리된다. 그리고 커널 수준 스레드는 커널에 의해 생성되고 운영체제에 의해 직접 관리된다. 사용자 수준 스레드보다 생성 및 관리 속도가 느리다.

Java의 스레드 모델은 Native Thread로, Java의 유저 스레드를 만들면 Java Native Interface(JNI)를 통해 커널 영역을 호출하여 OS가 커널 스레드를 생성하고 매핑하여 작업을 수행하는 형태이다.

Thread와 Runnable

자바에서 맨 처음 등장했던 멀티 쓰레드를 돕는 클래스이다.

ExecutorService

자바에서는 Thread와 Runnable이 가장 먼저 등장해 멀티 쓰레딩을 구현하였다. 그러나 이 두 가지의 클래스는 아래와 문제점이 있었다.

  • 지나치게 저수준의 API(쓰레드의 생성)에 의존함
  • 값의 반환이 불가능
  • 매번 쓰레드 생성과 종료하는 오버헤드가 발생
  • 쓰레드들의 관리가 어려움

위와 같은 문제점 때문에 Java는 쓰레드를 사용하는 방법들을 꾸준히 발전시켜왔는데, 그 중 하나가 ExecutorService이다.

자바는 쓰레드의 생성과 관리를 위한 쓰레드 풀을 위한 기능들도 추가되었다.

쓰레드풀이란 요청마다 쓰레드를 만드는 것이 비효율적이기 때문에 쓰레드를 미리 만들어두고 재사용하기 위한 기술이다.

ExecutorService는 작업(Runnable, Callable) 등록을 위한 인터페이스이다. ExecutorService는 Executor를 상속받아서 작업 등록 뿐만 아니라 실행을 위한 책임도 갖는다.

그래서 쓰레드 풀은 기본적으로 ExecutorService 인터페이스를 구현한다.

대표적으로 ThreadPoolExecutor가 ExecutorService의 구현체인데, ThreadPoolExecutor 내부에 있는 블로킹 큐에 작업들을 등록해둔다.

ExecutorService는 라이프사이클 관리를 위한 기능, 비동기 작업을 위한 기능의 퍼블릭 메서드를 가진다. 비동기 작업으로는 Runnable과 Callable을 작업으로 사용하기 위한 메서드를 제공한다. 동시에 여러 작업들을 실행시키는 메서드도 제공하고 있는데 비동기 작업의 진행을 추적할 수 있도록 Future를 반환한다.

여기서 사용한 submit은 실행할 작업을 추가하고, 작업의 상태와 결과를 포함하는 Future를 반환하고, Future의 get을 호출하면 성공적으로 작업이 완료된 후 결과를 얻을 수 있다.

ThreadPoolExecutor

ExecutorService의 구현체로 ThreadPoolExecutor를 사용했는데, 톰캣은 이를 톰캣에 맞춘 특화된 구현체로 바꾸어서 사용한다.

ThreadPoolExecutor는 다음과 같은 생성자를 가진다.

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
BlockingQueue<Runnable> workQueue)

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  • corePoolSize, maximumPoolSize
    • 풀 크기를 자동으로 조정하는 매개변수
    • corePoolSize보다 적은 쓰레드가 실행 중이면 다른 작업자 쓰레드가 idle 상태이더라도 요청을 처리하기 위한 새 쓰레드가 생성된다.
    • corePoolSize보다 크지만 maximumPoolSize보다 적은 쓰레드가 실행 중이면 큐가 가득 찬 경우에만 새 쓰레드가 생성된다. → 큐가 차지 않으면 큐에 Runnable을 넣는다.
    • 이 두 개를 동일하게 설정하면 고정 크기의 쓰레드 풀이 생성된다.
    • 쓰레드 개수 증가는 corePoolSize 증가 → workQueue가 가득찰 때까지 queue에 추가 → maximumPoolSize 증가의 절차를 따른다.
  • keepAliveTime, unit
    • 풀에 현재 corePoolSize 이상의 스레드가 있는 경우, keepAliveTime 이상 idle 상태이면 초과 스레드는 종료된다.
    • unit은 시간의 단위를 설정한다.
  • workQueue
    • Thread 대기열을 구성하는 Queue이다.
  • handler
    • 쓰레드 풀과 큐가 다 찼을 때 들어오는 쓰레드 요청에 대한 거절 정책을 담당한다.
    • 설정하지 않으면 기본 값은 AbortPolicy이다.
      • 처리되지 못한 요청에 대해서 요청을 무시하고 Exception을 던진다.
  • threadFactory
    • 스레드 풀에서 스레드를 생성할 때 사용할 threadFactory를 정의한다.

WorkQueue

  • ArrayBlockingQueue

    • 배열을 지원하는 bounded blocking 큐이다.
    • 이 큐는 요소 FIFO(선입선출) 순서를 지정한다.
    • 이 클래스는 대기 중인 생산자 및 소비자 스레드를 주문하기 위한 선택적 공정성 정책을 지원합니다. 기본적으로 이 순서는 보장되지 않는다.
    • 그러나 공정성이 true로 설정된 큐는 FIFO 순서로 스레드 액세스를 허용한다.
    • 공정성은 일반적으로 처리량을 감소시키지만 변동성을 줄이고 기아를 방지한다.
    • offer는 큐가 가득 차거나, 지정된 시간 동안 큐에 공간이 생기지 않으면 요청이 거절된다. (매개변수를 어떻게 넣냐에 따라 다름)
  • LinkedBlockingQueue

    • 연결된 노드를 기반으로 하는 선택적으로 boundedblocking 대기열이다.
    • 이 큐는 요소 FIFO(선입선출) 순서를 지정한다.
    • 생성자를 통해 명시적으로 용량을 설정하지 않으면, 기본적으로 Integer.MAX_VALUE로 설정되어 사실상 무제한 크기를 가진다.
    • 따라서, 큐가 가득 찼을 때의 처리는 큐의 설정된 용량에 따라 달라진다.
    • Integer.MAX_VALUE로 사용하는 경우 큐가 가득찰 일이 없어 메모리 용량에 의해서만 제한된다. → OOM 유발 가능
    • 용량을 설정하면 추가 요소로 삽입하려는 쓰레드를 대기 상태로 전환시킨다.
  • ArrayBlockingQueue vs LinkedBlockingQueue

    1. 기본 데이터 구조:
      • ArrayBlockingQueue는 고정 크기의 배열을 사용하여 요소를 저장합니다. 큐의 크기는 생성 시에 정의되어야 하며 동적으로 변경될 수 없습니다.
      • LinkedBlockingQueue는 연결된 노드 구조를 사용하여 요소를 저장합니다. 선택적으로 용량 제한을 가질 수 있으며, 지정되지 않은 경우 용량은 Integer.MAX_VALUE입니다.
    2. 용량:
      • ArrayBlockingQueue는 고정 용량을 가집니다. 가득 찬 큐에 요소를 추가하려고 하면, 공간이 생길 때까지 작업이 차단되거나 즉시 실패합니다(비차단 메소드 사용 시).
      • LinkedBlockingQueue는 경계가 있는 경우와 없는 경우 모두 가능합니다. 지정된 용량 없이 초기화되면 거의 무한대로 성장할 수 있습니다(메모리에 의해 제한됨).
    3. 성능 및 확장성:
      • ArrayBlockingQueue는 고정 크기와 삽입 및 제거를 위한 배열 인덱스 관리 필요성으로 인해 LinkedBlockingQueue보다 일반적으로 처리량이 낮습니다.
      • LinkedBlockingQueue는 동적 노드 기반 구조를 사용하기 때문에, 특히 많은 프로듀서 및 컨슈머 스레드가 있는 환경에서 더 높은 처리량을 제공하며 동시 접근하에 더 잘 확장됩니다.
    4. 메모리 오버헤드:
      • ArrayBlockingQueue는 주어진 용량에 대해 하나의 배열을 사용하기 때문에 더 낮은 메모리 발자국을 가질 수 있습니다.
      • LinkedBlockingQueue는 연결 리스트 구조에 필요한 추가 포인터와 노드 객체로 인해 요소당 더 높은 메모리 오버헤드를 가집니다.
    5. 공정성 정책:
      • 두 큐 모두 공정성 정책을 설정할 수 있습니다. ArrayBlockingQueue의 경우, 이 정책은 큐가 가득 차거나 비어 있을 때 스레드에 대한 접근 순서에 영향을 줄 수 있습니다. LinkedBlockingQueue의 경우, 공정성은 주로 삽입 및 제거를 제어하는 잠금에 대한 스레드 접근 순서에 영향을 미칩니다.
    6. 반복자 동작:
      • ArrayBlockingQueue 제공하는 반복자는 약하게 일관되며 ConcurrentModificationException을 발생시키지 않습니다.
      • LinkedBlockingQueueConcurrentModificationException을 발생시키지 않는 약하게 일관된 반복자를 제공합니다.

    요약하자면, ArrayBlockingQueueLinkedBlockingQueue 사이의 선택은 애플리케이션의 특정 요구 사항, 예를 들어 용량 요구 사항, 성능 특성 및 메모리 오버헤드 고려 사항에 따라 달라져야 합니다.

  • 이외에도 PriorityBlockingQueue, SynchronousQueue 등이 있다.

  • 출처

느낀점

이번주 WAS 만들기를 하면서 WAS를 톰캣으로 사용할 때 쓰레드 풀, Http 메시지 파싱, 소켓 프로그래밍, url 관련 매핑, 정적 리소스 관련 유틸 클래스 등을 제공해주었는데 이러한 부분을 직접 만들어보면서 굉장히 편하게 코딩했구나를 느끼게 되었습니다.

이러한 부분을 직접 만들 때에도 어떻게 접근하면 좋을지에 대해 고민하고 또한 Jar나 쓰레드 관련 내용, 소켓, 파일 입출력 같은 기본 자바의 내용이 많이 비어있었다는 것을 느꼈고 이를 채우고 가는 좋은 시간이였습니다.

또한 쓰레드 풀에 대해 어렴풋이 알고 있던 내용을 정리하고 Http 스펙에 따라 구현해보는 시간도 가지며 많이 부족하다는 것을 느끼게 되었네요 !

개인 공부로 운영체제와 같은 CS에 대한 지식을 더 열심히 탐구해보겠습니다 !

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

개인 활동 페이지

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

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally