개요
객체는 처리의 추상화다. 스레드는 일정의 추상화다. -제임스 O.코플리엔
동시성과 깔끔한 코드는 양립하기 어렵다.
다중 스레드 테스트해보기도 어렵다.
이 2가지 어려움을 해당 단원을 읽으면서 헤쳐나갈 수 있었다!
동시성이 필요한 이유
동시성은 Coupling을 없애는 전략이다.
동시성은 무엇(what)과 언제(when)을 분리하는 전략이다. 스레드가 하나인 프로그램은 무엇과 언제가 서로밀접하다.
- 무엇,언제를 분리하면 애플리케이션 구조와 효율이 극적으로 좋아진다.
- 응답 시간과 작업 처리량 개선에도 도움이 된다.
- 데이터를 대량으로 분석하는 시스템에서 병렬로 처리
구조 개선의 좋은 예는 Servlet 모델일 것이다. 이론적으로, Servlet 개발자는 요청을 개별적으로 처리하는 데에만 신경을 쓰며 요청 큐를 직접 관리하는 부담을 덜 수 있다. 물론, Servlet이 제공하는 의존성의 해소는 완벽하지 않지만 Servlet이 제공하는 구조적인 이점은 그 자체로 가치가 있다.
그리고 동시성과 관련된 타당한 생각들을 살펴보자.
- 동시성은 성능, 코드 작성 양쪽 모두에 오버헤드가 발생한다
- 동시성 관련 버그는 재현 어렵기 때문에 종종 일회성 문제로 여겨 무시하곤 한다
- 동시성 문제는 근본적인 디자인 개편이 필요하다
동시성에 대한 오해
- 동시성은 항상 성능을 높여준다 ( 아닌 경우도 있긴 하지만 잘 없다.. )
- 동시성을 구현해도 기존 설계는 변하지 않는다
- 일반적으로 무엇과 언제를 구분하는 것이기 때문에 설계가 많이 변한다
- Web 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다
- 어떻게 동작하는지 이해를 하고 있어야 하며, 동시 수정 문제, 데드락 문제를 피할 수 있는 방법을 알아야 한다
동시성 구현 난관 예제
/* Code 1-1 */
public class ClassWithThreadingProblem {
private int lastIdUsed;
public ClassWithThreadingProblem(int lastIdUsed) {
this.lastIdUsed = lastIdUsed;
}
public int getNextId() {
return ++lastIdUsed;
}
}
public static void main(String args[]) {
final ClassWithThreadingProblem classWithThreadingProblem = new ClassWithThreadingProblem(42);
Runnable runnable = new Runnable() {
public void run() {
classWithThreadingProblem.getNextId();
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
- t1이 43을, t2가 44를 가져간다. lastIdUsed는 44이다.(O)
- t1이 44을, t2가 43를 가져간다. lastIdUsed는 44이다.(O)
- t1이 43을, t2가 43를 가져간다. lastIdUsed는 43이다.(X)
동시성 방어 원칙
단일 책임 원칙!
동시성과 관련된 코드는 다른 코드들과 분리하라! 그렇게 해야 하는 이유와 방법은 아래와 같다
- Executor, Scheduler와 같은 곳으로 분리하라 ( 자세한 건 밑에 나올 예정 )
- 동시성 관련 코드는 그 자체가 하나의 역할이기 때문에 분리해야 한다
- 동시성 코드는 개발,변경,튜닝시 다른 코드와 다른 생명주기를 가진다.
자바 동시성 라이브러리 사용하기
자바5 스레드 환경에 안전한 컬렉션 제공
- java.util.concurrent : 다중 스레드 환경에서 안전한 클래스 제공
- ConcurrentHashMap : 거의 모든 상황에서 HashMap보다 빠르다. 동시 읽기/쓰기 지원
- ReentrantLock : 한 메서드에서 잠그고 다른 메서드에서 푸는 락이다
- Semaphore : 전형적인 세마포다. 개수가 있는 락
- CountDownLatch : 지정한 수만큼 이벤트가 발생하고 나서야 대기 중인 스레드를 모두 해제하는 락. 모든 스레에게 동시에 공평하게 시작할 기회를 줌
동시성 코드 테스트하기
동시성 코드 테스트는 통과 되는 경우도 있고 아닌 경우가 있기 마련이다. 이와 관련된 테스트를 진행할 때 어떤 것을 고려해야 할까?
- 말이 안되는 실패 = 잠정적인 스레드 문제로 취급
- 일회성 문제라고 치부하지 말자! 무시하고 개발을 진행하면 나중에는 더 큰 문제가 발생한다
- 다중 스레드를 고려하지 않은 순차코드부터 제대로 돌게 만들자
- 즉, 스레드가 호출하는 POJO를 만들자
- 스레드 환경에서 생기는 버그 / 스레드 환경 밖에서 생기는 버그 동시에 디버깅을 하지말자
- 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있께 쓰레드 코드를 구현하자
- 다양한 설정+목적(ex : 스레드 수 변경)으로 테스트 할 수 있게 코드를 구현하라
- 테스트 코드를 빨리, 천천히, 다양한 속도로 돌려보자
- 반복 테스트가 가능하도록 테스트케이스를 작성하자
- 프로세서 수보다 많은 스레드를 돌려보자
- 시스템이 스레드를 swapping(스와핑) 할 때도 문제가 발생한다
- 이 때, 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다
- 코드에 보조 코드(instrument)를 넣어 돌려라. 강제로 실패를 일으키게 해보라
- 이와 관련해서 자세한 내용은 밑에 예제를 살펴보자
동시성 코드 테스트하기 어려운 이유는 재현이 어렵다는 것이다. 이렇게 드물게 발생하는 오류를 자주 일으킬 방법은 없을까?
동시성 코드 테스트하기(1) - 직접 구현하기
테스트를 위해 비즈니스 로직에 sleep, yield 메서드를 사용해 실행 경로를 변경하게 한다. 아래 코드가 예제이다.
/* Code 3-1 */
public synchronized String nextUrlOrNull() {
if(hasNext()) {
String url = urlGenerator.next();
Thread.yield();
// inserted for testing.
updateHasNext();
return url;
}
return null;
}
하지만 위의 코드는 딱봐도 문제를 가지고 있다.
- 운영 환경에서 굳이 이 코드를 가지고 있을 필요가 있는가? 성능 떨어지게...
- 테스트 할 코드를 직접 찾아야 한다
동시성 코드 테스트하기(2) - 자동화
jiggle(흔들기) 기법을 이용해서 동시성 코드 테스트를 수천번 해보자. 아래 코드는 Aspect를 이용해 '아무것도 안하기', 'sleep','yield' 등을 무작위로 선택해서 테스트 할 수 있게 해주는 것이다.
/* Code 4-1 */
public class ThreadJigglePoint {
public static void jiggle() { }
}
public synchronized String nextUrlOrNull() {
if(hasNext()) {
ThreadJiglePoint.jiggle();
String url = urlGenerator.next();
ThreadJiglePoint.jiggle();
updateHasNext();
ThreadJiglePoint.jiggle();
return url;
}
return null;
}
해당 Aspect는 운영에서는 아무것도 안하게 하고, 테스트 환경에서는 '아무것도 안하기, sleep, yield' 3가지 중 렌덤으로 선택해서 동작되게 하면 된다.
'Clean code' 카테고리의 다른 글
Clean Code 3장 - 함수 (0) | 2020.01.11 |
---|---|
Clean Code 4장 - 주석 (0) | 2020.01.07 |
Clean Code 5장 - 형식 맞추기 (0) | 2020.01.07 |
리팩토링 - 12장. 복합 리팩토링 ( 절차코드를 객체로 전환 ) (0) | 2020.01.05 |
Clean Code 9장 - 단위테스트 (0) | 2018.09.18 |