실용주의 프로그래머 6
동시성
동시성은 둘 이상의 코드 조각이 실행될 때 동시에 실행중인 것처럼 행동하는 것이다. 그리고 병렬성이란 실제로 동시에 실행되는 것이다.
시간적 결합 깨트리기
시간적 결합이 무엇인지부터 알아야 한다. 시간적 결합은 순차적으로 실행되는 플로우가 코드와 결합되어 있는 것을 의미한다. 이런 접근 방식으로 개발을 하면 유연하지 않고, 현실과도 동떨어져 있다. 즉 시간적 결합을 깨트림으로서 동시성을 확보하고, 이를 통해서 분석하기 쉽고 안정적인 시스템을 만들 수 있다.
가장 먼저 작업 흐름을 분석하면서 동시에 일어나도 되는 것과 순서대로 일어나야 하는 것을 찾아야 한다. 활동 다이어그램을 통해서 동시에 작업할 수 있는 부분을 확인하고 해당 부분의 동시 수행으로 더 생산적인 프로그램을 만들 수 있을 것이다.
공유 상태는 틀린 상태
공유 상태는 틀린 상태다.
공유 상태는 원자성을 해친다. 이를 방지하기 위해서는 세마포어나 상호 배제 방법을 활용할 수 있다. 세마포어를 통해서 멀티 쓰레드에서 공유되는 자원에 대해 한 번에 하나의 쓰레드만 접근할 수 있도록 할 수 있다.
1
2
3
4
5
6
7
case_semaphore.lock();
// 잠금 획득으로 로직 수행
case_semaphore.unlock();
이 잠금 획득 로직은 로직의 수행 중 발생하는 예외에 대응할 수 있도록 항상 unlock 을 수행해줘야 한다.
1
2
3
4
5
6
7
try {
... logic
} ensure {
case_semaphore.unlock();
}
이처럼 공유 메모리는 동시성 문제의 원인이 되는데, 메모리 뿐 아니라 파일, 데이터베이스, 외부 서비스 등 어떤 리소스에 동시에 접근하는 것 자체로 잠재적인 문제가 발생할 수 있다. 만약 불규칙한 실패가 발생한다면 동시성 문제인 경우가 많다.
액터와 프로세스
액터와 프로세스를 사용하면 공유 메모리 접근에 대한 동기화 걱정 없이 동시성을 구현할 수 있다.
- 액터는 자신만의 비공개 지역 상태를 가진 독립적인 가상 처리 장치다. 각 액터는 우편함을 갖고 있는데, 우편함에 메시지가 도착하면 엑터는 메시지를 처리한다.
- 액터는 자율적이다. 메시지에 따라서 적절한 업무를 수행하며, 해당 업무 처리가 끝나면 다른 액터를 생성하거나, 다른 액터에게 메시지를 보낸다. 액터는 한 번에 하나의 메시지만 처리한다.
Node.js 용 액터 라이브러리인 Nact 를 활용해서 액터를 구현해보자. 메시지 흐름은 다음과 같다.
- 고객이 배고프다는 신호를 느끼고, 종업원에게 파이를 주문한다.
- 종업원은 고객에게 파이를 주라고 진열장에게 요청한다.
- 진열장에 파이가 있으면 파이 한 조각을 고객에게 보낸다. 종업원에게도 계산서에 추가하라고 알려준다.
- 파이가 없으면 종업원에게 알려준다. 종업원은 고객에게 양해를 구한다.
고객부터 시작해보자. 고객은 3가지 메시지를 받는다.
- 배고프다는 신호
- 테이블에 파이가 있다(진열장이 보냄)
- 파이가 다 떨어졌다(종업원이 보냄)
1
2
3
4
5
6
7
8
9
const customActor = {
'파이가 먹고 싶다': (msg, ctx, state) => {
return dispatch(state.waiter, {type: '주문', customer: ctx.self, wants: '파이'})
}
// 테이블에 놓다 메시지 수신 시 로직
// 남은 파이 없음 메시지 수신 시 로직
}
파이가 먹고 싶으면 종업원에게 메시지를 보낼 것이다.
1
2
3
4
5
6
7
8
9
const waiterActor = {
'주문': (msg, ctx, state) => {
if (msg.wants = '파이') {
dispatch(state.pieCase, {type: '파이 꺼내기', customer: msg.customer, wailter: ctx.self})
}
}
}
종업원은 주문 메시지를 고객에게 받는 순간, 파이 케이스에게 파이를 꺼내달라고 요청한다. 이 때 고객과 자신의 참조를 보낸다.
1
2
3
4
5
6
7
8
9
10
const pieCaseActor = {
'파이 꺼내기': (msg, ctx, state) => {
if (states.slice.length > 0) {
var food = state.slices.shift() + ' 파이 한 조각';
dispatch(msg.customer, {type: '테이블에 파이 추가', food: food});
}
}
}
진열장은 진열된 파이 조각이라는 상태를 가지고 있다. 해당 상태로, 남은 파이 조각을 확인해서 고객의 테이블에 파이를 제공할 것이다. 액터 모델에서는 동시성을 다루는 코드를 쓸 필요가 없다. 공유된 상태가 없기 때문이다. 그저 수신하는 메시지에 따라서 알아서 실행된다.
칠판
칠판 접근 방법은 형사들이 사건을 수사하기 위해 칠판을 만드는 방법에서 착안한 것이다. 형사들은 알아서 정보를 수집하고, 해당 정보를 칠판에서 수합한다. 일종의 자유방임주의 동시성이다. 컴퓨터 기반의 칠판 시스템은 해결해야 할 문제의 규모가 크고 복잡한 인공 지능에서 사용되었다.
칠판 시스템을 법적 요구 사항을 캡슐화하는 규칙 엔진과 함께 사용한다면, 어려운 문제에 대해서 적절한 규칙이 발동되도록 함으로써 결과에 대한 피드백을 쉽게 확인할 수 있다.
메시지 시스템은 칠판과 유사하다. 이벤트 로그의 형태로 영속성을 제공하고, 패턴 매칭 형태로 데이터를 가져오게 한다. 일종의 칠판 역할을 하는 것이다.