Chapter 04. 객체 구성
기본적으로 어떤 프로그램의 스레드 안전성을 확보하고 있는지 확인하는 것은 굉장히 어려운 일이다. 대신 스레드 안전성을 확보한 개별 컴포넌트를 가져다가 안전한 방법을 동원해 서로 연결해 사용한다면 규모있는 컴포넌트나 프로그램을 좀더 쉽게 작성할 수 있다.
4장에서는 컴포넌트의 스레드 안전성을 안정적으로 확보할 수 있고, 이와 함께 개발자가 코드를 작성하는 과정에서 실수한다 해도 스레드 안전성을 해치지 않도록 도와주는 클래스 구성 방법을 살펴보자.
4.1 스레드 안전한 클래스 설계
구조적인 캡슐화 없이 만들어 낸 결과물을 여러 스레드에서 사용해도 안전한지를 확인하기도 어려울 뿐더러 해당 객체를 나중에야 변경할 필요가 있을 때에도 스레드 공기화 문제 없이 변경하기란 더더욱 어려운 일이다.
클래스가 스레드 안전성을 확보하도록 설계하고자 할 때에는 다음과 같은 세 가지를 고려해야 한다.
객체의 상태를 보관하는 변수가 어떤 것인가?
객체의 상태를 보관하는 변수가 가질 수 있는 값이 어떤 종류, 어떤 범위에 해당하는가?
객체 내부의 값을 동시에 사용하고자 할 때, 그 과정을 관리할 수 있는 정책
객체의 상태는 항상 객체 내부의 변수를 기반으로 한다. 객체 내부의 변수가 모두 기본 변수형으로 만들어져 있다면 해당 변수만으로 객체의 상태를 완전하게 표현할 수 있다.
@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;
}
}
Counter
클래스는Value
라는 단 하나의 변수를 갖고 있으며, 따라서Counter
클래스의 상태는value
변수만 보면 완벽하게 알 수 있다.- 예를 들어,
LinkedList
객체의 상태는 추가되어 있는 모든 객체의 상태를 포함하는 범위에 해당한다.
객체 내부의 여러 변수가 갖고 있는 현재 상태를 사용하고자 할 때 값이 계속해서 변하는 상황에서도 값을 안전하게 사용할 수 있도록 조절하는 방법을 동기화 정책이라고 한다. 동기화 정책에는 객체의 불변성, 스레드 한정, 락 등을 어떻게 적절하게 활용해 스레드 안전성을 확보할 수 있으며 어떤 변수를 어떤 락으로 막아야 하는지 등의 내용을 명시한다. 클래스를 유지보수하기 좋게 관리하려면 해당 객체에 대한 동기화 정책을 항상 문서로 작성해둬야 한다.
4.1.1 동기화 요구사항 정리
- 정의 : 여러 스레드가 동시에 클래스를 사용하려 하는 상황에서 클래스 내부의 값을 안정적인 상태로 유지할 수 있다면 바로 스레드 안전성을 확보했다고 할 수 있다.
- 객체와 변수를 놓고 보면 항상 객체와 변수가 가질 수 있는 가능한 값의 범위를 생각할 수 있는데, 이런 값의 범위를
상태 범위 state space
라고 한다. - 상태 범위가 좁으면 좁을수록 객체의 논리적인 상태를 파악하기가 쉽다.
- 부분 부분마다
final
을 지정해두면 상태 범위를 크게 줄여주기 때문에 생각해야 할 논리의 범위를 줄일 수 있다. ( 가장 확실한 예로 불변 객체를 들 수 있다. 그 값이 변하지 않기 때문에 상태 범위에 단 하나의 값만 들어간다. )
- 객체와 변수를 놓고 보면 항상 객체와 변수가 가질 수 있는 가능한 값의 범위를 생각할 수 있는데, 이런 값의 범위를
- 대부분의 클래스에는 특정 상태가 올바른 상태인지 올바르지 확인할 수 있는 마지노선이 있다.
- 앞에서 봤던
counter
클래스의value
변수는long
타입으로 선언되어 있었다.long
으로 지정된 변수는 항상 가장 작은Long.MIN_VALUE
부터 가장 큰Long.MAX_VALUE
사이의 값을 가질 수 있다. 더군다나Counter
클래스는 동기화된 메소드를 사용해value
변수에 음수 값이 지정될 수는 없다.
- 앞에서 봤던
- 클래스 내부의 상태나 상태 변화와 관련해 여러 가지 제약 조건이 있을 수 있는데, 이런 제약 조건에 따라 또 다른 동기화 기벗이나 캡슐화 방법을 사용해야 할 수도 있다.
- 클래스가 특정 상태를 가질 수 없도록 구현해야 한다면, 해당 변수는 클래스 내부에 숨겨둬야만 한다.
- 변수를 숨겨두지 않으면 외부에서 클래스가
올바르지 않다
고 정의한 값을 지정할 수 있기 때문이다. 그리고 특정한 연산을 실행했을 때 올바르지 않은 상태 값을 가질 가능성이 있다면 해당 연산은 단일 연산으로 구현해야 한다. - 반대로 클래스에서 변수의 값에 별다른 제약 조건을 두지 않는다면 클래스의 유연성과 실행 성능을 높인다는 측면에서 이와 같은 동기화 방법이나 캡슐화 기법을 사용하지 않아도 되겠다.
뭔소린지 모르겠다… 객체 상태를 가지는 변수의 최소값과 최대값을 정확하게 인식하고 사용하라는 것 같다.
객체가 가질 수 있는 값의 범위와 변동 폭을 정확하게 인식하지 못한다면, 스레드 안전성을 완벽하게 확보할 수 없다. 클래스의 상태가 정상적이라는 여러 가지 제약 조건이 있을때 클래스의 상태를 정상적으로 유지하려면 여러 가지 추가적인 동기화 기법ㅂ을 적용하거나 상태 변수를 클래스 내부에 적절히 숨겨야 한다.
4.1.2 상태 의존 연산
클래스가 가질 수 있는 값의 범위와 값이 변화하는 여러 가지 조건을 살펴보면 어떤 상황이라야 클래스가 정상적인지를 정의할 수 있다. 특정 객체는 상태를 기반으로 하는 선행 조건 precondition
을 갖기도 한다. 예를 들어 현재 아무것도 들어있지 않은 큐에서는 값을 뽑아낼 수가 없다. 당연한 말이지만 큐에 뭔가 값이 들어 있어야 값을 뽑아낼 수 있기 때문이다. 당연한 말이지만 큐에 뭔가 값이 들어 있어야 값을 뽑아낼 수 있기 떄문이다. 현재 조건에 따라 동작 여부가 결정되는 연산을 상태 의존 state-dependent
연산이라고 한다.
출처 :브라이언 괴츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬 공저, 『 멀티코어를 100% 활용하는 자바 병렬 프로그래밍』, 에이콘(2008.7.30), 4장 인용.