선생님, 개발을 잘하고 싶어요.

자바 병렬 프로그래밍 - 1부 4장 - 객체 구성 본문

일상/책 리뷰

자바 병렬 프로그래밍 - 1부 4장 - 객체 구성

알고싶은 승민 2022. 6. 11. 12:02
  • 스레드 안전성 확보한 컴포넌트를 안전하게 서로 연결해 사용한다면 규모 있는 컴포넌트나 프로그램을 좀 쉽게 작성할 수 있을 것

스레드 안전한 클래스 설계

  • 🌟 스레드 안전성 확보할 때 고려사항
    • 객체의 상태를 보관하는 변수는?
    • 그 변수가 가질 수 있는 값의 범위는?
    • 객체 내부 값을 동시에 사용할 때 그 과정을 관리할 정책은?
  • 객체 상태는 내부 변수에 의해서 결정된다.
  • 동기화 정책 👉 값이 계속 변하는 상황에서도 값을 안전하게 사용하도록 조절하는 방법
    • 객체 불변성, 스레드 한정, 락 등 어떻게 사용해야 스레드 안전성을 확보할 수 있는지 명세
    • 어떤 변수를 어떤 락으로 막아야 하는지 명세 (e.g. @GuardedBy)

동기화 요구사항 정리

  • 스레드 안전성을 확보했다? 👉 여러 스레드가 동시에 클래스를 사용하는 상황에서, 클래스 내부의 값을 안정적인 상태로 유지할 수 있다.
  • 상태 범위(state space) 👉 객체와 변수가 가질 수 있는 가능한 값의 범위
    • 변경 불가한 val을 활용하면 상태 범위를 크게 줄여서 이해하기 쉽게 만든다.
  • 다중 연산일 수밖에 없는 거, 즉, 단일 연산으로 만들기 위해 동기화 필요한 경우
    • 현재 값을 기반으로 다음 상태를 바꾸는 경우
    • 여러 변수를 통해 클래스 상태가 올바른지 아닌지 정의하는 경우
  • 🌟 특정 연산을 실행했을 때 올바르지 않은 상태 값을 가질 수 있다면, 반드시 단일 연산으로 구현해야한다.

상태 의존 연산 (state-dependent)

  • 상태를 기반으로 선행 조건을 가지는 연산
  • 현재 조건에 따라 동작 여부가 결정되는 연산
  • 이런 문제를 해결하기 위해서
    • 상태가 올바르게 바뀔 경우 기다리다가 실제 연산을 수행할 수 있다. (wait, notify)
    • 이미 구현된 라이브러리 사용하는 편이 간단하고 안전하다 (세마포어, 블로킹 큐)

상태 소유권

  • 객체의 상태를 정의하고자 할 때, 객체가 실제로 소유하는 데이터만 기준으로 잡아야한다.
  • 소유권?
    • 가비지 컬렉션 기능을 고려한다면 객체 소유권 개념을 생각하기가 어렵다.
    • C++은 특정 메소드에 객체 인스턴스를 넘겨줄 때 (내가 아직 정확하게 알지는 못하는 개념이다.)
      • 객체와 함께 객체 소유권도 함께 넘기는지? (메모리 해체 책임이 거기?)
      • 잠시만 사용하도록 빌려주는 형식인지? (메모리 해체 책임은 여기?)
      • 메소드 인자로 넘기지만 계속해서 함께 사용할지? (새로운 객체 전달? 새로운 메모리 할당?)
  • 특정 변수에 대한 소유권을 갖고 있으면, 그 변수의 상태가 올바르게 유지되도록 조절하는 락 구조에 대한 소유권도 가진다.
  • 소유권 분리 형태를 사용하는 경우도 있다. 컬렉션 내부 구조에 대한 소유권은 컬렉션 클래스에, 컬렉션에 추가되어 있는 개체의 소유권은 클라이언트 쪽에
    • 이 경우 클라이언트는 컬렉션에 들어있는 객체를 사용할 때 동기화 작업을 수행해야한다.

인스턴스 한정 (aka. 한정 기법, feat. 자바 모니터 패턴)

  • 인스턴스 한정 👉 객체를 적절하게 캡슐화 해서 스레드 안전성을 확보하는 방법
  • 한정 기법과 락을 적절하게 활용하면 스레드 안전성이 검증되지 않은 객체도 마음 놓고 안전하게 사용 가능
  • 🌟 데이터가 캡슐화되어 있다면, 해당 데이터에 지정된 락이 적용되는지 쉽고 정확하게 파악 가능하다.
  • 다양한 레벨의 한정
    • 클래스 인스턴스에 한정 (private 으로 지정된 변수)
    • 블록 내부에 한정 (블록 내부 로컬 변수)
    • 특정 스레드 한정 (다른 스레드로는 넘겨주지 않는 객체 e.g. ThreadLocal)
  • 한정된 객체는 제한된 범위를 벗어나선 안된다. 이에 개발자가 충분한 주의를 기울여야 한다.
  • 인스턴스 한정 기법은 스레드 안전성을 확보할 수 있는 가장 쉬운 방법, 동기화 적용 방법도 마음대로 선택 가능 (여러 데이터를 여러 락을 사용해 따로 동기화 하는 등)
  • 스레드 안전성을 확보하는 방법으로 데코레이터 패턴 활용 가능 이러한 래퍼 클래스가 스레드 안전성을 확보하게 된다.

자바 모니터 패턴

  • 자바 모니터 패턴 👉 변경가능한 데이터를 객체 내부에 숨기고 락으로 데이터에 대한 동시 접근을 막는 방법
  • 클래스를 바닥부터 새로 만들거나 이미 만들어져 있지만 스레드 안전성이 없는 개체를 조합해 만들때 유용하다.

스레드 안전성 위임 (aka. 위임 기법)

  • 이미 스레드 안전한 클래스를 조합해서 사용한다고 해도 스레드에 안전하지 않을 수 있다. 주의할 것
  • 케이스 분석
    • 스레드 안전한 상태 하나만 관리할 때 👉 해당 상태 객체에 스레드 안전을 위임하며 별다른 처리 없이도 클래스가 안전하다.
    • 스레드 안전한 상태를 둘 이상 관리하는데 이 둘이 독립적일 때 👉 각 객체에 스레드 안전을 위임하여 별다른 처리 없이도 클래스 안전하다.
    • 🌟 스레드 안전한 상태가 둘 이상이지만 이 둘이 서로 상관관계가 있을 때 👉 명시적 동기화를 통해서 두 변수 사이의 상태를 조율해야한다. 전체적으로는 스레드 안전성을 잃을 수 있다.

내부 상태 변수를 외부에 공개

  • 공개하려는 상태에 따라 다르다.
  • 🌟 상태 변수가 스레드 안전하고, 클래스 내부에서 해당 값에 대한 의존성이 없고, 상태 변수에 대한 어떤 연산이 잘못된 상태를 야기할 수 없다면… 외부에 공개해도 괜찮다.

스레드 안전하게 구현된 클래스에 기능 추가

  • 필요한 기능을 구현해 추가하면서 스레드 안전성도 계속해서 유지하는 방법을 찾아야 한다.

클라이언트측 동기화

  • List에 값이 없다면 추가하는 putIfAbsent()를 설계하는 시나리오에서 ….
    • 리스트가 스레드 안전하다고 하더라도 두개의 연산 (리스트에 있나? 와 리스트에 값을 추가)을 조합 사용해야 하기 때문에 이를 단일 연산으로 만들어야한다.
    • 단순 상속으로 확장한다면? 하위 클래스에서 상위 클래스에 적용된 락을 필요한 부분에 적용하지 못할 수 있기 때문에 부주의하게 동기화가 깨질 수 있다.
    • Helper 클래스를 설계해서 리스트를 소유하고 추가 기능을 만들 수 있을 텐데 적절한 락을 사용하는지 주의해야한다.
    public class ListHelper<E> {
      public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    
      // 주의 !!! 의미 없는 락 list 에 접근을 막는 락이 아니다.
      // 따라서 여기서 락을 얻어도, 다른 스레드에서는 여전히 list에 값을 추가할 수 있다.
      // 즉 이 함수는 list에 대한 단일 연산이 아니다!!!
      public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contain(x);
        if (absent) list.add(x);
        return absent;
      }
    
      // List가 내부적으로 사용하는 락을 사용해야한다.
      public boolean putIfAbsent(E x) {
        // 주의 !!! List 클래스의 명세에 맞춰서 적절한 락을 선택해야한다.
        // 해당 클래스가 지원하는 락 리스트가 없다면... 불가능하거나 위험한 방법이다.
        synchronized (list) {
          ...
        }
      }
    }
    

클래스 재구성 (composition)

  • 자바 모니터 패턴 처럼 동기화를 래핑해서 활용하는 방법으로 성능상 부정적일 수 있지만 그 영향을 엄밀히 따저야한다. (구현에 따라 실제로는 크지 않을지도?)

동기화 정책 문서화하기

  • 문서를 남기는 건 스레드 안전성을 관리하는 가장 강력한 방법
  • 안전성을 해치지 않도록 동기화 전략을 파악할 때 동기화 관련 개발 문서를 가장 먼저 참조해야한다.
  • 동기화 정책을 정의하는게 중요하다. (synchronized, volatile 혹은 여러 동기화 관련 클래스 사용 등)
  • 설계 단계에서 스레드 안전성도 함께 다루자.
    • 어떤 변수를 어떤 락으로 막을지
    • 어떤 변수를 불변 클래스로 만들지
    • 어떤 변수를 어떤 스레드에 한정시킬지
    • 어떤 연산을 단일 연산으로 만들어야 할 지
  • 이러한 동기화 기법은 외부에도 영향을 미치기 때문에 스레드 안전성을 어디까지 보장하는지 문서로 남겨야한다.
  • 아주 작은 기법이라도 반드시 적어둘 것
Comments