Chapter 4 객체 구성 이어서…
차량 위치 추적
Map<String, Point> location = vehicles.getLocation();
for (String key : locations.keySet())
renderVehicle(key, locations.get(key));
이와 유사하게 업데이터 스레드는 차량에 장착된 GPS 장치에서 읽어낸 위치 정보를 자동으로 입력하거나, GUI 화면에서 수동으로 입력한 내용을 새로운 위치 정보로 업데이트한다.
void vehicleMoved(VehicleMovedEvent evt) {
Point loc = evt.getNewLocation();
vehicles.setLocation(evt.getVehicleId(), loc.x, loc.y);
}
이런 구조라면 뷰 스레드와 업데이터 스레드가 동시 다발적으로 데이터 모델을 사용하기 때문에 데이터 모델에 해당하는 클래스는 반드시 스레드 안전성을 확보하고 있어야만 한다.
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this") private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(String id, int x, int y) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
- 위 클래스는 차량 위치를 담는 MutablePoint 클래스를 활용해 자바 모니터 패턴에 맞춰 만들어진 차량 추적 클래스가 나타나 있다.
@NotThreadSafe
public class MutablePoint {
public int x, y;
public MutablePoint() {
x = 0;
y = 0;
}
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}
- 위 클래스 MutablePoint는 클래스가 스레드 안전하지 않지만 차량 추적 클래스는 스레드 안전성을 확보하고 있다. 위치를 담는 location 변수나 위치를 나타내는 Point 인스턴스 모두 외부에 전혀 공개되고 있지 않았다.
- 차량의 위치를 알고싶어하는 클라이언트 프로그램에게 위치를 넘겨줄 때는 MutablePoint 클래스의 생성자를 통해 MutablePoint의 복사본을 만들거나 deepCopy 메소드를 사용해 완전히 새로운 locations 인스턴스 복사본을 만들어 넘겨준다.
- deepCopy는 원래 Map 인스턴스에 Map 인스턴스 뿐만 아니라 그 안에 들어 있는 키와 값도 모두 복사해 완전히 새로운 인스턴스를 만든다.
- 단순하게 Map 인스턴스를 Collection 클래스의 unmodifiableMap 메소드로 감싸는 것으로는 deepCopy의 기능을 다 하지 못하는데, UnmodificableMap은 컬렉션 자체만 변경할 수 없게 막아주며 그 안에 보관하고 있는 객체의 내용을 손대는 것은 막지 못하기 때문이다. HashMap의 생성자에 HashMap을 넘겨 복사하는 기능도 앞서 설명한 것과 동일하게, 즉 위치를 나타내는 Point 값을 복사하는 것이 아니라 point를 가리키는 참조를 복사하기 때문에 올바른 결과를 얻을 수 없다.
- 외부에서 변경 가능한 데이터를 요청할 경우 그에 대한 복사본을 넘겨주는 방법을 사용하면 스레드 안전성을 부분적이나마 확보할 수 있다. 일반적인 경우라면 그다지 문제가 되지 않지만, 차량 추적 예제의 경우 추적하는 차량의 대수가 굉장히 많아진다면 성능에 문제가 발생할 수 있다.
4.3 스레드 안전성 위임
- 스레드 안전성을 확보한 객체를 조합해 만든 객체는 스레드에 안전한 객체일까?
상황에 따라 다르다.
가 정답이다. 스레드에 안전한 객체를 모아 만든 객체는 스레드에 안전할 수도 있고 조금더 나은 출발점일 수 있다.- 앞서 살펴 봤던 CountingFactorizer 클래스의 상태는 스레드 안전한 AtomicLong 클래스의 상태와 같다.
- CountingFactorizer 클래스를 표현하는 상태값은 AtomicLong 하나이기 때문이다. 또 AtomicLong에 보관하는 카운트 값에 아무런 제한 조건이 없기 때문이다.
- 이런 경우 CountingFactorizer는 스레드 안전성 문제를 AtomicLong 클래스에게
위임 delegate
한다고 하며, AtomicLong 클래스가 스레드에 안전하기 때문에 AtomicLong에게 스레드 안전성을 위임했던 Counting Factorizer 역시 스레드에 안전하다.
4.3.1 예제: 위임 기법을 활용한 차량 추적
- 먼저 Point 클래스를
immutable
하게 만들자.
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
- 값을 변경할 수 없는 Point 객체, Point 클래스는 불변이기 떄문에 스레드 안전하다. 불변의 값은 얼마든지 마음대로 안전하게 공유하고 외부에 공개할 수 있다.
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<String, Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null)
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
// Alternate version of getLocations (Listing 4.8)
public Map<String, Point> getLocationsAsStatic() {
return Collections.unmodifiableMap(
new HashMap<String, Point>(locations));
}
}
- 스레드 안전성을 ConcurrentHashMap 클래스에 위임한 추적 프로그램
- 위 DelegatingVehicleTracker 예제에서는 별다른 동기화 흔적이 없다.
- 모든 동기화 작업은 ConcurrentHashMap에서 담당하고, Map에 들어 있는 모든 값은 불변 상태이다.
- 앞서 소개했던 모니터 적용 버전은 현재 시점의 차량 위치 전부의 고정된 스냅샷을 알려줬다.
- 위임 기능을 적용한 버전은 언제든지 가장 최신의 차량 위치를 실시간으로 학인할 수 있는 동적인 데이터를 넘겨준다.
- 특정 시점의 차량 고정 위치 데이터를 갖고 싶다면 getLocations 메소드에서 locations 변수에 들어있는 Map 클래스에 대한 단순 복사본을 넘겨줄 수 있다.
public Map<String, Point> getLocations() {
return Collections.unmodifiableMap(new HashMap<String, Point>(locations));
}
4.3.2 독립 상태 변수
- 지금까지 모두 스레드 안전한 변수 하나에만 스레드 안전성을 위임했다.
- 위임하고자 하는 내부 변수가 두개 이상이라 해도 두 개 이상의 변수가 서로
독립적
이라면 클래스의 스레드 안전성을 위임할 수 있다. 독립적
이라 함은 변수가 서로의 상태 값에 대한 연관성이 없다는 말이다.
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}
}
- VisualComponent 클래스는 클라이언트가 마우스와 키보드 이벤트를 처리하는 리스너를 등록할 수 있는 화면 컴포넌트이다. VisualComponent는 각 종류별로 등록된 이벤트 리스너를 호출하는 기능을 갖고 있다. 내부적으로 보면 마우스 이벤트 리스너를 관리하는 목록 변수와 키보드 이벤트를 관리하는 목록 변수는 서로 아무런 연관이 없으므로 서로 독립적이다.
- 리스너 이벤트 목록을
CopyOnWriteArrayList
클래스에 보관한다. CopyOnWriteArrayList는 리스너 목록을 관리하기에 적당하게 만들어져 있는 스레드 안전한 List 클래스이다. - 위 두가지 리스트는 각각 스레드 안전성을 확보하고 있고 두 개의 변수를 서로 연동시켜 묶어주는 상태가 전혀 없기 때문에 VisualComponent는 스레드 안전성이라는 책임을
mouseListeners
와keyListeners
변수에게 완전히 위임할 수 있다.
4.3.3 위임할 때의 문제점
물론 위 VisualCompoenet 처럼 간단하게 구성되어 있는 클래스는 거의 없다. 거의 모든 클래스에서 내부 변수들이 상태 변수 간에 의존성을 가지고 있다.
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
클래스가 서로 의존성 없이 독립적이고 스레드 안전한 두 개 이상의 클래스를 조합해 만들어져 있고 두 개 이상의 클래스를 한번에 처리하는 복합 연산 메소드가 없는 상태라면, 스레드 안전성을 내부 변수에게 모두 위임할 수 있다.
4.3.4 내부 상태 변수를 외부에 공개
상태 변수가 스레드 안전하고, 클래스 내부에서 상태 변수의 값에 대한 의존성을 갖고 있지 않고, 상태 변수에 대한 어떤 연산을 수행하더라도 잘못된 상태에 이를 가능성이 없다면, 해당 변수는 외부에 공개해도 안전하다.
4.3.5 예제: 차량 추적 프로그램의 상태를 외부에 공개
@ThreadSafe
public class SafePoint {
@GuardedBy("this") private int x, y;
private SafePoint(int[] a) {
this(a[0], a[1]);
}
public SafePoint(SafePoint p) {
this(p.get());
}
public SafePoint(int x, int y) {
this.set(x, y);
}
public synchronized int[] get() {
return new int[]{x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}
- x, y의 set과 get을 따로둔다면 x,y를 갖고올때 x는 바뀐값을 y는 바뀌지 않은 값을 가져올 수 있다.
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
public SafePoint getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (!locations.containsKey(id))
throw new IllegalArgumentException("invalid vehicle name: " + id);
locations.get(id).set(x, y);
}
}
4.4 스레드 안전하게 구현된 클래스에 기능 추가
- 조심 또 조심.. 쥐도 새도 모르게 동기화가 깨질 수 있따. 잠온다..
4.4.1 호출하는 측의 동기화
@NotThreadSafe
class BadListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
@ThreadSafe
class GoodListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
- Vector 클래스와 Collections.synchronizedList 메소드에 대한 문서를 읽어보면 Vector 클래스 자체나 synchronizedList의 결과 List를 통해 클라이언트 측 동기화를 지원한다는 점을 알수 있다. 따라서 해당 list 변수로 락을 실행해 스레드 안전성을 확보했다.
4.4.2 클래스 재구성
- 기존 클래스에 새로운 단일 연산을 추가하고자 할 때 좀더 안전하게 사용할 수 있는 방법이 있는데, 바로
재구성 composition
이다.
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
/**
* PRE: list argument is thread-safe.
*/
public ImprovedList(List<T> list) { this.list = list; }
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public boolean contains(Object o) {
return list.contains(o);
}
public Iterator<T> iterator() {
return list.iterator();
}
public Object[] toArray() {
return list.toArray();
}
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
public synchronized boolean add(T e) {
return list.add(e);
}
public synchronized boolean remove(Object o) {
return list.remove(o);
}
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
public synchronized boolean addAll(Collection<? extends T> c) {
return list.addAll(c);
}
public synchronized boolean addAll(int index, Collection<? extends T> c) {
return list.addAll(index, c);
}
public synchronized boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
public synchronized boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
public boolean equals(Object o) {
return list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public T get(int index) {
return list.get(index);
}
public T set(int index, T element) {
return list.set(index, element);
}
public void add(int index, T element) {
list.add(index, element);
}
public T remove(int index) {
return list.remove(index);
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public ListIterator<T> listIterator() {
return list.listIterator();
}
public ListIterator<T> listIterator(int index) {
return list.listIterator(index);
}
public List<T> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public synchronized void clear() { list.clear(); }
}
- ImprovedList는 List 클래스의 기능을 구현할 때는 ImprovedList 내부의 List 클래스 인스턴스가 갖고 있는 기능을 불러와 사용하고, 그에 덧붙여 putIfAbsend 메소드를 구현하고 있다.
- ImprovedList 클래스는 그 자체를 락으로 사용해 그 안에 포함되어 있는 List와는 다른 수준에서 락을 활용하고 있다.
- 이런 방법으로 구현할 때에는 ImprovedList 클래스를 락으로 사용해 동기화하기 때문에, 내부의 List 클래스가 스레드 안전한지 아닌지는 중요하지 않고 신경 쓸 필요도 없다.
- 심지어 불러다 사용한 List클래스가 내부적으로 동기화 정책을 바꾼다 해도 신경 쓸 필요가 없다.
- 물론 이런 방법으로 동기화 기법을 한 단계 더 사용하면 전체적인 성능의 측면에서 약간 부정적인 영향이 있을 수 있다.
4.5 동기화 정책 문서화 하기
- 클래스의 동기화 정책에 대한 내용을 문서로 남기는 일은 스레드 안전성을 관리하는데 있어 가장 강력한 방법 가운데 하나다. (가장 많이 배척하는 방법 중임에도 틀림 없다.)
- 클래스를 가져다 사용하는 입장에서는 개발자가 작성한 문서를 가장 먼저 확인 할 것이다.
- 유지보수를 담당하는 팀은 현재 사용하고 있는 프로그램의 안전성을 해치지 않을 수 있도록 동기화 전략을 파악하고자 할 것이다.
구현한 클래스가 어느 수준까지 스레드 안전성을 보장하는지에 대해 충분히 문서를 작성해둬야 한다. 동기화 기법이나 정책을 잘 정리해두면 유지보수 팀이 원활하게 관리할 수 있다.
synchronized, volatile 등의 키워드나 기타 여러 가지 동기화 관련 클래스를 사용하는 일은 모두 멀티스레드 환경에서 동작하는 클래스의 내부에 담겨있는 데이터를 안전하게 사용할 수 있도록 동기화 정책 synchronization policy
를 정의하는 일이라고 볼 수 있다. 동기화 정책은 전체 프로그램 설계의 일부분이며 반드시 문서로 남겨야 한다.
동기화 정책을 구성하고 결정하고자 할 때 여러 가지 사항을 고려해야 한다.
- 어떤 변수를 volatile로 지정할 것인지.
- 어떤 변수를 사용할 때는 락으로 막아야 하는지.
- 어떤 변수는 불변 클래스로 만들고 어떤 변수를 스레드에 한정시켜야 하는지.
- 어떤 연산을 단일 연산으로 만들어야 하는지를 따져봐야 한다.
최소한 클래스가 스레드 안전성에 대해서 어디까지 보장하는지 문서로 남겨야 한다.
- 클래스가 스레드에 안전한가?
- 락이 걸린 상태에서 콜백 함수를 호출하는 경우가 있는가?
- 클래스의 동작 내용이 달라질 수 있는 락이 있는지?
이런 질문에 대해서 추측성 생각은 위험하다.
- 여러분이 개발한 클래스를 클라이언트 측에서 락으로 사용하지 못하게 해도 좋다.
- 하지만 클라이언트 측 락을 사용할 수 없다고 적어 놓아야 한다.
- 또 개발한 클래스를 유지보수시 어떤 락으로 동기화 해야 안전하게 구현할 수 있는지에 대해 문서로 알려야 한다.
GuardedBy 등의 Annotation만 활용해도 훌령하다.
- 동기화를 맞출때 사용하는 아주 작은 기법이라도 반드시 적어두자.
- 후임자나 유지보수 인력에게는 굉장히 큰 도움이 된다.
직관적으로 안전하겠다고 생각하는 클래스가 실제로는 그렇지 않은 경우가 많다.
- java.text.simpleDateFormat 클래스는 스레드 동기화가 되어 있지 않은데, JDK 1.4 버전 이전에는 API 문서에 동기화 언급이 전혀 없다.
4.5.1 애매한 문서 읽어내기
출처 :브라이언 괴츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬 공저, 『 멀티코어를 100% 활용하는 자바 병렬 프로그래밍』, 에이콘(2008.7.30), 4장 인용.