이전 글에서 try-with-resources가 자원 정리 문제를 어떻게 해결하는지 확인했다. 이번에는 그 이론을 실제 네트워크 프로그램에 직접 적용한다. 먼저 finally로 자원을 직접 정리하는 방식(V4)을 적용하고, 이후 try-with-resources로 더 개선한다(V5). 두 방식 모두 다루는 이유는, 실무에서는 상황에 따라 try-with-resources를 적용할 수 없는 경우도 있기 때문이다
SocketCloseUtil — 자원 정리 유틸리티
소켓과 스트림을 닫는 코드는 여러 곳에서 반복된다. 공통 유틸리티로 분리하자
import static network.tcp.SocketCloseUtil.closeAll;
public 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()); }
}
}
}
- 이 유틸리티가 처리하는 것들을 정리하면 다음과 같다
null체크를 통해 객체 생성 도중 예외가 발생한 경우를 안전하게 처리한다. 각 자원을 개별try-catch로 감싸기 때문에 하나를 닫다가 예외가 발생해도 나머지를 계속 닫는다. 자원 정리 중 발생한 예외를 로깅만 하고 흘려보낸다. 이 시점에 개발자가 직접 대응할 수 있는 방법이 없기 때문이다- 닫는 순서도 중요하다.
Socket을 기반으로InputStream과OutputStream을 만들기 때문에, 열 때의 역순인 스트림 먼저, 소켓 나중 순서로 닫아야 한다.InputStream과OutputStream사이의 순서는 무관하다
V4 — finally로 자원 정리
ClientV4
package network.tcp.v4;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
import static network.tcp.SocketCloseUtil.closeAll;
import static util.MyLogger.log;
public class ClientV4 {
public static final int PORT = 12345;
public static void main(String[] args) {
log("클라이언트 시작");
// finally 블록에서 접근해야 하므로 try 블록 밖에서 선언
Socket socket = null;
DataInputStream input = null;
DataOutputStream output = null;
try {
socket = new Socket("localhost", PORT);
input = new DataInputStream(socket.getInputStream());
output = new DataOutputStream(socket.getOutputStream());
log("소켓 연결: " + socket);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송 문자: ");
String toSend = scanner.nextLine();
output.writeUTF(toSend);
log("client -> server: " + toSend);
if (toSend.equals("exit")) break;
String received = input.readUTF();
log("client <- server: " + received);
}
} catch (IOException e) {
log(e);
} finally {
closeAll(socket, input, output);
log("연결 종료: " + socket);
}
}
}
SessionV4
import static network.tcp.SocketCloseUtil.closeAll;
import static util.MyLogger.log;
public class SessionV4 implements Runnable {
private final Socket socket;
public SessionV4(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// finally 블록에서 접근해야 하므로 try 블록 밖에서 선언
DataInputStream input = null;
DataOutputStream output = null;
try {
input = new DataInputStream(socket.getInputStream());
output = new DataOutputStream(socket.getOutputStream());
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 {
closeAll(socket, input, output);
log("연결 종료: " + socket);
}
}
}
ServerV4
public class ServerV4 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("서버 시작");
ServerSocket serverSocket = new ServerSocket(PORT);
log("서버 소켓 시작 - 리스닝 포트: " + PORT);
while (true) {
Socket socket = serverSocket.accept();// 블로킹
log("소켓 연결: " + socket);
SessionV4 session = new SessionV4(socket);
Thread thread = new Thread(session);
thread.start();
}
}
}
실행 결과 – 클라이언트 강제 종료 시
15:57:51.442 [ main] 서버 시작 15:57:51.444 [ main] 서버 소켓 시작 - 리스닝 포트: 12345 15:57:52.913 [ main] 소켓 연결: Socket[addr=/127.0.0.1,port=51722,localport=12345] 15:57:54.784 [ Thread-0] java.io.EOFException 15:57:54.785 [ Thread-0] 연결 종료: Socket[addr=/127.0.0.1,port=51722,localport=12345]
V3에서는 클라이언트를 강제 종료하면 서버 세션에서 예외가 발생하며 자원 정리 로그없이 종료됐다. V4에서는 예외가 발생해도 finally 블록이 반드시 실행되므로 “연결 종료” 로그가 남고 자원이 정상적으로 반납된다
V5 — try-with-resources 적용
ClientV5
Socket, DataInputStream, DataOutputStream 모두 AutoCloseable을 구현하고 있어 try-with-resources를 바로 적용할 수 있다
public class ClientV5 {
public static final int PORT = 12345;
public static void main(String[] args) {
log("클라이언트 시작");
try (Socket socket = new Socket("localhost", PORT);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream())) {
log("소켓 연결: " + socket);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송 문자: ");
String toSend = scanner.nextLine();
output.writeUTF(toSend);
log("client -> server: " + toSend);
if (toSend.equals("exit")) break;
String received = input.readUTF();
log("client <- server: " + received);
}
} catch (IOException e) {
log(e);
}
}
}
- 선언 역순으로
output → input → socket순서로 자동 닫힌다
SessionV5
서버 세션의 경우 Socket은 외부 (ServerV5)에서 생성해서 넘겨받는다. 이처럼 직접 생성하지 않은 객체도 try 선언부에 참조만 넣으면 AutoCloseable이 호출된다
public class SessionV5 implements Runnable {
private final Socket socket;
public SessionV5(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (socket; // 외부에서 받은 소켓도 참조만 넣으면 자동 close
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream())) {
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);
}
log("연결 종료: " + socket + " isClosed: " + socket.isClosed());
}
}
ServerV5
public class ServerV5 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("서버 시작");
ServerSocket serverSocket = new ServerSocket(PORT);
log("서버 소켓 시작 - 리스닝 포트: " + PORT);
while (true) {
Socket socket = serverSocket.accept();// 블로킹
log("소켓 연결: " + socket);
SessionV5 session = new SessionV5(socket);
Thread thread = new Thread(session);
thread.start();
}
}
}
실행 결과 – 클라이언트 강제 종료 시
16:13:26.171 [ main] 서버 시작 16:13:26.173 [ main] 서버 소켓 시작 - 리스닝 포트: 12345 16:13:27.334 [ main] 소켓 연결: Socket[addr=/127.0.0.1,port=53788,localport=12345] 16:13:29.256 [ Thread-0] java.io.EOFException 16:13:29.262 [ Thread-0] 연결 종료: Socket[addr=/127.0.0.1,port=53788,localport=12345] isClosed: true 마지막
- 마지막 줄의
isClosed: true가 핵심이다.try-with-resources가 소켓의close()를 정상적으로 호출했음을 확인할 수 있다
V4와 V5 비교
두 방식을 나란히 비교하면 try-with-resources의 장점이 명확하게 드러난다
| 항목 | V4(finally) | V5(try-with-resources) |
| 변수 선언 | try 밖에서 null 초기화 후 선언 | try 선언부에서 바로 할당 |
| 자원 정리 코드 | SocketCloseUtil 직접 호출 | 자동 |
| 닫는 순서 보장 | 개발자가 수동으로 처리 | 선언 역순으로 처리 |
| 코드 복잡도 | 상대적으로 복잡 | 간결 |
| 외부 주입 객체 처리 | closeAll(socket, …) | try (socket; …) |
그럼에도 finally를 직접 사용해야 하는 경우가 있다. 자원의 생명주기가 여러 메서드에 걸쳐 있거나, AutoCloseable을 구현하지 않은 자원을 다뤄야 할 때다. 두 방식 모두 익혀두는 것이 중요하다