클라이언트와 서버간 통신시 필요한 요소를 설명하시오
- 클라이언트 서버간 통신에는 기본적으로 소켓이 필요
- 소켓에는 데이터를 내보내는 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() 메소드가 클라이언트와 종료되었을 때, 서버를 종료할 때 두 곳에서 불릴 수 있기 때문에 동시성문제 해결 필요