Day_22 ( Java 기초 문법 실습 - 7 )

HD.Y·2023년 11월 28일
0

한화시스템 BEYOND SW

목록 보기
21/58
post-thumbnail

스레드(Thread) 란 ❓

  • 스레드(Thread)는 프로세스안에서 실질적으로 작업을 실행하는 단위를 말하며, 자바에서는 JVM(Java Virtual Machine)에 의해 관리된다.

  • 프로세스에는 적어도 한개 이상의 스레드가 있으며, "Main" 스레드 하나로 시작하여 스레드를 추가 생성하게 되면 멀티 스레드 환경이 된다. 이러한 스레드들은 프로세스의 리소스를 공유하기 때문에 효율적이긴 하지만 단점도 존재한다.

  • 먼저 장점으로는 작업속도가 빨라지고, 비동기 작업이 가능하다는 점이고,
    반대로 단점으로는 "문맥 교환(Context Switch)" 이 많이 발생하면 오히려 작업소속도가 느려질 수 있으며, 동기화가 어려워진다.

  • 특히, 여러 스레드가 동시에 작업할 때 스레드를 세이프하지 않는 상황이 발생할 수 있다. 스레드 세이프를 위해 "synchronized" 를 추가로 달아주는데, 이것은 스레드에서 작업중이면 그곳을 잠금함으로써 다른 쪽에서 읽지 못하도록 하는 설정인데, 오히려 이렇게 하는것 자체가 동시에 작업하는 것도 아니고, 작업이 늘어나는것(잠금, 잠금 해제)일 수 있다는 점이다.

  • 아래 예시는 스레드가 세이프되지 않는 상황과 "synchronized" 를 추가했을때 결과다.

// ✅ 은행 메인 클래스
public class BankMain {
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account();


        for(int i=0; i<10; i++) {
            Thread withdraw = new DepositThread(account);

            withdraw.start();
        }

        Thread.sleep(5000);
        System.out.println("마지막");
        account.check();
    }
}

// ✅ 계좌 클래스
public class Account {
    int balance = 0;

	// synchronized 걸어주지 않았을 때
    public void deposit(int amount) {
        this.balance = this.balance + amount;
    }

	// synchronized 걸어줬을 때
    public synchronized void deposit(int amount) {
        this.balance = this.balance + amount;
    }

    public void check() {
        System.out.println("현재 잔액은 " + this.balance + "원 입니다");
    }
}

// ✅ 예금 스레드
public class DepositThread extends Thread{
    Account account;

    DepositThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {

            for (int i = 0; i < 1000; i++) {
                account.deposit(100);

            }
            account.check();


    }
}

  • 위의 코드는 10개의 계좌에 100원씩 1000번 반복하여 예금을 들도록 한 것이다. 따라서 은행 클래스에서 출력되는 최종 잔액은 100만원이 되는게 정상이다. 하지만 스레드가 동시에 실행되다 보면 한 개의 스레드에서 작업중일 때 다른 곳에서도 작업을 하여 작업 후 저장하는 과정에서 스레드가 아래처럼 세이프( 최종 잔액이 100만원 보다 작다 )
    하지 않게 된다.

  • 하지만 3번째 결과는 "synchornized" 를 걸어준 결과인데 정상적으로 최종 잔액이 100만원이 출력되는 것을 볼 수 있었다.

  • 이처럼 스레드를 사용하면 이러한 단점이 있지만, 이부분은 다르게 해결할 수 있는 부분이 아니기 때문에 보통은 "synchornized" 를 사용하여 스레드 세이프하도록 작성한다고 한다.


💻 스레드를 활용한 네트워크 프로그래밍 실습하기 💻

  • 어제는 서버 ↔ 클라이언트 간 메시지, 파일을 주고 받는 실습을 하였다면,
  • 오늘은 스레드를 활용하여 서버 ↔ 클라이언트1, 클라이언트2, 클라이언트3 이 서로 메시지 및 이미지 파일을 주고 받고, 클라이언트들끼리는 다이렉트 메시지, 흔히 말하는 DM을 보내는 코드를 실습해봤다.
  • 결과를 먼저 말하자면 단체 채팅 개념의 메시지와 DM 은 구현을 성공하였지만, 이미지 파일은 스트림을 닫아주는 것에서 문제가 있는지, 해당 폴더에 이미지 파일은 생성되나 파일이 제대로 생성되지 않아 읽을 수 없는 파일로 생성되었다.
  • 이 부분은, 추후 수정해볼 예정이다. 일단은 오늘 성공한 코드를 적어보겠다.
// 서버 클래스

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// ✅ 메인 서버 클래스 생성
public class ServerMain {

    // 클라이언트 접속 시 클라이언트 ID와 소켓 번호를 저장할 해시맵
    static Map<String, Socket> socketMap = new HashMap<String, Socket>();

    public static void main(String[] args) throws IOException {
        try {
            // 서버 소켓 생성
            int port = 6666;
            ServerSocket serverSocket = new ServerSocket(port);

            // 클라이언트 소켓들의 접속을 허용
            while(true) {
                Socket socket = serverSocket.accept();
                
                
                // 클라이언트와의 메시지 송/수신 처리 스레드
                Thread message = new MessageThreadMain("id", socket);
                message.start();
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }
}

// ✅ 메인 서버 메시지 송/수신 처리 스레드

import java.io.*;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.function.DoubleToIntFunction;

public class MessageThreadMain extends Thread {
    Socket socket;
    String id;

    public MessageThreadMain(String id, Socket socket) {
        this.id = id;
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader bir = new BufferedReader(isr);
            String id = bir.readLine();
            System.out.println(id + "님이 입장하셨습니다.");
            ServerMain.socketMap.put(id, socket);

            while (true) {
                String message = bir.readLine();
				
                // DM 기능 구현
                if (message.startsWith("to:")) {

                    String content = message.substring(message.indexOf(" ") + 1);
                    Socket client = ServerMain.socketMap.get(message.split(" ")[0].substring(3));

                    OutputStream os = client.getOutputStream();
                    PrintStream ps = new PrintStream(os);
                    ps.println("DM : " + content);

				// 이미지 파일 생성 기능(추후 수정 예정)
                } else if (message.endsWith(".jpg")) {
                    for (String key : ServerMain.socketMap.keySet()) {
                        if (id.equals(key)) {
                            continue;
                        }
                        Socket client = ServerMain.socketMap.get(key);
                        OutputStream os = client.getOutputStream();

                        BufferedOutputStream bos = new BufferedOutputStream(os);
                        FileInputStream fileInputStream = new FileInputStream("c:\\test\\" + message);

                        byte[] bytes = fileInputStream.readAllBytes();
                        for (int i = 0; i < bytes.length; i++) {
                            bos.write(bytes[i]);
                        }

                        bos.flush();
                        fileInputStream.close();

                    }
				
                // 그 외, 채팅 기능
                } else {
                        for (String key : ServerMain.socketMap.keySet()) {
                            if (id.equals(key)) {
                                continue;
                            }
                            Socket client = ServerMain.socketMap.get(key);
                            OutputStream os = client.getOutputStream();
                            PrintStream ps = new PrintStream(os);
                            ps.println(message);
                            System.out.println("고객이 서버로 보낸 메시지입니다 : " + message);
                        }
                    }
                }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
// ✅ 클라이언트 메인 클래스

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class Client01 {
    Socket socket;

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

    public static void main(String[] args) {
        try {
            Socket socket= new Socket("192.168.88.1", 6666);

            OutputStream os = socket.getOutputStream();
            PrintStream ps = new PrintStream(os);
            Scanner sc = new Scanner(System.in);
            System.out.print("ID를 입력해주세요 : ");
            String id = sc.nextLine();

            ps.println(id);

            Thread messageOut = new MessageOutputThread(socket);
            Thread messageIn = new MessageInputThread(socket);
            messageOut.start();
            messageIn.start();

            while(true) {

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

// ✅ 클라이언트 메시지 송신 스레드

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class MessageOutputThread extends Thread {
    Socket socket;

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

    @Override
    public void run() {
        try {
            OutputStream os = socket.getOutputStream();
            PrintStream ps = new PrintStream(os);
            Scanner sc = new Scanner(System.in);

            while(true) {
                System.out.print("송신할 메시지를 입력하세요 : ");
                String message = sc.nextLine();
                if(message.endsWith(".jpg")) {
                    ps.println(message);
                    FileInputStream fis = new FileInputStream("c:\\test3\\" + message);

                    BufferedOutputStream bos = new BufferedOutputStream(os);
                    byte[] bytes = fis.readAllBytes();
                    for(int i=0; i<bytes.length; i++) {
                        bos.write(bytes[i]);
                    }
                    bos.flush();
                    fis.close();
                } else {
                    ps.println(message);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
// ✅ 클라이언트 메시지 수신 스레드

import java.io.*;
import java.net.Socket;

public class MessageInputThread extends Thread {
    Socket socket;

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

    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader bir = new BufferedReader(isr);

            while (true) {
                String message = bir.readLine();
                if(message.startsWith("DM : ")) {

                    String dmMessage = message.substring("DM : ".length());
                    System.out.println("수신받은 DM 메시지입니다 : " + dmMessage);

                } else if (message.endsWith(".jpg")) {
                    BufferedInputStream bis = new BufferedInputStream(is);
                    FileOutputStream fileOutputStream = new FileOutputStream("c:\\test2\\" + message);
                    byte[] bytes = bis.readAllBytes();
                    for(int i=0; i<bytes.length; i++) {
                        fileOutputStream.write(bytes[i]);
                    }
                    fileOutputStream.close();
                    // 여기까지 요청받는 코드
                } else {
                    System.out.println("수신받은 메시지입니다 : " + message);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

  • 위의 클라이언트 코드를 새로운 프로젝트 2개를 더 만들어서 생성한다음 총 4개를 모두 작동시키면 아래와 같이 단체 채팅 및 특정 클라이언트에게만 DM 보내는 것이 가능한 것을 볼 수 있다.

    1) 각 클라이언트는 서버에 접속 후 ID 를 입력받도록 되어있으며, ID를 입력 후 접속한다.


    2) 모든 클라이언트가 ID를 입력하면 서버에서는 접속한 클라이언트 ID를 출력해준다.


    3) 한 클라이언트가 메시지를 작성하면 서버 및 다른 클라이언트에게도 출력되며, 내가 설정한 "to:[클라이언트ID] [메시지]" 를 사용하면 특정 클라이언트에게 DM을 보낸다.


  • 일부 코드 출력 부분이나, 이미지 파일 생성하는 부분은 수정해야되지만, 일단 중요한것은 스레드를 사용하면 이렇게 동시에 채팅을 치는것이 가능한 것 처럼 서버가 멈추지 않고 계속 작동 하면서 클라이언트들이 어떠한 행위를 계속해서 할 수 있다는 것인것 같다.

  • 게임을 예로 들면, 한 유저가 아이템 구매를 하고 있는다고 게임 서버 전체가 멈춰있지 않은 것도 스레드의 한 예로 볼 수 있을것 같다.


오늘의 느낀점 👀

  • 어제 배운 스트림에 이어서 오늘은 스레드를 배워봤는데, 솔직히 말해서 아직 100% 이해했다고 말하긴 어려운것 같다. 특히나, 스트림의 연경 종료 부분은 아직도 정확하게 이해가 안된다. 어떨때는 닫아줘야 파일이 정상적으로 전송되고, 어떨때는 닫으니 연결자체가 끊기고 하는 부분을 실습하면서 실제로 겪다보니 아직도 헷갈리는게 사실이다.

  • 물론 네트워크 프로그래밍 같은 경우 추후에 스프링을 배우면 직접 자바 코드로 이렇게 타이핑 하지는 않는다고 강사님께서 말씀하셨으나, 결국 그또한 기본 베이스는 자바 프로그래밍으로부터 비롯됬다고 생각하기 때문에 아예 이해를 안하고 사용하는 것은 불가능하다고 생각한다.

  • 이번 주말에 자격증 시험이 끝나면, 오늘 해결하지 못한 부분과 추가적으로 보완할 부분을 수정/보완 해볼 예정이다.

profile
Backend Developer

0개의 댓글