본문 바로가기
네이버클라우드/JAVA 웹 프로그래밍

JAVA 36일차 (2023-07-12) 자바 프로그래밍_여러 클라이언트 요청을 동시에 처리하기: Thread 적용_Calculator

by prometedor 2023. 7. 12.
- 멀티태스킹의 메커니즘 이해
  - 프로세스 스케줄링: Round Robin 방식, Priority + Aging 방식
  - 컨텍스트 스위칭 개념
  - 프로세스 복제(fork)방식과 스레드 방식 비교
  - 임계영역(Critical Region, Critical Section): 세마포어(Semaphore)와 뮤텍스(Mutex)
- 스레드의 구동원리와 사용법
  - 스레드의 라이프사이클 이해
  - Thread 클래스와 Runnable 인터페이스 사용법

 

Stateful 방식으로 통신하기

ㄴ bitcamp.test 패키지 생성

 

ㄴ bitcamp.test 패키지에 CalcClient1, CalcServer1 생성

 

CalcClient1.java

ㄴ 클라이언트에서 로컬 호스트(127.0.0.1)의 8888 포트로 소켓 연결을 생성하도록 함

    => 클라이언트는 지정된 호스트와 포트에 연결을 시도하고, 서버와의 통신을 위한 소켓을 생성함

    => 이 소켓을 통해 클라이언트는 서버로 요청을 보내고 응답을 받을 수 있음

 

CalcClient1.java

ㄴ DataOutputStream과 DataInputStream은 소켓을 통해 데이터를 전송하고 수신하기 위한 입출력 스트림임

 

CalcClient1.java

ㄴ 데이터를 입력받을 Scanner 인 keyscan 을 준비

 

CalcClient1.java

ㄴ try 문 안에 socket, out, in, keyscan 을 넣어서 해당 변수들 모두 자동 close 기능을 이용

 

CalcClient1.java

ㄴ Scanner 를 이용해 입력받아 expr 변수에 값을 저장

ㄴ 입력한 다음 줄의 문자열을 읽어서 이를 expr라는 이름의 문자열 변수에 저장하도록 함

 

CalcClient1.java

ㄴ items 배열에 expr 에 저장된 문자열을 공백으로 분리함

 

CalcClient1.java

ㄴ 제대로 출력 안 됨 => 정규식 사용하기

=>

split 메서드에서 정규식 사용하기

=>

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Pattern.html#sum

 

Pattern (Java SE 17 & JDK 17)

All Implemented Interfaces: Serializable A compiled representation of a regular expression. A regular expression, specified as a string, must first be compiled into an instance of this class. The resulting pattern can then be used to create a Matcher objec

docs.oracle.com

ㄴ 참고

 

CalcClient1.java

=>
CalcClient1.java

ㄴ Pattern 과 Matcher 클래스를 이용

ㄴ expr 에 [0-9]+ : 0 부터 9까지 수 중에서 1개 이상

Pattern에 compile로 정규식(regEx)을 담고, Matcher에 타겟 스트링(target)을 담기
ㄴ 그 다음 Matcher의 find() 함수를 쓰면 1번째 값을 찾아내고, true 혹은 false를 반환함
ㄴ group() 을 쓰면 방금 찾은 1번째 스트링이 나옴

ㄴ 다시 find()를 쓰면 2번째 값을 찾고, group()을 쓰면 2번째 값이 나옴 ...

=> 이를 while 문으로 구현

      ㄴ matcher.find() 가 false 이면 while 문 종료됨

 

CalcClient1.java

ㄴ 정규 표현식 => | (or) 사용 가능

 

** +를 제외한  -, *, /, % 도 분리 가능하도록 \p{Punct} 사용

=>

=>

CalcClient1.java

ㄴ \p{Punct} 사용

 

CalcClient1.java

ㄴ parseExpressioin 메서드를 생성한 후 try 문에 있던 해당 코드 잘라내오기

 

CalcClient1.java

ㄴ 값을 담을 배열을 생성

 

CalcClient1.java

ㄴ matcher.group() 으로 얻는 값들을 values 배열에 저장

 

CalcClient1.java

ㄴ 리턴 값을 String 배열로 변경하고, values 리스트를 배열로 만듦

     => new String[] {} 이라는 빈 배열을 생성하여 리스트를 배열로 변경

 

CalcClient1.java

ㄴ 메인 메서드가 static 이므로 메인 메서드에서 해당 메서드 사용하기 위해 static 추가해주기

 

CalcClient1.java

ㄴ Pattern 값은 클래스 내에서 패턴을 여러 번 재사용하므로 메서드 내에서 패턴을 매번 컴파일하는 것을 피하기위해 밖으로 빼줌
ㄴ Pattern 값도 메인 메서드 안에서 사용하기 위해 static 붙여주기

 

CalcClient1.java

=>

CalcClient1.java

ㄴ 생성한 parseExpression 메서드 이용

ㄴ expr 변수 생성할 필요 없도록 함

=>

CalcClient1.java

ㄴ 해당 결과 값은 String[] 배열 values 에 담도록 함

=>

CalcClient1.java

ㄴ 네트워킹 부분 주석 제거해주기

 

CalcClient1.java

ㄴ 각 값을 적절한 데이터 형식으로 변환하여 DataOutputStream을 사용하여 전송

 

CalcClient1.java

ㄴ 해당 코드를 while(true) 문으로 무한 반복 하도록 함

 

CalcClient1.java

ㄴ 코드가 좀 더 명확하고 읽기 쉽도록 함(의미 파악 쉽도록)

 

CalcClient1.java

ㄴ quit 이라는 입력값이 넘어오면 quit 을 출력하도록 함

CalcClient1.java

ㄴ 연산자를 가장 먼저 전송하도록 변경

 

 

 

CalcServer1.java

ㄴ 8888 포트에서 연결을 수신하는 서버를 실행하도록 함

ㄴ 소켓 서버를 생성하고 해당 포트에서 연결을 대기함

ㄴ 메인 메서드에서 서버 실행되면 "서버 실행!" 이라는 문장이 출력되도록 함

 

CalcServer1.java

ㄴ 자원 자동으로 close 하도록 try 문 안에 넣어주기

 

CalcServer1.java

ㄴ 클라이언트가 서버에 연결되면 accept() 메서드에서 Socket 객체를 반환하고, 이를 사용하여 클라이언트의 IP 주소와 포트를 확인 할 수 있음

InetSocketAddress의 getHostString() 메서드는 클라이언트의 호스트 IP 주소를 반환하고, getPort() 메서드는 클라이언트가 사용한 포트 번호를 반환함 => 클라이언트가 서버에 접속했을 때 해당 정보를 출력함

     => getRemoteSocketAddress() 메서드는 Socket 객체의 원격 소켓 주소를 반환하는 메서드

          ㄴ InetSocketAddress 객체가 반환됨

          ㄴ InetSocketAddress는 IP 주소와 포트 번호를 포함하는 소켓 주소를 나타내는 클래스임

 

CalcServer1.java

ㄴ op 는 연산자를 입력받아 저장함

ㄴ a 에는 첫 번째 입력되는 숫자, b 에는 두 번째 입력되는 숫자를 저장함

 

CalcServer1.java

ㄴ switch 문을 이용해 연산자가 +, -, *, /, % 일 경우 각각 연산을 정의해줌

ㄴ default 문에는 "지원하지 않는 연산자입니다!" 를 전송함

 

CalcServer1.java

=>

CalcServer1.java

ㄴ 해당 코드를 while(true) 로 묶어 무한 루프로 만들기

ㄴ while 문으로 묶었으므로 return 대신 break 로 작성하기

 

 

 

CalcClient1.java

ㄴ 해당 코드 제거하여 연산자 1개와 숫자 1개를 받도록 함

 

CalcClient1.java

ㄴ 입력 방식 변경 => 예) + 3

 

CalcClient1.java

ㄴ 계산식을 잘못 입력하더라도 프로그램이 종료되지 않도록 해줌

 

CalcClient1.java

ㄴ Expression 클래스 생성

=>

CalcClient1.java

ㄴ parseExpression 메서드가 Expression 객체를 반환하도록 함

 

CalcClient1.java

ㄴ 위에있던 조건문 parseExpression 메서드로 복사해와서 RuntimeException 으로 처리시킴

    => 계산식 관련 처리

 

CalcClient1.java

=>

CalcClient1.java

ㄴ 계산식에서 연산자와 피연산자를 추출하여 Expression 객체의 op와 value 멤버 변수에 값을 설정하고, 이 객체를 반환하도록 함

 

CalcClient1.java

ㄴ 해당 코드 제거하고 values 의 타입을 Expression 으로 변경

=>

CalcClient1.java

ㄴ values => expr 로 변경

ㄴ 계산식의 연산자와 숫자를 전송하도록 함

 

 

 

CalcServer1.java

ㄴ 연산자 1개와 숫자 1개만 받으므로 하나 제거

 

CalcServer1.java

ㄴ 결과값에 입력값을 더할 것이므로 result 라는 변수 선언해주기

 

CalcServer1.java

ㄴ a => value 라는 이름으로 변경해줌

    ㄴ a 보다는 의미있는 이름으로 변경

 

CalcServer1.java

ㄴ result 값에 value 를 더하도록 코드 변경해줌

 

CalcClient1.java

ㄴ 예외처리 해주기

 

실행 테스트

 

 

예외처리

CalcClient1.java

=>

CalcClient1.java

ㄴ RuntimeException => Exception 으로 변경 시 throws Exceptioin 추가해줘야 함

 

CalcClient1.java

ㄴ 서버 통신 오류일 경우의 예외처리 추가

 

CalcClient1.java

ㄴ 계산식이 옳지 않을 경우의 예외처리

 

ㄴ ExpressionParseException 클래스를 생성

    => 클래스 이름만으로 어떤 예외인지 직관적으로 알 수 있음

 

ExpressionParseException.java

ㄴ private static final long serialVersionUID = 1L; 코드를 추가하여 경고 무시하도록 함

 

ExpressionParseException.java

ㄴ 수퍼클래스의 생성자를 생성해주도록 함

 

ExpressionParseException.java

ㄴ 주석 모두 제거해주고 나머지는 그대로 둠

 

CalcClient1.java

ㄴ 추가한 ExpressionParseException 클래스를 이용해 예외를 던져줌

 

CalcClient1.java

ㄴ ExpresionParseException 으로 던져주므로 선언부에도 동일하게 ExpresionParseException 를 써줌

 

CalcClient1.java

ㄴ 계산식 오류인 부분은 모두 ExpressionParseException 을 이용하여 예외처리 하도록 함

=>

CalcClient1.java

ㄴ 예외 메시지는 e.getMessage() 를 이용하여 받도록 함

 

 

 

CalcServer1.java

ㄴ 해당 부분 while(true) 문으로 묶어 무한 반복하도록 함

=>

CalcServer.java

ㄴ 해당 while문은 processRequest 메서드를 생성하여 넣어줌

ㄴ while 문 안에꺼만 복사하기

 

CalcServer1.java

ㄴ processRequest 에는 Socket 을 넘겨줌

 

CalcServer1.java

=>

CalcServer1.java

ㄴ 해당 코드는 밖으로 빼줌

 

CalcServer1.java

ㄴ 생성한 processRequest 메서드 이용

 

Stateless 방식으로 통신하기

ㄴ CalcClient1 을 복사하여 CalcClient2 를 생성

 

ㄴ CalcServer1 을 복사하여 CalcServer2 를 생성

 

 

CalcClient2.java

ㄴ 잘라내기

 

CalcClient2.java

ㄴ 잘라냈던 코드  해당 위치에 넣고 out.writeUTF("quit"); 코드는 제거

 

CalcClient2.java

=>

CalcClient2.java

ㄴ Scanner 코드는 바깥쪽으로 빼주기

 

CalcClient2.java

ㄴ 계산식을 잘못 입력해도 while 문이 종료되지 않도록 하여 서버가 종료되지 않도록 함

 

CalcClient2.java

ㄴ Scanner 에도 예외처리 해주기

 

CalcClient2.java

ㄴ 해당 코드 잘라내기

=>

CalcClient2.java

ㄴ 해당 위치에 붙여넣기

=>

CalcClient2.java

package bitcamp.test;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// Stateful 방식으로 통신하기
public class CalcClient2 {

  static Pattern pattern = Pattern.compile("[0-9]+|\\p{Punct}");

  public static void main(String[] args) throws Exception {
    try (Scanner keyscan = new Scanner(System.in);) {

      while (true) {
        System.out.print("계산식(예: + 3)> ");
        String input = keyscan.nextLine();
        if (input.equals("quit")) {
          break;
        }

        Expression expr = null;
        try { // 계산식을 잘못 입력해도 서버는 종료되지 않도록 함
          expr = parseExpression(input);
        } catch (ExpressionParseException e) {
          System.out.println(e.getMessage());
          continue;
        }

        try (Socket socket = new Socket("localhost", 8888);
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
            DataInputStream in = new DataInputStream(socket.getInputStream());) {

          out.writeUTF(expr.op);
          out.writeInt(expr.value);

          String result = in.readUTF();
          System.out.printf("결과: %s\n", result);
        } catch (Exception e) {
          System.out.println("서버 통신 오류!");
        }
      }
    }
  }

  public static Expression parseExpression(String expr) throws ExpressionParseException {
    try {
      Matcher matcher = pattern.matcher(expr);

      ArrayList<String> values = new ArrayList<>();
      while (matcher.find()) {
        values.add(matcher.group());
      }

      if (values.size() != 2) {
        throw new Exception("계산식이 옳지 않습니다!");
      }

      Expression obj = new Expression();
      obj.op = values.get(0);
      obj.value = Integer.parseInt(values.get(1));

      return obj;
    } catch (Exception e) {
      throw new ExpressionParseException(e);
    }
  }

  static class Expression {
    String op;
    int value;
  }
}

 

calcServer2.java

ㄴ while 문 제거

 

calcServer2.java

=>

calcServer2.java

ㄴ while 문이 아니므로 break => return 으로 변경

 

 

ㄴ CalcClient2 를 복사하여 CalcClient3 를 생성

 

ㄴ CalcServer2 를 복사하여 CalcServer3 를 생성

 

Stateful 방식 + Session 으로 통신하기

CalcClient3.java

ㄴ uuid 를 추가

 

CalcClient3.java

ㄴ uuid 도 전송해주도록 함

 

CalcClient3.java

ㄴ uuid = in.readUTF()는 서버로부터 새로운 UUID 값을 수신하여 uuid 변수에 저장

 

 

CalcServer3.java

ㄴ 클라이언트의 uuid 를 읽어와서 uuid 값이 빈 문자열이라면 UUID.randomUUID().toString() 코드를 통해 uuid 를 생성해줌

 

CalcServer3.java

 

CalcServer3.java

ㄴ 클라이언트 작업 결과를 보관할 저장소를 HashMap 으로 생성

ㄴ static 으로 해줘야 함 (메인 메서드에서 사용할 것인데, 메인 메서드가 static 으로 되어있기 때문)

 

CalcServer3.java

ㄴ 해시맵에에 저장된 읽어온 uuid 에 해당하는 값(이전에 접속했을 떄 수행한 작업 결과)을 result 에 저장

 

CalcServer3.java

ㄴ 작업 결과를 해시맵으로 생성된 저장소에 보관함

 

 

CalcClient3.java

ㄴ 서버가 클라이언트에게 다시 UUID를 전송하도록 함

 

 

값이 유지되도록 하기 

ㄴ CalcServer1 를 복사하여 CalcServer1x 를 생성

 

CalcServer1x.java

ㄴ RequestAgent 로컬클래스로 생성

ㄴ 각 RequestAgent 스레드는 생성자를 통해 클라이언트와 통신하기 위한 소켓을 전달받음

 

CalcServer1x.java

ㄴ run 메서드 오버라이딩

 

CalcServer1x.java

ㄴ run() 메서드에서는 processRequest(socket)을 호출하여 해당 클라이언트의 요청을 처리하는 작업을 수행함

 

CalcServer1x.java

ㄴ 각 RequestAgent 스레드는 독립적으로 동작하며, 별도의 클라이언트와 통신하고 요청을 처리하는 과정을 진행

ㄴ 새로운 클라이언트가 서버에 접속할 때마다 RequestAgent 스레드를 생성하고 시작하도록 함

ㄴ serverSocket.accept()은 클라이언트의 접속을 대기하고, 클라이언트가 접속하면 클라이언트와 통신을 위한 소켓 객체를 반환함

    => 이 반환된 소켓 객체는 RequestAgent 생성자에 전달되어 새로운 RequestAgent 스레드가 생성됨
ㄴ start() 메서드를 호출하여 해당 RequestAgent 스레드를 시작함

    => 새로운 클라이언트의 요청을 처리하기 위한 스레드가 백그라운드에서 실행되며, 다른 클라이언트와의 통신과 작업을 동시에 처리할 수 있게 됨

 

=> 이 방식으로 서버는 다수의 클라이언트의 동시 접속을 허용하고, 각각의 클라이언트와 별개의 스레드를 생성하여 동시에 처리할 수 있게 됨 (서버의 성능과 확장성을 향상시키는데 도움을 줌)
=> 각각의 클라이언트와 별개로 동작하는 RequestAgent 스레드가 동시에 작업을 처리하므로 다수의 클라이언트 요청에 대해 빠르고 효율적인 응답을 제공할 수 있음

 

CalcServer1x.java

 

 

ㄴ CalcServer3 를 복사하여 CalcServer3x 를 생성

 

CalcServer3x.java

ㄴ CalcServer1x 와 동일하게 RequestAgent 로컬 클래스 생성

 

CalcServer3x.java

=>

 CalcServer1x 와 동일하게 run 메서드도 오버라이딩

 

CalcServer3x.java

 CalcServer1x 와 동일하게 start() 메서드를 호출하여 해당 RequestAgent 스레드를 시작함

 

CalcServer3x.java