본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java Concurrency] 객체구성

by 돼지왕 왕돼지 2017. 4. 18.
반응형

 [Java Concurrency] 객체구성


@guardedby, client side lock, Collections, composition, concurrency, copyonwritearraylist, Decorator pattern, external lock, Final, guardedby, HashMap, iterator, Java, Map Entry, Notify, Private, private 락, state space, state-dependent, synchronizedlist, thread safe, volatile, Wait, Wrapper, [Java Concurrency] 객체구성, 가장 쉬운 방법, 간결함, 객체, 객체 상태, 객체구성, 객체의 불변성, 공개, 내부 락, 논리의 범위, 단일 연산, 단일 연산 추가, 데코레이터 패턴, 도우미 클래스, 독립적, 동기화, 동기화 기법, 동기화 정책, 동시성, 라이브러리, 락, 락이 걸린 상태에서 콜백 함수 호출, 래퍼 클래스, 로컬 변수, 메소드 동기화, 문서, 문서화, 반복 객체, 배척, 범위, 변수, 보장, 복사본, 복합 연산 메소드, 블로킹 큐, 블록 내부, 상속, 상속 문서, 상태 범위, 상태 의존, 생각의 범위, 설계, 성능, 세마포어, 소유, 소유권, 스레드 세이프, 스레드 안전, 스레드 안전성, 스레드 안전성 위임, 스레드 안전하게 구현된 클래스에 기능 추가, 스레드 안전한 클래스 설계, 스레드 한정, 안전, 암묵적인 락, 어노테이션, 외부 락, 위임 기법, 유지보수, 의존성, 인스턴스 한정, 자바 모니터 패턴, 장점, 재구성, 재사용, 전용 락, 정책, 종류, 캡슐화, 콜백 함수, 클라이언트 락, 클래스, 통제권, 팩토리 메소드, 표준, 후임자


4.1. 스레드 안전한 클래스 설계


-
객체가 갖고 있는 여러 가지 정보를 해당 객체 내부에 숨겨두면 전체 프로그램을 다 뒤져볼 필요 없이 객체 단위로 스레드 안전성이 확보되어 있는지 확인할 수 있다.


-
클래스가 스레드 안전성을 확보하도록 설계하고자 할 때에는 다음과 같이 세 가지를 고려해야 한다.
    객체의 상태를 보관하는 변수가 어떤 것인가?
    객체의 상태를 보관하는 변수가 가질 수 있는 값이 어떤 종류, 어떤 범위에 해당하는가?
    객체 내부의 값을 동시에 사용하고자 할 때, 그 과정을 관리할 수 있는 정책


-
n 개의 변수를 갖는 객체의 상태는 n개 변수가 가질 수 있는 값의 전체 조합이다.
A라는 객체 내부에 다른 객체 B를 가리키는 변수를 사용하고 있다면, A객체 내부의 변수뿐만 아니라 B 객체 내부에 들어 있는 변수의 조합까지 A객체가 가질 수 있는 전체 상태 범위에 포함시켜야 한다.


-
동기화 정책은 객체 내부의 여러 변수가 갖고 있는 현재 상태를 사용하고자 할 때 값이 계속해서 변하는 상황에서도 값을 안전하게 사용할 수 있도록 조절하는 방법이다.
동기화 정책에는 객체의 불변성, 스레드 한정, 락 등을 어떻게 적절하게 활용해 스레드 안전성을 확보할 수 있으며 어떤 변수를 어떤 락으로 막아야 하는지 등의 내용을 명시한다.
클래스를 유지보수하기 좋게 관리하려면 해당 객체에 대한 동기화 정책을 항상 문서로 작성해둬야 한다.


-
객체와 변수가 가질 수 있는 가능한 값의 범위를 상태 범위(state space)라고 한다.
상태 범위가 좁으면 좁을수록 객체의 논리적인 상태를 파악하기가 쉽다.
사용할 수 있는 부분마다 final 을 지정해두면 상태 범위를 크게 줄여주기 때문에 생각해야 할 논리의 범위를 줄일 수 있다.


-
클래스가 특정 상태를 가질 수 없도록 구현해야 한다면, 해당 변수는 클래스 내부에 숨겨둬야만 한다.
변수를 숨겨두지 않으면 외부에서 클래스가 "올바르지 않다" 고 정의한 값을 지정할 수 있기 때문이다.
그리고 특정한 연산을 실행했을 때 올바르지 않은 상태 값을 가질 가능성이 있다면 해당 연산은 단일 연산으로 구현해야 한다.
반대로 클래스에서 변수의 값에 별다른 제약 조건을 두지 않는다면 클래스의 유연성과 실행 성능을 높인다는 측면에서 이와 같은 동기화 방법이나 캡슐화 기법을 사용하지 않아도 되겠다.


-
여러 개의 변수를 통해 클래스의 상태가 올바른지 아닌지를 정의한다면 연산을 단일 연산으로 구현해야 한다.
다시 말하면, 서로 연관된 값은 단일 연산으로 한번에 읽거나 변경해야 한다.


-
객체가 가질 수 있는 값의 범위와 변동 폭을 정확하게 인식하지 못한다면, 스레드 안전성을 완벽하게 확보할 수 없다.
클래스의 상태가 정상적이라는 여러 가지 제약 조건이 있을 때 클래스의 상태를 정상적으로 유지하려면 여러 가지 추가적인 동기화 기법을 적용하거나 상태 변수를 클래스 내부에 적절히 숨겨야 한다.


-
현재 조건에 따라 동작 여부가 결정되는 연산을 상태 의존(state-dependent) 연산이라고 한다.


-
어떤 동작을 실행하기 전에 특정한 조건을 만족할 때까지 기다리도록 프로그램하고자 한다면, wait와 notify를 사용하는 대신
세마포어나 블로킹 큐와 같이 현재 알려져 있는 여러 가지 라이브러리를 사용하는 편이 훨씬 간단하고 안전하다.


-
변수를 통해 객체의 상태를 정의하고자 할 때에는 해당 객체가 실제로 "소유하는" 데이터만을 기준으로 삼아야 한다.
예를 들어 HashMap 객체의 논리적인 상태를 살펴보면, HashMap 내부에 있는 모든 Map.Entry 객체의 상태와 기타 여러 가지 객체의 상태를 한꺼번에 다뤄야 한다.


-
대부분의 경우 소유권과 캡슐화 정책은 함께 고려하는 경우가 많다.
캡슐화 정책은 내부에 객체와 함께 상태 정보를 숨기기 때문에 객체의 상태에 대한 소유권이 있다.
특정 변수에 대한 소유권을 갖고 있기 때문에 특정 변수의 상태가 올바르게 유지되도록 조절하는 락 구조가 어떻게 움직이는지에 대해서도 소유권을 갖는다.
소유권은 통제권이란 말과 비슷한 의미를 갖지만, 특정 변수를 객체 외부로 공개하고 나면 해당 변수에 대한 통제권을 어느 정도 잃는다.
클래스는 일반 메소드나 생성 메소드로 넘겨받은 객체에 대한 소유권을 갖지 않는다는 게 일반적인 모양이지만,
넘겨받은 객체의 소유권을 확보하도록 메소드를 특별하게 작성하면 소유권을 확보할 수도 있다.


-
소유권 분리는 컬렉션 클래스를 놓고 볼 때 컬렉션 내부의 구조에 대한 소유권은 컬렉션 클래스가 갖고,
컬렉션에 추가되어 있는 객체에 대한 소유권은 컬렉션을 호출해 사용하는 클라이언트 프로그램이 갖는 구조이다.



4.2. 인스턴스 한정


-
객체를 적절하게 캡슐화하는 것으로도 스레드 안전성을 확보할 수 있는데, 이런 경우 흔히 "인스턴스 한정" 기법을 활용하는 셈이다.
특정 객체가 다른 객체 내부에 완벽하게 숨겨져 있다면 해당 객체를 활용하는 모든 방법을 한눈에 확실하게 파악할 수 있고, 따라서 객체 외부에서도 사용할 수 있는 상황보다 훨씬 간편하게 스레드 안전성을 분석해 볼 수 있다.


-
데이터를 객체 내부에 캡슐화해 숨겨두면 숨겨진 내용은 해당 객체의 메소드에서만 사용할 수 있기 때문에 숨겨진 데이터를 사용하고자 할 때에는 항상 지정된 형태의 락이 적용되는지 쉽고 정확하게 파악할 수 있다.


-
객체 내부에서 사용할 목적으로 한정되어 있는 데이터는 사용 범위 밖으로 유출되면 안 된다.
특정 클래스 인스턴스에 한정시키거나(private 으로 지정), 블록 내부에 한정시키거나(로컬 변수), 아니면 특정 스레드에 한정시킬 수 있다.


-
인스턴스 한정 기법은 클래스를 구현할 때 스레드 안전성을 확보할 수 있는 가장 쉬운 방법이라 해도 무리가 없다.
인스턴스 한정 기법을 사용하면 동기화를 위해 락을 적용하는 방법도 마음대로 선택할 수 있다.


-
Collections.synchronizedList 와 같은 팩토리 메소드는 기본 클래스에 스레드 안전성을 확보하는 방법으로
대부분 데코레이터 패턴(decorator pattern)을 활용하며, 이런 팩토리 메소드의 결과로 만들어진 래퍼(wrapper) 클래스는 기본 클래스의 메소드를 호출하는 연동 역할만 하면서 그와 동시에 모든 메소드가 동기화되어 있다.


-
반복 객체(iterator)나 내부 클래스 인스턴스를 사용하면서 공개한다면 한정됐어야 하는 객체를 간접적으로 외부에 유출시킬 가능성이 있다.


-
인스턴스 한정 기법을 사용하면 전체 프로그램을 다 뒤져보지 않고도 스레드 안정성을 확보하고 있는지 쉽게 분석해 볼 수 있기 때문에 스레드에 안전한 객체를 좀 더 쉽게 구현할 수 있다.


-
자바 모니터 패턴을 따르는 객체는 변경가능한 데이터를 모두 객체 내부에 숨긴 다음 객체의 암묵적인 락으로 데이터에 대한 동시 접근을 막는다.
자바 모니터 패턴의 가장 큰 장점 가운데 하나는 바로 간결함이다.
자바 모니터 패턴은 단순한 관례에 불과하며 일정한 형태로 스레드 안정성을 확보할 수만 있다면 어떤 형태의 락을 사용해도 무방하다.


-
객체 자체의 암묵적인 락을 사용하기보다는 락으로 활용하기 위한 private 객체를 준비해 두면 여러 가지 장점이 있다.
이런 락은 private 으로 주로 선언되어 있기 때문에 외부에서는 락을 건드릴 수 없다.
만약 락이 객체 외부에 공개되어 있다면 다른 객체도 해당하는 락을 활용해 동기화 작업에 함께 참여할 수 있다.


-
외부에서 변경 가능한 데이터를 요청할 경우 그에 대한 복사본을 넘겨주는 방법을 사용하면 스레드 안전성을 부분적이나마 확보할 수 있다.
하지만 이는 성능에 문제가 발생할 수 있다.






4.3. 스레드 안전성 위임


-
CopyOnWriteArrayList 는 리스너 목록을 관리하기에 적당하게 만들어져 있는 스레드 안전한 List 클래스


-
두 개 이상의 변수를 사용하는 복합 연산 메소드를 갖고 있다면 위임 기법만으로는 스레드 안전성을 확보할 수 없다.
이런 경우에는 내부적으로 락을 활용해서 복합 연산이 단일 연산으로 처리되도록 동기화해야 한다.


-
클래스가 서로 의존성 없이 독립적이고,
스레드 안전한 두 개 이상의 클래스를 조합해 만들어져 있고,
두 개 이상의 클래스를 한번에 처리하는 복합 연산 메소드가 없는 상태라면,
스레드 안전성을 내부 변수에게 모두 위임할 수 있다.


-
상태 변수가 스레드 안전하고,
클래스 내부에서 상태 변수의 값에 대한 의존성을 갖고 있지 않고,
상태 변수에 대한 어떤 연산을 수행하더라도 잘못된 상태에 이를 가능성이 없다면,
해당 변수는 외부에 공개해도 안전하다.



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


-
현재 만들어져 있는 클래스를 가져다 스레드 안전을 구현하는 것이 대부분 적절한 방법이다.
이미 만들어져 있는 클래스를 재사용하면 개발에 필요한 시간과 자원을 절약할 수 있고, 개발할 때 오류가 발생할 가능성도 줄어들고, 유지보수 비용도 절감할 수 있다.


-
단일 연산 하나를 기존 클래스에 추가하고자 한다면 해당하는 단일 연산 메소드를 기존 클래스에 직접 추가하는 방법이 가장 안전하다.


-
소스코드 없이 기능을 가져다 사용하는 경우에는 상속받아 기능을 추가할 수 있는데,
이 방법은 기존 클래스를 외부에서 상속받아 사용할 수 있도록 설계했을 때나 사용할 수 있다.
기존 클래스를 상속받아 기능을 추가하는 방법은 기존 클래스에 직접 기능을 추가하는 방법보다 문제가 생길 위험이 훨씬 많다.
동기화를 맞춰야 할 대상이 두 개 이상의 클래스에 걸쳐 분산되기 때문이다.
만약 상위 클래스가 내부적으로 상태 변수의 스레드 안전성을 보장하는 동기화 기법을 약간이라도 수정한다면 그 하위 클래스는 본의 아니게 적절한 락을 필요한 부분에 적용하지 못할 가능성이 높기 때문에 쥐도 새도 모르게 동기화가 깨질 수 있다.


-
클래스를 상속받지 않고도 클래스에 원하는 기능을 추가할 수 있는 세 번째 방법은 도우미 클래스를 따로 구현해서 추가 기능을 구현하는 방법이다.
이 방법을 올바르게 구현하려면 클라이언트 측 락(client-side lock) 이나 외부 락(external lock)을 사용해 list 가 사용하는 것과 동일한 락을 사용해야 한다.
그러나 이 방법 역시 기존 클래스의 동기화 정책에 dependent 하기에 썩 좋은 해결책은 아니다.


-
기존 클래스에 새로운 단일 연산을 추가하고자 할 때 좀 더 안전하게 사용할 수 있는 방법은 재구성(Composition) 이다.
이 방법은 전체적인 성능 측면에서는 약간 부정적일 수 있지만, 동기화 기법 측면에서는 훨~씬 안정적이다.



4.5. 동기화 정책 문서화하기


-
클래스의 동기화 정책에 대한 내용을 문서로 남기는 일은 스레드 안전성을 관리하는 데 있어 가장 강력한 방법 가운데 하나이다.
그러면서도 가장 많이 배척하고 사용하지 않는 방법이기도 하다.


-
구현한 클래스가 어느 수준까지 스레드 안전성을 보장하는지에 대해 충분히 문서를 작성해둬야 한다.
동기화 기법이나 정책을 잘 정리해두면 유지보수 팀이 원활하게 관리할 수 있다.


-
동기화 정책은 전체적인 프로그램 설계의 일부분이며 반드시 문서로 남겨야 한다.
당연한 말이지만 설계와 관련한 여러 가지 내용을 결정짓기에 가장 좋은 시점은 바로 설계 단계이다.


-
동기화 정책을 구성하고 결정하고자 할 때에는 여러 가지 사항을 고려해야 한다.
어떤 변수를 volatile 로 지정할 것인지, 어떤 변수를 사용할 때는 락으로 막아야 하는지,
어떤 변수는 불변 클래스로 만들고, 어떤 변수를 스레드에 한정시켜야 하는지,
어떤 연산을 단일 연산으로 만들어야 하는지 등을 따져 봐야 한다.


-
최소한 클래스가 스레드 안전성에 대해 어디까지 보장하는지는 문서로 남겨야 한다.
클래스가 스레드 안전한가?
락이 걸린 상태에서 콜백 함수를 호출하는 경우가 있는가?
클래스 동작 내용이 달라질 수 있는 락이 있는지?


-
클래스에 단일 연산을 추가하고자 한다면 어떤 락으로 동기화해야 안전하게 구현할 수 있는지에 대해서 문서로 알려줘야 한다.
내부적으로 사용하는 특정 상태 변수를 락으로 동기화시켰다면 유지보수 인력이 알아볼 수 있도록 적어둬야 한다.
방법도 아주 간단하다.
@GuardedBy 등의 어노테이션만 활용해도 훌륭하다.
동기화를 맞출 때 사용하는 아주 작은 기법이라도 반드시 적어두자.
후임자나 유지보수 인력에게는 굉장히 큰 도움이 된다.


-
표준이 명확하지 않으면 개발자는 명확하지 않은 부분을 모두 추측할 수밖에 없다.
이왕 추측하는 김에 좀 더 잘 추측하는 방법이 있지 않을까?
스펙에 명확하게 정의되어 있지 않은 부분을 좀 더 근접하게 추측하려면 스펙을 작성하는 사람의 입장에서 생각해야 한다.






반응형

댓글