이정우
1. 📌 핵심 개념 정리
✅ 요약하기
각자 해당 챕터에서 중요하다고 느낀 개념이나 아이디어를 간략하게 정리하고 개선 전, 후에 대한 예시 코드를 비교하며 개념을 설명합니다.
- 동시성이 필요한 이유
-
동시성이란?
- 동시성은
결합(coupling)
을 줄이는 전략이다. - 프로그램에서
무엇(what)
과언제(when)
를 분리하는 역할을 한다. - 단일 스레드 프로그램은
무엇과 언제가 강하게 결합
되어 있어 상태 관리가 어렵다. - 반면, 동시성을 도입하면 프로그램이 독립적인 작업 단위로 나뉘어
구조적 설계 및 효율성
이 향상된다.
- 동시성은
-
성능 및 응답성 향상
정보 수집기(information aggregator)
같은 프로그램은 여러 웹사이트에서 정보를 가져오는데, 단일 스레드보다 동시성을 활용하면 성능이 크게 향상된다.- 사용자 요청이 증가할수록 동시성을 통해 시스템이 효율적으로 응답할 수 있다.
- ex) 다수의 사용자 요청을 한 번에 처리하는 웹 애플리케이션
-
작업 분배 및 병렬 처리
- 동시성을 활용하면 시스템을 여러 작은 단위로 나눠 병렬로 처리할 수 있다.
- 예) 빅데이터 분석 시스템에서는 대량의 정보를 병렬로 처리하면 처리 속도를 크게 향상시킬 수 있다.
-
미신과 오해
- 동시성이 항상 성능을 높여주지는 않는다.
- 긴 대기 시간을 공유하는 경우 성능 향상이 가능하지만, 독립적인 계산을 수행하는 경우에는 성능 향상이 크지 않을 수도 있다.
- 동시성을 구현해도 설계는 변하지 않는다.
- 단일 스레드 시스템과 다중 스레드 시스템은 설계 방식이 다르므로 이를 고려해야 한다.
- 웹 애플리케이션에서 항상 필요한 것은 아니다.
- 웹 서버는 본래 각 요청을 독립적인 스레드에서 처리하므로, 필요하지 않은 경우도 있다.
- 동시성이 항상 성능을 높여주지는 않는다.
- 동시성의 난관
- 다수의 부하를 유발한다.
- 성능이 저하될 수 있으며, 코드가 복잡해진다.
- 설계가 복잡해진다.
- 단순한 문제라도 동시성을 고려하면 복잡성이 증가한다.
- 테스트가 어렵다.
- 동시성 버그는 재현하기 어려우며, 특정 경로에서만 발생하는 문제를 찾기 어렵다.
- 근본적인 설계 전략이 필요하다.
- 동시성은 단순히 추가하는 것이 아니라, 설계 단계부터 고려해야 한다.
public class X {
private int lastIdUsed;
public int getNextId() {
return ++lastIdUsed;
}
}
- 위 코드에서
lastIdUsed
를 공유하는 두 개의 스레드가getNextId()
를 호출하면, 예상치 못한 결과가 발생할 수 있다. - 두 스레드가 동시에 실행되면
경합 조건(race condition)
이 발생하여, 예상과 다른 값이 반환될 가능성이 높다. - 자바 메모리 모델과 JIT 최적화 등을 이해하지 않으면 문제 해결이 어려울 수 있다.
- 동시성 방어 원칙
- 동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙과 기술
- 단일 책임 원칙(SRP)
- 메서드/클래스/컴포넌트를 변경할 이유가 단 하나여야 한다는 원칙
- 동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분 -> 동시성 관련 코드는 다른 코드와 분리해야 한다.
- 동시성 구현 시 고려사항
- 동시성 코드는 독자적 개발,변경,조율 주기가 있다.
- 동시성 코드는 독자적 난관이 있다.
- 잘못된 동시성 코드의 실패원인은 다양하다.
[권장사항] 동시성 코드는 다른 코드와 분리하라.
- 따름 정리 (corollary) : 자료 범위를 제한하라
- 객체 하나를 공유한 후 동일 필드르 ㄹ수정하던 두 스레드간의 간섭으로 오류발생
- 이에 관한 해겨랙으로 공유객체를 사용하는 코드 내
임계영역(critical section)
을synchronized
키워드로 보호 하는 것을 권장한다. - 공유 자료 수정 위치가 많을 수록 다음 가능성도 증가한다.
- 보호할 임계영역을 빼먹는다. 이로인해 공유 자료를 수정하는 모든 코드가 망가진다.
- 모든 ㅇ미계영역을 올바로 보호했는지 확인하느라 같은 고생을 반복한다.
- 버그 찾기가 더욱 힘들어진다.
[권장사항] 자료를 캡슐화(encapsulation)하라. 공유 자료를 최대한 줄여라
- 따름 정리 : 자료 사본을 활용하라
- 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.
- 공유 객체를 피하는 방법이 있다면 코드가 문제를 일으킬 가능성이 대폭 감소한다.
- 따름 정리 : 스레드는 가능한 독립적으로 구현하라
- 다른 스레드와 자료를 공유하지 않는 독자적 스레드를 구현한다.
- 각 스레드는 클라이언트 요청 하나를 처리한다.
- 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.
- 각 스레드는 다른 스레드와 동기화할 필요가 없어 독자적으로 돌아갈 수 있다.
[권장사항] 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 독립적인 단위로 자료를 분할하라
- 단일 책임 원칙(SRP)
- 라이브러리를 이해하라
- 자바 5로 스레드 코드 구현 시 고려사항
- 스레드 환경에 안전한 컬렉션을 사용한다. (자바 5이상 제공)
- 서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.
- 가능하다면 스레드가 차단(blocking)`않는 방법을 사용한다.
- 일부 클래스 라이브러리는 스레드에 안전하지 못하다.
- 스레드 환경에 안전한 컬렉션
- java.util.concurrent
- 다중 스레드 환경에서 사용해도 안전하고 성능이 좋다.
- ConcurrentHashMap은 실제로 대부분의 상황에서 HashMap보다 빠르다.
- 동시 읽기/쓰기 지원
- 자주 사용하는 복합 연산을 다중 스레드 상에서 안전하게 만든 메서드로 제공한다.
- ReentrantLock : 한 메서드에서 잠그고 다른 메서드에서 푸는 락(lock) 이다.
- Semaphore : 전형적인 세마포다. 개수(count)가 있는 락이다.
- CountDownLatch : 지정한 수만큼 이벤트가 발생하고 나서야 대기 중인 스레드를 모드 해제하는 락이다. 모든 스레드에게 동시에 공평하게 시작할 기회를 준다.
[권장사항] 언어가 제공하는 클래스를 검토하라. - java.util.concur-rent - java.util.concurrent.atomic - java.util.concurrent.locks
- java.util.concurrent
- 실행 모델을 이해하라
-
한정된 자원 (Bound Resource)
- 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적이다. 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등이 예다.
-
상호 배제(Mutual Exclusion)
- 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
-
기아(Stravation)
- 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다.
-
데드락(Deadlock)
- 여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다.
-
라이브락(Livelock)
- 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만,
공명(resonance)
으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.
- 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만,
-
생산자-소비자
- 하나 이상 생산자 스레드가 정보를 생성해 버퍼나 대기열에 넣는다.
- 하나 이상 소비자 스레드가 대기열에서 정보를 가져와 사용한다.
- 생산자 스레드와 소비자 스레드가 사용하는 대기열은
한정된 자원
이다. - 생산자는 대기열에 빈공간이 생길 때까지 기다린다. 이후 정보를 채우고 시그널을 보낸다.
- 소비자는 대기열에 정보가 채워질 때까지 기다린다. 이후 정보를 사용하고 시그널을 보낸다.
- 잘못하면 생산자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 동시에 서로에게서 시그널을 기다릴 수 있다.
-
읽기-쓰기
- 읽기 스레드를 위한 주된 정보원으로 공유자원을 사용하지만, 쓰기 스레드가 공유자원을 이따금 갱신한다고 하자.
- 이런 경우
처리율(throughput)
이 문제의 핵심이다. - 처리율을 강ㅈㅗ한다면 기아(stravation)현상이 생기거나 오래된 정보가 쌓인다.
- 갱신을 허용하면 처리율에 영향을 미친다.
- 보통 쓰기 스레드가 버퍼를 오랫동안 점유해 읽기 스레드의 처리율이 떨어진다.
- 해결 전략으로는 읽기 스레드가 없을 떄까지 갱신을 원하는 쓰기 스레드가 버퍼를 기다리는 방법이 있다.
- 하지만 읽기 스레드가 계속 이어진다면 쓰기 스레드는 기아 상태에 빠진다.
- 반면, 쓰기 스레드에게 우선권을 준 상태에서 쓰기 스레드가 계속 이어진다면 처리율이 떨어진다.
- 양쪽 균형을 잡으면서 동시 갱신 문제를 위한 해법이 필요하다.
[식사하는 철학자들] 원형 테이블에 앉아있는 철학자들. 각 철학자의 왼쪽에는 포크가, 테이블의 중앙에는 스파게티 한 접시가 놓여있다. 스파게티를 먹기 위해서는 철학자의 양 손에 포크가 있어야 한다.
- 철학자-스레드 / 포크 - 자원
- 많은 기업 애플리케이션이 겪는 문제이고, 주의해서 설계하지 않으면 데드락,, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다.
[권장사항] 위에서 설명한 기본 알고리즘과 각 해법을 이해하라.
- 동기화하는 메서드 사이에 존재하는 의존성을 이해하라
-
동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 발생한다.
-
자바 언어는 개별 메서드를 보호하는
synchronized
라는 개념을 지원한다. -
공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 다시 한 번 확인하라.
[권장사항] 공유 객체 하나에는 메서드 하나만 사용하라.
-
공유 객체 하나에 여러 메서드가 필요한 상황에는 해당 방법을 고려한다.
- 클라이언트에서 잠금
- 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다.
- 마지막 메서드를 호출할 떄까지 잠금을 유지한다.
- 서버에서 잠금
- 서버에서 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다.
- 클라이언트는 이 메서드를 호출한다.
- 연결 서버
- 잠금을 수행하는 중간 단계를 생성한다. '서버에서 잠금' 방식과 유사하지만 원래 서버는 변경하지 않는다.
- 클라이언트에서 잠금
- 동기화 하는 부분을 작게 만들어라
- 자바에서
synchronized
키워드를 사용하면 락을 설정한다. - 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행 가능하다.
- 락은 스레드를 지연시키고 부하를 가중시킨다.
- 여기저기서
synchronized
문을 남발하는 코드는 바람직하지 않다. 임계영역
은 반드시 보호해야 한다.- 따라서 코드를 짤 떄는 임계영역 수를 최소화 해야한다.
- 필요 이상으로 임계영역 크기를 키우면 스레드 간 경쟁이 늘어나 프로그램 성능이 저하된다.
[권장사항] 동기화하는 부분을 최대한 작게 만들어라.
- 올바른 종료 코드는 구현하기 어렵다.
-
다중 스레드 코드 작성의 어려움
- 다중 스레드 코드는 깨끗하고 구조적으로 작성하지 않으면 복잡성과 오류가 증가함.
- 단일 책임 원칙(SRP) 을 따르고, POJO를 활용해 스레드와 무관한 코드와 스레드 코드 분리 필요.
- 스레드 코드는 개별적으로 철저하게 테스트해야 하며, 스레드 간 공유 데이터를 다룰 때 동기화 및 경계 조건에 주의해야 함.
-
스레드 코드의 종료 처리
- 정확한 종료 처리는 어려움 → 데드락(deadlock)과 같은 문제 발생 가능.
- 예를 들어, 부모 스레드가 자식 스레드가 끝나기를 기다리는데, 자식 스레드 중 하나가 데드락 상태라면 부모 스레드는 영원히 대기.
- 생산자-소비자 패턴에서도 생산자는 종료되었지만 소비자가 메시지를 기다려 차단(blocked) 상태가 된다면, 소비자는 종료 신호를 받지 못함 → 전체 시스템이 멈출 위험.
-
해결 방법 및 권장 사항
- 종료 코드는 처음부터 신중하게 설계해야 하며, 시간이 오래 걸릴 수 있음.
- 검증된 알고리즘을 활용하는 것도 방법.
- 테스트 주도 개발(TDD) 원칙을 적용하여 지속적으로 테스트하며 문제를 최소화하는 것이 중요.
-
결론
- 다중 스레드 코드는 신중하게 작성하지 않으면 복잡성과 오류가 증가하며, 특히 종료 처리는 세심한 설계가 필요함. 처음부터 종료 로직을 고민하고, 이미 검증된 알고리즘을 활용하는 것이 권장됨.
- 스레드 코드 테스트하기 멀티스레드 코드는 동시성 문제로 인해 예측할 수 없는 오류가 발생하기 쉽다. 따라서 효과적인 테스트 전략이 필요하다.
-
테스트 전략
- 문제를 일으키는 테스트 케이스를 작성하고, 다양한 환경에서 반복적으로 실행해라.
- 테스트 실패의 원인을 철저히 분석하되, 단순히 "다시 돌려서 통과했다"고 넘어가지 말라.
-
오류 탐지 및 디버깅
- 스레드 문제는 종종 간헐적으로 발생하는데, 이를 ‘일회성 오류’로 치부하지 말고 원인을 분석하라.
- 단일 스레드 환경에서 코드가 정상적으로 동작하는지 먼저 확인하고, 이후 다중 스레드 환경을 고려하라.
-
다중 스레드 환경에서의 테스트
- 다양한 설정에서 스레드 개수를 조정하면서 테스트하라.
- 프로세스를 여러 개 실행해 다중 스레드 상황을 시뮬레이션하라.
- 다양한 속도와 환경에서 테스트하여 예측하지 못한 동작을 포착하라.
-
강제 실패를 유도하는 방법
- 테스트를 보다 확실히 하기 위해 보조 코드(instrumentation) 를 삽입하여 오류를 유도하라.
- 예를 들어 wait(), sleep(), yield(), priority() 등을 코드에 추가하여 실행 순서를 변경해라.
- 이를 통해 스레드 실행 순서의 영향을 확인하고, 잠재적 오류를 드러나게 할 수 있다.
-
자동화된 테스트 도구 활용
- AOP(Aspect-Oriented Programming)나 CGLIB, ASM 같은 도구를 활용하여 보조 코드를 자동으로 삽입하라.
- 테스트 환경과 배포 환경에서 다르게 동작하는 보조 코드를 활용하면 디버깅이 수월해진다.
- IBM의 ConTest 같은 도구도 활용할 수 있다.
-
결론
- 스레드 코드 테스트는 단순히 정상 동작 여부를 확인하는 것이 아니라, 다양한 환경에서 반복적으로 실행하며 오류를 적극적으로 찾아내는 것이 핵심이다.
- 결론
-
다중 스레드 코드 작성 원칙 및 주의점
- 다중 스레드 코드는 복잡하여 오류가 발생하기 쉽기 때문에 각별히 주의해야 함.
- SRP를 준수하여 스레드와 무관한 코드와 스레드 관련 코드를 분리하는 것이 중요함.
- 스레드 코드 테스트 시에는 스레드만을 독립적으로 테스트해야 함.
- 동시성 오류를 방지하기 위해 공유 자원의 접근을 신중하게 관리해야 함.
-
안정적인 코드 작성을 위한 방법
- 라이브러리의 기본 알고리즘을 활용하여 문제를 해결하는 것이 바람직함.
- 공유하는 데이터는 최소한으로 제한하며, 필요하다면 객체 설계를 변경하는 것도 고려해야 함.
- 테스트 주도 개발(TDD)의 원칙을 따라 지속적으로 테스트하며, 테스트 용이성을 고려한 설계가 필요함.
-
보조 코드 활용과 오류 방지
-
보조 코드를 추가하여 오류를 줄일 수 있으며, 자동화 기술을 적극적으로 활용하는 것이 좋음.
-
출시 전에 충분한 오랫동안의 테스트를 거쳐 코드의 안정성을 확보해야 함.
체계적인 접근 방식을 따른다면 코드가 정상적으로 동작할 가능성이 크게 향상됨.
2. 🤔 이해가 어려운 부분
🔍 질문하기
책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.
- 개념 또는 원칙의 이름
- 어려웠던 부분
해당 개념이 헷갈리거나 명확하지 않았던 점을 구체적으로 설명합니다. - 궁금한 점
해당 개념이 어떤 원리로 동작하는지, 실무에서 어떻게 활용되는지 등을 질문 형태로 정리합니다.
- 어려웠던 부분
- 개념 또는 원칙의 이름
- 어려웠던 부분
. - 궁금한 점
.
- 어려웠던 부분
- 개념 또는 원칙의 이름
- 어려웠던 부분
. - 궁금한 점
.
- 어려웠던 부분
3. 📚 참고 사항
📢 논의하기
관련된 자료가 있다면 공유하고, 더 깊이 논의하고 싶은 아이디어나 의견을 정리합니다.
- 관련 자료 공유
- 추가 자료
관련 블로그 글이나 공식 문서 링크를 제공합니다.
- 추가 자료
- 논의하고 싶은 주제
- 주제
논의하고 싶은 내용을 간략히 정리합니다. - 설명
논의하고 싶은 이유를 작성합니다.
- 주제