KDT 공부 일지 - 2026.01.23

김시코·2026년 1월 23일

KDT 공부 일지

목록 보기
15/27

Thread와 Process

[p1]
package p1;

public class App {
    public static void main(String[] args) {
        // Thread 복습

        HelloPrint helloPrint = new HelloPrint();
        YahoPrint yahoPrint = new YahoPrint();

        helloPrint.run();
        yahoPrint.run();

        System.out.println("프로그램 종료!!");
    }
}

class HelloPrint extends Thread {   // 비동기식
    @Override
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.println("안녕: " + i);
        }
    }
}

class YahoPrint extends Thread {    // 비동기식
    @Override
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.println("야호: " + i);
        }
    }
}

[프로세스]
프로세스(Process)는 하나의 프로그램 단위로, 실행 중인 프로그램을 의미한다.
웹브라우저, 카카오톡, VSCode 등의 프로그램을 실행시키면 각각 하나의 프로세스가 생성되며,
여러 개의 프로세스가 동시에 경쟁적으로 실행된다.

[쓰레드]
쓰레드(Thread)는 하나의 프로세스 내부에서 실제로 작업을 진행하는 실행 흐름이다.
main메서드가 실행되면 하나의 main 쓰레드를 생성하며, main 쓰레드 하나만 있다면 프로그램은 동기식으로 진행이 된다. 그리고 여기서 원하는 클래스에 Thread를 상속(extends)받아서 구현한 후에 직접 작성한 메서드가 아닌 start 메서드를 호출하면 비동기식으로 내부의 로직이 실행된다. 이런걸 멀티 쓰레드(Multi Thread)라고 부른다.

쓰레드의 사용 목적은 CPU를 최대한 활용하는것에 있는데, 이러면 퍼포먼스가 매우 향상된다.
하지만 쓰레드를 다루는 프로그램이 가장 어려운데, 그 이유로는 여러 쓰레드끼리 자원을 공유하는 상황이 발생하기 때문이다. 쓰레드끼리 자원을 공유하면서 오류를 일으킬 수 있는데, 그러한 오류가 매번 발생 하는것이 아니라, 대부분은 괜찮다가 어쩌다보니 한번씩 오류가 발생되기 때문에 테스트와 디버깅이 매우 힘들다. 왜 오류가 나는지 알아야 하는데 그 오류가 나는 상황을 만들기 힘들기 때문에. 그리고 코드가 복잡해 질 수록 상황의 재현이 어렵고, 그러니까 수정이 더욱 어려워진다. 그래서 쓰레드는 자원을 공유하는 상황이 아닌 경우에는 매우 좋은 선택이지만, 여러 쓰레드가 자원을 공유할 때는 버그(논리적 결함)가 생겨버린다. 그렇기 때문에 동기화 기법을 적용해야 한다.

[p2]
package p2;

public class App {
    public static void main(String[] args) {
    // 쓰레드 여러개가 서로 자원을 공유하는 상황이 되면 프로그램의 난이도가 올라간다.
        Some s1 = new Some();
        Some s2 = new Some();

        s1.start();
        s2.start();
        // 이러면 메인 쓰레드는 금방 끝나고 s1. s2 쓰레드가 동시실행됨
        // 이때, 두개의 쓰레드는 자원을 공유하나? - 자원 범위 매우 넓음. (그래서 어려움)
        // 대표적 자원으로는 메모리가 있으며, 이때 지역 변수는 서로 공유하지 않고
        // 쓰레드 마다 메모리를 다 따로 생성한다. 그렇기 때문에 자원을 공유하는
        // 상황이 아니라고 할 수 있다.
        // 같은 코드를 한번에 실행시키는게 아니라 하나의 쓰레드에서 따로따로 실행됨
    }
}

class Some extends Thread {
    @Override
    public void run() {
        int sum = 0;
        for(int i = 0; i < 10; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
}

[동기화 기법]
다양한 동기화 기법들이 있는데, 상황에 따라 적절한 동기화를 적용해야 한다. 어떤게 적절한 방법인지 판단하는게 힘든데, 왜냐하면 버그가 해결됨과 동시에 퍼포먼스 하락(병목현상) 이 일어나지 않아야 하기 때문에 난이도가 높아지는 원인이 된다.
synchronized: synchronized가 붙은 블록을 임계영역(Critical Section)으로 만들어서 동기화 시켜버린다. 그렇게 되면 쓰레드들이 계속해서 메서드를 경쟁적으로 호출하는데, 이미 그 메서드를 실행중인 쓰레드가 있다면 잠금을 걸어서 다른 쓰레드의 접근을 막는다.
사실 쓰레드는 효율을 위해 병렬로 쓰레드를 만들어서 동시에 실행시키기 위한 것인데, synchronized를 쓰게 되면 병렬로 잘 실행되던 쓰레드들을 한곳으로 모아서 하나씩 처리하기 때문에 병목현상이 일어나게 된다. 그렇기 때문에 동기화 범위가 너무 넓으면 쓰레드를 쓰지 않는것보다 더 느려질 수 있기 때문에 좋지앟다. 이를 해결하기 위해서는 동기화의 범위를 딱 필요한 부분으로 좁혀서 병목현상을 방지해야 한다.

[p3]
package p3;

public class App {
    public static void main(String[] args) throws Exception {
        // 자원을 공유하는 가장 쉬운 예 (난이도 높아짐)
        // 이럴때는 동기화를 해준다. (이러면 병목 현상이 나타나서 퍼포먼스가 다시 낮아짐)

        Some s1 = new Some();
        Some s2 = new Some();

        s1.start();
        s2.start();

        s1.join();  // 서브 쓰레드가 끝날때까지 메인 쓰레드를 종료시키지 말고 멈추란 뜻.
        s2.join();  // 안쓰면 메인 쓰레드 종료 후에 for문이 돌기 때문에 그냥 0만 나옴.

        System.out.println(Data.count);
          // 값이 200이 나와야 하는데 가끔씩 이상하게 나옴
    }
}

class Some extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++) {
           Data.increaseCount();
        }
    }
}

class Data {
    public static int count = 0;    // static은 죽어도 메모리가 하나이다.

    public static synchronized void increaseCount() {
      // synchronized 가 붙으면 동기가 돼서 오류가 나타나지 않음. 
        int value = count;

        String str = "qqq";
        str += "안녕"; // 오버헤드를 일부러 높여서 오류 자주 발생하게 함.
        System.out.println(str);

        value++;

        count = value;  // 테스트를 위해 일부러 더럽게 짯다고 함.
    }   
    // 근데 이러면 Thread를 사용하는 의미가 없다. 왜냐면 동기화의 범위가 너무 크기 때문에.
    // synchronized는 동기화를 시켜버리는거라 실행을 위해 메서드 호출할때 start가 아니라
    // run으로 쓰는거랑 마찬가지. 오히려 run이 더 빠르다.
    // *분명 효율을 위해 쓰는건데 더 느려져버린다.
    // 그러니까 클래스 범위를 잘게 잘라서 동기화가 되는 범위를 좁혀야 한다.
}
[p4]
package p4;

public class App {
    public static void main(String[] args) throws Exception {

        long time = System.currentTimeMillis();

        SumCalculator sumCalculator1 = new SumCalculator(1, 250000000);
        SumCalculator sumCalculator2 = new SumCalculator(250000001, 500000000);
        SumCalculator sumCalculator3 = new SumCalculator(500000001, 750000000);
        SumCalculator sumCalculator4 = new SumCalculator(750000001, 1000000000);

        sumCalculator1.start();
        sumCalculator2.start();
        sumCalculator3.start();
        sumCalculator4.start();

        sumCalculator1.join();
        sumCalculator2.join();
        sumCalculator3.join();  // 뭐가 먼저 끝나는지 모름 OS가 알아서 함.
        sumCalculator4.join();  // 그래서 4개 다 끝날떄까지 멈춰야 하니까 다 써줌

        long result = 0;
        result += sumCalculator1.getSum();
        result += sumCalculator2.getSum();
        result += sumCalculator3.getSum();
        result += sumCalculator4.getSum();

        System.out.println(result);

        System.out.println("시간: " + (System.currentTimeMillis() - time) + "ms");
    }
}

class SumCalculator extends Thread {

    private long start;
    private long end;
    private long sum;

    public SumCalculator(long start, long end) {
        this.start = start;
        this.end = end;
        this.sum = 0;
    }

    @Override
    public void run() {
        for(long i = start; i < end; i++) {
            sum += i;
        }
    }

    public long getSum() {
        return sum;
    }
}
[p5]
package p5;

import java.util.concurrent.CompletableFuture;

public class App {
    public static void main(String[] args) throws Exception {
        Some some = new Some();

        // some.test1();
        // some.test2();

        CompletableFuture.runAsync(() -> some.test1()); // 람다식
        CompletableFuture.runAsync(some::test2)         // 비동기식
            .thenRun(() -> {
                System.out.println("위에것 끝난 다음에 실행될 코드");
            });
		CompletableFuture.runAsync(some::test3);
        
        Thread.sleep(3000);

        System.out.println("프로그램 종료");
    }
}

class Some {
    public void test1() {
        for(int i = 0; i < 10; i++) {
            System.out.println("헬로: " + i);
        }
    }

    public void test2() {
        for(int i = 0; i < 10; i++) {
            System.out.println("헬로: " + i);
        }
    }

    public void test3() {
        for(int i = 0; i < 10; i++) {
            System.out.println("헬로: " + i);
        }
    }
}

네트워크

이제부터는 하나의 예제를 만들때 한 패키지 안에 Server, Client 두개의 클래스를 만든다. 네트워크 API를 써보는게 목적이기 때문에 예외처리는 하지 않고 throws Exception으로 한다.
또한, 실제 실무에서는 이렇게까지 로우레벨로 만드는 경우는 많지 않으며, 네트워크 API는 파일 입출력 API와 겹치는 API가 많다.

[p6] - 단일통신

[ServerApp]
package p6;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerApp {
    public static void main(String[] args) throws Exception {
        System.out.println("서버 시작");   

        // 클라이언트를 받을 준비
        ServerSocket serverSocket = new ServerSocket(7777); // 7777은 포트 번호.
        System.out.println("클라이언트로부터 접속 대기 중");

        // 참고로 소켓이 한개 뿐이라 단일통신 프로그램이다.
        // 사용자를 계속 받기 위해서는 이 아래 코드가 무한반복이 되어야 함.
        Socket socket = serverSocket.accept();
        // 클라이언트로 부터 접속 대기. 클라이언트가 접속하기 전까지 여기서 멈춰있음.
        // accept도 리턴타입이 Socket이기에 앞에 이렇게 Socket 타입의 변수를
        // 추가하면 타입이 같아지게 만들 수 있다.
        
        System.out.println("누군가가 접속했다...");
        // 이때 프로그램을 두번 실행시키면 BindException 발생함.
        // 포트번호 중복이란 뜻. 같은 포트 번호를 가지는 서버는 하나만 실행 가능.

        InputStream inputStream = socket.getInputStream();
        DataInputStream dos = new DataInputStream(inputStream);

        System.out.println("클라이언트로부터 값을 보낼때까지 대기 중...");
        String value = dos.readUTF();
        System.out.println("클라이언트로부터 받은 값: " + value);

        socket.close();
        serverSocket.close();
        
        System.out.println("서버 끝");
    }
}
[ClientApp]
package p6;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

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

        System.out.println("클라이언트 시작");
        
        System.out.println("접속 중...");	// 예시 IP
        Socket socket = new Socket("192.168.x.x", 7777);
          // 접속. 소켓을 생성한다는것 자체가 접속하겠다는 뜻임.
          // 앞의 문자열은 이 컴퓨터 IP주소이고, 뒤 숫자는 ServerSocket의 포트번호이다.
        System.out.println("접속 완료!");

        System.out.print("서버로 보낼 값 > ");
        String value = scanner.nextLine();

        OutputStream outputStream = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(outputStream);

        dos.writeUTF(value);
        System.out.println("전송 완료");

        outputStream.close();
        scanner.close();
        socket.close();

        System.out.println("클라이언트 끝");
    }
}

[p7] - 다중통신

[ServerApp]
package p7;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerApp {
    public static void main(String[] args) {
        // 다중통신 시스템. (여러명 접속 가능하게)
        System.out.println("서버 시작");

        try (ServerSocket serverSocket = new ServerSocket(8888)) {
            while (true) {
                System.out.println("접속 대기중...");
                Socket socket = serverSocket.accept();
                System.out.println("클라이언트가 접속되었습니다... Thread 시작");
                ReceiveThread receiveThread = new ReceiveThread(socket);
                receiveThread.start();
                
                
            }

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

class ReceiveThread extends Thread {
    
    private Socket socket;

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

    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            DataInputStream dis = new DataInputStream(inputStream);
            while (true) {
                String message = dis.readUTF();
                System.out.println(message);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("누군가 접속을 종료했습니다.");
        } finally {
            if(socket != null) {
                try {
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
[ClientApp]
package p7;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;

public class ClientApp {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in, Charset.forName("MS949"));
        System.out.print("닉네임 입력 > ");
        String nickname = scanner.nextLine();

        System.out.println("접속 중...");
        try (Socket socket = new Socket("172.30.1.72", 8888)) {
            System.out.println("접속완료.");

            OutputStream outputStream = socket.getOutputStream();
            DataOutputStream dos = new DataOutputStream(outputStream);
            while (true) {
                System.out.print("메세지(종료=0) > ");
                String message = scanner.nextLine();

                if(message.equals("0")) break;

                dos.writeUTF(nickname + ":" + message);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("이용해주셔서 감사합니다!!");

        scanner.close();
    }
}
profile
취업하고싶다...

0개의 댓글