2014년 3월 19일 수요일

Java 와 멀티쓰레드.


자바는 멀티쓰레드 코딩을 위해 만들어진 언어이다?
단지 "synchronized" 키워드 하나 추가한 것밖에 없는데 지나친 과장이 아니냐고 반문할 수 있다. 그러나 이는 멀티쓰레드 코딩의 편리성을 위해 자바가 얼마나 커다란 대가를 치르고 있는지 잘 모르기 때문일 것이다.

애초에 자바는 Process 가 지원되지 않는 저급한 RTOS 상에서 실행될 수 있도록 설계되었고, 프로세스 대신에 쓰레드 단위로 어플리케이션을 관리할 수 있어야만 했다.

이에, 자바 플랫폼에 내장된 기본 클래스(SDK)들이 멀티쓰레드 환경에서도 사용 가능할 수 있도록 다음과 같이 파격적인 설계를 가지게 되었다.

1) Java 의 모든 객체는 동기화 가능하다.
모든 자바 객체는 최소 4byte이상의 숨겨진 동기화구조체(Monitor)를 내부에 가지고 있다. (참고로 모니터는 mutex 와 condvar 를 합쳐 놓은 것과 같다.).
JVM의 메모리가 꽉 차서 GC가 발생하는 상황을 생각해보자. 자바는 모든 객체를 Heap 에 할당하기 때문에, 큰 어레이를 제외하곤 일반적인 객체의 평균 데이터 크기는 수십 byte 에 불과하다.
프로그램마다 실행 시의 평균 객체 크기는 다양하겠지만 40 byte 라면 무려 메모리의 10%, 80 byte라고 해도 5% 이상 동기화구조체를 위해 사용되는 것이다.
실로 엄청난 양이 아닐 수 없다.

2) 자바가 제공하는 대부분의 기본 클래스는 자체적으로 동기화를 처리한다. 
예전에 피쳐폰용 자바 플랫폼(MID)의 화면 출력용 클래스인 Canvas  함수들의 동기화 처리 속도를 분석해 본 적이 있다. 특정 데모 프로그램의 경우, 단지 20개 정도 함수의 동기화 처리 여부에 따라 무려 20%나 속도 차이가 발생했다.
그 외 일반 어플들의 profile 통계를 개인적으로 집계해 본 결과 CPU 타임의 평균 15% 정도가 동기화 처리에 사용되고 있었다.
당시 MIDP프로그램들은 대부분 단일 쓰레드 프로그램이었다. 그럼에도 불구하고, 약 5~10%의 메모리와 15% 정도의 CPU 타임을 동기화 처리에 사용하고 있었다.
(참고로, 근래에 자바의 기본 Collection 클래스들의 함수는 동기화처리가 생략되어 있다. 대신 별도의 Wrapper 클래스를 통해 동기화처리를 제공하도록 변경되었으므로 위의 통계치는 다소 달라졌을 것이다.)
가용 메모리가 줄어든 만큼 GC도 더욱 자주 발생해서 퍼포먼스 문제는 가중될 수밖에 없다.
Java 언어 설계 당시 모든 클래스에 동기화 기능을 넣는 문제로 관련자들이 얼마나 심사숙고했을지 능히 짐작할 수 있는 부분이다.

3) 가장 단순하고 쉬운 동기화 방식을 지원한다.
최근에는 세마포어나 AtomicValue 뿐 아니라 병렬처리용 클래스 들도 자바 플랫폼에 많이 추가되었다. 그러나 이들 클래스는 일종의 확장 utility에 해당하고, 여전히 자바가 제공하는 기본적인 동기화 방법은 synchronized 키워드를 사용하는 것이다.
특정 함수 또는 특정 block 을 synchronized 영역으로 선언하면, JVM 내부에서 동기화를 위한 lock 과 unlock 을 자동으로 처리한다. 즉, 프로그래머의 실수에 의해 동기화 설정 및 해제가 잘못될 가능성이 전혀 없다.
이 외에 최상위 클래스인 Object 에 추가된 동기화 관련 API 5개와, Thread 클래스에 속한 10여개 API가 자바로 멀티쓰레드 코딩을 하기 위해 알아야 할 전부이다. 더 이상 없다. 이 얼마나 단순한가!
물론, 해당 기능 정도는 C++ 로도 충분히 동일하게 구현할 수 있고 상황에 맞춰 훨씬 더 최적화할 수 있을 것이다. 그렇다면 자바는 메모리와 속도 감소를 감수하고, 단지 동기화 관련 API 를 좀 더 쉽게 쓸 수 있게 한 것에 불과할까?
메모리와 속도 문제는 애초 자바의 출발점이었던 임베디드 환경에서는 훨씬 더 치명적인 문제이다. 그런 큰 희생은 쉽고 단순함 그 이상의 의미를 위한 것일 것이다. 그건 바로 자바를 최상의 멀티쓰레드 개발 플랫폼으로 만들기 위한 것이었음을 이어서 이야기하고자 한다.


4) Java 는 클래스 단위로 내부 데이터를 동기화한다.자바의 제공하는 모든 기본 클래스는 그 내부 데이터 동기화를 스스로 처리한다. 또한 이 원칙은 어플리케이션 개발 시에도 그대로 적용될 수 있다. 모든 객체가 동기화 처리가 가능한 구조이기 때문이다. 이는 멀티 쓰레드 시스템 개발시 매우 큰 차이를 만들어낸다.

이런 동기화 기능을 내장하지 않는 언어 사용시에는 각 객체를 사용할 때마다 동기화처리를 각각 해주어야만 하며, 그 중 단 하나라도 실수한다면 해당 프로그램은 정상적인 수행이 불가능해진다. C/C++ 코딩 시 자바처럼 객체 단위로 동기화 범위를 잘게 나누어 많은 수의 동기화 객체를 다루는 것은 거의 불가능에 가까운 난해한 코드가 될 것이다.

좁은 의미에서 멀티쓰레드 프로그램 개발 용이성이란, 단일 쓰레드용으로 개발된 코드를 멀티쓰레드용으로 변경하는 것이 얼마나 쉬운가를 나타낸다고 할 수 있다.

C/C++ 의 경우, 이 문제는 정말 난해한 것이다. 구조체를 다루는 거의 모든 함수가 동기화 처리가 되어 있지 않을 뿐 아니라, 어떻게 동기화 기능을 넣을지 암담할 것이다. 특히 다른 쓰레드와 공유된 객체의 메모리 해제 시점을 정확히 정하는 것도 큰 문제가 된다. 이에 제한된 코드 영역을 따로 나누어 쓰레드화하게 되는데, 만일 해당 쓰레드가 사용하는 코드와 구조체가 명확하고 양이 적지 않다면 엄두를 낼 수 없는 일이 되고 만다.

자바의 경우, 기본 클래스는 이미 동기화 처리가 되어 있고, 새로 추가한 클래스들도 함수명 앞에 synchronized 란 키워드만 추가하여 쓰레드 동기화 문제를 쉽게 처리할 수 있다. 정확히 말해 그 이상 할 수 있는 것이 아예 없다. 또한, 특별히 메모리 관리에 따로 신경 쓸 필요도 전혀 없다.
단지 멀티 쓰레드 환경에 맞게 기존 코드를 변경하는 문제만 비교한다면 자바와 필적할 만한 다른 플랫폼을 찾기 어려울 것이다.

실제로 멀티쓰레드 프로그램 개발이 어려운 이유는 동기화 API 사용법 때문이 아니라 디버깅이 어려운 것이 주 요인이다. 관련 버그는 재현도 어렵고, 추적도 매우 어렵다.

자바의 단순함이 빛을 발하는 것은 이때이다. 소스 코드만 읽어서 멀티쓰레드 관련 버그를 찾아내는 것이 다른 언어보다 월등하게 쉬운 것이 자바이다. 그것은 관련 구조의 간명함 때문에 가능해진다.

5) 결론: 자바는 쉬운 멀티쓰레드 코딩을 위해 만들어진 언어이자 플래폼이다.
병렬 컴퓨팅이란 예외적인 연구 분야를 제외하면, 일반적인 멀티쓰레드 코딩이란 펑펑 노는 CPU 자원을 효율적으로 나눠쓰도록 코딩하는 것을 말한다.
대부분의 인터넷 서버가 이에 해당하는데, DB 서버 연결을 포함한 네트웍 통신 속도나 하드디스크 IO속도가 CPU 보다 수백만배 이상 느리다. 이런  서버 환경에서는 자바의 실행 속도가 전혀 문제되지 않는다. 한 트랜젝션을 수행하는 동안 실제 CPU 동작 시간은 1~2%도 채 되지 않으므로 멀티쓰레드를 적절히 사용하여 서버 가용성을 높히는 것이 중요하기 때문이다.

이러한 멀티쓰레드 아키텍쳐 설계 시 반드시 필요한 두 가지 구조체가 있는데, 그 둘은 버퍼와 큐이다.
버퍼는 실행 처리 속도가 다른 두 쓰레드 간에 데이터를 교환하기 위한 수단이고, 큐는 서로 개수가 다른 입력 쓰레드들과 출력 쓰레드들간에 m:n 방식의 태스크 데이터를 공유하는 방식이다.
버퍼는 단일 쓰레드 환경에서도 사용되는 범용적인 것이지만 큐는 멀티쓰레드 환경에 최적화된 시스템에서 주로 사용된다. 쓰레드 수가 어느 한계선 이상 많아지면 그 자체만으로 컨텍스트 스위칭 부하가 더 커지고, 동기화 충돌이 자주 일어나 오히려 처리 속도가 더 감소하는 문제가 발생할 수 있다. 쓰레드 풀과 큐를 이용하면 이러한 문제를 적절히 처리할 수 있다.

큐나 버퍼에 넣을 데이터 생성 및 처리 시간이 매우 짧지 않는 한, 데이터 전달 시의 동기화 처리 속도는 문제가 되지 않는다. 결과적으로 멀티쓰레드 시스템의 효율성은 동기화 관련 API 의 속도가 아니라 관련 아키텍쳐의 효율성에 의해 크게 좌우되게 된다.

즉, 멀티쓰레드 코딩의 편리성은 아키텍쳐 변경의 용이성과 밀접한 연관성을 가진다.

Java 는 어떤 언어보다 리팩토링이 쉽다. 자바의 동기화는 늘 객체 단위로 처리되며 한 함수 또는 한 함수 내의 일정 영역 내에서만 이루어지기 때문이다. 이러한 단순함이 다소 불편할 수도 있으나 대신에 디버깅과 리펙토링 부분에서 큰 보상을 얻을 수 있다.

C/C++로 작성된 멀티쓰레드 서버를 예를 들어보자. 그러저럭 베타 수준 작업이 된 상태에서 누군가 아키텍쳐 개선 얘기를 꺼낸다면 다른 개발자의 반응이 어떨까? 이것이 가장 핵심적이고 심각한 문제이다. 만약 매우 중요한 제안이었음에도 불구하고 단지 위험을 회피하고, 그대로 출시하였다면, 결국 그 상태에서 코드에 더께가 계속 쌓여 나중엔 조금만 수정해도 문제가 발생하는 최악의 상황으로 치달을 수 있다.

단지 멀티쓰레드 코딩이 쉽다고해서 해당 언어가 인터넷 서버 개발에 적합하다고 말하는 것은 성급할 것이다. 그러나 근래엔 어떠한 서버 프로그램이든 그 서비스가 성공적으로 운영되는 시점에는 거의 필수적으로 멀티쓰레드 아키텍쳐를 필요로 하게 되었다. 만약 선호도나 숙련도의 문제를 떠나 여러 언어 중에 하나를 서버 개발용으로 선택하고자 한다면, 개인적으로는 자바를 강권하고 싶다.
개인적인 좁은 경험 내에서 아래의 내용은 사실이기 때문이다.
“멀티쓰레드를 많이 사용하는 인터넷 서버는 자바 언어를 쓸 때 개발 속도가 빠르며, 그 성능도 더 빠르다”.

댓글 1개:

  1. C++과 Java의 Multi Thread 예제가 있으면 더욱더 비교가 잘 될듯...
    그리고 Multi Thread에서 synchronized는 당연히 Processing block을 나타내는 거지만 object class에 있는 notify와 wait는 Multi thead나 java를 처음 다뤄보는 사람들에게 조금 생소한 개념입니다. 이거 설명하는 좋을 듯.

    답글삭제