. Part12에서도 해당 글을 여러 번 보면서 힘들게 한 장 한 장을 넘겼다. 그런데 Part12는 동시성에 대해 노하우를 전수해준다고 한다.
내가 아는 세상은 매우 좁지만 좁은 현실에서 조심스레 동시성에 대해 말해보자면, 매우 방대한 양의 지식을 필요로 한다.
어떤 강사님은 동시성을 처리하는 일은 많이 없어서.. 프로그래밍 기본서에는 동시성 이야기 자체가 단순 메서드와 설명만으로 끝난다. 이 책의 제목은 클린 코드이다. 깨끗한 코드를 작성하기 위해서 동시성을 어떻게 처리해야 할지 고민한 흔적을 살펴보자.
여러 스레드를 동시에 돌리는 이유, 여러 스레드를 동시에 돌리는 어려움, 대처하고 깨끗한 코드를 작성하는 몇 가지 방법의 제안과 동시성 테스트를 하는 방법과 문제점 이번 글에서 다루게 될 이 야이다. 다만 복잡한 주제인 만큼 간략히 조감한다. 추가적으로 알아봐야 할 것들은 살을 붙여보자.
첫 번째로 동시성은 왜 필요할까?
동시성은 무엇과 언제를 분리하는 전략이라고 한다. 스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다.
자바스크립트를 통해 프로그래밍을 할 때 우리가 동시성에 대해 고려한 적이 있는가? 없다. 자바스크립트는 단일 스레드 프로그래밍을 지원한다. 콜 스택에 차곡차곡 코드 블록을 쌓아가기 때문이다. 디버깅 시에도 해당 지점을 breakPoint를 통해 어느 지점에 무엇이 문제인지 확인하지 않는가?
무엇과 언제를 분리해버리면 애플리케이션의 구조와 효율이 극적으로 나아질 수 있다. 마치 하나의 거대한 프로그램이 작은 협력 프로그램들로 구성된 것처럼 보이기 때문이다.
간단한 예시로 서블릿 모델에서 우리는 여러 웹 요청을 받았을 때 비동기식으로 각 각의 요청을 처리한다. 또는 응답 시간과 작업 처리량 개선을 위해서 사용하기도 하고, 대량의 정보를 병렬로 처리하려고 사용하기도 한다.
동시성은 매우 어려운 주제다. 매우 신중하게 사용해야 한다. 다음은 동시성에 대한 미신과 오해들이다.
- 동시성은 항상 성능을 향상한다.
때로 성능을 높여준다. 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.
(대기시간이 길다는 조건과 프로세서의 계산량이 많다는 것은 결국 테스트를 해야 한다는 것을 의미한다.) - 동시성을 구현해도 설계는 변하지 않는다.
단일 스레드 시스템과 다중 스레드 시스템은 설계가 매우 다르다. - 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다. - 동시성은 다수 부하를 유발한다.
실제로 성능 측면에서 부하가 걸린다.
동시성 문제를 겪을 확률은 현저 낮다. 동시성 문제는 대략 여러 스레드가 공유 변수를 동시 참조하여 발생하는 문제가 대부분이다. 여러 스레드가 코드 한 줄을 거쳐가는 경로는 매우 많은데, 재수 없게 잘못된 결과를 내놓는 일부 경로 때문이다.
동시성 방어 원칙
1. 단일 책임 원칙
SRP는 변경에 있어서는 이유가 하나여야 한다는 원칙이다. 즉 동시성은 원래도 복잡하기에 동시성 관련 코드는 다른 코드와 분리해야 한다는 뜻이다.
2. 자료 범위 제한
객체 하나를 공유한 후 동일 필드를 수정하는 두 스레드가 서로 간섭하기에 예상치 못한 버그를 발생시킨다. 이런 문제를 해결하기 위한 방안으로는 공유 객체를 사용하는 코드 내 임계 영역(한 번에 하나의 프로세스만 이용하도록 보장해줘야 하는 영역)을 synchronized 키워드로 보호하는 방법을 권장한다. 이런 임계 영역을 줄이는 기술도 중요한데, 임계 영역 내부에서 공유 자료를 수정하는 위치가 많을수록 다음 가능성도 커지기 때문이다.
- 보호할 임계영역을 빼먹을 수 있다.
- 모든 임계영역을 올바르게 보호했는지 확인하느라 똑같은 노력과 수고를 반복한다.
- 버그가 숨어버린다.
3. 자료 사본을 사용
각 스레드가 객체를 복사해 사용한 후 스레드가 해당 사본에서 결과를 가져오는 방법, 혹은 읽기 전용으로 객체를 사용하는 방법이 있다. 만약 복사 비용이 걱정스럽다면, 복사 비용에 대해 실측해볼 필요가 있다고 한다.
4. 스레드는 가능한 독립적으로
예를 들어 HttpServlet 클래스에서 파생한 클래스는 모든 정보를 doGet과 doPost의 매개변수로 받는다.
아마 HttpServletRequest, HttpServletResponse 매개변수를 의미할 것이다.
각 서블릿은 독자적인 시스템에서 동작하는 양 요청을 처리한다. 서블릿 코드가 로컬 변수만 사용한다면 서블릿이 동기화 문제를 일으킬 가능성은 전무하지만, 대다수의 애플리케이션은 데이터베이스 연결과 같은 자원을 공유하는 상황을 맞이한다.
저자는 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라 권고한다.
5. 라이브러리 이해
언어가 제공하는 클래스를 검토해야 한다. java.util.concurent, atomic, locks를 공부해라.
6. 실행 모델의 이해
기본 개념
- 한정된 자원 (Bound Resource) : 다중 스레드 환경에서 사용하는 자원으로 크기나 숫자가 제한적
- 상호 배제 (Mutual Exclusion) : 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우
- 기아 (Starvation) : 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다.
- 데드락 (Deadlock) : 여러 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 더 이상 진행이 되지 않는 경우
- 라이브락 (Livelock) : 락을 거는 단계에서 각 스레드가 서로를 방해한다.
기본 개념을 가지고 다중 스레드 프로그래밍에서 사용하는 실행 모델 몇 가지를 공부하고 해법을 직접 구현하자.
1) Producer-Consumer
2) Readers-Writers
3) Dining Philosophers
7. 동기화하는 메서드 사이에 존재하는 의존성을 이해하라.
공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 다시 한번 확인하자.
메서드 사이에 의존성을 만드는 간단한 예제를 살펴보자.
public class IntegerIterator implements Iterator<Integer> {
private Integer nextValue = 0;
public synchronized boolean hasNext() {
return nextValue < 100000;
}
public synchronized Integer next() {
if (nextValue == 100000)
throw new IteratorPastEndException();
return nextValue++;
}
public synchronized Integer getNextValue(){
return nextValue;
}
}
다음은 IntergerIterator 클래스를 사용하는 코드를 보자
IntegerIterator iterator = new IntegerIterator();
while(iterator.hasNext()) {
int nextValue = iterator.next();
}
만약 두 개의 스레드가 IntegerIterator 인스턴스 하나를 공유한다면 각 스레드가 값을 가져와서 처리하는 도중에 서로 간섭에 의해 예외가 발생할 가능성이 존재한다.
이 예제는 특히 까다롭다. 정수 목록 끝에서 스레드가 서로 간섭할 때만 문제가 발생하기 때문이다.
구체적으로 말하면 스레드 A가 hasNext()를 통해 true값을 얻는다. 이때 스레드 A가 선점을 당해 스레드 B가 끼어들어 hasNext()를 통해 true값을 얻는다. 이어서 스레드 B가 정수 값 중 마지막 값을 가져와 hasNext() 값이 false가 된다.
하지만 실행을 재개한 스레드 A는 false가 된 줄 모르고 next()를 호출한다.
이는 개별 메서드는 서로 간섭되지 않는 상황이지만 클라이언트에서 메서드를 두 개 사용함으로써 발생하는 문제이다.
이 해결 방안은 세 가지이다.
- 실패를 인정하고 받아들인다.
실패해도 괜찮도록 프로그램을 조정한다. - 클라이언트-기반 잠금 메커니즘 구현
각 클라이언트는 synchronized 키워드를 이용해 객체에 락을 건다. 다만 모든 프로그래머가 락을 기억해 객체에 걸었다 풀어야 하므로 위험한 전략이다. 그리고 저자는 말하는데 클라이언트 기반 잠금 메커니즘은 사람이 할 짓이 아니다. - 서버-기반 잠금
Intergerlterator를 다음과 같이 변경하면 클라이언트에 중복해서 락을 걸 필요가 없어진다.
public class IntegerIteratorServerLocked {
private Integer nextValue = 0;
public synchronized Integer getNextOrNull() {
if (nextValue < 100000)
return nextValue++;
else
return null;
}
}
그럼 클라이언트 코드도 다음과 같이 변경된다.
while (true) {
Integer nextValue = iterator.getNextOrNull();
if (nextValue == null)
break;
//...
}
이 경우 서버 한 곳에서 관리하기에 코드 중복이 줄고, 공유 변수 범위가 줄어든다. 또한 오류가 발생한 가능성이 줄어든다.
만약 클래스의 API에 손대지 못하는 경우에는 어댑터 패턴을 통해 해당 문제를 해결하면 된다.
결론은 공유 객체 하나에는 메서드 하나만 사용하라는 말이다.
8. 동기화하는 부분을 작게 만들자.
자바에서 synchronized 키워드를 사용하면 락을 설정한다. 락은 스레드를 지연시키고 부하를 가중시킨다. 그렇기에 synchronized 문을 남발하는 코드는 바람직하지 않다.
또한 임계 영역의 개수를 줄이는 것도 중요하다. 다만 개수를 줄이기 위해 크기가 매우 큰 하나의 임계 영역으로 구현하면 스레드 간의 경쟁이 늘어나고 프로그램 성능이 떨어진다.
9. 올바른 종료 코드는 구현하기 어렵다.
잠시 돌다 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다. 가장 흔히 발생하는 문제가 데드락이다.
종료 코드를 개발 초기부터 고민하여 초기부터 구현해야 한다. 이미 나온 알고리즘을 검토하는 것을 추천한다.
10. 스레드 코드 테스트
다중 스레드 코드는 테스트에 대하여 고려할 사항이 매우 많다고 한다. 그렇기에 프로그램 설정과 시스템 설정과 부하는 계속 변경하면서 테스트를 하는 것을 추천한다.
다중 스레드 코드는 말이 안 되는 오류를 발생시킨다. 여기서 중요한 점은 말도 안 되는 오류의 발생을 일회성으로 치부하고 처리해버리면 안 된다는 것이다!!
또한 스레드 환경이 아닌 외부 환경에서 테스트를 먼저 성공시켜야 한다. 동시에 디버깅하지 말고, 스레드 환경 밖에서 먼저 테스트를 진행하자.
다중 스레드를 쓰는 코드를 다양한 설정으로 실행하기 쉽게 구현하라 조언한다. 스레드 수의 증감, 실제 환경 및 테스트 환경에서 돌려보고, 속도의 증감, 반복할 수 있도록 한다. 즉 다양한 설정에 대해 대비해야 한다.
우리가 커넥션 풀에 커넥션이 얼마나 있어야 할지 고민하는 것보다 더 적절한 스레드 수를 파악하려면 수많은 시행착오가 필요하다. 그렇기에 스레드 개수를 조율할 수 있도록 처음부터 코드를 구현하는 것을 추천한다.
시스템이 스레드를 스와핑 할 때도 문제가 발생한다. 스와핑은 대게 프로세서 단위로 메모리에서 하드디스크 혹은 SSD로 왔다 갔다 하는 것을 의미하는데 스와핑이 잦을수록 임계 영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다고 한다. 테스를 위해선 프로세서 수보다 많은 스레드를 돌리면 된다.
다양한 플랫폼을 대비해야 한다. 운영체제마다 스레드를 처리하는 정책이 달라 결과가 달라질 수 있다. 따라서 모든 목표 플랫폼에서 코드를 돌려야 한다.
결론적으로 다중 스레드 코드는 구현하기 매우 어렵다.
지금까지만 봐도 깊은 전문지식이 필요하고 전문지식이 있다고 해도 모든 걸 고려해 시작하는 것은 매우 어려워 보인다.
그렇기에 저자는 다음과 같이 정리했다.
다중 스레드 코드를 작성하기 위해선 우선 SRP를 준수해야 한다. POJO를 사용해 스레드와 스레드를 모르는 코드를 반드시 분리한다. 그렇다 스레드 코드는 반드시 작아야 하고 집약되어 관리되어야 한다.
동시성 오류를 일으키는 원인을 철저히 이해야 한다. 공유 자원을 조작, 자원 풀 공유, 루프 반복을 끝내거나 종료하는 등 까다롭기에 조심한다.
사용하는 라이브러리와 기본 알고리즘을 이해해야 한다. 또한 공유하는 객체 수와 범위를 최대로 줄인다.
스레드 코드는 애플리케이션 출시하기 전까지 최대한 오랫동안 돌려봐야 한고 한다.
동시성에 대한 거대한 서막이 끝난 듯하다. 빙산의 일부분을 본 기분이다.
최대한 이해하려고 애썻다. 많이 이해했고 무엇이 부족한지 알게 되었다.
'독서에서 한걸음' 카테고리의 다른 글
Refactoring .Chapter 02 (0) | 2022.08.15 |
---|---|
Refactoring .Chapter 01 (0) | 2022.08.06 |
Clean Code .Part12 (1) | 2022.08.04 |
Clean Code .Part11 (0) | 2022.08.02 |
Clean Code .Part10 (0) | 2022.07.30 |
댓글