자바병렬프로그래밍 4. 객체 구성 Composing Objects

Eugene Lim
10 min readApr 17, 2022

--

이 챕터에서는 기존 라이브러리를 사용하면서 어떻게 동기화 정책을 구현하는지에 대해 자세히 알아본다.

4.1 Thread-Safe Class 설계

이전에도 계속 책에서 강조하지만, 객체의 상태는 내부 변수 variable를 기반으로 한다. 객체 내부가 모두 기본 변수형 primitive type 이라면, 이 값들이 객체의 모든 상태를 구성할 수 있다.

Counter 클래스 내부에서 쓰이는 value라는 변수를 안 쪽으로 숨긴 다음, value에 접근하는 함수에 synchronized를 붙여줬다. 즉, this@Counter 로 락을 잡는다.

4.1.2 상태 의존 연산 State-dependent Operation

상태의존 state-dependent 연산은 조건에 따라 동작여부가 결정되는 것을 말한다.

  • 아무것도 들어있지 않은 큐에서 값을 뽑아낼 수 없다.
  • 단일 스레드에서는 올바른 상태가 아닌 객체는 명확하게 오류가 발생하지만, 멀티스레드 상황에서는 아닐 수도 있다.

4.1.3 상태 소유권

변수를 통해 객체의 상태를 정의하고자 할 때에는 해당 객체가 실제로 ‘소유하는’ 데이터만을 기준으로 삼아야 한다.

예를들어, HashMap의 인스턴스를 만들었다면, 내부에서 구현하는데 사용한 여러 개의 객체들도 만들어진다. 이 여러 객체들의 상태가 HashMap 인스턴스의 상태를 정의한다.

C++ 에서는 특정 메소드에서 객체 인스턴스를 넘겨준다면

  • 이 객체에 대한 소유권을 넘겨주는 것인지
  • 잠시만 사용하도록 빌려주는 형식인지
  • 메소드 인자로 넘겨준다면 계속 함께 사용하는 모양인지

등등 명확하게 정의할 수 있다.

여기서 말하는 객체에 대한 소유권은, 어떤 객체를 만들었고, 내부에서 쓰이는 arraylist가 있다고 하자. 어떤 함수에서 arraylist 자체를 내어준다면, 객체 외부에서 add/remove 가 가능해진다. 이런 함수를 arraylist에 대한 소유권을 넘겨줬다고 한다.

자바는 GC 때문에 소유권 개념이 불명확한 경우가 많다. 오브젝트(객체)는 갖고 있는 상태를 캡슐화해야하고, 캡슐화된 상태에 소유권이 있다. 그리고 이런 변수의 상태를 조절하는 락 구조에 대해서도 소유권을 갖는다. 위에서도 설명했지만, 만약 어떤 내부 변수를 mutable 형태로 내보낸다면, 그 클래스는 변수에 대해 소유권을 잃어버린다.

‘소유권 분리’ — 컬랙션클래스들은 내부 구조에 대한 소유권은 클래스가 갖고, 컬랙션에 추가할 아이템에 대한 소유권은 호출하는 쪽에서 갖도록 하고 있다. 컬랙션클래스들은 이 객체들이 안전하게 공유될 수 있도록 동기화 작업을 하고 있다.

4.2 인스턴스 한정

특정 객체가 다른 객체 내부에 완벽하게 숨겨져 있다면 해당 객체를 활용하는 모든 방법을 한눈에 확실하게 파악할 수 있고, 따라서 객체 외부에서도 사용할 수 있는 상황보다 훨씬 간편하게 스레드 안정성을 분석해 볼 수 있다.

이전에 살펴본, thread-confinement는 한 스레드만 특정 코드를 실행할 수 있도록 제한하는 방법이었는데, 여기서 말하는 인스턴스 한정은 객체 object에서만 값을 제어할 수 있는 것을 말한다. 즉, 캡슐화에 대해서 이야기 하고 있다.

  • 특정 클래스 인스턴스에 한정 private variable
  • 특정 스레드 한정(다른 스레드에 넘겨주지 않기)

보다시피, Person에 대해서 안정성은 이 클래스에서 기술하지 않는다. 다만, 이 객체를 추가하고 체크할 때만 동기화 작업을 하고 있다. 이렇게 mySet은 해당 인스턴스에 캡슐화되어 인스턴스 한정이라는 것을 한 눈에 알 수 있고, synchronizsed로 동기화되어 있지만, 쉽게 다른 락 유형으로 대체할 수 있다.

자바에서는 thread-safe 하지 않은 객체들을 쉽게 바꿔주는 라이브러리를 제공하고 있다. Collections.synchronizedList 와 같은 팩토리 메소드들 살펴보자.

여기서 보면, 같은 lock으로 synchronized를 잡아주는 것을 확인할 수 있다. 이런 팩토리메소드들은 데코레이더 패턴 Decorator Pattern 을 활용하여 기본 클래스들의 모든 메소드들을 Wrapping 하고 있고, 그러므로써 Wrapper Class는 동기화되어 있어서 스레드 안정성을 확보할 수 있다.

동기화 되지 않은 arraylist를 사용다하가 lock이 필요하면, Collections.synchronizedList(arraylist)로 간단하게 구현할 수 있다.

하지만 우리가 이렇게 사용해야한다면, iterator() hasNext() next()오퍼레이션이 전부 따로 연산이 되므로 반드시 단일연산으로 synchronized를 해줘야 한다.

4.2.1 자바 모니터 패턴

변경 가능한 데이터를 모두 객체 내부에 숨긴 다음 객체의 암묵적 락으로 데이터에 대한 동시 접근을 막는 것을 모니터 패턴이라고 한다.

private final 로 선언된 객체를 락으로 활용해 자바 모니터패턴을 구현하였다. private 객체로 락을 선언하면, 암묵적 락을 사용했을 때에 비해 락이 외부로 공개되지 않는다.
암묵적 락은 외부로 같이 공개되어서 다른 곳에서 같은 락으로 작업할 수 있는 반면, 락 활용까지 체크해야한다(Collections.synchronizedList 처럼)

4.2.2 차량 위치 추적

뷰 스레드에서 특정 차량의 ID와 위치를 읽어서 화면상에 데이터를 업데이트 한다고 생각하자.

업데이터 스레드는 차량에 달린 GPS 장치에서 읽어낸 위치 정보를 입력한다.

이런 데이터를 담는 MutablePoint 객채가 있고, 모니터패턴을 활용한 MonitorVehicleTracker 클래스가 있다.

MutablePoint는 thread-safe 하지 않지만 MonitorVehicleTracker 내에서는 안정성을 확보하고 있다. MutablePoint는 외부로 절대 공개되지 않는다. 그 이유는

  • 생성자를 통해 들어온 list는 deepCopy를 통해 복사본으로 들어오고
  • 밖으로 나갈 때도 new MutablePoint(loc)로 복사되어서 나간다.

하지만 차량이 이동하는 값이 MutablePoint에 계속 반영이 된다면, 매번 복사본이 나가기 때문에 UI는 바뀐 Point를 알 수가 없고, 성능에 문제가 생길 수도 있다.

❗️deepCopy()에서 Collections.unmodifiableMap(result) 만으로 충분하다고 생각할 수 있다. 하지만 unmodifiable은 객체 자체에 add나 delete를 하지 못하게 막는것이지, 내부 Item은 충분히 조작할 수 있다. 만약 새로운 Mutlable(loc)으로 생성해주지 않는다면 MonitorVehicleTracker 객체의 locations의 아이템들이 조작될 수 있다.

❗️그리고 차량이 많아지면 deepCopy() 호출 시, 새로 생성해야할 객체가 기하급수적으로 늘어나므로 성능에 문제가 생길 수 있다.

4.3 스레드 안정성 위임

대부분의 클래스는 여러 객체의 조합으로 이루어져있고, thread-safe 하지 않는 객체들로 구성될 수도 있다. 이런 클래스들의 스레드 안정성을 확보하려면, 자바 모니터 패턴이 유용하다.

Chaper.2 에서 나왔던 CountingFactorizer 클래스는 AtomicLong 객체를 제외하고는 상태가 없다. AtomicLong 자체가 thread-safe 하므로 이런 경우 스레드 안전 문제를 AtomicLong 클래스에게 ‘위임’한다고 한다.

4.3.1 위임 기법을 활용한 차량 추적

MutablePoint 를 immutable로 바꿨다.

그리고 locations 를 ConcurrentHashMap으로 구현했고, locations를 unmodifiableMap으로 갖고있다. 그리고 다른 동기화 방법은 없어졌다. getLocations()에서 unmodifiableMap 객체 자체가 외부로 유출되지만 escaping, Map 내부의 아이템들이 immutable 이어서 더이상 값이 바뀔 걱정을 할 필요가 없다. 그리고 getLocations()로 받은 unmodifiableMap으로 차량 정보가 계속 업데이트 된다면, getLocation(id)로 UI를 계속해서 새로 바뀐 값으로 표현할 수 있게 된다.

4.3.2 독립 상태 변수 Independent State Variables

내부 변수가 서로 독립적이란 말은, 서로의 상태값에 연관성이 없다는 뜻이다.

CopyOnWriteArrayList 클래스를 사용해서 thread-safe를 구현했고, keyListeners와 mouseListeners는 서로 연관성이 없다.
CopyOnWriteArrayList는 chapter 5.에서 자세히 알아본다.

4.3.3 위임할 때의 문제점

AtomicInteger로 thread-safe를 구현했지만, 두 변수가 서로 의존성이 있다. 예를 들어, 현재 lower=0, upper=10이고, thread A가 setLower(5)를 호출하고, thread B가 setUpper를 4로 호출한다면, lower=5, upper=4가 될 수 있다. 이 두 연산을 단일 연산으로 처리해야한다.

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

상태 변수가 스레드 안전하고, 내부에서 상태 변수 값에 의존성이 없으며, 상태 변수에 대한 어떤 연산을 하더라도 잘못된 상태에 이를 가능성이 없다면 외부에 공개해도 안전하다

keyListeners와 mouseListeners는 외부에 공개해도 안전하다.

4.3.5 차량 추척 프로그램의 상태를 외부에 공개

차량 추척 프로그램의 Point가 외부로 나갈 수 있도록 수정해보자.

여기서는 getX(), getY()를 따로 호출하게 되면 값이 달라질 수 있기 때문에, int[] get()로 처리했다.

이전에 작업했던 DelegatingVehicleTracker에서 result item이 SafePoint로 바뀌었다. 이렇게 하면, 외부 프로그램에서 SafePoint에 대한 값을 수정할 수 없게 된다.

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

자바 기본 클래스 라이브러리에는 여러 가지 thread-safe한 클래스들이 많다. 이런 클래스들을 이용하면서, 필요한 기능을 추가하면서 스레드 안전성도 유지하는 방법을 찾아보자.

thread-safe한 List 클래스에 특정 아이템이 없다면 추가하는 기능을 단일연산으로 구현해야한다면

  1. 기존 클래스에 직접 putIfAbsent()-contains()를 체크하고, 없으면 add()하는 단일 연산- 함수를 추가한다.
    (하지만, 이렇게 했을 때 단점은 해당 클래스의 코드를 다 파악해야하고, 소스코드가 없을 수도 있다.)
  2. 기존 클래스에 상속받아서 putIfAbsent() 함수를 추가한다.

이렇게 되면, 상속된 클래스가 있는 줄 모르고 Vector의 동기화 방법을 바꿔버린다면 (현재는 this에 락이 걸려있지만, 명시적인 Lock클래스로 바뀐다면) 하위클래스는 thread-safe가 깨질 수 있다.

그리고 또한 동기화 기능을 여러 개의 클래스에 분산시키기 때문에 안정적인 방법이 아니다.

4.4.1 호출하는 측의 동기화 client-side Locking

한글책에서는 호출하는 측의 동기화, 원서에는 클라이언트 사이드라는 표현을 썼는데, 클라이언트 사이드는 라이브러리를 가져다 쓰는 코드라고 생각하면 된다. 세번째 방법은 라이브러리를 사용하여(client-side), 도우미 클래스를 구현하는 것이다.

이렇게 Helper class를 만들었을 때, putIfAbsent() 함수는 제대로 동작하지 않는다. 왜냐하면 Collections.synchronizedList()는 list variable에 lock이 걸려있기 때문이다. 즉, helper class를 만들 때 올바로 구현하려면 어디에 락이 걸려있는지 알고, 같은 락을 꼭 사용해줘야 한다.

이렇게 helper class를 안전하게 구현하려면 락이나 동기화 전략에 대한 내용을 정확하게 알고 사용해야한다. 클라이언트 사이드 락은 클래스 상속과 여러가지 공통점이 있는데,

  • 원래 클래스의 구현 내용과 밀접한 관련이 있고
  • 캡슐화 되어 있는 동기화 정책을 위반할 수 있다.

4.4.2 클래스 재구성 Composition

네번째 방법은 재구성이다.

ImprovedList 내부의 List 클래스가 갖고 있는 기능을 불러와 사용하고(clear() 같은) 그리고 putIfAbsent() 함수를 추가하였다. (여기서 주의할 점은 절대로 list가 외부로 공개되면 안된다) 이렇게 하면 내부 list variable이 어떤 락을 사용하는지 알 필요가 없고, ImprovedList 자체의 락을 사용하게 되었다.

--

--