시작하며
본 포스팅은 글또 9기 활동 중 Udemy 로부터 강의 쿠폰을 지원받아 작성되었습니다.

이번 포스팅은 ‘글또 9기’ 활동의 일부로, Udemy에서 지원해주신 쿠폰으로 수강한 【한글자막】 Java 멀티스레딩, 병행성 및 성능 최적화 강의에 대한 후기 글이다. 멀티스레드의 필수(기초)이론부터 심화주제까지 폭넓게 다루고 있고, 멀티스레딩과 병행성에 대한 이론적인 학습은 물론 관련 코드를 포함한 실습과정이 잘 구성되어 있었다.

데이터 엔지니어링에 관심을 가질수록 그 기반이 되는 백엔드 엔지니어링에도 관심이 생긴다. 회사 업무인 마이데이터 시스템 아키텍처에서도 멀티스레딩을 통한 병행성은 성능 최적화의 핵심이 된다. 사실 멀티스레딩은 취준생 시절에 이론적인 공부만 얕게 해봤지 제대로 된 강의로 학습해 본 적은 없었는데, 이번 기회로 멀티스레드 아키텍처의 기초부터 성능 최적화, ReentrantLock, Lock-Free 알고리즘, 비동기-논블록킹 IO와 가상 스레드까지 공부할 수 있었다. 추천하는 이유는 다음과 같다.
- 강사님(Michael Pogrebinsky) 설명이 굉장히 깔끔하다.
- Thread, Runnable과 같은 정말 기본 내용으로 시작하여 심화된 개념까지 깔끔하게 정리하여 전달해준다.
- 불필요한 내용으로 혼란을 주지 않고 핵심만 짚어준다. 수강생이 이해하기 편하도록 흐름을 배려해 구성했다는 것이 느껴졌다.
- 강의 구성이 개념을 익히기에 적합하다.
- 이론을 코드로 옮겨 실습하는 과정과 섹션별 코딩 실습은 중간에 점검하고 넘어가기 좋은 구성이다.
- 개념을 전체적으로 정리한 뒤 그대로 옮겨진 코드 실습, 적절한 예제(코딩 연습)로 개념을 재점검한다.
- 실무와 밀접하게 연관된 내용을 배운다.
- 코드를 그대로 적용할 수 있는 것은 아니지만, 다루는 개념들이 실무에 직접적으로 연관되어 있어 의심스러웠던 부분들이 강의를 듣고 훨씬 깔끔하게 정리되었다.
배우는 내용
강의에서 배우게 되는 내용은 다음과 같다.
- 운영 체제의 기본 내용 및 멀티스레딩과 병행성이 필요한 이유
- 멀티스레딩의 기본 - Java에서 스레드를 생성하는 방법 및 스레드 간에 소통하는 방법
- 멀티스레드 병렬 실행 애플리케이션의 성능 관련 고려 사항 및 설계 패턴 (지연 시간 또는 처리량 최적화)
- Java에서 스레드 간 데이터를 공유하는 방법, 발생할 수 있는 함정과 솔루션 및 모범 사례
- 반응성과 성능을 향상시킬 수 있는 락이 걸리지 않은 고급 알고리즘 및 데이터 구조
강의 목차로 정리하면 다음과 같다.
- 개요
- 스레딩 기초 - 스레드 생성
- 스레딩 기초 - 스레드 조정
- 성능 최적화
- 스레드 간 데이터 공유
- 병행성 문제와 솔루션
- 락킹 심화
- 스레드 간 통신
- Lock-Free 알고리즘, 데이터 구조 및 기술
- 고성능 IO를 위한 스레딩 모델
- 가상 스레드와 고성능 IO
- 마무리
사전 점검 - 멀티스레드 기초 이론
강의 처음은 개요를 통해 멀티스레딩의 기본 이론을 다루고 시작한다. 아래 내용은 바로 코드 실습부터 들어가고 싶은 분들을 위해 첫 개요 섹션을 빠르게 훑어볼 수 있도록 정리한 것이다.
멀티스레드가 필요한 이유
- responsiveness(응답성)
- 개별 task들을 각각의 thread가 처리함으로써 응답성을 향상시킬 수 있다.
- 이런 multitasking은
Concurrency라고 하며, 한 개의 코어로도 구현할 수 있다.
- performance(성능)
- 단일 thread로 구현하는 것보다 훨씬 빠르게 정해진 기간 동안 일을 해결할 수 있다.
- 동일한 이유로 하드웨어 수나 서비스를 유지하는 공수가 줄어든다.
프로세스, 스레드 동작 방식
- 컴퓨터 전원을 켜면 운영체제가 disk에서 메모리로 로드된다. (이때 다른 프로그램들은 파일 형태로 디스크에 있다.)
- 사용자가 프로그램을 실행시키면 해당 프로그램을 디스크에서 가져와 인스턴스 형태로 메모리에 생성한다.
- 이때의 인스턴스를 ‘프로세스(Process)’ 혹은 ‘어플리케이션 컨텍스트(Context of an Application)‘라고 한다. 따라서 각각의 프로세스는 완전히 구분되어 있다.
- 프로세스의 메타데이터는 다음과 같다.
- PID : 어플리케이션이 읽고 쓰기 위해 여는 파일
- Code : CPU에서 실행되는 프로그램 명령어
- Data(Heap) : 어플리케이션 실행에 필요한 모든 데이터가 들어있음
- Main Thread : 적어도 하나의 메인 스레드가 있음 (멀티 스레드 환경에서는
Stack,Instruction Pointer덩어리가 여러 개 생성되고 나머지는 공유한다.)- Stack : 지역변수가 생성되고 함수가 실행되는 영역
- Instruction Pointer : 스레드가 수행할 다음 영역을 가리키는 포인터
컨텍스트 스위치(Context Switch)
- 컨텍스트 스위치의 정의
- 일반적으로 프로세스는 다른 프로세스들과 독립적으로 움직인다.
- 코어 수보다 프로세스가 더 많고, 각 프로세스는 스레드 하나를 기본으로 가진다.
- 스레드 하나를 수행하고 멈춘 뒤 스케줄링에 따라 다른 스레드를 수행하는 것을
Context Switch라 한다.
- 컨텍스트 스위치의 주의점
- 동시에 많은 스레드를 다룰 때는 효율이 떨어질 수밖에 없다. 이는 병행성의 대가이다.
- CPU에서 실행되는 각 스레드는 레지스터, 캐시, 메모리의 커널 리소스를 사용하는데,
Context Switch단계에서 이를 전부 저장해두고 다른 스레드의 데이터를 CPU와 메모리에 복원해야 하기 때문이다.
- CPU에서 실행되는 각 스레드는 레지스터, 캐시, 메모리의 커널 리소스를 사용하는데,
- 너무 많은 스레드를 다루면
Thrashing이 발생한다. Thrashing은 운영체제가 실제 작업보다 Context Switch 작업에 더 많은 시간을 할애하게 되는 것이다. - 한 프로세스 내의 스레드는 자원을 공유하므로, 동일 프로세스 내의 컨텍스트 스위치가 다른 프로세스 간 스레드 컨텍스트 스위치보다 효율적이다.
- 동시에 많은 스레드를 다룰 때는 효율이 떨어질 수밖에 없다. 이는 병행성의 대가이다.
운영체제의 스레드 스케줄링 방법
- First Come, First Serve : 먼저 도착한 스레드를 먼저 실행 — 다른 thread의 기아 문제를 야기한다.
- Shortest Job First : 실행시간이 짧은 스레드를 먼저 실행 — 실행시간이 긴 스레드는 영영 실행이 안 될 수 있다.
- (실제)
Epochs: 운영체제가 Epoch에 맞추어 시간을 적당한 크기로 나누고 각 스레드의 타임 슬라이스를 종류별로 에폭에 할당한다. 단, 모든 스레드가 각 에폭에서 완료되는 것은 아니다. - (실제)
Dynamic Priority: Dynamic-priority = Static-priority(개발자가 미리 정해둔 우선순위) +bonus(각 Epoch마다 OS가 세팅, 예: 실시간 스레드에 더 점수를 부여하고 기아현상도 고려)
Thread를 사용하는 경우, Process를 사용하는 경우
- Thread를 사용하는 multi-thread architecture
- 자원을 많이 공유하는 경우 유리하다.
- Thread는 생성과 파괴가 훨씬 빠르며, 같은 프로세스 내의 컨텍스트 스위치는 프로세스 간 스위치보다 빠르다.
- Process를 사용하는 multi-process architecture
- 보안성과 안정성이 중요하다면 독립된 프로세스로 수행하는 것이 나을 수 있다. (멀티스레드 앱은 스레드 하나가 앱 전체를 다운시킬 수 있음)
- 서로 관련 없는 작업을 하나의 프로세스로 통합하는 것 역시 의미가 없다.
정리하며
Udemy에는 참 좋은 강의들이 많은 것 같다. 이번 강의도 꼼꼼하게 정리하여 멀티스레딩 관련 포스팅을 연재해보려 한다. 위 강의는 JVM, 멀티스레딩, 성능 최적화에 관심은 많으나 시간이 부족한 분들, 이론과 실무를 병행하여 빠르게 학습하고 싶은 분들께 추천한다. 👋