서론

결국 좋은 품질의 애플리케이션을 개발하기 위해서는 CS지식을 활용하여 성능과 안정성을 최대로 이끌어내야한다. 이 때문에 나도 다시 CS지식을 학습하고 있는데 운영체제를 공부하다보니 이러한 이론들을 실제 개발에 어떻게 접목시켜야 좋을지 감이 오지 않았다. 스레드, 동기화, 스케줄링 부분을 학습하면서 이론들을 어떻게 실제 성능, 안전성, 자원 관리같은 운영품질 향상과 같은 실전에 연결 시킬 수 있는지 정리해보았다.
스레드의 본질
백엔드 개발 관점에서 스레드를 이해할 때의 핵심은 하나이다.
- 같은 프로세스의 스레드들은 같은 주소 공간(메모리)를 공유한다. 즉, 코드/힙/전역 데이터같은 메모리를 함께 사용한다.
- 대신 각 스레드들은 자신만의 스택을 가진다. 스택은 스택 포인터 레지스터로 관리되기 때문에 스레드별로 분리된다.
- 스레드들은 종종 파일 디스크럽터 같은 커널 자원도 공유할 수있다.
백엔드 개발에서 이것이 의미하는것
"코드는 깔끔한데 왜 데이터 결과값이 이상하지?"와 같은 문제의 출발점이 이곳이다.
- 전역 캐시, 싱글톤, 커넥션 풀, 공용 큐, 로거 버퍼처럼 공유 상태가 끼어드는 순간, 스레드는 서로의 영역을 침범할 수 있다.
- 따라서 병행성 설계의 가장 첫번째 원칙이 공유 상태를 줄이거나, 공유한다면 경계를 명시하는것이다(락/불변/메시지 패싱/격리 등).
스케줄링
운영체제가 스레드를 다루는 방식에서 백엔드 개발자에게 가장 중요한 포인트는 선점(preemption)이다.
Linux 서버의 스케줄링은 선점형이며, 우선순위/정책에 따라 준비된 스레드가 실행중인 스레드를 밀어내고 CPU를 가져올 수 있다.
백엔드 개발에서 이것이 의미하는 것
멀티스레드 환경에서 내가 의도한 대로 "A다음에 B가 순서대로 실행되겠지"라는 생각은 위험할 수 있다.
- 코드가 내가 의도한대로 한줄씩 이어지는것처롬 보여도, 중간에 스레드가 바뀌면 공유 데이터의 관찰/갱신 타이밍이 섞일 수 있다.
- 따라서 경쟁조건 같은 버그가 재현이 어렵고, 트래픽/머신 부하에 따라 가끔만 터지는 형태로 발생하는것.
백엔드 서버의 대표 병행성 모델 3가지
대부분의 운영 서버 구조는 대체로 아래 3가지중 하나 또는 혼합된 구조로 설명할 수 있다.
스레드풀 기반
이 모델은 작업을 큐에 넣고, 제한된 수의 워커 스레드가 꺼내어 처리한다.
- Java의 `ExecutorService`는 작업 제출/종료 제어를 제공하며, `shutdown()`/`shutdownNow()`로 새 작업 거부 및 종료 방식을 명시하고 있다.
- `Futuer`같은 개념으로 비동기 작업 진행 상태를 추적하는 패턴 역시 이 모델의 하나라고 볼 수 있다.
실무에서 활용하기
- 스레드 요청 수를 무한히 늘리기보다, 풀 크기와 큐 용량을 설계 변수로 두어야한다.
- 폴/큐가 곧 백프래셔 장치이다. 트래픽이 늘 때 느려지지만 버틸지, 버티지못해 전체 서버가 뻗을지는 여기서 나뉘는 경우가 많다. 고심해서 설계하자.
이벤트 루프 + 논블로킹 I/O
리눅스 서버에서 이 모델의 대표적인 메커지즘이 `epoll`이다.
- `epoll`은 edge-triggered(`EPOLLET`) 사용시 noblockingFD를 사용하고, `EAGIN`이 나올때까지 읽기/쓰기를 드레인하는 식의 사용법을 권장한다. 또한 블로킹 I/O가 여러 FD를 처리하는 태스크에게 자원을 굶길 수 있다고 경고한다.
- libuv 디자인 문서도 이벤트 루프가 단일 스레드 비동기 I/O이며, 네트워크 I/O를 논블로킹 소켓으로 수행하고, 플랫폼별 최적 메커니즘을 사용한다고 설명하고 있다.
실무에서 활용하기
- 이 모델의 가장 지양해야할 일은 이벤트 루프(또는 I/O스레드)에서 블로킹 호출을 해버리는 것이다.
- 블로킹이 길어지면 단일 루프가 다른 연결을 계속 처리하지 못해서, 마치 서버 전체가 죽은것처럼 보일 수 있기 때문이다.
하이브리드: 이벤트 기반 + 스레드풀 (격리)
대부분의 시스템은 보통 이벤트 기반 핫패스에 블로킹+고비용 작업을 섞지 않기 위해 하이브리드 방식을 사용한다.
- OS가 논블로킹 버전을 제공하지 않는 I/O나 CPU 비용이 높은 큰 작업들을 처리하고, 이벤트 루프나 워커 풀을 막는 코드가 성능/응답성 문제를 유발한다.
- NGINX에서 `thread_pool`을 통해 파일 읽기/전송이 워커 프로세스를 블로킹하지 않도록 멀티스레드로 오프로딩하는 설정도 존재한다.
실무에서 활용하기
- 요청을 빨리 받아서 빠르게 분기하고, 오래걸리는 일은 별도 실행단위로 넘기는것이 병행성 설계의 핵심 패턴이다.
- 이벤트 루프/워커의 핫패스는 짧고 예측 가능하게유지하고, 무거운 작업을 별도로 격리하자.
이론을 실제 개발에 녹이는 방법
병목이 CPU인지 I/O인지부터 파악하자
- `epoll`/이벤트 루프 계열은 많은 FD의 준비 이벤트를 효율적으로 처리하기 위한 도구이다.
- 반대로 CPU가 큰 작업을 이벤트 루프에서 길게 돌리면 전체 처리량이 떨어 질 수 있다.
실무적 관점에서는
- I/O바운드: 논블로킹 I/O + 이벤트 기반 + 블로킹 작업 오프로딩(스레드풀/잡큐)
- CPU바운드: 워커(스레드/프로세스)분리, 비동기 잡 처리, 요청 경로에서는 짧게 처리하고 결과는 나중에.
블로킹이 어디서 발생하는가를 추적하자
- `epoll(7)`이 edge-triggered에서 nonblocking을 권장하는 이유 자체가 “블로킹 read/write가 여러 FD를 다루는 처리 흐름을 굶길 수 있기 때문"이다.
즉, 백엔드에서 중요한건 비동기 API를 사용했느냐? 보다 아래의 내용이 더 중요하다.
- 핫패스(요청 처리 핵심 경로)에서 블로킹을 했는가?
- 블로킹이 있다면 별도 풀/별도 워커로 분리했는가?
공유 상태를 최소화하고, 공유한다면 규칙/경계를 설정하자
스레드는 주소 공간과 커널 자원을 공유한다. 그렇기때문에 아래는 언제든지 사고가 발생해도 이상하지 않은 지점으로 생각해야한다.
- 전역 캐시/맵, 싱글톤, 메모리 기반 rate limiter, 공용 큐, 커넥션 풀 등
- 체크 후 갱신 패턴: 없으면 만들고 넣기
이럴 때의 선택지는 보통 3가지이다.
- 공유 상태 자체를 제거(불변/요청 단위 상태로 이동)
- 경계를 만들어 단일 스레드/단일 워커로 직렬화(메시지 패싱 등)
- 명시적 동기화(락, 원자 연산 등)
DB의 동시성은 OS다음 단계의 병행성임을 인지하자
백엔드 개발에서 동시성 문제는 메모리에서 끝나지 않고 DB로 이어진다. DB에서 동시 접근 상황에서의 목표는 "모든 세션에 효율적 접근을 허용하면서도 엄격한 데이터 무결성을 유지"하는것에 있다. 따라서 백엔드 개발에서는 다음과 같은 설계 요소를 고려해야한다.
- 트랜잭션 경계, 격리 수준, 락과 데드락 가능성
- 애플리케이션 레벨에서의 재시도/타임아웃/순서 보장 전략
기초적인 내용만 일단 정리해보았다. 이제 추가로 학습을 하면서 궁금한 내용, 더 자세히 알아두어야할 내용, 자바-스프링에서는 어떻게 활용하는지에 대한 내용들을 공부하면서 또 정리해보려 한다.
Reference
Linux 메뉴얼: https://man7.org/linux/man-pages/man7/epoll.7.html
Java 공식 문서: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
NGINX 공식 문서: https://nginx.org/en/docs/ngx_core_module.html
'Develop > CS' 카테고리의 다른 글
| [HTTP] 쿠키 (1) | 2025.12.08 |
|---|---|
| [Web] JWT가 사용하기 어려운 이유 (0) | 2025.12.06 |
| [Web] Session vs JWT (0) | 2025.12.05 |
| [CS] 멱등성이란? (0) | 2025.12.04 |