5 min read

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

스레드 안전한 클래스 설계

객체가 가진 여러가지의 정보들을 객체 내부에 캡슐화 시켜두면 다른 클래스를 분석할 필요없이 객체 단위로 스레드 안전성을 확인해볼 수 있다.

만약 하나의 객체가 하나의 원시 타입의 변수로만 구성된다면 해당 객체의 상태는 하나의 변수만 보면 완벽하게 파악가능하다. 하지만 하나의 객체 내에 여러개의 객체가 존재한다면 해당 객체의 상태를 보기 위해서는 내부에 존재하는 여러 참조 객체들의 필드들까지 모두 고려해야한다. 예를 들어 List<T> 타입에 여러 객체가 담길 경우 리스트의 상태를 확인하려면 내부의 모든 요소들의 필드들을 고려해야한다.

특정 객체의 동기화의 여부를 결정하려면 해당 객체의 내부의 값이 안정적인지 살펴보아야 한다. 예를 들어 final 키워드를 통해 특정 필드의 상태 범위를 한정하게 되면 이러한 안정성을 파악하는데 도움이 된다. 보통 클래스 내부에서 한 개만의 상태 변수를 사용하는 일은 거의 없을 것이다. 즉, 여러개의 상태 변수들이 변경될 때의 안정성을 고려해야한다. 예를 들어 최고 점수와 최저 점수를 가지는 변수가 내부에 있다. 이 경우, 최저 점수는 항상 최고 점수보다 낮아야한다는 조건을 가진다. 즉, 해당 변수들이 변경될 때 클래스가 올바른 상태를 가지려면 원자적으로 읽어지거나 변경되어야 한다. 상태 범위에 두 개 이상의 변수가 연결되어 동시에 관여한다면 이런 변수들을 사용하는 부분에서는 Lock을 통해 동기화 처리를 해줘야한다.

객체와 변수를 보면 항상 객체와 변수가 가질 수 있는 가능한 값의 범위를 고려한다. 이러한 범위를 상태 범위라고 한다. 상태 범위가 좁다는 것은 앞서 말한 것처럼 객체의 안정성을 파악하고 논리적인 상태를 파악하기 쉽다.

객체의 상태 범위는 구현에 따라 여러가지 제약 조건이 존재할 수 있다. 이러한 제약 조건에 따라 동기화 또는 캡슐화를 사용해야 한다. 예를 들어, 클래스가 특정 상태를 가질 수 없도록 설계하면 해당 변수는 캡슐화를 통해 내부에 숨겨두어야 한다.

인스턴스 한정

객체를 적절하게 캡슐화하여 스레드 안정성을 확보하는 경우를 인스턴스 한정이라고 한다. 캡슐화를 할 경우 객체 내부의 필드들은 해당 객체의 메소드에서만 사용 가능하다. 그래서 락을 사용하더라도 명확하게 적용범위를 정해줄 수 있다. 아래 코드가 그 예시다. 클래스의 내부 변수인 mySet은 캡슐화 되어 접근하려면 addPerson, containsPerson을 호출하는 방법 뿐이다. 그리고 이 두가지 메서드는 모두 묵시적 락을 통해 동기화가 걸려있어 스레드 안전성을 확보한다.

@ThreadSafe
public class PersonSet {
    @GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();

    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }

    public synchronized boolean containsPerson(Person p) {
        return mySet.contains(p);
    }

    interface Person {
    }
}

기본적인 컬렉션 클래스인 ArrayList나 HashMap 같은 클래스는 스레드에 안전하지 않지만 Collections.syncrhonizedList와 같은 팩토리 메소드를 사용해 스레드 안전성을 확보할 수 있다. 이러한 팩토리 메소드는 데코레이터 패턴을 활용하며 그 결과로 만들어지 Wrapper 클래스를 생성한다. 이러한 래퍼 클래스는 기본 클래스의 메소드를 호출하는 연동 역할만 하면서 그와 동시에 모든 메서드가 동기화 시켜준다.

인스턴스 한정 기법은 결국 자바의 모니터 패턴과 동일하다. 아래 코드는 모니터 패턴을 따르는 전형적인 예시이다. Counter 클래스는 value 변수를 클래스 내부에 숨기고 value를 사용하는 모든 메소드는 동기화시킨다. 이러한 패턴은 자바에 내장된 여러 컬렉션 자료구조에도 사용된다. 락의 경우에도 암묵적인 락인 synchronized 말고도 Lock 객체를 생성해서 이를 통해 사용해도 모니터 패턴을 구현할 수 있다.

@ThreadSafe
public final class Counter {
    @GuardedBy("this") private long value = 0;

    public synchronized long getValue() {
        return value;
    }

    public synchronized long increment() {
        if (value == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
        return ++value;
    }
}