본문 바로가기

국비 교육

2020.10.6 일자 수업 : 네트워크 실습, 계산기 서버

 실습 - 네트워크 프로그램 

 

git/eomcs-java-basic/src/main com.eomcs.net.ex11

Desktop App vs Network App

java, eclipse와 같은 컴퓨터 안에서 실행될 수 있는 어플리케이션을 데스크탑 어플리케이션이라고 하며, 이메일 프로그램과 같이 이용자가 네트워크에 접속하여 사용하는 프로그램을 네트워크 어플리케이션이라고 한다. 데스크 탑 어플리케이션은 다음과 같은 특징이 있다.

  • 로컬에 설치해야 사용 가능하다
  • 버전이 바뀔 때마다 재설치해야 한다.
  • 이용자들의 로컬에 모두 설치해줘야하므로 대량의 pc 관리가 힘들다.

네트워크 어플리케이션은 서버에서 돌아가고 있는 프로그램에 이용자가 접근하는 형태이므로, 이 프로그램을 실행하는 서버를 Application Server라고 하며, 웹에서 제공되는 어플리케이션이면 Web Appication Server(WAS)라고 칭한다. 이 네트워크 어플리케이션은 데스크탑 어플리케이션에 비해 다음과 같은 이점이 있다.

  • 로컬에 설치할 필요 없이 네트워크에 접속하여 사용할 수 있다.
  • 버전이 업데이트되더라도 재설치할 필요 없다.
  • 이용자들의 로컬에 설치할 필요가 없으므로 대량의 이용자들을 관리할 수 있다.

서버에서 프로그램을 만들어 클라이언트에게 서비스를 제공하는 간단한 예제들을 만들며 기초적인 네트워크 어플리케이션의 구조를 익힐 것이다. 서버에서 실행되는 프로그램은 계산기로 클라이언트가 어떤 식에 대한 계산을 요청하면 그에 응답하여 결과물을 보내주는 아주 간단한 프로그램이다. 


서버와 클라이언트 연결하기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step01

일단 클라이언트와 연결될 서버를 만들어서 클라이언트 요청에 응답하는 프로그램의 간단한 틀을 짠다. 클라이언트를 기다리다가 클라이언트가 요청을 하면 accept를 한 후, 간단한 인삿말을 건넨다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      try (Socket socket = serverSocket.accept();
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          PrintStream out = new PrintStream(socket.getOutputStream());) {

        out.println("계산기 서버에 오신 걸 환영합니다!");
        out.println("계산식을 입력하세요!");
        out.println("예) 23 + 7");
        out.flush();
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

한편, 서버에 요청할 클라이언트는 서버와 연결되어 간단한 인삿말 세줄을 읽어 차례로 콘솔에 띄운다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class CalculatorClient {
  public static void main(String[] args) {

    try (Socket socket = new Socket("localhost", 8888);
        PrintStream out = new PrintStream(socket.getOutputStream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      String input = in.readLine();
      System.out.println(input);

      input = in.readLine();
      System.out.println(input);

      input = in.readLine();
      System.out.println(input);

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

서버 응답의 종료 조건 설정하기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step02

서버에서 응답할 때, 클라이언트에게 응답 메시지가 끝났음을 알리기 위해 메시지의 마지막에 빈 줄을 보낸다.

        out.println("계산기 서버에 오신 걸 환영합니다!");
        out.println("계산식을 입력하세요!");
        out.println("예) 23 + 7");
        out.println(); // 응답의 끝을 표시하는 빈 줄을 보낸다.
        out.flush();

클라이언트에서는 반복문을 돌려서 빈줄을 읽기 전까지 계속 입력받고 콘솔에 띄우는 것을 반복하게 한다.

      while (true) {
        String input = in.readLine();
        if (input.length() == 0) {
          // 빈 줄을 읽었다면 읽기를 끝낸다.
          break;
        }
        System.out.println(input);
      }

메서드 추출하기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step03

서버에서 클라이언트에 인삿말과 안내 메시지를 전송하는 코드를 sendIntroMessage 스태틱 메서드로 추출하고 원래의 자리에서 이 메서드를 호출한다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      try (Socket socket = serverSocket.accept();
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          PrintStream out = new PrintStream(socket.getOutputStream());) {

        sendIntroMessage(out);
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  static void sendIntroMessage(PrintStream out) throws Exception {
    out.println("[비트캠프 계산기]");
    out.println("계산기 서버에 오신 걸 환영합니다!");
    out.println("계산식을 입력하세요!");
    out.println("예) 23 + 7");
    out.println(); // 응답의 끝을 표시하는 빈 줄을 보낸다.
    out.flush();
  }
}

 한편, 클라이언트에서는 서버의 응답을 읽는 코드를 readResponse 스태틱 메서드로 추출하고 원래의 자리에서는 이 메서드를 호출한다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class CalculatorClient {
  public static void main(String[] args) {

    try (Socket socket = new Socket("localhost", 8888);
        PrintStream out = new PrintStream(socket.getOutputStream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      readResponse(in);

    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  static void readResponse(BufferedReader in) throws Exception {
    while (true) {
      String input = in.readLine();
      if (input.length() == 0) {
        break;
      }
      System.out.println(input);
    }
  }
}

클라이언트의 계산식 입력 받기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step04

이번에는 클라이언트 쪽에서 계산식을 서버쪽으로 보낼 것이다. 사용자로부터 원하는 계산식을 입력받아 그것을 서버측으로 출력하고 바로 서버에서 보내는 응답을 읽는 과정을 반복한다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class CalculatorClient {
  public static void main(String[] args) {

    try (
        Scanner keyboardScanner = new Scanner(System.in);
        Socket socket = new Socket("localhost", 8888);
        PrintStream out = new PrintStream(socket.getOutputStream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      readResponse(in);

      while (true) {
        String input = keyboardScanner.nextLine();
        out.println(input);
        out.flush();
        readResponse(in);
      }

    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  static void readResponse(BufferedReader in) throws Exception {
    while (true) {
      String input = in.readLine();
      if (input.length() == 0) {
        // 빈 줄을 읽었다면 읽기를 끝낸다.
        break;
      }
      System.out.println(input);
    }
  }
}

서버에서는 안내 메시지를 출력한 후, 반복문을 돌려 클라이언트에서 보낸 계산식을 입력받고, 그것을 그대로 다시 클라이언트에 출력하는 과정을 반복할 것이다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      try (Socket socket = serverSocket.accept();
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          PrintStream out = new PrintStream(socket.getOutputStream());) {

        sendIntroMessage(out);

        while (true) {
          String request = in.readLine();
          out.println(request);
          out.println();
          out.flush();
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  static void sendIntroMessage(PrintStream out) throws Exception {
    out.println("[비트캠프 계산기]");
    out.println("계산기 서버에 오신 걸 환영합니다!");
    out.println("계산식을 입력하세요!");
    out.println("예) 23 + 7");
    out.println(); // 응답의 끝을 표시하는 빈 줄을 보낸다.
    out.flush();
  }
}

리팩토링

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step05

클라이언트에게 받은 메시지를 바로 되돌려주는 코드를 sendRespose 스태틱 메서드로 추출한다. 이때 메서드의 파라미터는 출력 도구와 출력할 문자열이다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      try (Socket socket = serverSocket.accept();
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          PrintStream out = new PrintStream(socket.getOutputStream());) {

        sendIntroMessage(out);

        while (true) {
          String request = in.readLine();
          sendResponse(out, request); 
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  static void sendResponse(PrintStream out, String message) {
    out.println(message);
    out.println();
    out.flush();
  }


  static void sendIntroMessage(PrintStream out) throws Exception {
    out.println("[비트캠프 계산기]");
    out.println("계산기 서버에 오신 걸 환영합니다!");
    out.println("계산식을 입력하세요!");
    out.println("예) 23 + 7");
    out.println(); 
    out.flush();
  }
}

서버에서도 요청을 보내는 코드를 serdRequest 스태틱 메서드로 추출하고 파라미터로는 출력도구와 출력할 문자열을 지정한다. 또한 일관적인 의미를 주기 위해 readResponse를 receiveResponse라는 이름으로 바꾼다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class CalculatorClient {
  public static void main(String[] args) {

    try (
        Scanner keyboardScanner = new Scanner(System.in);
        Socket socket = new Socket("localhost", 8888);
        PrintStream out = new PrintStream(socket.getOutputStream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      receiveResponse(in); 
      while (true) {
        String input = keyboardScanner.nextLine();
        sendRequest(out, input); 
        receiveResponse(in); 
      }

    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  static void sendRequest(PrintStream out, String message) throws Exception {
    out.println(message);
    out.flush();
  }

  static void receiveResponse(BufferedReader in) throws Exception {
    while (true) {
      String input = in.readLine();
      if (input.length() == 0) {
        break;
      }
      System.out.println(input);
    }
  }
}

계산을 수행하여 결과 전송하기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step06

일단 클라이언트에서 일정한 규칙을 가진 계산식을 이용자에게서 입력 받아 리턴하는 Prompt 스태틱 메서드를 정의하고 메인메서드에서 이를 호출하여 리턴받은 계산식 문자열을 서버로 출력한다. 단, Prompt 메서드 안에서 이용자가 틀에 맞지 않는 계산식을 입력하면 null을 리턴하므로 메인 메서드에서 리턴받은 문자열이 null이면 다시 Prompt를 호출하게 한다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class CalculatorClient {
  public static void main(String[] args) {

    try (
        Scanner keyboardScanner = new Scanner(System.in);
        Socket socket = new Socket("localhost", 8888);
        PrintStream out = new PrintStream(socket.getOutputStream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      receiveResponse(in); 

      while (true) {
        String input = prompt(keyboardScanner);
        if (input == null) {
          continue;
        }
        sendRequest(out, input); 
        receiveResponse(in); 
      }

    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  static String prompt(Scanner keyboardScanner) {
    System.out.print("계산식> ");
    String input = keyboardScanner.nextLine();

    if (input.split(" ").length != 3) {
      System.out.println("입력 형식이 올바르지 않습니다. 예) 23 + 5");
      return null;
    }
    return input;
  }

  static void sendRequest(PrintStream out, String message) throws Exception {
    out.println(message);
    out.flush();
  }

  static void receiveResponse(BufferedReader in) throws Exception {
    while (true) {
      String input = in.readLine();
      if (input.length() == 0) {
        break;
      }
      System.out.println(input);
    }
  }
}

한편 서버에서는 어떤 계산식에 대한 계산 결과를 리턴하는 compute 스태틱 메서드를 정의하고, 클라이언트에게서 계산식을 받으면 그것을 읽은 후 compute 메서드를 실행하여 얻은 결과를 그대로 클라이언트에게 출력한다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      try (Socket socket = serverSocket.accept();
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          PrintStream out = new PrintStream(socket.getOutputStream());) {

        sendIntroMessage(out);

        while (true) {
          String request = in.readLine();
          String message = compute(request);
          sendResponse(out, message); 
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  static String compute(String request) {
    String[] values = request.split(" ");

    int a = Integer.parseInt(values[0]);
    String op = values[1];
    int b = Integer.parseInt(values[2]);
    int result = 0;

    switch (op) {
      case "+": result = a + b; break;
      case "-": result = a - b; break;
      case "*": result = a * b; break;
      case "/": result = a / b; break;
      default:
        return String.format("%s 연산자를 지원하지 않습니다.", op);
    }
    return String.format("결과는 %d %s %d = %d 입니다.", a, op, b, result);
  }

  static void sendResponse(PrintStream out, String message) {
    out.println(message);
    out.println();
    out.flush();
  }

  static void sendIntroMessage(PrintStream out) throws Exception {
    out.println("[비트캠프 계산기]");
    out.println("계산기 서버에 오신 걸 환영합니다!");
    out.println("계산식을 입력하세요!");
    out.println("예) 23 + 7");
    out.println(); 
    out.flush();
  }
}

프로그램 종료 명령 처리하기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step07

클라이언트 측에서 사용자가 계산기 프로그램을 종료하고 싶을 때, quit이라는 입력을 하면 프로그램이 종료될 수 있도록 명령어를 지정한다. 일단, Pormpt 메서드에서 사용자가 계산식 대신 quit을 입력하면 quit을 그대로 리턴하고 메인 메서드에서는 그 리턴값을 그대로 서버에게 출력한다. 출력하고 난 뒤에 클라이언트 프로그램 자체에서 사용자가 입력한 값이 quit이면 반복문을 나와 서버와의 연결을 끊는다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class CalculatorClient {
  public static void main(String[] args) {

    try (
        Scanner keyboardScanner = new Scanner(System.in);
        Socket socket = new Socket("localhost", 8888);
        PrintStream out = new PrintStream(socket.getOutputStream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

      receiveResponse(in); 

      while (true) {
        String input = prompt(keyboardScanner);
        if (input == null) {
          continue;
        }
        sendRequest(out, input); 
        receiveResponse(in); 

        if (input.equalsIgnoreCase("quit")) {
          break;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  static String prompt(Scanner keyboardScanner) {
    System.out.print("계산식> ");
    String input = keyboardScanner.nextLine();

    if (input.equalsIgnoreCase("quit")) {
      return input;
    } else if (input.split(" ").length != 3) { 
      System.out.println("입력 형식이 올바르지 않습니다. 예) 23 + 5");
      return null;
    }
    return input;
  }

  static void sendRequest(PrintStream out, String message) throws Exception {
    out.println(message);
    out.flush();
  }

  static void receiveResponse(BufferedReader in) throws Exception {
    while (true) {
      String input = in.readLine();
      if (input.length() == 0) {
        // 빈 줄을 읽었다면 읽기를 끝낸다.
        break;
      }
      System.out.println(input);
    }
  }
}

서버는 클라이언트에게서 계산식 대신 quit이라는 문자열을 받으면 그것을 compute 메서드에 넘겨주기 전에 종료메시지를 띄우고 반복문을 나오게 한다. 반복문을 나오면 서버는 자동으로 종료된다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      try (Socket socket = serverSocket.accept();
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          PrintStream out = new PrintStream(socket.getOutputStream());) {

        sendIntroMessage(out);

        while (true) {
          String request = in.readLine();
          if (request.equalsIgnoreCase("quit")) {
            sendResponse(out, "안녕히 가세요!");
            break;
          }

          String message = compute(request);
          sendResponse(out, message);
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
.
.
.

예외 처리

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step08

compute 메서드 실행 중, 숫자가 아닌 것을 잘못 입력받아 int로 변환하지 못하는 오류가 발생할 수 있으므로, 그것에 대한 예외처리를 따로 해준다.

  static String compute(String request) {
    try {
      String[] values = request.split(" ");

      int a = Integer.parseInt(values[0]);
      String op = values[1];
      int b = Integer.parseInt(values[2]);
      int result = 0;

      switch (op) {
        case "+": result = a + b; break;
        case "-": result = a - b; break;
        case "*": result = a * b; break;
        case "/": result = a / b; break;
        default:
          return String.format("%s 연산자를 지원하지 않습니다.", op);
      }
      return String.format("결과는 %d %s %d = %d 입니다.", a, op, b, result);

    } catch (Exception e) {
      return String.format("계산 중 오류 발생! - %s", e.getMessage());
    }
  }

리팩토링

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step09

서버 프로그램을 구현하던 CalculatorServer에서 클라이언트의 요청을 처리하는 역할을 RequestProcessor 라는 별도의 클래스로 분리하고, 원래의 클래스는 클라이언트와의 연결만을 수행하도록 한다.

 

RequestProcessor는 CalculatorServer에서 클라이언트와 연결하면서 생성한 클라이언트 정보를 담은 소켓을 생성자의 파라미터로 받아 객체를 생성하도록 한다. 원래의 클래스에서 클라이언트에게 서비스를 제공하던 코드들을 추출하여 service라는 메서드를 정의한다. 서비스를 제공하는 데 사용되었던 CalculatorServer의 스태틱 메서드들도 그대로 가져와서 인스턴스 메서드로 바꾼다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class RequestProcessor {
  Socket socket;

  public RequestProcessor(Socket socket) {
    this.socket = socket;
  }

  public void service() throws Exception {
    try (Socket socket = this.socket;
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintStream out = new PrintStream(socket.getOutputStream());) {

      sendIntroMessage(out);

      while (true) {
        String request = in.readLine();
        if (request.equalsIgnoreCase("quit")) {
          sendResponse(out, "안녕히 가세요!");
          break;
        }

        String message = compute(request);
        sendResponse(out, message); // 클라리언트에게 응답한다.
      }
    }
  }

  private String compute(String request) {
    try {
      String[] values = request.split(" ");

      int a = Integer.parseInt(values[0]);
      String op = values[1];
      int b = Integer.parseInt(values[2]);
      int result = 0;

      switch (op) {
        case "+": result = a + b; break;
        case "-": result = a - b; break;
        case "*": result = a * b; break;
        case "/": result = a / b; break;
        default:
          return String.format("%s 연산자를 지원하지 않습니다.", op);
      }
      return String.format("결과는 %d %s %d = %d 입니다.", a, op, b, result);

    } catch (Exception e) {
      return String.format("계산 중 오류 발생! - %s", e.getMessage());
    }
  }

  private void sendResponse(PrintStream out, String message) {
    out.println(message);
    out.println();
    out.flush();
  }


  private void sendIntroMessage(PrintStream out) throws Exception {
    out.println("[비트캠프 계산기]");
    out.println("계산기 서버에 오신 걸 환영합니다!");
    out.println("계산식을 입력하세요!");
    out.println("예) 23 + 7");
    out.println(); // 응답의 끝을 표시하는 빈 줄을 보낸다.
    out.flush();
  }
}

CalculatorServer에서는 accept를 통해 리턴받은 클라이언트 소켓을 파라미터로 주어 RequestProcessor 객체를 생성하고, service 메서드를 호출한다.

import java.net.ServerSocket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      new RequestProcessor(serverSocket.accept()).service();

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

여러 개의 클라이언트 접속 처리

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step10

지금까지는 한개의 클라이언트와 연결이 종료되면 서버가 함께 종료되지만, 이제 서버 측에서 무한정으로 클라이언트를 받을 수 있도록 accept를 호출하여 클라이언트 소켓을 리턴받고 그에 대해 서비스를 제공하는 코드를 while 문 안에 집어넣는다. 단, 클라이언트에 대한 소켓을 새로 받을 때마다 매번 RequestProcessor 객체를 생성할 필요 없이, requestProcessr 안의 socket 필드 값만 그때그때마다 바꿔준다. 이렇게 하면 서버는 동시에 딱 하나의 클라이언트에게만 서비스를 제공할 수 있으며 클라이언트가 quit을 통해 종료해야만 다음 클라이언트를 받을 수 있다.

import java.net.ServerSocket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      RequestProcessor requestProcessor = new RequestProcessor();

      while (true) {
        requestProcessor.setSocket(serverSocket.accept());
        requestProcessor.service();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Stateless 방식 연결 처리

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step11

지금까지는 서버가 클라이언트와 한번 연결되면 클라이언트 측에서 quit을 보낼 때까지 계속 서로 데이터를 주고 받으며 연결을 지속할 수 밖에 없었으므로 다음 클라이언트가 서버와의 연결을 기다리는 시간이 길었다. 이것을 stateful 연결 방식이라고 한다. 이제는 다음 클라이언트의 대기 시간을 줄이기 위해 한번의 요청과 응답으로 연결이 끝나는 stateless 방식을 구현한다. 이것은 클라이언트의 요청과 거의 동시에 연결되고, 응답과 거의 동시에 연결이 끊기기 때문에 서버가 요청을 받아 응답을 하는 시간이 길지 않다면 연결 시간도 대폭 줄어든다. 이 예제는 요청을 받은 후 응답을 하기 까지 한번의 숫자 연산 과정만 거치기 때문에 아주 짧은 시간이 소모되므로 연결 시간도 짧다.

 

RequestProcessor의 service 메서드에서 계산식을 입력 받으면, 그것에 대한 연산 결과를 클라이언트에 출력하는 이 과정을 반복하지 않고, 바로 연결을 끊는다. 한번의 요청과 응답만 할 것이므로 안내 메시지 전송은 생략한다. 

  public void service() throws Exception {
    try (Socket socket = this.socket;
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintStream out = new PrintStream(socket.getOutputStream());) {

      // 클라이언트 접속에 대해 더이상 안내 메시지를 제공하지 않는다.

      // 한 번 접속에 한 번의 요청만 처리한다.
      sendResponse(out, compute(in.readLine())); 
    }
  }

Client는 원하는 계산식을 입력받는 동안 서버와 연결되지 않도록 미리 계산식을 이용자로부터 입력받은 후, 그것을 서버측에 출력하기 직전에 서버와 연결한다. 출력한 후 바로 응답을 읽고 연결을 바로 끊는다. 그 대신 이 입력 -> 연결 -> 요청 -> 응답 -> 연결 종료 과정을 반복문으로 돌려 이용자가 quit을 입력할 때까지 계속해서 서버와 연결과 연결 종료를 반복할 수 있도록 한다. 이로써 이용자 측에서는 매번 연결이 끊기고 새로 연결되고 있다는 것을 알 수가 없다.

  public static void main(String[] args) {
    Scanner keyboardScanner = new Scanner(System.in);

    while (true) {

      // 요청 때 마다 연결하기 때문에 서버의 인사말은 더이상 출력하지 않는다.
      String input = prompt(keyboardScanner);
      if (input == null) {
        continue;
      } else if (input.equalsIgnoreCase("quit")) {
        // quit 명령을 입력할 경우 서버에 접속할 필요가 없이 즉시 클라이언트를 종료한다.
        break;
      }

      try (Socket socket = new Socket("localhost", 8888);
          PrintStream out = new PrintStream(socket.getOutputStream());
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

        sendRequest(out, input); // 서버에 요청을 보내기
        receiveResponse(in); // 서버의 실행 결과를 받기

      } catch (Exception e) {
        e.printStackTrace();
      }
    }

    keyboardScanner.close();
  }

Thread 만들기

git/eomcs-java-basic/src/main com.eomcs.net.ex11.step12

stateless 방식도 한계가 있는데, 만약 서버에서 클라이언트의 요청을 받아 응답을 해주기까지의 과정이 시간이 걸린다면 그 동안 다른 클라이언트가 기다려야하는 상황이 발생한다는 점이다. 이것을 해결하기 위해 main 메서드의 실행 이외에 또 다른 스레드를 만들어 클라이언트마다 별도로 서비스를 제공해줄 수 있도록 한다. 현실에서는 마치 카페 직원을 여러 명으로 늘려 손님의 대기 시간을 줄이는 방법과 유사하다.

 

이번 예제는 클라이언트 하나당 하나의 스레드를 만들어 클라이언트가 대기 시간 없이 서비스를 제공받을 수 있도록 한다. 자바 API에서는 Thread라는 클래스를 제공하여 손쉽게 별도의 클래스를 추가할 수 있도록 하고 있다. 사용방법은 다음과 같다.

어떤 메서드의 실행에 대한 스레드를 늘리고 싶은 클래스가 Thread를 상속받게 한 후, run이라는 메서드를 오버라이딩하여 그 안에 새로운 Thread가 실행할 코드를 넣는다. 이렇게 하면 새로 만들어진 스레드는 run 메서드를 딱 한번만 호출한다. 이 예제에서 새로운 스레드가 실행할 코드는 RequestProcessor의 service 메서드 구현부이다.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class RequestProcessor extends Thread {
  Socket socket;

  public RequestProcessor(Socket socket) {
    this.socket = socket;
  }

  @Override
  public void run() {
    try (Socket socket = this.socket;
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintStream out = new PrintStream(socket.getOutputStream());) {

      sendResponse(out, compute(in.readLine())); 
    } catch (Exception e) {
      System.out.printf("클라이언트 요청 처리 중 오류 발생! - %s\n", e.getMessage());
    }
  }

스레드를 생성할 클래스는 기존에 RequestProcessor를 생성하던 CalculatorServer로, 서버와 클라이언트가 연결되어 accept가 리턴되는 시점에 RequestProcessor 객체를 생성하여 새로운 스레드를 추가한다. 스레드가 독립적으로 실행되도록 start() 메서드를 호출하면 스레드는 main 실행과 분리되어 별도의 실행모드에서 run 메서드를 호출한다. 그리고 main 실행에서는 run의 리턴 여부와 상관없이 start 메서드가 리턴되므로 다른 스레드의 영향을 받지 않고 메인 메서드의 남은 부분을 계속 실행한다. 따라서 이 과정을 while 문 안에 넣어 반복시켜주면 다른 클라이언트의 프로그램 실행과 상관없이 accept가 리턴될 때마다 새로운  스레드를 만들어 서비스를 제공할 수 있다.

import java.net.ServerSocket;

public class CalculatorServer {
  public static void main(String[] args) {

    try (ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행 중...");

      while (true) {
        RequestProcessor thread = new RequestProcessor(serverSocket.accept());
        thread.start(); 
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}