2022.08.25/자바 정리/스레드/Runnable

Jimin·2022년 8월 25일
0

비트캠프

목록 보기
29/60
post-thumbnail
  • 스레드 사용법(com.eomcs.concurrent.*)(계속)
    • Thread, Runnable 사용법
    • 중첩 클래스를 다루는 방법
  • board-app 프로젝트 수행
      1. Thread를 이용한 멀티 태스킹 구현하기: 동시 요청 처리하기(계속)
      • 리팩토링: 중첩 클래스 활용

runnable 인터페이스로 구현하면 스레드 생성하는 곳에서 new Thread(Runnable 객체).start()
해주면 되고,
Thread 상속한 거는
그 클래스 이름.start() 해주면 된다.
상속 받은 클래스도 어차피 Thread 클래스라고 생각할 수 있으므로!


JVM과 스레드


main/ 스레드 그룹에는 "main"스레드만 존재한다. 하위 그룹도 존재하지 않는다.
이 "main" 스레드는 main() 메소드를 호출한다.

스레드(Thread)

  • 이 순간 실행 중인 흐름이 무엇인지 알고 싶다면?
Thread t = Thread.currentThread();
System.out.println("실행 흐름명 = " + t.getName());
  • 실행 흐름을 전문적인 용어로 "Thread(실 타래)"라 부른다.
  • JVM이 실행될 때 main() 메서드를 호출하는 실행 흐름(스레드)의 이름은 "main"이다.

JVM의 스레드 계층도(MAC)


스레드 정의1

MyThread t = new MyThread();
t.start();

start() → 별도의 실행 흐름을 만든 후, run()을 호출한다.
→ 그리고 즉시 리턴한다. → run() 실행이 끝날 때까지 기다리지 않는다.
⇒ 비동기 실행(Asynchronous)


스레드 정의2

Thread t = new Thread(new MyRunnable());
t.start();


board-app project
board-app-server

Thread를 이용한 멀티 태스킹 구현하기: 동시 요청 처리하기

1단계 - 클라이언트 요청을 별도의 실행흐름으로 처리할 스레드를 만든다.

  • com.bitcamp.board.RequestThread 클래스 생성

2단계 - 스레드를 통해 클라이언트 요청을 처리한다.

  • com.bitcamp.board.ServerApp 클래스 변경

3단계 - 리팩토링1: RequestThread를 ServerApp의 static nested class로 만든다.

  • com.bitcamp.board.RequestThread 클래스 삭제

    • 삭제 전 클래스 백업: RequestThread.java.01
  • com.bitcamp.board.ServerApp 클래스 변경

    • 변경 전 클래스 백업: ServerApp.java.01
      RequestThread 클래스를 ServerApp 클래스 내부, main 메소드 외부로 옮겨준다.
      main 내부에서 스레드를 사용할 것이므로 RequestThread 클래스는 static으로 설정해준다.
  • ServerApp class

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

        // 클라이언트가 연결되면,
        Socket socket = serverSocket.accept();

        // 클라이언트 요청을 처리할 스레드를 만든다.
        RequestThread t = new RequestThread(socket, servletMap);

        // main 실행 흐름에서 분리하여 별도의 실행 흐름으로 작업을 수행시킨다.
        t.start();
      
     ...
     
  }

  static class RequestThread extends Thread {

    private Socket socket;
    private Map<String,Servlet> servletMap;

    public RequestThread(Socket socket, Map<String,Servlet> servletMap) {
      this.socket = socket;
      this.servletMap = servletMap;
    }

    // 별도의 실행흐름에서 수행할 작업 정의
    @Override
    public void run() {
      try (Socket socket = this.socket;
          DataInputStream in = new DataInputStream(socket.getInputStream());
          DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {

        System.out.println("클라이언트와 연결 되었음!");

        String dataName = in.readUTF();

        Servlet servlet = servletMap.get(dataName);
        if (servlet != null) {
          servlet.service(in, out);
        } else {
          out.writeUTF("fail");
        }

        System.out.println("클라이언트와 연결을 끊었음!");

      } catch (Exception e) {
        System.out.println("클라이언트 요청 처리 중 오류 발생!");
        e.printStackTrace();
      }
    }
  }
}

4단계 - 리팩토링2: RequestThread를 main()의 local class로 만든다.

  • com.bitcamp.board.ServerApp 클래스 변경

    • 변경 전 클래스 백업: ServerApp.java.02
      로컬 클래스이므로 static은 지운다.
      이제 servletMap 객체에 접근이 가능하기 때문에 객체 변수를 지워도 된다.
  • ServerApp class

public class ServerApp {

  public static void main(String[] args) {

    ...

    class RequestThread extends Thread {

      private Socket socket;

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

      @Override
      public void run() {
        ...
      }
    }

    ...

      while (true) {
        // 클라이언트가 연결되면,
        Socket socket = serverSocket.accept();

        // 클라이언트 요청을 처리할 스레드를 만든다.
        RequestThread t = new RequestThread(socket);

        // main 실행 흐름에서 분리하여 별도의 실행 흐름으로 작업을 수행시킨다.
        t.start();
      }
      
    ...
    
  }  
}

5단계 - 리팩토링3: RequestThread를 Runnable 구현체로 만든다.

  • com.bitcamp.board.ServerApp 클래스 변경

    • 변경 전 클래스 백업: ServerApp.java.03
  • 내가 Thread를 실행하면, Thread가 Runnable의 run() 메소드를 실행한다.

  • ServerApp class

public class ServerApp {

  public static void main(String[] args) {

    ...

    // 스레드로 만드는 대신에 Thread가 실행할 수 있는 클래스로 변경한다.
    class RequestRunnable implements Runnable {

      private Socket socket;

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

      @Override
      public void run() {
        ...
      }
    }

    ...
    
        Socket socket = serverSocket.accept();

        // 클라이언트 요청을 처리할 스레드를 만들고
        // main 실행 흐름에서 분리하여 별도의 실행 흐름으로 작업을 수행시킨다.
        new Thread(new RequestRunnable(socket)).start();
        
     ...
      
  }
}

6단계 - 리팩토링4: RequestRunnable을 익명 클래스로 만든다.

  • com.bitcamp.board.ServerApp 클래스 변경

    • 변경 전 클래스 백업: ServerApp.java.04
  • ServerApp class

public class ServerApp05 {

  public static void main(String[] args) {

    ...

      while (true) {
        new Thread(new Runnable() {
          Socket socket = serverSocket.accept();
          @Override
          public void run() {
            try (Socket socket = this.socket;
                DataInputStream in = new DataInputStream(socket.getInputStream());
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {

              System.out.println("클라이언트와 연결 되었음!");

              String dataName = in.readUTF();

              Servlet servlet = servletMap.get(dataName);
              if (servlet != null) {
                servlet.service(in, out);
              } else {
                out.writeUTF("fail");
              }

              System.out.println("클라이언트와 연결을 끊었음!");

            } catch (Exception e) {
              System.out.println("클라이언트 요청 처리 중 오류 발생!");
              e.printStackTrace();
            }
          }
        }).start();
      }
    }
    ...
  }
}

7단계 - 리팩토링5: 익명클래스를 람다(lambda) 표현식으로

  • com.bitcamp.board.ServerApp 클래스 변경
    • 변경 전 클래스 백업: ServerApp05.java
    • 인터페이스 껍데기를 제거한다. (Runnable)
    • 하나의 메소드의 파라미터 괄호만 남기고 메서드 바디를 향해 ->를 붙여준다.
    • 람다 문법에서는 인스턴스 필드는 처리할 수 없다. → this를 지운다.

람다로 만들기 위해서
1. 인터페이스 껍데기 걷어내기
2. 파라미터와 메서드 바디만 남긴다.

틀 먼저 만들기:
Thread를 상속 받은 익명 클래스 생성 -> 생성자를 위하여 () 붙이기
new Thread().start();

  • ServerApp class
public class ServerApp {

  public static void main(String[] args) {

    ...

      while (true) {
        // 여러 클라이언트의 요청을 동시에 처리하기 위해서 클라이언트가 연결되면,
        //람다 문법에서는 인스턴스 필드는 처리할 수 없다. 따라서, 다시 로컬변수로 전환한다.
        Socket socket = serverSocket.accept();  // 생성자가 생성될 때 함께 실행된다.,  클라이언트가 들어올때까지 넘어가지 않는다. blocking method

        new Thread(() -> {
          try (Socket socket2 = socket;
              // socekt을 가지고 입출력 stream 얻기
              DataInputStream in = new DataInputStream(socket.getInputStream());
              DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {

            System.out.println("클라이언트와 연결 되었음!");

            // 클라이언트와 서버 사이에 정해진 규칙(protocol)에 따라 데이터를 주고 받는다.
            String dataName = in.readUTF();

            Servlet servlet = servletMap.get(dataName);
            if (servlet != null) {
              servlet.service(in, out);
            } else {
              out.writeUTF("fail");
            }

            System.out.println("클라이언트와 연결을 끊었음!");
          } // 안쪽 try
          catch(Exception e) {
            System.out.println("클라이언트 요청 중 오류 발생!");
            e.printStackTrace();
          }
        } // Runnable인터페이스 (lambda 문법으로 작성) 
            ).start(); // Thread()
      }//while()
    } catch (Exception e) {
      e.printStackTrace();
    } // 바깥 쪽 try 

    System.out.println("서버 종료!");
  }
}

기타 정리

- socket을 run() 메소드 바깥에서 만드는 이유?


socket.accept()를 Thread 클래스 안에 넣어서 client의 요청을 받든, Thread 클래스 밖에서 client의 요청을 받든 상관없다.
하지만, run() 메소드 안에서 client의 요청을 받으면 큰일난다.
→ 왜냐! accept() 메소드는 client의 요청이 들어올 때까지 while문을 멈추는 역할을 하는 blocking method인데,
이 메소드가 run() 안으로 들어가 버리면, start()는 run() 메소드가 끝나지 않아도 계속 실행하는 비동기적 실행이기에 while문이 멈추지 않고 계속 돌아가서 start()가 계속 호출되어,
이를 통해 무수히 많은 스레드가 계속 생성되어 그 스레드의 run()메서드가 계속 실행되게 되고,
그 무수히 많은 스레드들이 client의 요청을 기다리게 된다.
(메모리를 다 쓸때까지 스레드가 만들어짐.)
⇒ 즉, 너무 많은 스레드가 만들어지는 것을 방지하기 위해 blocking 역할을 하기 위해 accpet()를 run() 메소드 밖에서 받아준다.
run() 밖에서 걸리면 Runnable 객체가 생성이 안되어서 start()가 실행이 안된다.

Thread 객체를 익명으로 생성하는 이유?

한 번 끝난 스레드 변수는 재 start() 할 수 없다. → 그래서 굳이 레퍼런스 변수에 저장할 필요가 없다.

익명 클래스의 이점

익명 클래스는 클래스가 어떤 코드를 가지고 있는지 바로 확인할 수 있다.
하지만, 너무 코드가 길고 복잡하다면, 일반 클래스로 분리하는 것이 좋다.

profile
https://github.com/Dingadung

0개의 댓글