Skip to content

김준기 2주차 학습 일지

June edited this page Jul 8, 2024 · 1 revision

try with resources

try ~ catch 구문에서 어떤 리소스를 사용할 때 주의해야할 점은 해당 리소스를 모두 사용하고 나서 반드시 .close()해줘야 한다. 이를 누락하면 메모리 누수의 원인이 될 수 있다.

Socket socket = serverSocket.accept();
InputStream inputStream = null;
try {
	inputStream = socket.getInputStream();
} finally {
	inputStream.close();
}

finally 구문에서 close 하게 되는데, 휴먼 에러등으로 인해 자원의 반납을 누락할 수 있다. 이 때 try with resource 구문을 사용하면 좋다. try 구문에서 사용되는 자원은 자동으로 반납 close()된다.

try (InputStream inputStream = socket.getInputStream()) {
    // something..
}

주의해야할 점은 Closeable 인터페이스를 구현하고 있어야, try with resource문에서 활용할 수 있다. 내부 구현의 구체적 원리까진 모르겠으나, JVM이 Runtime에 자원을 반납 close() 하기 위해 표준 인터페이스를 둔게 아닐까 싶다. 반드시 Closeable 인터페이스를 구현해해야하므로, Compile Time에 이를 검증할 수도 있게 된다.

public abstract class InputStream implements Closeable { }



Socket

Java Socket API를 오랜만에 사용하게 되면서 헷갈렸던 내용이 있다. 아래 코드의 clientSocket 이 무슨 용도인지 의문이었다. 처음에 단순하게 들었던 생각은 서버 소켓을 이미 열었는데, 왜 클라이언트의 요청을 accept할 때마다 새로운 소켓을 만들어내지? 였다. 아무래도 clientSocket네이밍이 서버/클라이언트 소켓 프로그래밍할 때 클라이언트측 소켓 네이밍과 다소 혼동이 있었기 때문이다.

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    try (Socket clientSocket = serverSocket.accept()) {
        OutputStream clientOutput = clientSocket.getOutputStream();
        // something..
    }
}

ServerSocket이 클라이언트의 요청을 accept 할 때마다 새로운 Socket을 생성하는 이유는 간단하다. 일반적으로 TCP 소켓은 클라이언트와 서버가 일대일 통신이기 때문이다. 그리고 그 소켓은 <Source IP, Source Port, Dest IP, Dest Port> 로 유일하게 식별한다. ServerSocket 하나만으로는 이를 구현할 수 있는 방법이 없다. 참고로 <Source IP, Source Port, Dest IP, Dest Port> 중 일부는 동일해도 무방하다. 하지만 전체가 동일한 경우는 있을 수 없다.



정적 자원 처리와 동적 자원 처리의 구분

HTTP 요청 메시지를 서버측에서 수신 및 디코딩하면 결국 문자열일 뿐이다. 다음과 같이 클라이언트에게 HTTP GET 메서드와 URI를 수신받았다고 해보자.


WAS는 HTTP 메소드 문자열URI 문자열정보를 바탕으로 정적 자원 및 동적 자원 처리를 구분하여 클라이언트측에게 적절한 응답을 내려줄 책임이 있다. HTTP 메소드는 비교적 구분이 어렵지 않다. 하지만 URI 문자열은 정적 자원의 디렉터리 경로를 의미하는지, 동적 자원의 API End Point를 의미하는지 어떻게 적절히 구분하겠는가? 이 부분이 개인적으로 까다로운 부분이었다.

데일리 스크럼 때 고민하고 있는 부분을 이야기하는 시간을 가졌고, API End Point를 일종의 예약어라고 생각하여 이를 매핑하는 로직을 구현했다는 팀원의 조언에 힌트를 얻을 수 있었다. 그리고 디스패처 서블릿에선 이를 어떤식으로 해결해나갔는지 래퍼런스를 참고하며 학습을 진행했다.

핵심은 요청이 들어왔을 때, 이 요청을 처리할 수 있는 적절한 핸들러만 찾아주면 되는 것이다. 해시맵의 Key에 API End Point를 등록하고, Value에 관련 핸들러를 저장하여 관리하면 될 것이다. 현재는 GET /create End Point만 필요하므로 하나만 등록했다.

public class HandlerMapper {

    private final Map<String, Handler> handlers = new ConcurrentHashMap<>();

    public HandlerMapper() {
        handlers.put("/create", new UserRegistrationHandler());
    }

    public Optional<Handler> findHandler(String path) {
        return Optional.ofNullable(handlers.get(path));
    }

}
public void handle(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
    HttpMethod method = httpRequest.getMethod();
    HttpPath path = httpRequest.getPath();

    // If API End Point Handler?
    // Yes: Optional is Present, No: Optional is Empty
    Optional<Handler> handler = handlerMapper.findHandler(path.getDefaultPath());
    if (handler.isPresent()) {
        Handler userRegistrationHandler = handler.get();
        userRegistrationHandler.service(httpRequest, httpResponse);
        httpResponse.setDefaultHeaders(zonedDateTimeGenerator.now());
        return;
    }

    // If Static Resource Handler?
    // Yes: no execption, No: exception
    staticResourceHandler.service(httpRequest, httpResponse);
    httpResponse.setDefaultHeaders(zonedDateTimeGenerator.now());

    // Bad Request
    httpResponse.setDefaultHeaders(zonedDateTimeGenerator.now());
    httpResponse.setBadRequest();
}

아직 손봐야할 부분이 많은 코드이다. 정적 핸들러와 동적 핸들러를 한 번에 처리하지 못한 점, API End Point를 문자열로 처리한 점, HTTP 메서드는 관리하지 않은 점 등 아쉬운 점이 많다. 하지만 아래의 핵심 위주로 코드를 봐주면 좋을듯 하다.
1.요청에 대해 정적 / 동적 자원 처리를 구분해야 한다.
2.요청을 처리할 수 있는 핸들러가 존재하는지 확인함으로서 이를 해결할 수 있고,
3.구체적으로는 Map의 <Key: API End Point, Value: Handler> 로 구현할 수 있다.



Thread Pool

Core Size, Max Size, Task Waiting Queue

참고로 여기서 Task Waiting Queue는 스레드 풀을 생성할 때 인자로 전달하는 BlockingQueue를 의미한다.

LinkedBlockingQueue<Runnable> taskWaititingQueue = new LinkedBlockingQueue<>();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 1, 10, 1000L, TimeUnit.SECONDS, taskWaititingQueue);

1. 스레드에게 작업이 할당(execute / submit) 되면 core size 만큼 스레드를 점진적으로 생성한다.


2. 이 때 기존에 생성된 스레드가 idle 상태여도 상관없이 core size만큼 생성부터 시킨다.


3. core size 만큼 스레드 풀의 스레드가 찼고, max size 보다 스레드가 적다면, Task Waiting Queue에 Task를 대기 시킨다.


4. Task Wait Queue 마저도 다 찼다면, max size 만큼 스레드를 생성한여 Task를 할당한다.



HTTP

HTTP는 HTTP 완벽 가이드 서적을 통해 기본적 내용을 학습했다. 그 중에서 인상 깊었던 내용들 위주로 정리하고자 한다.

HTTP connection less와 TCP 연결 지향, 지속 커넥션

HTTP를 학습하다보면 connection less라는 단어를 많이 접할 수 있다. HTTP 요청/응답 트랜잭션이 마무리되면, 연결을 끊는다는 내용인데, 이는 TCP 연결 지향의 내용과 다소 상충되는 것처럼 보여 혼동이 있을 수 있다.

복잡하게 생각할 것 없이, HTTP 요청/응답 트랜잭션이 마무리되면 TCP 커넥션 연결을 끊는 것이 맞다. TCP 연결 지향의 경우 하나의 HTTP 요청/응답 트랜잭션 동안 논리적으로 연결이 유지된다는 것으로 이해하면 된다.

지속 커넥션 (Persistence Connection)의 경우 HTTP 요청/응답 트랜잭션이 종료 되어도, 해당 TCP 커넥션의 연결을 유지하겠다는 의미이다. 그러면 왜 TCP 커넥션을 종료하지 않고 유지하는지 그 이유에 대해 생각해볼 필요가 있다. HTTP 완벽 가이드 서적에 따르면, 다음과 같은 이유를 기술하고 있다.

1. TCP 커넥션을 맺고 끊는데 발생하는 시간을 단축
익히 알고있듯, TCP 커넥션은 3-way handshake, 4-way handshake를 통해 두 컴포넌트 간의 연결/비연결 여부를 논리적으로 확인한다. 이 과정에서 필연적으로 시간이 소요될 수밖에 없다. 하지만 하나의 커넥션을 재활용한다면 연결을 맺고 끊는데 발생하는 시간을 절약할 수 있을 것이다. 그리고 이는 사이트 지역성(locality)와도 연관이 있다. 한 번 방문한 사이트는 다시 방문할 가능성이 높으므로, 커넥션을 재활용하는 것이 유의미할 것이다.

2. TCP 느린 시작을 최소화
HTTP 완벽 가이드에 따르면, TCP는 갑작스러운 부하와 혼잡을 방지하기 위해 느린 시작 메커니즘을 적용한다고 한다. TCP 연결이 맺어지면 커넥션의 최대 속도 제한을 점진적으로 증가시키는 방법이다.

똑같은 사이트에 요청을 보낼 때마다 TCP 연결을 새로 맺는다면, 매 번 TCP 느린 시작으로 인해 응답에 지연이 생길 수 있다. 하지만 지속 커넥션을 활용한다면, 커넥션이 유지되는 동안 튜닝된 커넥션을 활용하여 응답 시간이 줄어들 것이다.

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

개인 활동 페이지

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

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally