이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/benefits.html



멀티쓰레딩이 그 위험요소에도 불구하고 여전히 사용되는 이유는 얻을 수 있는 장점이 있기 때문이다. 그 중 몇가지는 다음과 같다.

  • 향상된 자원 활용성
  • 상황에 따른 더 간단한 프로그램 디자인
  • 프로그램의 반응성 향상

향상된 자원 활용성


로컬 파일 시스템의 파일을 읽고 처리하는 어플리케이션을 생각해보자. 디스크에서 파일을 읽는 데에 5초가 소요되고, 이를 처리하는 데에는 2초가 소요된다. 2개의 파일을 가공한다면 다음과 같은 시간이 소요된다.


  5 seconds reading file A
  2 seconds processing file A
  5 seconds reading file B
  2 seconds processing file B
-----------------------
 14 seconds total


읽기 작업에서는, 디스크가 데이터를 읽는 것을 기다리는 데에 CPU의 시간 대부분을 소모한다. 이 시간동안 CPU는 일하지 않는다. 이건 다른 것이 될 수 있다. 작업의 순서를 바꾼다면, CPU의 효율성은 더 나아질 수 있다.


  5 seconds reading file A
  5 seconds reading file B + 2 seconds processing file A
  2 seconds processing file B
-----------------------
 12 seconds total


CPU는 첫 번째 파일이 읽혀지길 기다린다음 두 번째 파일을 읽기 시작한다. 두 번쨰 파일이 읽혀지는 동안, CPU 프로세스들은 첫 번째 파일을 처리한다. 여기서, 디스크에서 파일이 읽히는 동안 CPU는 거의 놀고 있다는 점을 기억하라.

보통의 경우, CPU는 입출력을 기다리는 동안 다른 작업을 할 수 있는데, 이건 디스크 입출력 뿐만 아니라 네트워크 입출력 혹은 사용자의 입력의 경우도 마찬가지이다. 네트워크와 디스크 입출력은 CPU와 메모리 입출력보다 상당히 느리다.


상황에 따른 더 간단한 프로그램 디자인


 당신이 싱글쓰레드 어플리케이션에서 위와 같은 작업 순서로 읽기와 처리 작업을 직접 한다면, 각 파일에 대한 읽기/처리 작업 상태를 계속 파악하고 있어야 할 것이다. 한 파일에 대한 읽기/쓰기 작업을 두 개의 쓰레드에게 각각 맡긴다면, 이 쓰레드들은 디스크가 파일을 읽어올 때까지 블록 상태에 놓일 것이다. 이 디스크의 작업을 기다리는 동안, 다른 쓰레드들은 파일의 이미 읽혀진 부분들에 대한 처리 작업을 위해 CPU를 사용할 수 있다. 이 과정은 디스크가 파일을 읽어 메모리에 적재하기 위해 계속해서 가동되는(kept busy at all times) 결과를 낳는다. 이건 디스크과 CPU 모두에게 더 효율적인 운용 방법이 된다. 각 쓰레드는 한 파일에만 집중할 수 있기에, 이는 또한 프로그램에게도 더 쉬운 것이다.



프로그램의 반응성 향상


 싱글쓰레드를 멀티쓰레드로 바꾸는 또다른 목적은 어플리케이션의 더 나은 반응성을 위해서이다(to achieve a more responsive). 몇 포트로 사용자의 요청을 받는 서버 어플리케이션을 생각해보자. 하나의 요청이 오면 어플리케이션은 이를 처리하고 대기 상태로 돌아간다(listening). 이런 서버의 동작은 다음과 같다.


 
  while(server is active){
    listen for request
    process request
  }


한 요청이 처리되는 데에 긴 시간이 소요되면, 그동안 다른 새로운 클라이언트의 요청을 받을 수가 없다. 서버가 요청 대기상태여야만 클라이언트의 요청은 처리될 수 있다.

이를 대체할 수 있는 디자인은 요청 대기 쓰레드가 워커 쓰레드에게 요청을 맡기고 자신은 즉각 대기 상태로 돌아가는 형태가 될 수 있다. 워커 쓰레드는 요청을 처리하고 클라이언트에게 결과를 전송한다. 이 디자인은 다음과 같다.


  while(server is active){
    listen for request
    hand request to worker thread
  }


이 디자인에서 서버 쓰레드는 더 빠르게 대기 상태로 회귀할 것이고, 클라이언트들은 서버에게 요청을 보낼 수 있다. 서버의 반응성이 향상된 것이다.

데스크톱 어플리케이션의 경우도 같다. 당신의 클릭 버튼이 긴 작업 시간을 요구한다면, 그리고 이 작업의 쓰레드가 윈도우나 버튼을 업데이트하는 쓰레드라면, 어플리케이션은 이 작업 동안 반응할 수 없는 상태에 될 것이다. 여기서 이 작업이 워커 쓰레드로 전달된다면 워커 쓰레드가 작업을 처리하는 동안 윈도우 쓰레드는 유저의 요청을 받을 수 있다. 워커 쓰레드가 작업을 마치면 윈도우 쓰레드에게 신호를 보낸다. 윈도우 쓰레드는 어플리케이션 윈도우에 작업 결과를 업데이트한다. 워커 쓰레드가 존재하는 프로그램 디자인의 반응성은 보다 향상될 것이다.



'Java > Concurrency' 카테고리의 다른 글

자바 쓰레드 시작하기  (0) 2017.04.09
컨커런시 vs. 페러럴리즘  (0) 2017.04.09
컨커런시 모델  (0) 2017.04.09
멀티쓰레딩의 단점  (0) 2017.04.09
자바 컨커런시 / 멀티쓰레딩 튜토리얼  (0) 2017.04.09

이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/index.html



 오래전 컴퓨터는 단일 CPU로 가동되고 한번에 한 프로그램만 실행할 수 있었다. 후에 멀티태스킹이 가능한 컴퓨터가 출현하였고, 여러 프로그램(작업 or 프로세스)을 동시에 실행하는 일이 가능해졌다. 사실 이 멀티태스킹은 엄밀히 말하면 '동시에' 실행되는 것이 아니었다. 여러 프로그램이 하나의 CPU를 공유하였고, 오퍼레이팅 시스템이 프로그램들 사이에서 아주 짧은 시간을 두고 포커스를 전환(switching)하는 원리였다.


멀티태스킹의 출현은 소프트웨어 개발자들에게 새로운 과제를 주었다. 하나의 프로그램이 더이상 CPU의 시간과 메모리, 리소스를 독점할 수 없게 되었고, '좋은 프로그램(A "good citizen" program)'이란 다른 프로그램들이 CPU의 자원을 활용할 수 있도록, 사용한 자원을 해제할 수 있는 것이어야 한다. 


지금의 멀티태스킹은 다수의 쓰레드가 같은 프로그램 안에서 실행되는 것이다. 한 쓰레드의 실행은 한 CPU가 실행중인 하나의 프로그램이라 할 수 있다. 당신이 한 프로그램을 실행하는 멀티쓰레드를 가질 때, 이것은 마치 다수의 CPU가 하나의 프로그램을 실행하는 것과 같은 일이 된다.


 어떤 프로그램들에게 멀티쓰레딩은 성능 향상에 있어 훌륭한 방법이 된다. 그러나, 멀티쓰레딩은 멀티태스킹과 다르고, 더 어려운 일이기도 하다. 하나 이상의 쓰레드는 한 프로그램 안에서 실행될 수 있고, 이런 이유로 읽기와 쓰기 작업이 같은 메모리에 동시에 발생된다. 이 현상은 싱글쓰레드 프로그램에선 보지 못했던 에러를 뱉어낼 수 있다. 이러한 에러들 중 몇몇은 싱글 CPU 머신에서는 좀처럼 보이지 않는데, 왜냐하면 이런 경우에는 두개의 쓰레드가 '동시에' 실행되는 일은 절대로 일어나지 않기 때문이다. 요즘의 컴퓨터는 멀티코어 CPU를 가지고 나오며, 아예 CPU 자체가 두 개 이상인 것들도 존재한다. 이것이 의미하는 바는 별개의 쓰레드들이  별개의 코어 혹은 CPU에서 동시에 실행될 수 있다는 것이다.



만일 하나의 쓰레드가 어떤 하나의 메모리 영역을 읽는데 다른 쓰레드가 이 영역에 쓰기 작업을 한다면, 첫번째 쓰레드가 읽게 되는 값은 어떤 것일까? 쓰기 작업 이전의 값? 두번째 쓰레드가 쓰기 작업을 마치고 난 다음의 값? 두가지가 섞인 어떤 값? 아니면, 두 쓰레드가 동시에 한 메모리 영역에 쓰기 작업을 한다면? 이 작업 뒤에는 어떤 값이 남겨질까? 첫번째 쓰레드의 값? 두번째 쓰레드의 값? 두 값이 섞인 값?


적절한 예방책이 없다면 이런 상황은 얼마든지 가능하며, 그 결과는 예상할 수 없을 것이다. 이 결과는 때에 따라 바뀔 수 있는 것이다. 때문에 개발자가 적절한 예방책을 숙지하는 것은 중요한 일이다 - 적절한 예방책을 숙지하는 일이란, 메모리, 파일, 데이터베이스 기타 등등 과 같은 공유된 자원을 쓰레드가 어떻게 접근하는지 컨트롤하는 방법을 배우는 것이다. 이것이 이 자바 컨커런시 튜토리얼이 다루는 토픽 중 하나이다.



자바의 멀티쓰레딩과 컨커런시


 자바는 개발자가 멀티쓰레딩을 쉽게 사용할 수 있게끔 만들어진 초기의 언어들 중 하나였다. 자바는 애초부터 멀티쓰레딩 능력을 가지고 있었는데, 때문에 자바 개발자들은 위에서 설명한 문제점들을 자주 직면한다. 이것이 내가 이 자바 컨커런시 코스(trail)를 작성이는 이유이다. 내가 그랬던 것처럼, 자바 개발자라면 누구나 이 코스에서 도움을 받을 수 있다.


이 코스는 주로 자바에서의 멀티쓰레딩에 관하여 다룰 것이지만, 멀티쓰레딩에서의 문제점들 중 몇가지는 멀티태스킹이나 분산처리 시스템 환경에서 발생되는 문제점들과 비슷하다. 때문에 멀티태스킹과 분산처리 시스템에 대한 레퍼런스도 존재할 것이다. 이런 이유로 '멀티쓰레딩' 보다는 '컨커런시' 가 더 어울린다.



2015년, 그리고 이후의 자바 컨커런시


 자바 컨커런시 서적이 처음 쓰여지고 자바 5 컨커런시 유틸리티가 배포된 이래로 컨커런트 아키텍처의 세계에는 많은 일들이 있었다. Vert.x와 Play / Akka and Qbit 과 같은 새로운, 비동기적인 'shared-nothing' 플랫폼과 API들이 나왔다. 이 플랫폼들은 스탠다드 자바/JEE의 쓰레딩 컨커런시 모델과는 다른 컨커런시 모델을 사용한다. 새로운 non-blocking 컨커런시 알고리즘이 발표되었고, LMax Disrupter 와 같은 새로운 non-blocking 툴들이 우리의 툴킷에 추가되었다. Fork/Join 프레임워크와 함께 새로운 함수형 프로그래밍 병행 구조가 자바 7 과 자바 8 의 컬렉션 스트림 API에 소개되었다.


나는 이 모든 새로운 개발물들과 함께 이 컨커런시 튜토리얼을 업데이트한다. 그러니까, 이 튜토리얼은 진행중 에 있다. 새 튜토리얼은 시간이 허락될 때면 언제든 개재될 것이다.



'Java > Concurrency' 카테고리의 다른 글

자바 쓰레드 시작하기  (0) 2017.04.09
컨커런시 vs. 페러럴리즘  (0) 2017.04.09
컨커런시 모델  (0) 2017.04.09
멀티쓰레딩의 단점  (0) 2017.04.09
멀티쓰레딩의 장점  (0) 2017.04.09

 초기의 아스키는 각 문자를 7비트로 표현하여 총 128개의 문자를 처리할 수 있었다. 그리고 후에 추가적인 문자를 지원해야 할 필요가 있어 여기에 1비트를 추가한 확장 아스키(extended ASCII)가 등장한다. 8비트를 사용하는 이 확장 아스키는 256개의 문자를 표현할 수 있다. 처리해야 할 문자의 종류가 지금처럼 다양하지 않았던 시절, 문자 표현은 이 아스키만으로 충분했다. 오늘날의 글로벌 커뮤니케이션이 예상되지 않았던 시기의 일이다. 이 때는 영어, 아랍어, 중국어 등 다양한 언어가 한 문서에 존재하는 일은 흔치 않았다.


세상에는 수많은 종류의 언어를 사용하는 사람들이 존재하고, 이들 중 라틴계열 문자를 사용하지 않는 사용자는 거의 절반 이상이 될 것이다. 이렇게 다양한 사용자에 대응하는 소프트웨어를 개발하는 데 있어 오직 자신이 사용하는 언어만을 취급한다면 이는 사려깊지 못한 일이다.


이러한 이유로 모든 언어를 수용할 수 있는 문자셋의 필요성이 대두되어 탄생한 것이 유니코드이다. 유니코드는 모든 문자에 대해 각각의 유니크한 번호를 부여하였는데, 이를 코드 포인트라 한다. 유니코드의 이점 중 하나는 처음 256개의 코드는 ISO-8859-1 과 동일하다는 것이다. 고로, 이는 아스키와 동일하다. 그리고 널리 쓰이는 문자들 중 절대 다수의, 엄청나게 많은 문자들이 단 2바이트만으로 표현될 수 있다(Basic Multilingual Plane, BMP). 이제 문자셋에 접근하기 위해 필요한 인코딩에 대해 이야기한다. 여기서는 UTF-8, UTF-16 에 대해 다룰 것이다.



메모리 관련


어떤 종류의 문자가 얼마만큼의 바이트를 필요로 하는가


UTF-8:

1 바이트: 표준 아스키

2 바이트: 아랍, 히브리, 대부분의 유럽계(조지안 문자를 제외한)

3 바이트: BMP

4 바이트: 모든 유니코드 문자


UTF-16:

2 바이트: BMP

4 바이트: 모든 유니코드 문자


여기서 BMP 에 속하지 않은 문자들에는 고대 문자, 수학/음악 기호, 중국/일본/한국 문자 중 몇몇 드물게 보이는 문자들이 포함된다.


만약 대부분의 작업에 아스키 문자를 사용한다면 UTF-8은 메모리에 있어 확실히 더 나은 효율성을 제공한다. 그러나 비 유렵계 문자를 사용한다면, UTF-16 이 UTF-8보다 최대 1.5배 나은 메모리 효율성을 보인다. 이런 사항은 웹페이지나 큰 사이즈의 워드 문서와 같은 거대한 양의 텍스트를 처리할 때 성능에 영향을 줄 수 있다.



인코딩에 있어


UTF-8: 표준 아스키(0-127)와 UTF-8 코드는 완전히 동일하다. 이는 기존 아스키 텍스트의 호환성을 고려해야 할 때 UTF-8이 이상적으로 꼽히는 이유가 된다. 이 밖에 다른 문자들은 2-4 바이트를 필요로 한다. 이 문자들에 대한 처리는 각 바이트의 일부 비트를 예약하는 방법으로 수행되는데, 이는 멀티 바이트 문자의 일부임을 나타내기 위함이다. 특히 아스키 문자와의 충돌을 피하기 위해 각 바이트의 첫 번째 비트는 1이다.


UTF-16: UTF-16의 BMP 문자 표현법은 단순하게도 코드 포인트로 이루어진다. 이에 비해 BMP 외 문자에 대한 표현은 좀 더 복잡한데, 여기에는 surrogate pairs 라 불리는 방법을 이용한다. 유니코드의 2바이트 기본 범위(BMP) 영역을 넘어선 문자들을 Supplementary Characters 라 하는데, surrogate pairs는 이 문자들을 표현하기 위해 도입된 방법이다. surrogate pairs는 두 쌍의 2바이트를 이용하여 2바이트를 넘어서는 문자를 표현한다. 이 2바이트 코드는 BMP 범위에 포함되지만, 유니코드 표준에 의해 BMP 문자가 아님을 보장받는다. 그리고 UTF-16은 기본 단위를 2바이트로 가지기 때문에 엔디안의 영향을 받는다. 이를 보완하기 위해 엔디안을 나타내는 데이터 스트림의 시작부에 예약된 바이트 순서 마크가 위치한다. 고로 엔디안이 지정되어 있지 않은 UTF-16 입력을 읽을 때는 이를 확인할 필요가 있다.


이처럼 UTF-8과 UTF-16은 서로 호환되지 않는다. 입출력을 처리할 때는 어떤 인코딩을 사용하고 있는지 정확히 알 필요가 있다. 



프로그래밍에서의 고려사항


Character 와 String 데이터 타입: 사용하는 프로그래밍 언어에서 어떤 방식으로 인코딩을 하는지가 중요하다. raw 바이트를 비 아스키 문자로 출력하려고 한다면 문제가 발생할 수 있다. 또한 사용하는 문자 타입이 UTF 기반이라고 해도 이것이 알맞는 UTF 문자열을 보장해주지는 않는다. 문자열은 적합하지 않은 바이트 시퀀스를 허용할 수 있다. 일반적으로 이러한 처리에는 UTF를 제대로 지원하는 라이브러리가 사용된다. 어떤 경우든 입출력에 기본 인코딩이 아닌 다른 인코딩을 사용하려 한다면 먼저 컨버팅 작업을 수행하야 할 것이다.


권장되는 인코딩 방식: 어떤 UTF 방식을 사용할지 결정해야 할 경우에는 대부분 작업 환경의 표준을 따르는 것이 최선의 선택이 된다. 예를 들어, 웹 환경에서는 UTF-8이, 다른 자바 환경에서는 UTF-16이 권장된다.


라이브러리 지원: 사용중인 라이브러리가 어떤 인코딩을 지원하는가? 흔치 않는 경우에 발생하는 이슈에 대해서도 커버할 수 있는가? 1-3바이트 문자는 흔히 등장하기 때문에, UTF-8 라이브러리는 대부분 4바이트 문자를 정확하게 지원한다. 그러나 UTF-16을 지원한다고 알려진 모든 라이브러리가 surrogate pairs 를 제대로 지원하지는 않는다. (surrogate pairs가 등장하는 경우는 실제로 매우 드물기 때문이다)


문자 카운팅: 유니코드에는 결합 문자들이 존재한다. 예를 들어 코드 포인트 U+006E (n), U+0303(틸드 부호) 는 ñ을 표현한다. 그런데 코드 포인트 U+00F1 이 표현하는 것도  이다. 이들은 동일한 문자로 보이지만, 단순한 카운팅 알고리즘은 전자에 대해 2를 반환하고, 후자에 대해서는 1를 반환할 것이다. 이것은 잘못된 것이 아니다. 하지만 괜찮은 결과도 아닐 수 있다.


일치성 비교: A, А, Α. 이 A는 모두 같아 보인다. 그러나 앞에서부터 하나는 라틴, 하나는 키릴, 나머지 하나는 그리스 문자이다. 또 이런 경우도 있다. "C, Ⅽ". 하나는 문자이고 다른 하나는 로마 숫자이다. 이것들 외에도 위에서 언급한 결합 문자열 등 여러가지 고려사항이 있다.


Surrogate pairs: 위에서 언급했으므로 패스.

'기술 일반 > 일반' 카테고리의 다른 글

Kotlin] Jackson Json getter setter 사용  (0) 2022.11.24
CLOB 컬럼 핸들링 중 인코딩 문제  (0) 2019.03.08

+ Recent posts