네트워크 - 프로그램(2)
#Java/adv2/Network
/네트워크 프로그램4 - 자원 정리1: 네트워크 프로그램에 try-catch-finally
도입
- 스트림, 소켓은 여러 곳에서 닫는다 유틸리티 클래스로 뽑자
- close하다가 예외 발생시 단순히 로그만 남기자
- input, output, socket 닫는 것 각각을 메서드로 뽑아서, 각각 예외를 잡도록 하자. 하나를 닫는 과정에서 예외가 발생해도 다음 자원을 닫을 수 있다.
- 순서에 신경쓰자(여기선 socket을 마지막에 닫자)
- Client
- 자원 선언은 try 밖으로 빼고, 자원 정리 시
finally
코드 블럭에서SocketCloseUtil.closeAll()
만 호출
- 자원 선언은 try 밖으로 빼고, 자원 정리 시
- Session
- Client와 동일한 구조 적용
- Server는 동일
- 해결한 문제: 클라이언트에서 연결 종료 시 서버의
Session
에EOFException
이 발생하면서 자원을 제대로 정리하지 못했던 문제를 finally를 통해 자원을 정리하도록 하였다.- 참고)
EOFException
는IOException
의 자식이다.
- 참고)
/네트워크 프로그램5 - 자원 정리2 : try-with-resources
도입
- Client
try (자원 선언 및 할당)
- 선언되는 순서의 반대로 자원 정리가 적용된다는 점에 유의하자.
OutputStream
,InputStream
,Socket
모두AutoCloseable
을 구현하고 있다.
- Session
- 세션의 경우 socket을 생성하여 사용하는게 아니라, 생성할 때 외부에서 받아온다.
try (socket; DataInputStream input = ….)
이렇게 객체의 참조만 넣어두면 자원 정리 시점에AutoCloseable
이 호출된다.socket.isClosed()
를 통해 실제로 자원이 정리가 되었는지 체크할 수 있다.
- Server는 동일
- 서버를 종료할 때, 서버 소켓과 연결된 모든 소켓 자원을 다 반납하고 서버를 안정적으로 종료하는 방법
- 서버의 특정 비즈니스 로직을 넣어 종료하는 경우가 아니라, 서버를 직접 종료하면서 자원도 함께 정리하는 방법
- 셧다운 훅(Shutdown Hook) 적용
- 자바는 프로세스가 종료될 때, 자원 정리나 로그 기록과 같은 종료 작업을 마무리 할 수 있는 셧다운 훅이라는 기능을 지원한다
- 프로세스 종료 2가지 분류
- 정상 종료
- 모든 non 데몬 스레드의 실행 완료로 자바 프로세스 정상 종료
- 사용자가 Ctrl+C를 눌러서 프로그램을 중단
kill
명령 전달 (kill -9
제외)- IntelliJ의 stop 버튼
- 강제 종료
- 운영체제에서 프로세스를 더 이상 유지할 수 없다고 판단할 때 사용
- 리눅스/유닉스의
kill -9
나 Windows의taskkill /F
- 정상 종료의 경우에는 셧다운 훅이 작동해서 프로세스 종료 전에 필요한 후 처리를 할 수 있다.
- Client는 기존과 동일
- Server
- 세션 매니저 필요
- 원래 메인 스레드에서는 while문을 통해 OS의 백로그 큐에서 연결을 확인하고 별도의 스레드에서 세션(소켓)을 만들어줬다.
- 문제는 세션을 만들고나서 부터는 별도 스레드에서 동작하기 때문에, 이 세션들을 추적하지를 않는다. 서버가 종료될 때 이 세션들을 추적해서 다 종료를 해줘야한다.
- 자료구조에다가 다 담아둔 다음에 꺼내서 종료해주자. 요걸 SessionManager라고 하자.
- SessionManager
add()
: 클라이언트의 새로운 연결을 통해, 세션이 새로 만들어지는 경우add()
를 호출해서 세션 매니저에 세션을 추가한다.세션
이세션매니저
의add()
에자신(this)
을 전달하여 추가하는 구조로 구현.remove()
: 클라이언트의 연결이 끊어지면 세션도 함께 정리된다. 이 경우remove()
를 호출해서 세션 매니저에서 세션을 제거한다.
- 세션 매니저 필요
- Session
- 세션은 다음 두 가지 경우에 종료될 수 있다.
- 클라이언트 연결 종료 (or IOException 발생)
- 서버 종료 시 서버의 SessionManager가 세션을 종료시킴
- 이를 위해
자원.close()
하는 코드를 별도의 public 메서드로 분리하자.- 이를 위해서는 socket, input, output을 Session의 필드에 선언해야 한다. (run, close 두 군데에서 접근)
- 또한, SessionManager 또한 필드에 선언하여 Session 생성시 SessionManager의 참조를 얻고, 클라이언트에서 연결 종료시 sessionManager의 세션 목록에서 자신을 제거할 수 있도록 하자.
- 세선 종료 시, 서버 종료 시 동시에 호출될 수 있으므로
synchronized
로 동시성 제어를 하자.- 동시 접근시 첫 번째 이후에 접근하는 close()가 이미 닫힌 자원을 다시 닫지 않도록 하기 위해서,
closed
변수를 플래그 용도로 선언하여, 이미 닫힌 경우 return 하도록 해준다.
- 동시 접근시 첫 번째 이후에 접근하는 close()가 이미 닫힌 자원을 다시 닫지 않도록 하기 위해서,
- try-with-resources를 사용할 수 없는 구조다.try-with-resources는 사용과 해제를 함께 묶어서 처리할 때 사용한다.
- 세션은 다음 두 가지 경우에 종료될 수 있다.
/네트워크 프로그램6 - 자원 정리4 : ShutDownHook 등록
ShutDownHook
클래스- 필드에 ServerSocket과 sessionManager 참조
Runnable
구현run()
에서sessionManager.closeAll();
,serverSocket.close();
호출- 호출 후
Thread.sleep(1000);
로 자원 정리 대기 해야 한다.- 보통 모든 non 데몬 스레드의 실행이 완료되면 자바 프로세스가 정상 종료되지만, Ctrl+C, kill 명령, 인텔리제이 stop 등으로 종료하면 non 데몬 스레드 종료 여부랑 무관하게 자바 프로세스가 종료된다.
- 단, 셧다운 훅의 실행이 끝날 때 까지는 기다려준다.
- sleep을 통해 다른 스레드가 자원을 정리하거나 필요한 로그를 남길 수 있도록 셧다운 훅의 실행을 잠시 대기한다.
- 셧다운 훅 등록
ShutdownHook shutdownHook = new ShutdownHook(serverSocket, sessionManager);
Runtime.getRuntime().addShutdownHook(new Thread(shutdownHook, "shutdown"));
- 정상 종료
-
ShutdownHook
이sessionManger.closeAll()
호출-
ShutdownHook
스레드가session.close()
- 기존에
readUTF()
로 블로킹 중이던 스레드는 SocketException을 만나 catch, finally문으로 이동하여 sessionManager에서 자신을 제거하고close()
호출. ShutdownHook
스레드와 세션을 실행중이던 스레드 동시에close()
호출될 수 있음 어차피 synchronized로 먼저 실행된 얘만 close함. 뒤 얘는 플래그 보고 return
-
-
ShutdownHook
이serverSocket.close()
호출- 메인 스레드는
serverSocket.accept()
에서 블로킹이였고, 셧다운 훅의close()
로SocketException
가 발생해 catch문으로 이동
- 메인 스레드는
ShutdownHook
이 세션매니저와 서버 소켓을 close하고 Thread.sleep()을 해야, 블로킹 상태던 기존 스레드들이 catch로 이동해 로그를 남기거나 다른 필요한 자원들을 정리할 수 있음. (ShutDownHook의 자원 정리 대기 이유)
-
네트워크 연결 시 발생할 수 있는 예외들
- /네트워크 예외1 - 연결 예외
UnknownHostException
: 호스트를 알 수 없다.Socket socket = new Socket("999.999.999.999", 80);
Socket socket = new Socket("google.gogo", 80);
ConnectException
: 연결이 거절됨Socket socket = new Socket("localhost", 45678); // 미사용 포트
java.net.ConnectException: Connection refused
- 연결이 거절되었다는 것은, 우선은 네트워크를 통해 해당 IP의 서버 컴퓨터에 접속은 했다는 뜻
- 해당 서버 컴퓨터가 45678 포트를 사용하지 않기 때문에 TCP 연결을 거절
- IP에 해당하는 서버는 켜져있지만, 사용하는 PORT가 없을 때 주로 발생
- 네트워크 방화벽의 무단 연결 방지 시에도 이 예외 발생.
- 서버 컴퓨터의 OS는 이때 TCP RST(Reset)라는 패킷을 보내서 연결을 거절한다. 클라이언트가 연결 시도 중에 RST 패킷을 받으면 이 예외 발생
- TCP RST(Reset) 패킷: 연결에 문제가 있다는 뜻. 이 패킷을 받은 대상은 연결을 바로 해제해야 함.
- /네트워크 예외2 - 타임아웃: 네트워크 연결을 시도해서 서버 IP에 연결 패킷을 전달했지만 응답이 없는 경우
- 실무에서 자주 발생하는 부분
- 타임아웃이 발생하는 경우
- 해당 IP로 연결 패킷을 보내지만 해당 서버가 너무 바쁘거나 문제가 있어서 연결 응답 패킷을 보내지 못하는 경우도 있다.
- 공유기를 이용하여 공유기가 사용하는 IP 대역의
192.168.1.250
을 사용하여 연결하면 연결 패킷을 보내지만 IP를 사용하는 서버가 없으므로 TCP 응답이 오지 않는다.
- OS 기본 대기 시간
- OS에는 연결 대기 타임아웃이 설정되어 있다. 맥은 75초, 윈도우는 21초, 리눅스는 75~180초 사이
- 해당 시간이 지나면
java.net.ConnectException: Operation timed out
이 발생한다. - 이렇게 오래 대기하는 것은 좋은 방법이 아니다. 연결이 안 되면 빠르게 고객에게 문제가 있다고 알려줘야 한다.
- /TCP 연결 타임아웃 - 직접 설정
Socket socket = new Socket();
socket.connect(new InetSocketAddress("192.168.1.250", 45678), 1000); // 1초대기
Socket
객체를 생성할 때 인자로 IP, PORT를 모두 전달하면 생성자에서 바로 TCP 연결을 시도한다.- 하지만 IP, PORT를 모두 빼고 객체를 생성하면, 객체만 생성되고, 아직 연결은 시도하지 않는다.
- 추가적으로 필요한 설정을 더 한 다음에
socket.connect()
를 호출하면 그때 TCP 연결을 시도한다.- 대표적으로 타임아웃을 설정할 수 있음.
public void connect(SocketAddress endpoint, int timeout) throws IOException { ... }
InetSocketAddress
:SocketAddress
의 자식이다. IP, PORT 기반의 주소를 객체로 제공한다.timeout
: 밀리초 단위로 연결 타임아웃을 지정할 수 있다.
- 타임아웃 시간이 지나도 연결이 되지 않으면
SocketTimeoutException
이 발생한다
- /TCP 소켓 타임아웃 - read 타임아웃
- 소켓 타임아웃 또는 read 타임 아웃이라고 부르는 타임아웃이다. (매우 중요!!)
- 연결이 잘 된 이후에 클라이언트가 서버에 어떤 요청을 했다고 가정하자. 서버가 계속 응답을 주지 않는다면?
- ex) 서버에 사용자가 폭주하고 매우 느려져서 응답을 계속 주지 못하는 상황
socket.setSoTimeout(3000); // 타임아웃 시간 설정
int read = input.read();
의 경우 기본은 무한대기 이지만,socket.setSoTimeout()
을 사용하면 밀리초 단위로 타임아웃 시간을 설정할 수 있다. 여기서는 3초를 설정했다.
- read 타임아웃 시간이 지나면
SocketTimeoutException
이 발생한다. - 실무에서 소켓 타임아웃(read 타임 아웃)을 누락하여 장애가 자주 발생한다고 한다.
- 외부 서버와 통신을 하는 경우 반드시 연결 타임아웃과 소켓 타임아웃을 지정하자.
- TCP에는 정상 종료, 강제 종료, 즉 2가지 종류의 종료가 있다.
- /정상 종료
socket.close()
를 호출하면 4-way handshake 과정을 거친다.- 누가 close했든 FIN 패킷을 받으면
socket.close()
를 호출해서 FIN 패킷을 상대방에게 전달해야 한다. - 서버가 연결 종료 한다고 해보자. 서버는 종료를 위해
socket.close()
를 호출한다. (나 종료 했어~)- 서버는 클라이언트에 FIN 패킷을 전달한다.
- 클라이언트는 FIN 패킷을 받는다.
- 클라이언트의 OS에서 FIN에 대한 ACK 패킷을 전달한다. (ㅇㅋ 확인했어)
- 그리고 클라이언트도 종료를 위해
socket.close()
를 호출한다.- 클라이언트는 서버에 FIN 패킷을 전달한다. (나도 종료했어~)
- 서버의 OS는 FIN 패킷에 대한 ACK 패킷을 전달한다. (ㅇㅋ 확인했어)
- 예를 들어 서버가 연결을 맺고 1초뒤에 소켓을 close하는 상황을 생각해보자.
- 서버와 클라이언트가 연결을 맺는다.
- 클라이언트는 서버의 메시지를 3가지 방법으로 읽는다.
read()
: 1byte 단위로 읽음readLine()
: 라인 단위로String
으로 읽음readUTF()
:DataInputStream
을 통해String
단위로 읽음
- 1초 뒤 서버에서
socket.close()
를 호출하여 클라이언트에 FIN 패킷을 보낸다. - 클라이언트는 FIN 패킷을 받는다.
- 서버가 소켓을 종료했다는 의미이므로 클라이언트는 더는 읽을 데이터가 없다.
- FIN 패킷을 받은 클라이언트의 소켓은 더는 서버를 통해 읽을 데이터가 없음을 -1(EOF) 로 반환한다.
read()
-1BufferedReader().readLine()
null
DataInputStream.readUTF()
EOFException
- if문이나, try-catch-finally로 위 조건이 충족되면 자원을 정리해준다.
- 여기서 중요한 점은 EOF가 발생하면 상대방이 FIN 메시지를 보내면서 소켓 연결을 끊었다는 뜻이다.
- 이 경우 소켓에 다른 작업을 하면 안되고, FIN 메시지를 받은 클라이언트도
close()
를 호출해서 상대방에 FIN 메시지를 보내고 소켓 연결을 끊어야 한다.
- 이 경우 소켓에 다른 작업을 하면 안되고, FIN 메시지를 받은 클라이언트도
- /강제 종료: TCP 연결 중에 문제가 발생하면
RST
라는 패킷이 발생한다. 이 경우 연결을 즉시 종료해야 한다. - 서버가 연결을 종료하여 FIN 패킷을 클라이언트에 보냈다. 이 때 만약 클라이언트가
output.write()
로 서버에 메시지를 전달하면?- 일단 먼저 FIN 패킷을 받으면 클라이언트의 OS에서 FIN에 대한 ACK 패킷을 전달한다.
- 그리고 output.write()로 인해 데이터 전송하는 PUSH 패킷이 서버에 전달된다.
- 서버는 이미 FIN으로 종료를 요청했는데, PUSH 패킷으로 데이터가 전송되었다. 서버가 기대하는 건 FIN 패킷.
- 서버는 TCP 연결에 문제가 있다고 판단하고 즉각 연결을 종료하라는 RST 패킷을 클라이언트에 전송한다.
- RST 패킷이 도착했다는 것은 현재 TCP 연결에 심각한 문제가 있으므로 해당 연결을 더는 사용하면 안된다는 의미이다.
- RST 패킷이 도착하면 자바는
read()
로 메시지를 읽을 때 다음 예외를 던진다.- MAC:
java.net.SocketException: Connection reset
- MAC:
- RST 패킷이 도착하면 자바는
write()
로 메시지를 전송할 때 다음 예외를 던진다.- MAC:
java.net.SocketException: Broken pipe
- MAC:
- (TCP에서 RST 패킷은 연결 상태를 초기화(리셋)해서 더 이상 현재의 연결을 유지하지 않겠다는 의미를 전달한다.)
- RST 패킷이 도착하면 자바는
네트워크 예외를 다루는 기준
- 예외를 다 따로따로 이해하고 구분해서 다루는 건 어렵다. 기본적으로 정상 종료, 강제 종료 모두 자원 정리하고 닫도록 설계하고, 자세히 분류해야 하는 경우 그 때 예외를 세부적으로 구분하여 처리하자.
'Java > IO, Network' 카테고리의 다른 글
[Java] 53. HTTP 서버 만들기 (0) | 2025.07.01 |
---|---|
[Java] 52. 채팅 프로그램(feat. Command Pattern) (0) | 2025.07.01 |
[Java] 50. 네트워크 - 프로그램(1) (0) | 2025.07.01 |
[Java] 49. File, Files (0) | 2025.07.01 |
[Java] 48. 자바 I/O 활용 (0) | 2025.07.01 |