동시성이란 무엇인가

동시성이란 무엇인가

동시성이란 무엇인가

1. 동시 시스템의 정의

동시 시스템이란 여러 일을 동시에 처리하는 시스템을 말한다.

순차 처리 vs 동시 처리

순차 처리 (Sequential Processing)

작업A 시작 → 작업A 완료 → 작업B 시작 → 작업B 완료 → 작업C 시작 → 작업C 완료
[========10초========][========10초========][========10초========]
총 소요시간: 30초

동시 처리 (Concurrent Processing)

작업A 시작 ─────────→ 작업A 완료
작업B 시작 ─────────→ 작업B 완료
작업C 시작 ─────────→ 작업C 완료
[=============10초=============]
총 소요시간: 10초

예제: 순차 처리 vs 동시 처리

import java.util.concurrent.*;

public class SequentialVsConcurrent {

    // 시간이 걸리는 작업을 시뮬레이션
    static String fetchDataFromServer(String server) {
        try {
            Thread.sleep(1000); // 1초 대기 (네트워크 지연 시뮬레이션)
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return server + "로부터 데이터 수신 완료";
    }

    // 순차 처리 방식
    static void sequentialApproach() {
        long start = System.currentTimeMillis();

        String result1 = fetchDataFromServer("서버A");
        String result2 = fetchDataFromServer("서버B");
        String result3 = fetchDataFromServer("서버C");

        long end = System.currentTimeMillis();
        System.out.println("순차 처리 소요시간: " + (end - start) + "ms");
        // 출력: 순차 처리 소요시간: 3000ms (약 3초)
    }

    // 동시 처리 방식
    static void concurrentApproach() throws Exception {
        long start = System.currentTimeMillis();

        ExecutorService executor = Executors.newFixedThreadPool(3);

        Future<String> future1 = executor.submit(() -> fetchDataFromServer("서버A"));
        Future<String> future2 = executor.submit(() -> fetchDataFromServer("서버B"));
        Future<String> future3 = executor.submit(() -> fetchDataFromServer("서버C"));

        // 모든 결과 수집
        String result1 = future1.get();
        String result2 = future2.get();
        String result3 = future3.get();

        executor.shutdown();

        long end = System.currentTimeMillis();
        System.out.println("동시 처리 소요시간: " + (end - start) + "ms");
        // 출력: 동시 처리 소요시간: 1000ms (약 1초)
    }

    public static void main(String[] args) throws Exception {
        sequentialApproach();
        concurrentApproach();
    }
}

동시성(Concurrency) vs 병렬성(Parallelism)

구분동시성 (Concurrency)병렬성 (Parallelism)
정의여러 작업을 번갈아가며 처리여러 작업을 실제로 동시에 처리
하드웨어단일 코어에서도 가능멀티 코어 필수
비유한 사람이 여러 일을 번갈아 처리여러 사람이 각자 일을 처리
목적응답성 향상, 자원 효율화처리량 극대화
[동시성 - 단일 코어]
시간 →  |작업A|작업B|작업A|작업C|작업B|작업A|작업C|
        컨텍스트 스위칭으로 번갈아 실행

[병렬성 - 멀티 코어]
코어1:  |========= 작업A =========|
코어2:  |========= 작업B =========|
코어3:  |========= 작업C =========|
        실제로 동시에 실행

2. 동시성의 필요성: 현실 세계 모델링

현실 세계에서는 여러 일이 동시에 일어난다. 이러한 현실 세계를 모델링하려면 동시성 프로그래밍이 필요하다.

현실 세계의 동시성 예시

현실 세계 상황프로그래밍 모델링
은행에서 여러 고객이 동시에 ATM 사용다중 스레드로 각 고객 요청 처리
식당에서 여러 테이블 주문을 동시 처리비동기 이벤트 처리
공장에서 여러 기계가 동시에 작동병렬 작업 스케줄링
채팅방에서 여러 사용자가 동시에 메시지 전송이벤트 기반 동시 처리

예제: 은행 ATM 시스템

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class BankATMSimulation {

    // 은행 계좌 (동시 접근 가능)
    static class BankAccount {
        private final String accountId;
        private final AtomicInteger balance;  // 스레드 안전한 잔액

        public BankAccount(String accountId, int initialBalance) {
            this.accountId = accountId;
            this.balance = new AtomicInteger(initialBalance);
        }

        public boolean withdraw(int amount) {
            int current = balance.get();
            if (current >= amount) {
                balance.addAndGet(-amount);
                return true;
            }
            return false;
        }

        public void deposit(int amount) {
            balance.addAndGet(amount);
        }

        public int getBalance() {
            return balance.get();
        }
    }

    // ATM 기기 시뮬레이션
    static class ATM implements Runnable {
        private final String atmId;
        private final BankAccount account;
        private final int operationCount;

        public ATM(String atmId, BankAccount account, int operationCount) {
            this.atmId = atmId;
            this.account = account;
            this.operationCount = operationCount;
        }

        @Override
        public void run() {
            for (int i = 0; i < operationCount; i++) {
                if (Math.random() > 0.5) {
                    account.deposit(100);
                    System.out.println(atmId + ": 100원 입금 완료. 잔액: " + account.getBalance());
                } else {
                    boolean success = account.withdraw(100);
                    if (success) {
                        System.out.println(atmId + ": 100원 출금 완료. 잔액: " + account.getBalance());
                    } else {
                        System.out.println(atmId + ": 잔액 부족으로 출금 실패");
                    }
                }

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BankAccount sharedAccount = new BankAccount("ACC-001", 1000);

        // 3개의 ATM이 동시에 같은 계좌에 접근
        Thread atm1 = new Thread(new ATM("ATM-1", sharedAccount, 5));
        Thread atm2 = new Thread(new ATM("ATM-2", sharedAccount, 5));
        Thread atm3 = new Thread(new ATM("ATM-3", sharedAccount, 5));

        atm1.start();
        atm2.start();
        atm3.start();

        atm1.join();
        atm2.join();
        atm3.join();

        System.out.println("최종 잔액: " + sharedAccount.getBalance());
    }
}

3. 동시성의 장점

동시성을 적용하면 지연 시간을 드러나지 않게 하고, 기존 처리 자원의 활용도를 높여 시스템의 성능과 처리율을 크게 개선할 수 있다.

3.1 지연 시간(Latency) 숨기기

I/O 작업 중 CPU가 놀고 있을 때 다른 작업을 수행하여 대기 시간을 활용한다.

[동시성 없음 - 지연 시간 노출]
시간 →  |CPU작업|---I/O 대기---|CPU작업|---I/O 대기---|
        CPU가 I/O 대기 동안 유휴 상태

[동시성 적용 - 지연 시간 숨김]
작업1:  |CPU|---I/O 대기---|CPU작업|
작업2:      |CPU|---I/O 대기---|CPU작업|
작업3:          |CPU|---I/O 대기---|
        I/O 대기 중에 다른 작업의 CPU 처리 수행

예제: I/O 대기 시간 활용

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

public class LatencyHiding {

    static String readFile(String filename) {
        System.out.println(Thread.currentThread().getName() + ": " + filename + " 읽기 시작");
        try {
            Thread.sleep(500);  // I/O 대기 시뮬레이션
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() + ": " + filename + " 읽기 완료");
        return filename + " 내용";
    }

    static String processData(String data) {
        return data.toUpperCase() + " [처리됨]";
    }

    public static void main(String[] args) throws Exception {
        List<String> files = List.of("file1.txt", "file2.txt", "file3.txt",
                                      "file4.txt", "file5.txt");

        // 순차 처리
        long start = System.currentTimeMillis();
        List<String> results1 = new ArrayList<>();
        for (String file : files) {
            String content = readFile(file);
            results1.add(processData(content));
        }
        System.out.println("순차 처리 시간: " + (System.currentTimeMillis() - start) + "ms\n");

        // 동시 처리
        ExecutorService executor = Executors.newFixedThreadPool(5);
        start = System.currentTimeMillis();

        List<Future<String>> futures = new ArrayList<>();
        for (String file : files) {
            futures.add(executor.submit(() -> {
                String content = readFile(file);
                return processData(content);
            }));
        }

        List<String> results2 = new ArrayList<>();
        for (Future<String> future : futures) {
            results2.add(future.get());
        }

        executor.shutdown();
        System.out.println("동시 처리 시간: " + (System.currentTimeMillis() - start) + "ms");
        // 순차: 약 2500ms, 동시: 약 500ms
    }
}

3.2 처리율(Throughput) 개선

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThroughputImprovement {

    static AtomicInteger processedCount = new AtomicInteger(0);

    static void handleRequest(int requestId) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        processedCount.incrementAndGet();
    }

    public static void main(String[] args) throws Exception {
        int totalRequests = 100;

        // 단일 스레드 처리
        processedCount.set(0);
        long start = System.currentTimeMillis();

        for (int i = 0; i < totalRequests; i++) {
            handleRequest(i);
        }

        long singleThreadTime = System.currentTimeMillis() - start;
        double singleThreadThroughput = (double) totalRequests / singleThreadTime * 1000;
        System.out.println("단일 스레드:");
        System.out.println("  처리 시간: " + singleThreadTime + "ms");
        System.out.println("  처리율: " + String.format("%.1f", singleThreadThroughput) + " 요청/초\n");

        // 멀티 스레드 처리
        processedCount.set(0);
        ExecutorService executor = Executors.newFixedThreadPool(10);
        start = System.currentTimeMillis();

        for (int i = 0; i < totalRequests; i++) {
            final int requestId = i;
            executor.submit(() -> handleRequest(requestId));
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);

        long multiThreadTime = System.currentTimeMillis() - start;
        double multiThreadThroughput = (double) totalRequests / multiThreadTime * 1000;
        System.out.println("멀티 스레드 (10개):");
        System.out.println("  처리 시간: " + multiThreadTime + "ms");
        System.out.println("  처리율: " + String.format("%.1f", multiThreadThroughput) + " 요청/초");
        System.out.println("  성능 향상: " + String.format("%.1f", (double)singleThreadTime/multiThreadTime) + "배");
    }
}

3.3 자원 활용도 향상

[CPU 사용률 비교]

순차 처리:
CPU 사용률: ████░░░░░░░░░░░░░░░░ 20%
           (대부분 I/O 대기)

동시 처리:
CPU 사용률: ████████████████░░░░ 80%
           (I/O 대기 중 다른 작업 수행)

4. 확장성: 수직 확장 vs 수평 확장

확장성은 수직 확장성과 수평 확장성으로 나뉜다.

비교 표

특성수직 확장 (Scale Up)수평 확장 (Scale Out)
방법단일 서버 성능 향상서버 수 증가
예시CPU/RAM 업그레이드서버 추가
한계하드웨어 물리적 한계이론적으로 무한 확장 가능
비용고성능일수록 기하급수적 증가선형적 증가
복잡도낮음 (코드 변경 불필요)높음 (분산 시스템 설계 필요)
가용성단일 장애점 존재장애 허용 가능

시각적 비교

[수직 확장 - Scale Up]

Before:         After:
┌─────────┐     ┌─────────────────┐
│ Server  │     │   BIG Server    │
│ 4 CPU   │ →   │   16 CPU        │
│ 8GB RAM │     │   64GB RAM      │
└─────────┘     └─────────────────┘
처리량: 100/s   처리량: 400/s


[수평 확장 - Scale Out]

Before:              After:
┌─────────┐          ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Server  │          │ Server  │ │ Server  │ │ Server  │ │ Server  │
│ 4 CPU   │    →     │ 4 CPU   │ │ 4 CPU   │ │ 4 CPU   │ │ 4 CPU   │
└─────────┘          └─────────┘ └─────────┘ └─────────┘ └─────────┘
처리량: 100/s        처리량: 400/s (이론상)

예제: 수평 확장을 위한 Stateless 설계

import java.util.concurrent.*;
import java.util.*;

public class HorizontalScalingExample {

    // Stateless 작업 처리기
    static class StatelessWorker {
        private final String workerId;

        public StatelessWorker(String workerId) {
            this.workerId = workerId;
        }

        public int processTask(int input) {
            System.out.println(workerId + ": 작업 처리 중 - 입력값: " + input);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return input * 2;
        }
    }

    // 간단한 로드 밸런서
    static class LoadBalancer {
        private final List<StatelessWorker> workers;
        private int currentIndex = 0;

        public LoadBalancer(List<StatelessWorker> workers) {
            this.workers = workers;
        }

        public synchronized StatelessWorker getNextWorker() {
            StatelessWorker worker = workers.get(currentIndex);
            currentIndex = (currentIndex + 1) % workers.size();
            return worker;
        }
    }

    public static void main(String[] args) throws Exception {
        List<StatelessWorker> workers = List.of(
            new StatelessWorker("Worker-1"),
            new StatelessWorker("Worker-2"),
            new StatelessWorker("Worker-3")
        );

        LoadBalancer loadBalancer = new LoadBalancer(workers);
        ExecutorService executor = Executors.newFixedThreadPool(3);

        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 1; i <= 9; i++) {
            final int taskInput = i;
            final StatelessWorker worker = loadBalancer.getNextWorker();
            futures.add(executor.submit(() -> worker.processTask(taskInput)));
        }

        System.out.println("\n=== 결과 ===");
        for (int i = 0; i < futures.size(); i++) {
            System.out.println("작업 " + (i + 1) + " 결과: " + futures.get(i).get());
        }

        executor.shutdown();
    }
}

5. 복잡한 문제의 분해 전략

복잡한 문제를 서로 통신하는 여러 개의 더 단순한 구성 요소로 분해할 수 있다.

분해 패턴

[복잡한 문제]
┌────────────────────────────────────────────────┐
│     대용량 이미지 처리 파이프라인               │
└────────────────────────────────────────────────┘
                    ↓ 분해
┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ 이미지   │ → │ 필터    │ → │ 크기    │ → │ 저장    │
│ 읽기    │   │ 적용    │   │ 조정    │   │ 처리    │
└──────────┘   └──────────┘   └──────────┘   └──────────┘
   스레드1        스레드2        스레드3        스레드4

예제: Fork-Join을 활용한 분할 정복

import java.util.concurrent.*;

public class ForkJoinDecomposition extends RecursiveTask<Long> {

    private final long[] array;
    private final int start;
    private final int end;
    private static final int THRESHOLD = 10000;

    public ForkJoinDecomposition(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;

        if (length <= THRESHOLD) {
            return computeDirectly();
        }

        int mid = start + length / 2;

        ForkJoinDecomposition leftTask = new ForkJoinDecomposition(array, start, mid);
        ForkJoinDecomposition rightTask = new ForkJoinDecomposition(array, mid, end);

        leftTask.fork();
        Long rightResult = rightTask.compute();
        Long leftResult = leftTask.join();

        return leftResult + rightResult;
    }

    private long computeDirectly() {
        long sum = 0;
        for (int i = start; i < end; i++) {
            sum += array[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        int size = 100_000_000;
        long[] numbers = new long[size];
        for (int i = 0; i < size; i++) {
            numbers[i] = i + 1;
        }

        // 순차 처리
        long start = System.currentTimeMillis();
        long sequentialSum = 0;
        for (long num : numbers) {
            sequentialSum += num;
        }
        System.out.println("순차 처리:");
        System.out.println("  합계: " + sequentialSum);
        System.out.println("  시간: " + (System.currentTimeMillis() - start) + "ms\n");

        // Fork-Join 병렬 처리
        ForkJoinPool pool = ForkJoinPool.commonPool();
        start = System.currentTimeMillis();

        ForkJoinDecomposition task = new ForkJoinDecomposition(numbers, 0, numbers.length);
        long parallelSum = pool.invoke(task);

        System.out.println("Fork-Join 병렬 처리:");
        System.out.println("  합계: " + parallelSum);
        System.out.println("  시간: " + (System.currentTimeMillis() - start) + "ms");
        System.out.println("  사용된 스레드: " + pool.getParallelism() + "개");
    }
}

6. 정리: 동시성의 핵심 개념

개념설명장점
동시 처리여러 작업을 동시에 처리전체 처리 시간 단축
지연 시간 숨기기I/O 대기 중 다른 작업 수행응답성 향상
처리율 개선단위 시간당 처리량 증가더 많은 요청 처리 가능
수평 확장여러 서버에 부하 분산무한한 확장 가능성
문제 분해복잡한 문제를 작은 단위로 분할유지보수성, 확장성 향상

동시성 프로그래밍의 도전 과제

┌─────────────────────────────────────────────────────────────┐
│                    동시성의 어려움                           │
├─────────────────────────────────────────────────────────────┤
│ 1. 경쟁 조건 (Race Condition)                               │
│    - 여러 스레드가 동시에 같은 자원에 접근                   │
│                                                             │
│ 2. 교착 상태 (Deadlock)                                     │
│    - 스레드들이 서로의 자원을 기다리며 무한 대기             │
│                                                             │
│ 3. 기아 상태 (Starvation)                                   │
│    - 특정 스레드가 영원히 자원을 얻지 못함                   │
│                                                             │
│ 4. 디버깅의 어려움                                          │
│    - 비결정적 실행으로 버그 재현이 어려움                    │
└─────────────────────────────────────────────────────────────┘

이러한 도전 과제들은 이후 챕터에서 자세히 다룰 예정이다.


참고 자료

  • “Grokking Concurrency” by Kirill Bobrov
  • Java Concurrency in Practice
  • Oracle Java Documentation - Concurrency