자바 - 네트워크 프로그램 자원 정리

이제 자원 정리를 네트워크 프로그램arrow-up-right에 도입해서, 안전하게 자원을 정리하도록 해보자. 참고로 항상 try-with-resources를 사용할 수 있는 것은 아니다. finally에서 직접 자원을 정리해야 하는 경우도 많다.

다음은 소켓과 스트림을 종료하기 위한 유틸 클래스이다.

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import static util.MyLogger.log;

public abstract class SocketCloseUtil {

    public static void closeAll(Socket socket, InputStream input, OutputStream output) {
        close(input);
        close(output);
        close(socket);
    }

    public static void close(InputStream input) {
        if (input != null) {
            try {
                input.close();
            } catch (IOException e) {
                log(e.getMessage());
            }
        }
    }

    public static void close(OutputStream output) {
        if (output != null) {
            try {
                output.close();
            } catch (IOException e) {
                log(e.getMessage());
            }
        }
    }

    public static void close(Socket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                log(e.getMessage());
            }
        }
    }
}
  • 자원 정리 과정에서 문제가 발생해도 코드에서 직접 대응할 수 있는 부분은 거의 없다. 이 경우 간단히 로그를 남겨서 개발자가 인지할 수 있는 정도면 충분하다.

  • 각각의 예외를 잡아서 처리했기 때문에 예외가 발생해도 다음 자원을 닫을 수 있다.

  • Socket을 기반으로 InputStreamOutputStream을 생성하기 때문에 닫는 순서를 반대로 해야 한다.

  • InputStream, OutputStream의 닫는 순서는 상관 없다.


V4

클라이언트 강제 종료 시 서버의 로그

  • 기존에는 클라이언트를 강제 종료하면 서버의 Session에 예외가 발생하면서 자원을 제대로 정리하지 못했다.

  • 이번에는 서버에 접속한 클라이언트를 강제 종료해도 서버의 Session이 "연결 종료" 메시지를 남기면서 자원을 잘 정리하는 것을 확인할 수 있다.


V5

이번에는 자원 정리에 try-with-resources를 적용해보자.

OutputStream, InputStream, Socket 모두 AutoCloseable을 구현하고 있다.


V6

이번에는 서버를 종료할 때 서버 소켓과 연결된 모든 소켓 자원을 다 반납하고 서버를 안정적으로 종료해보자. 서버를 종료하려면 서버에 종료라는 신호를 전달해야 한다. 예를 들어서 서버도 콘솔 창을 통해서 입력을 받도록 만들고 "종료" 메시지를 입력하면 모든 자원을 정리하면서 서버가 종료되도록 하면 된다. 하지만 보통 서버에서 콘솔 입력은 잘 하지 않으므로 서버를 직접 종료하면서 자원도 함께 정리해보자.

자바는 프로세스가 종료될 때 자원 정리나 로그 기록과 같은 종료 작업을 마무리 할 수 있는 셧다운 훅(Shutdown Hook) 이라는 기능을 지원한다.

프로세스 종료는 크게 2가지로 분류할 수 있다.

  1. 정상 종료

  • 모든 non 데몬 스레드의 실행 완료로 자바 프로세스 정상 종료

  • 사용자가 Ctrl+C를 눌러서 프로그램을 중단

  • kill 명령 전달(kill -9 제외)

  • IDE의 Stop 버튼

  1. 강제 종료

  • 운영체제에서 프로세스를 더 이상 유지할 수 없다고 판단할 때 사용

  • kill -9(리눅스/유닉스)나 taskkill /F(Windows)

정상 종료의 경우에는 셧다운 훅이 작동해서 프로세스 종료 전에 필요한 후 처리를 할 수 있다. 반면 강제 종료의 경우에는 셧다운 훅이 작동하지 않는다.

img_13.png
  • 서버는 세션을 관리하는 세션 매니저가 필요하다.

  • 각 세션은 소켓과 연결 스트림을 가지고 있다. 따라서 서버를 종료할 때 사용하는 세션들도 함께 종료해야 한다.

  • 모든 세션들을 찾아서 종료하기 위해 생성한 세션을 보관하고 관리할 객체가 필요하다.

  • SessionManager가 이 역할을 수행한다.

  • SessionManager에는 여러 스레드에서 접근할 수 있다. 예를 들어 closeAll()로 자원을 정리하는 중에 add()remove()가 호출될 수 있는 것이다.

  • Session은 이제 try-with-resources를 사용할 수 없다. 서버를 종료하는 시점에도 Session의 자원을 정리해야 하기 때문이다.

  • try-with-resources는 사용과 해제를 함께 묶어서 처리할 때 사용한다.

    • try-with-resourcestry 선언부에서 사용한 자원을 try가 끝나는 시점에 정리한다.

    • 따라서 try에서 자원의 선언과 자원 정리를 묶어서 처리할 때 사용할 수 있다.

    • 하지만 지금은 서버를 종료하는 시점에도 Session이 사용하는 자원을 정리해야 한다. 서버를 종료하는 시점에 자원을 정리하는 것은 Session 안에 있는 try-with-resources를 통해 처리할 수 없다.

  • 또한 자원을 정리하는 close() 메서드는 두 곳에서 호출될 수 있다.

    • 클라이언트와 연결이 종료되었을 때(exit 또는 예외 발생)

    • 서버를 종료할 때

  • 따라서 close()가 다른 스레드에서 동시에 중복 호출될 가능성이 있다.

  • 이런 문제를 막기 위해 synchronized 키워드를 사용했으며, 자원 정리 코드가 중복 호출 되는 것을 막기 위해 closed 변수를 플래그로 사용했다.

  • Runtime.getRuntime().addShutdownHook()을 사용하여 자바 종료 시 호출되는 셧다운 훅을 등록할 수 있다. 여기에 셧다운이 발생했을 때 처리할 작업과 스레드를 등록하면 된다.

img_14.png
  • 셧다운 훅이 실행될 때 모든 자원을 정리한다.

  • SessionManagercloseAll()을 호출하여 모든 세션이 사용하는 자원(Socket, InputStream, OutputStream)을 정리한다.

  • ServerSocketclose()를 호출하여 서버 소켓을 닫는다.

  • 중간에 자원 정리를 대기하는 이유

    • 보통 모든 non 데몬 스레드의 실행이 완료되면 자바 프로세스가 정상 종료되지만, Ctrl+C, kill 명령어, Stop 버튼과 같은 종료도 있다.

    • 이런 경우에는 non 데몬 스레드의 종료 여부와 관계없이 자바 프로세스가 종료된다.

    • 단 셧다운 훅의 실행이 끝날 때 까지는 기다려준다.

    • 즉 셧다운 훅의 실행이 끝나면 non 데몬 스레드의 실행 여부와 상관 없이 자바 프로세스는 종료된다.

    • 따라서 다른 스레드가 자원을 정리하거나 필요한 로그를 남길 수 있도록 셧다운 훅의 실행을 잠시 대기한다.

  • 서버를 종료하면 shutdown 스레드가 shutdownHook을 실행하고, 세션의 Socket의 연결을 close()로 닫는다.

    • [ Thread-0] java.net.SocketException: Socket closed

    • Sessioninput.readUTF()에서 입력을 대기하는 Thread-0 스레드는 SocketException 예외를 받고 종료된다. (이 예외는 자신의 소켓을 닫았을 때 발생한다.)

  • shutdown 스레드는 서버 소켓을 close()로 닫는다.

    • [ main] 서버 소켓 종료: java.net.SocketException: Socket closed

    • accept()에서 대기하고 있는 main 스레드는 SocketException 예외를 받고 종료된다.


이전 ↩️ - 자원 정리 이해arrow-up-right

메인 ⏫arrow-up-right

다음 ↪️ - 네트워크 예외arrow-up-right

Last updated