1. 동시성 프로그래밍이란
1) 동시성이 구현되지 않은 경우
- 서버(코어)가 클라이언트의 요청이 완료되도록 그저 기다리기 때문에 다른 작업이 실행되지 못한다.
2) 병렬성을 구현한 경우(서버 증설)
- 서버의 수가 늘어나서 한번에 처리할 수 있는 작업의 수는 늘어났지만, 이 역시 받은 요청을 처리하는 동안 서버는 다른 작업을 하지 못한다.
3) 동시성을 구현한 경우
- 클라이언트의 요청이 완료되지 않더라도 중간에 다른 작업을 할 수 있다.
- 동시성을 구현하다고 해서 클라이언트의 입장에서 자신의 요청이 빨리 처리되는 것은 아니다.
- 어플리케이션 입장에서 효율적으로 코어를 사용해 처리량이 높아지는 것이다.
- 보통 cpu는 멀티 코어를 지원한다. 언어레벨에서 하드웨어의 멀티코어를 적절하게 사용하도록 지원하기 때문에 동시성만 신경써서 개발하면 된다.
- 더불어 내 어플리케이션이 동작하는 머신의 환경이 효율적을 돌아가도록 내 어플리케이션에 메모리 누수나 자원이 낭비되지 않도록 신경쓴다.
2. 동시성 프로그래밍이 필요한 이유
1) 동시성 프로그래밍의 미신과 오해
(1) 동시성은 항상(X)/ 때로(O) 성능을 높여준다.
(2) 동시성을 구현해도 설계는 변하지 않는다.(X) / 설계를 바꿔야 한다.(O)
- 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다.
- '무엇'과 '언제'를 분리하면 시스템의 구조가 크게 달라진다.
(3) Web이나 EJB와 같은 컨테이너를 사용해도 동시성을 이해해야한다.(O)
- 어플리케이션을 컨테이너를 통해 멀티 쓰레드를 사용하는 것이기 때문에 컨테이너의 동작을 이해해야한다.
- 동시 수정, 데드락 같은 문제를 피할 수 있는지를 알아야한다.
3. 안전한 동시성 프로그래밍 규칙
1) 단일 책임 원칙(SRP) 설계
동시성 관련 코드는 다른 코드와 분리하라.
- 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
- 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.
- 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.
2) 자료 범위를 제한하라.
공유 자료를 최대한 줄여라
- 동시 수정 문제를 피하기 위해 객체를 사용하는 코드 내 임계영역을 synchronized 키워드로 보호하라
- 보호할 임계영역을 빼먹거나, 모든 임계영역을 보호했는지 확인하느라 수고가 드므로 임계영역의 수를 최소화 해야 한다.
3) 자료 사본을 사용하라
공유 자료를 줄이려면, 최대한 공유하지 않는 방법이 제일 좋다.
- 객체를 복사해 읽기 전용으로 사용한다.
- 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져온다.
- 사본을 사용하는 방식으로 내부 잠금을 없애 수행 시간을 절약하는 것이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다.
4) Thread는 가능한 독립적으로 구현한다.
다른 스레드와 자료를 공유하지 않는다.
- 서블릿처럼 각 Thread는 클라이언트 요청 하나를 처리한다.
- 모든 정보는 비공유 출처(client의 request)에서 가져오며 로컬 변수에 저장한다.
- 각 서블릿은 마치 자신이 독자적인 시스템에서 동작하는 양 요청을 처리한다.
5) 라이브러리를 이해하라
java.util.concurrent 패키지를 익혀라
- Thread Safe한 컬렉션을 사용한다.
- ex) ConcurrnetHashMap, AtomicLong
- 서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.
- 가능하다면 Thread가 Blocking되지 않는 방법을 사용한다.
6) 동기화하는 메서드 사이에 존재하는 의존성을 이해하라
공유 객체 하나에는 메서드 하나만 사용하라
- 클라이언트 잠금
- 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지 잠금을 유지한다.
- 자원을 사용하는 클라이언트마다 synchronized 처리를 해줘야 하므로 비효율적이다.
- 서버에서 잠금
- 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다. 클라이언트는 이 메서드를 호출하기만 하면 된다.
- 연결(Adapter) 서버
- 잠금을 수행하는 중간 단계를 생성한다.
- '서버에서 잠금' 방식과 유사하지만, 원래 서버는 변경하지 않는다.
- 서버의 코드가 외부 코드라서 수정할 수 없을 때 우리 코드에서 Adapter를 만들어 사용한다.
4. 동시성 테스트 방법
- 테스트를 했다고 동시성 코드가 100% 올바르다고 증명하기는 불가능하다. 하지만 충분한 테스트는 위험을 낮춘다.
- 문제를 노출하는 테스트 케이스를 작성하라
- 프로그램의 설정과 시스템 설정과 부하를 바꿔가면 자주 돌려라
- 테스트가 실패하면 원인을 추적하라
- 다시 돌렸더니 통과한다는 이유로 그냥 넘어가면 절대 안된다.
1) 코드에 보조 코드를 넣어 돌려라
드물게 발생하는 오류를 자주 발생시키도록 보조 코드를 추가한다.
- 코드에 wait(), sleap(), yield(), priority() 함수를 추가해 직접 구현한다.
- 보조코드를 넣어주는 도구를 사용해 테스트한다.
- 다양한 위치에 ThreadJigglePoint.jiggle()을 추가해 무작위로 sleep(), yield()가 호출되도록 한다.
- 테스트 환경에서 보조 코드를 돌려본다.
2) 동시성 코드를 실제 환경이 테스트 환경에서 돌려본다.
다양한 요청과 상황에서 동시성 코드가 정상적으로 동작하는지 확인한다.
- 배포하기 전에 테스트 환경에서 충분히 오랜시간 검증한다.
- 동시성 코드를 배포한 후에 모니터링을 통해 문제가 발생하는지 지켜본다.