Programming Language/Java

[Java] 출력 스트림과 입력 스트림 불일치

lumana 2025. 4. 7. 00:11

채팅 서버와 클라이언트를 구현하던 중에, 서버가 보낸 메시지를 클라이언트가 수신하지 못하는 문제가 발생했다.

 

OutputStream에 메시지를 전달하는 Session 객체

package main.server.session;

import static main.server.config.ServerConstant.CHANGE_USERNAME;
import static main.server.config.ServerConstant.CLOSE_CONNECTION;
import static main.server.config.ServerConstant.FIND_ALL_USER;
import static main.server.config.ServerConstant.INVALID_COMMAND_FORMAT_MESSAGE;
import static main.server.config.ServerConstant.INVALID_COMMAND_MESSAGE;
import static main.server.config.ServerConstant.JOIN;
import static main.server.config.ServerConstant.MESSAGE_TIME_FORMATTER;
import static main.server.config.ServerConstant.REQUEST_USERNAME_REGISTRATION_MESSAGE;
import static main.server.config.ServerConstant.SEND_MESSAGE;
import static main.server.config.ServerConstant.SYSTEM;
import static main.util.MyLogger.log;
import static main.util.SocketCloseUtil.*;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.List;

public class Session implements Runnable {

    private final Socket socket;
    private final DataInputStream input;
    private final PrintStream output;
    private final SessionManager sessionManager;
    private boolean isClosed = false;
    private String userName = null;

    public Session(Socket socket, SessionManager sessionManager) throws IOException {
        this.socket = socket;
        this.input = new DataInputStream(socket.getInputStream());
        this.output = new PrintStream(socket.getOutputStream());
        this.sessionManager = sessionManager;
        sessionManager.add(this);
    }

    @Override
    public void run() {
        try {
            while (true) {
                String received = input.readUTF();
                log(received);
                logic(received);
            }
        } catch (IOException e) {
            log(e);
        } finally {
            sessionCloseProcess();
        }
    }

    private void logic(String received) {
        if (isInvalidCommandFormat(received)) {
            sendMessage(SYSTEM, INVALID_COMMAND_FORMAT_MESSAGE);
            return;
        }

        String[] commandAndContent = received.substring(1).split("\\|", 2);
        String command = commandAndContent[0];
        String content = commandAndContent[1];
        if (isInvalidCommand(command)) {
            sendMessage(SYSTEM, INVALID_COMMAND_MESSAGE);
            return;
        }

        if (command.equals(JOIN)) {
            registerUsername(commandAndContent[1]);
            return;
        }

        if (userName == null) {
            sendMessage(SYSTEM, REQUEST_USERNAME_REGISTRATION_MESSAGE);
            return;
        }

        if (command.equals(SEND_MESSAGE)) {
            sessionManager.sendMessageToAll(userName, content);
            return;
        }

        if (command.equals(CHANGE_USERNAME)) {
            changeUsername(content);
            sendMessage(SYSTEM, "변경된 이름: " + userName);
            return;
        }

        if (command.equals(FIND_ALL_USER)) {
            List<String> usernames = sessionManager.findConnectedUserAll();
            StringBuilder sb = new StringBuilder();
            for (String username : usernames) {
                sb.append(sb).append("\n");
            }
            output.println(sb);
            return;
        }

        if (command.equals(CLOSE_CONNECTION)) {
            sessionCloseProcess();
            return;
        }

    }

    private void changeUsername(String username) {
        this.userName = username;
    }

    private void registerUsername(String username) {
        this.userName = username;
    }

    private boolean isInvalidCommand(String command) {
        if (command.equals("join")
                || command.equals("message")
                || command.equals("change")
                || command.equals("users")
                || command.equals("exit")) {
            return false;
        }
        return true;
    }

    private boolean isInvalidCommandFormat(String received) {
        return !received.startsWith("/") || !received.contains("|");
    }

    public void sendMessage(String from, String message) {
        String time = LocalDateTime.now().format(MESSAGE_TIME_FORMATTER);
        output.printf("[%s] %s: %s", time, from, message);
    }

    public String getUserName() {
        return userName;
    }

    private void sessionCloseProcess() {
        sessionManager.remove(this);
        close();
    }

    public void close() {
        if (isClosed) {
            return;
        }

        closeAll(socket, input, output);
        isClosed = true;
        log("연결 종료: " + socket);
    }

}

 

클라이언트측 메시지 수신 스레드

package main.client;

import static main.util.MyLogger.log;

import java.io.DataInputStream;
import java.io.IOException;

public class ReceiveTask implements Runnable {

    private final DataInputStream input;

    public ReceiveTask(DataInputStream input) {
        this.input = input;
    }

    @Override
    public void run() {
        try {
            while (true) {
                String receivedMessage = input.readUTF();
                System.out.println("[수신] " + receivedMessage);
            }
        } catch (IOException e) {
            log(e);
        }
    }
}

 

이유를 찾아보니 스트림 통신 방식 불일치로 인해 문제가 발생했던 것이다. 스트림 통신 방식이 불일치하면 예외가 발생할 줄 알았는데, 예외 자체가 발생하지 않아서 원인 자체를 찾는데 시간이 걸렸다.

printStream()로 보낸 데이터를 readUTF()로 읽을 시 발생하는 일

readUTF()의 경우 메시지 앞에 2바이트의 길이 정보를 보고 데이터를 읽어들이는데, printStream()을 사용해 outputstream에 데이터를 존생했기 때문에, 2바이트의 길이 정보가 존재하지 않는다.

 

readUTF() 입장에서는

 

  • 길이 정보 미흡:
    스트림에서 2바이트의 길이 정보를 기대하지만, PrintStream이 출력한 데이터는 단순 텍스트일 뿐이므로, 해당 길이 정보가 없어서 올바른 값을 읽지 못한다.
  • 블로킹 상태:
    readUTF()는 2바이트로 읽어낸 길이를 바탕으로 그 길이만큼의 데이터를 더 읽으려 한다. 그런데, 실제로는 필요한 바이트 수만큼 데이터가 오지 않기 때문에, readUTF()는 계속해서 필요한 데이터를 기다리며 블로킹 상태에 빠지게 된다.
  • 예외 발생 없음:
    연결이 끊기거나 스트림이 종료되지 않는 이상, readUTF()는 내부적으로 예외를 발생시키지 않고 단순히 데이터가 충분히 수신될 때까지 대기하게 된다.

자바로 채팅 서버와 클라이언트를 단일 프로젝트에서 모두 개발하고 있어서 스트림을 대충 신경쓰다 보니 겪었던 문제였다. 스트림이 불일치하는 경우에 컴파일 시간에 오류를 발견할 수도 없고, 실제 통신할때에도 예외가 발생하지 않는다. 개발할 때 특히 네트워크를 타는 경우에는 스펙을 잘 명시하고 개발하는게 중요한 것 같다.

 

'Programming Language > Java' 카테고리의 다른 글

[Java] 32. Set  (0) 2025.01.19
[Java] 31. HashSet  (0) 2025.01.19
[Java] 30. Hash  (0) 2025.01.19
[Java] 29. 컬렉션- ArrayList, LinkedList, List  (0) 2025.01.19
[Java] 28. 제네릭 - Generic(2)  (0) 2025.01.13