김영한의 실전 자바 - 고급 2편(네트워크 자원정리)

2025. 1. 28. 20:47·김영한의 실전 자바 - 고급 2편

클라이언트와 서버간 통신시 필요한 요소를 설명하시오

  • 클라이언트 서버간 통신에는 기본적으로 소켓이 필요
  • 소켓에는 데이터를 내보내는 OutputStream, 데이터를 들이는 InputStream이 있어야 함.
  • 해당 스트림을 통해 Byte 단위 통신을 수행함.

 

클라이언트와 서버간 통신 플로우를 설명하시오

public class ServerV1 {
    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("서버 시작");

        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트: " + PORT);

        Socket socket = serverSocket.accept();
        log("소켓 연결: " + socket);

        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());

        // 클라이언트로부터 문자 받기
        String received = input.readUTF();
        log("client -> server: " + received);

        // 클라이언트에게 문자 보내기
        String toSend = received + " World!";
        output.writeUTF(toSend);
        log("client <- server: " + toSend);

        // 자원 정리
        log("연결 종료: " + socket);
        input.close();
        output.close();
        socket.close();
        serverSocket.close();
    }
}
  • 서버가 12345 포트로 서버 소켓을 열어둠
  • 클라이언트가 서버의 12345 포트로 연결을 시도
  • OS 계층에서 TCP 3 way handshake가 발생하고 TCP 연결이 완료됨
  • TCP 연결이 완료되면 서버는 OS backlog queue라는 곳에 클라이언트와 서버의 TCP 연결 정보를 보관
    • OS backlog queue 에는 클라이언트의 IP, Port, 서버의 IP, Port 정보가 들어감
    • 서버의 포트는 무조건 명시적으로 존재해야 하지만 클라이언트의 포트는 일반적으로 지정하지 않으며 사용되지 않는 랜덤 포트가 일반적으로 할당 됨
  • accept 함수가 실행되면 OS backlog queue에서 TCP 연결 정보를 조회하며, 해당 정보를 기반으로 Socket 객체를 생성
  • 사용된 TCP 연결 정보는 backlog queue에서 제거 됨.
  • 데이터를 서로 주고 받음
  • 모든 작업이 끝나면 input, output 스트림, 소켓, 서버소켓을 모두 닫음.

 

다음 코드를 설명하시고, 개선할 수 있는 방안을 말하시오

public class ResourceV1 {
    private String name;

    public ResourceV1(String name) {
        this.name = name;
    }

    public void call() {
        System.out.println(name + " call");
    }

    public void callEx() throws CallException {
        System.out.println(name + " callEx");
        throw new CallException(name + " ex");
    }

    public void close() {
        System.out.println(name + " close");
    }

    public void closeEx() throws CloseException {
        System.out.println(name + " closeEx");
        throw new CloseException(name + " ex");
    }
}
public class ResourceCloseMainV3 {
    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            e.printStackTrace();
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            e.printStackTrace();
        }
    }

    private static void logic() throws CallException, CloseException {
        ResourceV1 resource1 = null;
        ResourceV1 resource2 = null;

        try {
            resource1 = new ResourceV1("resource1");
            resource2 = new ResourceV1("resource2");
            resource1.call();
            resource2.callEx(); // CallException
        } catch (CallException e) {
            System.out.println("ex: " + e);
            throw e;
        } finally {
            if (resource2 != null) {
                try {
                    resource2.closeEx();
                } catch (CloseException e) {
                    // close()에서 발생한 예외는 버린다. 필요하면 로깅 정도
                    System.out.println("close ex: " + e);
                }
            }
            if (resource1 != null) {
                try {
                    resource1.closeEx();
                } catch (CloseException e) {
                    System.out.println("close ex: " + e);
                }
            }
        }
    }
}
  • 위는 자원을 정리할 때 발생할 수 있는 예외사항까지 모두 고려한 코드
  • finally에서 에러 처리를 함으로써 어디서 에러가 발생했던 finally에서 결국 자원 정리를 시도할 수 있음
  • 자원이 생성도 되기 전에 에러가 발생한 경우를 대비해 resource가 null이 아닐 경우에는 자원 정리 프로세스 수행
  • CallException은 실제 로직에 대한 예외고 CloseException은 닫는 부분에 대한 예외기 때문에 실제 중요한 것은 로직 부분은 CallException이라 Close하다가 발생한 예외는 던져지지 않도록 사라지게 try~catch로 묶고 사라지게 처리.
  • 자원 1,2가 서로 연관이 있다면 1,2 순으로 불리고 2,1 순으로 자원을 닫도록 설정.
  • 하지만, 이는 코드가 복잡하고 가독성이 떨어져, 아래처럼 try catch resource로 해결 가능
public class ResourceV2 implements AutoCloseable {
    private String name;

    public ResourceV2(String name) {
        this.name = name;
    }

    public void call() {
        System.out.println(name + " call");
    }

    public void callEx() throws CallException {
        System.out.println(name + " callEx");
        throw new CallException(name + " ex");
    }

    @Override
    public void close() throws CloseException {
        System.out.println(name + " close");
        throw new CloseException(name + " ex");
    }
}
public class ResourceCloseMainV4 {
    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            Throwable[] suppressed = e.getSuppressed();
            for (Throwable throwable : suppressed) {
                System.out.println("suppressedEx = " + throwable);
            }
            e.printStackTrace();
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            e.printStackTrace();
        }
    }

    private static void logic() throws CallException, CloseException {
        try (ResourceV2 resource1 = new ResourceV2("resource1");
             ResourceV2 resource2 = new ResourceV2("resource2")) {
            resource1.call();
            resource2.callEx(); // CallException;
        } catch (CallException e) {
            System.out.println("ex: " + e);
            throw e; // CallException;
        }
    }
}
  • Resource에 대해서는 AutoClosable을 구현하고 close() 함수를 Override를 하여, try catch resource 구문에서 close가 자동으로 불릴 수 있도록 개발
  • try() 구문에 자원 생성을 명시할 수 있으며 이는 변수 선언과 동시에 할당이 가능(앞선 코드에서는 try 안에서 변수 선언 불가능했음)
  • 개발자가 실수로 close()를 호출하지 않아도 자동으로 호출됨
  • close 호출 순서를 reousrce 1,2 로 했으면 close는 2,1로 해야 하는데, 이 과정에서 발생할 수 있는 실수를 방지할 수 있으며 자동으로 반대로 호출해줌
  • 기존에는 try -> catch -> finally 순으로 자원 반납을 했다면 try with resources는 try가 끝나면 즉시 close를 호출함.
  • call 예외만 반납되게 되며, close에서 발생할 수 있는 예외는 e.getSuppressed()를 통해 활용할 수 있게 안어 넣어줌

 

프로세스의 정상 종료와 강제 종류는 어떤 케이스가 있는가?

  •  정상 종료
    • 모든 non 데몬 스레드의 실행 완료로 자바 프로세스 정상 종료
    • 사용자가 Ctrl+C를 눌러서 프로그램을 중단
    • kill 명령 전달 ( kill -9 제외)
    • IntelliJ의 stop 버튼
    • 셧다운 훅이 작동한다.
  • 강제 종료
    • 운영체제에서 프로세스를 더 이상 유지할 수 없다고 판단할 때 사용
    • 리눅스/유닉스의 kill -9 나 Windows의 taskkill /F
    • 셧다운 훅이 작동하지 않는다.

 

프로세스가 정상 종료될 때 모든 네트워크 자원을 종료할 수 있는 코드를 추가하시오

static class ShutdownHook implements Runnable {
    private final ServerSocket serverSocket;
    private final SessionManagerV6 sessionManager;

    public ShutdownHook(ServerSocket serverSocket, SessionManagerV6 sessionManager) {
        this.serverSocket = serverSocket;
        this.sessionManager = sessionManager;
    }

    @Override
    public void run() {
        log("shutdownHook 실행");
        try {
            sessionManager.closeAll();
            serverSocket.close();
            Thread.sleep(1000); // 자원 정리 대기
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("e = " + e);
        }
    }
}
public static void main(String[] args) throws IOException {
    log("서버 시작");

    SessionManagerV6 sessionManager = new SessionManagerV6();
    ServerSocket serverSocket = new ServerSocket(PORT);
    log("서버 소켓 시작 - 리스닝 포트: " + PORT);

    // ShutdownHook 등록
    ShutdownHook shutdownHook = new ShutdownHook(serverSocket, sessionManager);
    Runtime.getRuntime().addShutdownHook(new Thread(shutdownHook, "shutdown"));

	...
public class SessionManagerV6 {
    private List<SessionV6> sessions = new ArrayList<>();

    public synchronized void add(SessionV6 session) {
        sessions.add(session);
    }

    public synchronized void remove(SessionV6 session) {
        sessions.remove(session);
    }

    public synchronized void closeAll() {
        for (SessionV6 session : sessions) {
            session.close();
        }
        sessions.clear();
    }
}
public class SessionV6 implements Runnable {
    private final Socket socket;
    private final DataInputStream input;
    private final DataOutputStream output;
    private final SessionManagerV6 sessionManager;
    private boolean closed = false;

    public SessionV6(Socket socket, SessionManagerV6 sessionManager) throws IOException {
        this.socket = socket;
        this.input = new DataInputStream(socket.getInputStream());
        this.output = new DataOutputStream(socket.getOutputStream());
        this.sessionManager = sessionManager;
        this.sessionManager.add(this);
    }

    @Override
    public void run() {
        try {
            while (true) {
                // 클라이언트로부터 문자 받기
                String received = input.readUTF();
                log("client -> server: " + received);

                if (received.equals("exit")) {
                    break;
                }

                // 클라이언트에게 문자 보내기
                String toSend = received + " World!";
                output.writeUTF(toSend);
                log("client <- server: " + toSend);
            }
        } catch (IOException e) {
            log(e);
        } finally {
            sessionManager.remove(this);
            close();
        }
    }

    // 세션 종료시, 서버 종료시 동시에 호출될 수 있다.
    public synchronized void close() {
        if (closed) {
            return;
        }
        closeAll(socket, input, output);
        closed = true;
        log("연결 종료: " + socket);
    }
}
  • Shutdown 훅을 추가하면 정상 종료시 자동으로 Shutdown 훅을 호출
  • Ctrl+C, kill, IntelliJ의 Stop은 non-데몬 스레드의 종료와 상관없이 자바 프로세스를 종료하는데, 이 때 셧다운 훅의 실행은 기다려줌.
  • Thread.sleep(1000)을 해서 다른 스레드의 자원 정리를 기다려 주거나 로그를 남길 수 있음.
  • Session은 try 이후 close 외에 서버 종료시점에도 Session의 자원을 정리해야 하기 때문에 try-with-resource를 사용할 수 없음.
  • synchronized를 사용하는 이유는 close() 메소드가 클라이언트와 종료되었을 때, 서버를 종료할 때 두 곳에서 불릴 수 있기 때문에 동시성문제 해결 필요

 

'김영한의 실전 자바 - 고급 2편' 카테고리의 다른 글

김영한의 실전 자바 - 고급 2편(HTTP서버)  (2) 2025.01.29
김영한의 실전 자바 - 고급 2편(네트워크 예외)  (2) 2025.01.28
김영한의 실전 자바 - 고급 2편(네트워크, TCP, IP, UDP, DNS)  (1) 2025.01.28
김영한의 실전 자바 - 고급 2편(스트림 활용)  (2) 2025.01.25
김영한의 실전 자바 - 고급 2편(스트림)  (2) 2025.01.23
'김영한의 실전 자바 - 고급 2편' 카테고리의 다른 글
  • 김영한의 실전 자바 - 고급 2편(HTTP서버)
  • 김영한의 실전 자바 - 고급 2편(네트워크 예외)
  • 김영한의 실전 자바 - 고급 2편(네트워크, TCP, IP, UDP, DNS)
  • 김영한의 실전 자바 - 고급 2편(스트림 활용)
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      김영한
      jdk 동적 프록시
      @within
      Target
      프록시 팩토리
      requset scope
      reentarantlock
      jpq
      단방향 맵핑
      cglib
      고급
      프록시
      빈 후처리기
      WAS
      Thread
      JPQL
      페치 조인
      log trace
      hibernate5module
      락
      스레드
      버퍼
      조회 성능 최적화
      gesingleresult
      자바
      typequery
      양방향 맵핑
      @discriminatorvalue
      @args
      @discriminatorcolumn
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 실전 자바 - 고급 2편(네트워크 자원정리)
    상단으로

    티스토리툴바