동기화하는 메서드 사이에 존재하는 의존성을 이해하라
동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다.
자바는 개별 메서드를 보호하는 synchronized라는 개념을 지원한다. 하지만 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 확인해야 한다.
권장사항: 공유 객체 하나에 메서드 하나만 사용하기
- 공유 객체 하나에 여러 메서드가 필요한 상황은 다음 세 가지 방법을 고려
클라이언트에서 잠금 - 클라이언트에서 첫 번째 메서드 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지 잠금을 유지한다.
서버에서 잠금 - 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다. 클라이언트는 이 메서드를 호출한다.
연결(Adapted)서버 - 잠금을 수행하는 중간 단계를 생성한다. '서버에서 잠금' 방식과 유사하지만 원래 서버는 변경하지 않는다.
동기화하는 부분을 작게 만들어라
synchronized 키워드를 사용하면 락을 설정한다. 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행이 가능하다. 락은 스레드를 지연시키고 부하를 가중시킨다. 따라서 남발하면 안된다.
반면, 임계 영역은 반드시 보호해야반드시 한다. 코드를 짤 때 임계 영역 수를 최대한 줄여야 한다.
거대한 임계영역 하나로 구현하라는 말이 아니다. 작고 적게.
올바른 종료 코드는 구현하기 어렵다
영구적으로 돌아가는 시스템 구현 방법과 잠시 돌다 깔끔하게 종료하는 시스템 구현하는 방법은 다르다
깔끔하게 종료하는 코드는 구현이 어렵다. 가장 흔한 이유는 데드락이다. 스레드가 절대 오지 않을 시그널을 기다린다.
부모 스레드가 자식 스레드 여러 개 만든 후 모두가 끝나기를 기다렸다 자원을 해제하고 종료한다고 하면, 자식 스레드 중 하나가 데드락에 걸렸다면? 부모 스레드는 영원히 기다린다.
이번에는 유사한 시스템이 사용자에게서 종료하라는 지시를 받았다고 가정. 부모 스레드는 모든 자식 스레드에게 작업을 멈추고 종료하라는 시그널을 전달한다. 그런데 자식 스레드 중 두 개가 생상자/소비자 관계라면? 생산자 자식 스레드는 금세 종료하지만 소비자 자식 스레드는 생상자 스레드에서 오는 메시지를 기다린다면? 생산자에서 메시지를 기다리는 소비자 스레드는 차단 상태에 있으므로 종료하라는 시그널을시그널을 못 받는다. 소비자 스레드는 생산자 스레드를 영원히 기다리고, 그 소비자 스레드 부모 스레드는 영원히 기다린다.
권장사항: 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라
스레드 코드 테스트하기
현실적으로 불가능. 테스트가 정확성을 보장하지 않는다. 그럼에도 충분한 테스트는 위험을 낮춘다.
권장사항: 문제를 노출하는 테스트 케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안된다.
고려할 사항이 아주 많다는 뜻이다. 아래에 몇 가지 구체적인 지침을 제시한다.
- 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라
- 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.
- 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있도록 스레드 코드를 구현하라
- 다중 스레드를 쓰는 코드 부분을 상황에 맞춰 조정할 수 있게 작성하라
- 프로세서 수보다 많은 스레드를 돌려보라
- 다른 플랫폼에서 돌려보라
- 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해 보라
말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라
다중 스레드 코드는 때때로 말이 안 되는 오류를 일으키기 때문이다.
책 저자들을 포함해서 대다수 개발자는 스레드가 다른 코드와 교류하는 방식을 직관적으로 이해하지 못한다. 이유는 스레드 코드에 잠입한 버그는 수천~수백만 번에 한 번씩 드러나, 도저히 실패를 재현하기 어렵기 때문이다. 다만, 이런 문제를 일회성 문제로 취급하면취급하면 안 된다. 그냥 일회성 문제는 존재하지 않는 개념이라 생각하라. 계속 일회성 문제를 무시하면 잘못된 코 드위에 코드가 계속 쌓인다.
권장사항: 시스템 실패를 '일회성'이라 치부하지 마라.
다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
당연한 소리지만 다시 한번 강조한다. 스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인한다. 일반적인 방법으로, 스레드가 호출하는 POJO를 만든다. POJO는 스레드를 모른다. 따라서 스레드 환경 밖에서 테스트가 가능하다.
즉, 순차 코드도 검증 안된 상태에서 다중 스레드를 동시에 테스트하면 가뜩이나 어려운 게 더 어려워진다.
권장사항: 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라. 먼저 스레드 환경 밖에서 코드를 올바로 돌려라.
다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
적절한 스레드 개수 파악은 상당한 시행착오가 필요. 그래서 처음부터 다양한 설정으로 프로그램의 성능 측정 방법을 강구해야 한다. 스레드 개수를 조율하기 쉽게 코드를 구현해 프로그램이 돌아가는 도중에 스레드 개수를 변경할 방법도 고려한다. 더 나아가 프로그램 처리율과 효율에 따라 스스로 스레드 개수를 조율하는 코드도 고민한다
프로세서 수보다 많은 스레드를 돌려보라
시스템이 스레드를 스와핑 할 때도 문제가 발생한다. 스와핑을 일으키려면 프로세서 수보다 많은 스레드를 돌린다. 스와핑이 잦을수록 임계 영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다
다른 플랫폼에서 돌려보라
단지 운영체제마다 스레드를 처리하는 정책이 달라 결과가 달라질 수 있다. 다중 스레드 코드는 플랫폼에 따라 다르게 돌아간다. 따라서 코드가 돌아갈 가능성이 있는 플랫폼 전부에서 테스트를 수행해야 한다.
권장사항 : 처음부터 그리고 자주 모든 목표 플랫폼에서 코드를 돌려라
코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해 보라
스레드 코드는 오류를 찾기 쉽지 않다. 간단한 테스트로는 버그가 드러나지 않는다. 버그가 몇 달만에 나타날 수도 있다.
스레드 버그가 산발적이고 우발적이고 재현이 어려운 이유는 코드가 실행되는 수천 가지 경로 중에 아주 소수만 실패하기 때문이다.
드물게 발생하는 오류를 좀 더 자주 일으킬 방법은 없을까? 보조 코드를 추가해 코드가 실행되는 순서를 바꿔준다.
예를 들어 Object.wait(), Object.sleep(), Object.yield(), Object.priority() 등과 같은 메서드를 추가해 코드를 다양한 순서로 실행한다.
각 메서드는 스레드가 실행되는 순서에 영향을 미친다. 따라서 버그가 드러날 가능성도 높아진다.
- 코드에 보조 코드를 추가하는 방법은 두 가지
직접 구현하기
자동화
직접 구현하기
코드에다 직접 wait(), sleep(), yield(), priority() 함수를 추가
특별히 까다로운 코드를 테스트할 때 적합
yield() 삽입하면 코드가 실행되는 경로가 바뀐다. 그래서 버그를 발견할 확률이 증가한다. 즉, yield()때문이 아니라 원래 있던 버그가 발견된 것이다.
이 방법의 문제점
- 보조 코드 삽입할 적정 위치 선정
- 어떤 함수를 어디서 호출해야 적당한지
- 배포 환경에 보조 코드를 그대로 남겨두면 프로그램 성능 저하
- 무작위적, 버그 발견이 확률임
스레드를 전혀 모르는 POJO와 스레드를 제어하는 클래스로 프로그램을 분할하면 보조 코드를 추가할 위치 찾기가 쉬워진다.
자동화
AOF(Aspect-Oriented Framework), CGLIB, ASM 등과 같은 도구를 사용
ThreadJigglePoint.jiggle() 호출은 무작위로 sleep이나 yield를 호출한다. 때론 아무 동작도 하지 않는다
ThreadJigglePoint클래스를 두 가지로 구현하면 편리하다
하나 jiggle() 메서드를 비워두고비워두고 배포 환경에서 사용
둘 무작위로 nop(무동작), sleep, yield 등을 테스트 환경에서 수행
테스트 환경에서 두 번째 방법으로 수천번 통과했다면 나름 할 만큼 했다고 말해도 된다. 좀 더 복잡한 방법이 있지만, 어렵다면 이 방법이 합리적인 대안이다
IBM이 개발한 ConTest라는 도구가 있다. 위와 같은 방식의 테스트를 지원하지만 좀 더 복잡하다
코드를 흔드는 이유는 스레드를 매번 다른 순서로 실행하기 위해서다. 좋은 테스트 케이스와 흔들기 기법은 오류가 드러날 확률을 크게 높여준다.
권장사항: 흔들기 기법을 사용해 오류를 찾아내라.
결론
다중 스레드 코드는 올바로 구현하기 어렵다. 간단한 코드가 여러 스레드와 공유 자료를 추가하면서 악몽으로 변한다
먼저, SRP를준수한다. POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다. 스레드 코드를 테스트할 때는 전적으로 스레드만 테스트한다, 즉, 스레드 코드는 최대한 집약되고 작아야 한다
동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다.
여러 스레드가 공유 자료를 조작하거나 자원 풀을 공유할 때 동시성 오류가 발생한다.
루프 반복을 끝내거나 프로그램을 깔끔하게 종료하는 등 경계 조건의 경우가 까다로우므로 특히 주의한다.
사용하는 라이브러리와 기본 알고리즘을 이해한다. 특정 라이브러리 기능이 기본 알고리즘과 유사한 어떤 문제를 어떻게 해결하는지 파악한다.
보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법을 이해한다.
잠글 필요가 없는 코드는 잠그지 않는다.
잠긴 영역에서 다른 잠긴 영역을 호출하지 않는다.
그러려면 공유하는 정보와 공유하지 않는 정보를 제대로 이해해야 한다.
공유하는 객체 수와 범위를 최대한 줄인다.
클라이언트에게 공유 상태를 관리하는 책임을 떠넘기지 않는다.
필요하다면 객체 설계를 변경해 클라이언트에게 편의를 제공한다.
어떻게든 문제는 생긴다. 초반에 들어가지 않는 문제는 일회성으로 치부하지 말자 무시하지 말자
대게 일회성 문제는 시스템 부하 상태나 뜬금없이 발생한다. 그러므로 스레드 코드는 다 플랫폼에서 많은 설정으로 반복적으로 테스트해야 한다
테스트 용이성은 TDD 3대 규칙을 따른 자연히 얻어진다.
테스트 용이성은 또한 좀 더 넓은 설정 범위에서 코드를 수행하기 위해 필요한 기능을 제공하는 플러그인 수준을 의미한다.
시간을 들여 보조 코드를 추가하면 오류가 드러날 가능성이 크게 높아진다.
직접 구현도 좋고 몇 가지 자동화 기술을 사용해도 괜찮다.
초반부터 보조 코드를 고려한다. 스레드 코드는 출시 전까지 최대한 오랫동안 돌려봐야 한다.
'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글
다시 시작 (0) | 2023.11.10 |
---|---|
마무리 (0) | 2022.11.15 |
13장 동시성 - 1 (0) | 2022.11.08 |
12장 창발성 (0) | 2022.11.05 |
11장 시스템 (0) | 2022.11.01 |