17 Oct 2017

새로운 프로그래밍 모델의 필요성

링크를 참고하여 작성하였습니다.

현대의 서비스들은 수요에 비례하여 많은 분산 시스템을 구축하고 있고, 그 과정에서 전통적인 객체지향 프로그래밍으로 해결되지 않는 문제가 발견되었습니다. Java와 같은 OOP 언어가 세상에 나온지 20년이 되었고 컴퓨팅 환경과 사용자 수요가 많이 변화하였기 때문입니다. 현대의 시스템은 여러개의 CPU를 보유하고 있으며 모든 스레드를 효율적으로 사용하는 동시성 프로그램이 개발이 요구됩니다.

기본적으로 공유자원 접근시 발생할 수 있는 문제는 레이스 컨디션, 메모리 가시성, 데드락 등이 있습니다.

캡슐화의 문제

첫번째 주제는 캡슐화(encapsulation)의 문제점 입니다. 객체 지향 프로그래밍의 핵심 객념중 하나로, 외부에서 직접적으로 내부 데이터에 접근하는 것을 막고, 잘 설계된 메서드를 통해 접근하게 하는 것 입니다. 객체(Object)는 안전한 연산을 제공하여 내부 데이터를 지킬 의무가 있습니다. 예를들어, 정렬된 이진 트리의 어떤 연산에 의해 임의로 순서가 변경되어서는 안됩니다. 보통 호출자는 이러한 정렬 불변성을 믿고 트리를 사용합니다.

다음과 같이 동작하는 프로그램이 있습니다.

//첫번째 스레드

obj1.callA(); //내부적으로 a.call() 호출

...

//두번째 스레드

obj3.callA(); //내부적으로 a.call() 호출

...
//메시지 시퀀스 차트를 그려보면 쉽게 확인할 수 있습니다.

첫번째 스레드와 두번째 스레드의 a.call() 호출이 거의 겹친다고 가정했을때, 캡슐화 모델은 이런 상황에서 아무것도 보장할 수 없습니다.

두개의 호출의 명령어는 서로 교차로 실행 되고 자료구조의 불변성을 유지하기 어렵게 만듭니다. 스레드가 3개 이상이라면 상황은 더욱 더 복잡해집니다. 일반적인 해결책은 메서드에 잠금을 추가하여 해결하는 것 입니다. 스레드가 순서대로 1개씩 메서드에 진입할 수 있습니다. 이 해결책에는 크게 2가지의 문제점이 있습니다.

  • 잠금은 동시성을 제한하고 멀티 CPU를 제대로 활용하지 못합니다. 컨텍스트 스위칭 비용도 발생하게 됩니다.
  • 호출자 스레드는 블로킹이 되며 다른 의미있는 작업을 할 수 없습니다. 예를들면, UI 스레드는 UI 관련 작업만 수행해야 합니다.
  • 데드락 문제를 발생시킬 수 있습니다.
  • 일반적인 잠금은 분산환경을 잘 지원할 수 없습니다. 일반적으로 분산 잠금은 스케일 아웃을 제한하고 여러대의 머신을 왕복하면서 네트워크 지연이 높아지게 됩니다.

잠금 사용이 최선은 아닙니다.

  • 잠금을 사용하면, 성능 문제와 데드락 문제가 발생
  • 잠금을 사용하지 않으면 상태가 오염되는 문제가 발생

공유 메모리에 대한 환상(illusion)

80~90년대의 프로그래밍 모델에서 변수에 값을 쓰고 메모리에 바로 반영되는것이 개념화되었습니다. 하지만 현대 아키텍처에서는 CPU는 메모리 대신 캐시(cache)에 값을 씁니다. 각 캐시들은 각 CPU 코어에 연결되어 있고, 캐시에 쓰인 값은 다른 코어에 보이지 않습니다.

JVM 에서는 스레드간에 같은 값을 볼 수 있도록 volatile이나 Atomic을 사용할 수 있습니다. 그렇지 않으면 잠금을 얻는 구간에서만 같은 값을 볼 수 있습니다. 모든 변수를 volatile로 선언하면 값을 매번 다른 메모리에 값을 기록해야 하므로, 매우 많은 비용이 발생합니다. (병목구간 발생이 발생한다고 볼 수 있습니다.)

공유 메모리에는 환상이 있습니다.

  • 멀티 CPU 환경에서 캐시에 대한 가시성은 보장되지 않습니다.

콜 스택에 대한 환상(illusion)

개발자들은 콜스택을 아무런 의심없이 사용하고 있습니다. 하지만 콜스택은 멀티 CPU가 없던 시절에 개념화 되었고, 다른 스레드의 스택을 넘나들 수 없으며, 비동기 콜 체인을 사용할 수 없습니다. 특히, 현재 하는 작업을 다른 스레드에 위임 할때 문제가 발생합니다. 간단히 메서드나 함수 호출로 다른 스레드에 위임할 수 없습니다.

일반적으로 위임할 작업을 공유 자료구조에 넣고 이벤트 루프에서 읽어가서 처리하도록 구현해야 합니다. 호출자 스레드는 작업을 워커 스레드에 위임한 뒤 다른 작업을 이어갈 수 있습니다.

이 방식의 관심 중 하나는 작업의 완료, 예외를 호출자 스레드에 전달하는 방식 입니다. 다른 스레드의 스택을 조작하거나 할 수 없으니, 이는 오직 부채널을 통해서만 가능할 것 같습니다. 예를들면 에러 코드를 호출자 스레드가 기대하는 위치에 넣어주는 것 입니다. 만약 어떤 버그로 인해 알림이 오지 않는다면 호출자는 실패 알림을 받지 못하고, 작업의 결과는 손실되게 됩니다.

재시도 역시 쉽지 않은데 예외의 전파로 인해 스택이 풀려서, 메시지가 완전히 유실될 수 있기 때문입니다. 이것은 네트워크로 연결된 시스템간에 메시지가 손실됐을때의 상항과 유사합니다.

  • 현대의 시스템에 알맞는 동시성과 성능을 갖추기 위해, 블로킹없이 효율적인 방식으로 작업을 위임하여야 합니다.
  • 이러한 동시성 시스템에서는 예외를 해결할 수 있는 잘 정립된 방식이 있어야 합니다. 작업이 매우 오래걸릴 수 있고, 재시작 이후에 메시지가 손실될 수 있다는 사실 역시 깨달아야 합니다.

참조


Tags:
0 comments