콘텐츠로 이동

동시성 컬렉션

동시성 컬렉션

동시성 컬렉션: 멀티스레드 환경에서 여러 스레드가 동시에 접근하더라도 데이터의 일관성과 안정성이 보장되는 컬렉션. 내부적으로 동기화 메커니즘을 포함하여 개발자가 직접 synchronizedLock을 걸 필요가 없다.

Thread safe

  • 여러 스레드가 동시에 접근하더라도 데이터의 일관성이 깨지지 않고 항상 의도한 결과가 보장되는 상태

핵심 요소

요소 설명
원자성 (Atomicity) 연산이 중간에 끊기지 않음
가시성 (Visibility) 한 스레드의 변경이 다른 스레드에 보임
순서 보장 (Ordering) 실행 순서가 예측 가능

Proxy

  • 우리말로 대리자, 대신 처리해주는 자라는 뜻이다.
  • 실제 객체(Real Subject)를 직접 호출하지 않고, 중간에서 대신 처리하거나 부가 기능을 제공

목적

  • 접근 제어
  • 로깅
  • 트랜잭션 처리
  • 동기화 처리

정적 의존 관계 VS 런타임 의존 관계

구분 정적 의존 관계 (Compile-Time) 런타임 의존 관계 (Runtime)
의존성 결정 시점 컴파일 시점 실행 시점
의존 대상 구체 클래스 인터페이스 / 추상 타입
코드 예시 new ArrayList() new ArrayList()를 감싼 Proxy
유연성 낮음 높음
확장성 변경에 취약 변경에 강함
테스트 용이성 낮음 (Mock 어려움) 높음 (Mock 주입 가능)
OCP(Open–Closed) 위반 가능성 높음 준수하기 쉬움
대표 사례 직접 객체 생성 Proxy, DI, AOP

Proxy Pattern

  • 실제 객체를 감싸는 대리 객체를 두고 클라이언트는 실제 객체가 아닌 Proxy를 통해 접근
    Client → Proxy → Real Object
    

예시 코드

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Bank bank = new ProxyBank(new DefaultBank());
        Runnable job = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1_000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                bank.deposit(1000);
                bank.withdraw(1500);
            }
        };
        Thread thread1 = new Thread(job, "Thread1");
        Thread thread2 = new Thread(job, "Thread2");
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        bank.printMoney();
    }

    private interface Bank {
        public void deposit(int money);
        public void withdraw(int money);
        public void printMoney();
    }

    private static class DefaultBank implements Bank {

        private int money;

        @Override
        public void deposit(int money) {
            System.out.println("[입금] " + money + "원");
            this.money += money;
            System.out.println("[현재잔액] " + this.money + "원");
        }

        @Override
        public void withdraw(int money) {
            if (this.money - money < 0) {
                System.out.println("[출금실패] 잔액: " + this.money + "원, 출금액: " + money + "원 [" + (money - this.money) + "원 부족]");
                return;
            }
            System.out.println("[출금] " + money + "원");
            this.money -= money;
            System.out.println("[현재잔액] " + this.money);
        }

        @Override
        public void printMoney() {
            System.out.println("[잔액] " + money + "원");
        }

    }

    private static class ProxyBank implements Bank {

        private final Bank bank;

        ProxyBank(Bank bank) {
            this.bank = bank;
        }

        @Override
        public synchronized void deposit(int money) {
            bank.deposit(money);
        }

        @Override
        public synchronized void withdraw(int money) {
            bank.withdraw(money);
        }

        @Override
        public synchronized void printMoney() {
            bank.printMoney();
        }

    }

}

주요 목적

  1. 기능 확장
  2. 기존 코드 수정 없이 부가 기능 추가 (OCP)
  3. 관심사 분리
  4. 비즈니스 로직과 부가 기능 분리
  5. 제어 기능 제공
  6. 접근 제어, 동기화, 캐싱, 지연 로딩

Synchronized Proxy 단점

  1. 성능 저하
  2. 모든 메서드가 하나의 락을 공유
  3. 경쟁 심화 시 병목 발생
  4. 확장성 부족
  5. 스레드 수 증가에 비례해 처리량 감소
  6. 불필요한 동기화
  7. 읽기 작업도 락을 획득해야 함

동시성 컬렉션 종류

분류 컬렉션 특징 사용 시점
List CopyOnWriteArrayList 읽기 시 락 없음, 쓰기 시 전체 복사 읽기 ≫ 쓰기 환경
Set CopyOnWriteArraySet 중복 제거, 내부적으로 CopyOnWriteArrayList 사용 데이터 수 적고 읽기 위주
Set ConcurrentSkipListSet 정렬 보장, O(log n) 정렬 + 동시성 필요 시
Map ConcurrentHashMap 락 분할 + CAS, 고성능 일반적인 멀티스레드 Map
Map ConcurrentSkipListMap 정렬 보장, 범위 검색 가능 정렬된 Map 필요 시
Queue ConcurrentLinkedQueue Lock-Free, FIFO 높은 처리량 필요 시
Queue ConcurrentLinkedDeque Lock-Free, 양방향 Deque 앞/뒤 삽입·삭제 빈번

스레드를 차단하는 블로킹 큐 Blocking Queue

  • 큐가 비어있거나 가득 차면 스레드를 자동으로 차단(Block) 시키는 큐
  • “생산자–소비자 패턴”을 쉽게 구현하기 위해 만들어진 스레드 안전 큐입니다.

특징

  • 큐가 비어 있으면 소비자 스레드는 기다려야 함
  • 큐가 가득 차면 생산자 스레드는 기다려야 함
  • 여기서 ‘차단(block)’이란, 스레드를 CPU에서 제거하고, 조건이 충족될 때까지 실행을 멈추는 것을 의미합니다.
  • 실제 대기 상태는 WAITING 또는 TIMED_WAITING

종류

컬렉션 내부 구조 크기 제한 정렬/우선순위 주요 특징 사용 시점
ArrayBlockingQueue 배열 기반 고정 없음 (FIFO) 단일 락 사용, 예측 가능한 성능 큐 크기를 엄격히 제한해야 할 때
LinkedBlockingQueue 링크 기반 고정 또는 무제한 없음 (FIFO) put/take 분리 락 사용 처리량이 크고 안정성 필요 시
LinkedBlockingDeque 링크 기반 고정 또는 무제한 없음 양방향 삽입·삭제 가능 앞/뒤 작업이 모두 필요한 경우
PriorityBlockingQueue 힙 기반 무제한 있음 우선순위에 따라 처리 작업 중요도 기반 처리
DelayQueue 힙 기반 무제한 지연 시간 기반 지정 시간 이후에만 꺼냄 스케줄링, 캐시 만료 처리