카테고리 보관물: Java

자바로 다중 작업(Multi Task)을 효과적으로 처리하는 방법들

자바로 다중 작업(Multi Task)을 효과적으로 처리하는 방법을 살펴보자. 자바에서 다중 작업을 처리하는데 기본으로 Thread 클래스와 Runnable 인터페이스를 사용한다. 하지만 이 Thread나 Runnable의 경우에는 병행처리를 하는 데 필요한 태스크 관리와 태스크의 분리(Divide)와 처리를 직접 구현해야 하는 단점이 있다. 그래서 자바 java.util.concurrent 패키지에는 다중 작업을 효과적으로 처리할 방법을 여러 가지 제공한다.

1. Executor 프레임웍

이 프레임웍은 처리해야 할 작업을 프로듀서-컨슈머 패턴의 큐에 저장하고, N개의 스레드를 사용해서 병행으로 처리하는 프레임웍이다. 일반적으로 프로듀스는 큐에 작업을 추가하고, 스레드 풀(컨슈머 스레드)에서 작업을 처리한다.

2. 카운트다운래치(CountDownLatch) & 사이클릭배리어(CyclicBarrier)

카운트다운래치(CountDownLatch)와 사이클릭배리어(CyclicBarrier)는 둘 다 큰 작업을 작은 작업으로 분리(개별 구현)하고, 이 분리한 작업의 실행을 완료한 뒤에 흐름을 진행하도록 흐름을 동기화시키는 클래스이다.
그림에서 보듯이, 카운트다운래치는 메인 스레드에서 작업을 처리하는 실행하는 스레드를 만들고 실행한다. 그리고 개별 스레드가 카운트다운래치의 값을 하나씩 제거하면, 메인 스레드가 시그널을 받아서 흐름을 진행하는 형태이다.

사이클릭배리어는 두 스레드 사이에 로직을 동기화하는 데 사용한다. 그래서 배리어를 사용해서 N개의 스레드 실행 흐름을 중단하다가, 배리어액션(단계를 지날 수 있는 과정)을 완료하면 N개의 스레드 실행 결과를 병합할 수 있는 형태이다. 그리고, 두 개의 스레드가 데이터를 교환하는데 사용하는 Exchanger는 사이클릭배리어의 특정 예이다.

3. ForkJoin 프레임웍

포크조인(ForkJoin) 프레임웍은 작업을 분리(분리하기 쉽게 선언)하고, 실행하고, 그리고 결과를 병합하기 쉽게 도와준다. 포크(Fork)는 작업을 분리하고, 조인(Join)은 분리한 작업이 완료한 뒤에 결과를 병합하는 과정이다. 이 프레임웍에 대한 자세한 내용은 http://www.oracle.com/technetwork/articles/java/fork-join-422606.html 에서 살펴보자.

이 그림에서 보듯이, 작업은 내부에서 fork()로 작업을 분리하고, join()으로 태스크를 병합한다. 그리고 이 작업은 재귀(Recursive)로 작업을 분리하는 형태라서 RecursiveTask 클래스를 상속해서 구현한다.

이제 어느 상황에서 이런 다중 작업을 처리하는 것이 좋은지 살펴보자.

1. 작업이 분리되지 않는 경우 “Executor”를 사용하자.

– 가장 많이 사용하는 형태이다.

2. 작업이 분리되는 경우에는 “카운트다운래치” 또는 “포크조인”을 사용하자.

– 많은 양의 데이터 정렬, 최대, 최소값 찾는 경우.
– 많은 양의 데이터를 합산하는 경우.
– 네트웍을 스캔하는 경우.
– 기타
개인적으로는 카운트다운래치를 많이 사용했지만, 작업 분리는 동일하지만 스레드 관리가 불편하다. 그래서 포크조인을 사용하는 것이 좋겠다.

3. 작업이 분리될 수도 있고 아닐 수도 있는 단계가 있는 작업은 “사이클릭배리어”를 사용하자.

– 예를 들어서 게임의 단계를 배리어도 두고 여러 스레드의 작업을 제어할 수 있다. 1단계를 지나면서, 랭킹정보, 개인정보, 게임정보등등을 가져오는 스레드를 실행하고, 필요한 정보를 다 가져온 뒤에 화면을 넘기는 예로 사용할 수 있겠다.

메시지팩(MessagePack) C# <-> 자바 직렬화 데이터 전달시 주의할 점

메시지팩(MessagePack, http://msgpack.org/)은 이 기종의 데이터 통신을 원활하게 할 수 있게, 데이터 포맷을 투명하게 직렬화하는 라이브러리이다. 메시지팩은 종종 프로토콜 버퍼, 스리프트등과 같은 직렬화 프레임웍과 비교되는 오픈소스 라이브러리이다. 이 라이브러리의 특징은 다른 직렬화 프레임웍과는 다르게 메세지 포맷 파일을 정의하는 정적 형태가 아니라, 소스코드에서 바로 사용하도록 지원하는 동적 형태이다. 그래서 다른 프레임웍과 비교해서 약간의 성능상 단점은 있지만, 추가 작업(IDL 컴파일 등)이 필요하지 않아서 쉽게 사용할 수 있다.

* 이 기종 : 윈도, 리눅스 등에서 자바를 사용하는 형태가 아니다. 윈도 머신을 사용한다고 해도 C#, C++등과 자바를 사용해서 서로 통신하는 형태가 이 기종이라고 인식하면 된다.
* 투명하다 : 쉽게 이해하고, 인식할 수 있는 형태이다. 메시지팩은 여러 데이터 타입을 직렬화하는데 고유한 스펙을 사용한다. 스펙은 https://github.com/msgpack/msgpack/blob/master/spec.md 에서 확인할 수 있다.
* 직렬화/역직렬화 : 직렬화는 개별 플랫폼에서 사용하는 데이터를 바이트 배열로 변경하는 작업이다. 역직렬화는 직렬화된 데이터인 바이트 배열을 개별 플랫폼에서 사용하는 형태(객체)로 변경하는 작업이다.

테스트한 필자의 환경은 다음과 같다.
서버 : 자바
클라이언트 : 자바
에이전트 : C#, C++

위의 구성에서 자바 플랫폼에서 사용하는 라이브러리는 https://github.com/msgpack/msgpack-java 을 사용하고 있고 문제가 없다. 문제는 에이전트와 자바 서버간의 통신에서 발생했다. 프로젝트에서 사용하는 C# 라이브러리는 https://github.com/msgpack/msgpack-cli을 사용하고 있다.

C# 코드
– EventMessage 메시지는 에이전트가 서버와 통신하는데 사용하는 이벤트 모델의 최상위 클래스이다.

var stream = new MemoryStream();
var serializer = SerializationContext.Default.GetSerializer<EventMessage>();
serializer.Pack(stream, registerMessage);
byte[] messageBytes = stream.ToArray();

자바 코드
– 자바는 아래와 같이 MessageBufferPacker 클래스를 사용해서 직렬화 한다.

MessageBufferPacker msgPacker = MessagePack.newDefaultBufferPacker();
try {
msgPacker.packString("string")
 .packString("string")
 .packInt(eventValue)
 .packInt(resultValue);

return msgPacker.toByteArray();
} finally {
 msgPacker.close();
}

이제 위의 코드가 들어가 있는 소스를 실행하면 아래와 같은 에러 로그를 보게 된다. 로그의 내용으로 봐서는 읽어야 하는 바이트 배열에 특정 바이트 값이 잘 못 들어가 있다는 것을 알 수 있다.

ERROR 2016-04-06/10:23:32.503/PDT [MessageReader] com.xxx.xxx.xxx.MessageHandlerImpl:exceptionCaught(139): org.msgpack.core.MessageTypeException: Expected String, but got Array (93)

그래서 이것저것 확인 하다가 C# 라이브러리의 Packer 클래스를 사용하는 형태를 확인할 수 있고, 아래와 같은 형태로 소스 코드를 변경했다. 예상은 바이트 배열의 코덱정도로 볼 수 있는 PackerCompatibilityOptions 차이로 생각했는데, 에러를 내는 SerializationContext.Default.GetSerializer<EventMessage>(); 의 객체도 동일한 옵션을 사용하고 있다. 그래서 정확한 문제 파악은 시간도 없고 패스했다(능력부족 ㅠ.ㅠ).

아래는 Packer를 사용해서 직렬화 부분을 수정한 소스코드이다.

var stream = new MemoryStream();
var packer = Packer.Create(stream, PackerCompatibilityOptions.None, true);

packer.Pack("string");
packer.Pack("string");
packer.Pack(event);
packer.Pack(result);

stream.Position = 0;
byte[] messageBytes = stream.ToArray();

이제 메시지팩을 사용하는 C# 플랫폼에서 자바 플랫폼에 바이트 배열을 제대로 보내고 받을 수 있는 것을 알 수 있다. 어찌보면 간단하지만 처음 프로젝트에 도입해서 사용하는 경우에는 C#에서 Packer를 모른다면 난감할 수 있어서 포스팅 했다.

자바에서 unsigned 타입을 사용하는 프로토콜을 처리하기 위한 클래스들..

자바(Java)를 사용해서 개발한 서비스나 애플리케이션에서 C/C++로 개발된 서버에 연결해서 데이터를 전송하거나 수신하는 경우에 unsigned로 인해서 통신에 불편함이 발생하게 된다. 그래서 이런 문제를 쉽게 해결하기 위해서 구글에서는 프로토콜 버퍼(https://developers.google.com/protocol-buffers/), 스리프트(페이스북에서 개발해서 아파치에 기증, https://thrift.apache.org/)와 같은 직렬화 라이브러리를 많이 사용하고 있다.
그래도 이런 직렬화 라이브러리를 사용하지 않고, unsigned 타입을 프로토콜로 사용하는 서버와 통신하기 위해서 unsigned를 처리하는 자바 클래스 몇 개를 살펴보자. 아래 클래스는 1, 2, 4 byte의 배열을 읽어서 2, 4, 8 byte의 데이터 타입으로 사용하고, 2, 4, 8 byte를  unsigned 1, 2, 4 byte로 변경하는 코드이다.

1. UnsignedByte

public class UnsignedByte {

    public static short parse( byte[] data, int offset ) {
        return (short)(data[offset] & 0xff);
    }

    public static short parse( byte[] data ) {
        return parse( data, 0 );
    }

    public static byte[] parse( short number ) {
        return new byte[] { (byte)(number & 0xff) };
    }

}

2. UnsignedShort

public class UnsignedShort {

    public static int parse( byte[] data ) {
        return parse( data, 0 );
    }

    public static int parse( byte[] data, int offset ) {
        return ((data[offset] & 0xff) << 8) | (data[offset+1] & 0xff);
    }

    public static byte[] parse( int number ) {
        byte[] data = new byte[2];

        data[0] = (byte)((number >> 8) & 0xff);
        data[1] = (byte)(number & 0xff);

        return data;
    }

}

3. UnsignedInt

public class UnsignedInt {

    public static long parse( byte[] data ) {
        return parse( data, 0 );
    }

    public static long parse( byte[] data, int offset ) {
        return (((long) data[offset] & 0xffL) << 24)
             | (((long) data[offset+1] & 0xffL) << 16)
             | (((long) data[offset+2] & 0xffL) << 8)
             |  ((long) data[offset+3] & 0xffL);
    }

    public static byte[] parse( long number ) {
        byte[] data = new byte[4];
        
        data[0] = (byte) ((number >> 24) & 0xff);
        data[1] = (byte) ((number >> 16) & 0xff);
        data[2] = (byte) ((number >> 8) & 0xff);
        data[3] = (byte) (number & 0xff);

        return data;
    }

}

* Reference
https://github.com/ryantenney/oscar

파일 타입을 좀 더 정확하게 확인하도록 도와주는 자바 라이브러리

파일을 처리하는 애플리케이션을 개발하면서 많은 경우에 파일 처리는 확장자를 기준으로 파일의 타입을 결정하고 처리하게 된다. 예를 들면, 확장자가 .docx 이면 마이크로소프트 워드 파일이라는 것을 알 수 있고, 이 파일의 타입에 적합하게 처리(오픈 등)한다. 이 블로그의 파일 타입별 확장자 분류.. 포스팅을 확인해 보면, 비교적 자주 사용되는 파일 확장자와 그 확장자를 가지고 있는 파일이 어떤 타입의 파일인지 확인할 수 있다.

파일의 포맷을 인식하는 기본 방법이 확장자를 기준으로 타입을 구분해서 처리하는 방법이지만, 종종 파일의 타입을 알 수 없는 경우가 발생한다. 이런 경우는 다음과 같다.
– 수동으로 파일의 확장자를 수정한다.
– 특정 폴더에 이미지의 썸네일을 확장자 없이 저장(안드로이드에서 종종 볼 수 있다)한다.
– 각종 다운로드로 인해서 발생한다.

일반적으로 위 경우에는 파일을 처리하지 못하거나, 제대로 열 수 없는 애플리케이션을 호출하게 된다. 따라서 좀 더 정확하게 파일을 처리하기 위해서는 파일의 헤더를 분석해서 파일의 타입을 확인하고, 그 타입에 맞는 처리가 필요하다. 그래서, 이 글에서는 자바로 파일의 타입을 정확하게 확인할 수 있는 라이브러리를 확인해 보겠다.

파일의 타입을 확인할 수 있는 자바 라이브러리를 찾아보면 아래의 목록을 확인할 수 있다. 그리고 이 라이브러리는 안드로이드에서도 사용할 수 있다.

  1. SimpleMagic
    1. https://github.com/j256/simplemagic
    2. 의존 : 없음
  2. Java Mime Magic Library
    1. https://github.com/arimus/jmimemagic
    2. 의존 : Apache Common Logging 라이브러리
  3.  Apache Tika
    1. http://tika.apache.org/
    2. 의존 : 많음

이 글에서는 Apache Tika는 제외하고, 1.번과 2.번의 라이브러리를 확인해 보겠다. Apache Tika를 제외한 이유는 의존하는 라이브러리가 너무 많아서이다. 대상은 아래에 있는 5개 타입에 대해서 확인해 봤다.

docx = new java.io.File(".").getCanonicalPath()     + "/files/docx_file";
jpg = new java.io.File(".").getCanonicalPath() + "/files/jpg_file";
odt = new java.io.File(".").getCanonicalPath() + "/files/open_office_odt_file";
pdf = new java.io.File(".").getCanonicalPath() + "/files/pdf_file";
exe = new java.io.File(".").getCanonicalPath() + "/files/prgrep_exe_file";

위에서 파일의 확장자를 수동으로 변경했고, 이 파일에 대해서 포맷을 정확하게 인식하는 것을 테스트 했고, 결과는 다음과 같다.

– SimpleMagic 결과

0 ContentType : word
0 MimeType : application/vnd.openxmlformats-officedocument.wordprocessingml.document
0 File Extension 0 : docx
1 ContentType : jpeg
1 MimeType : image/jpeg
1 File Extension 0 : jpeg
1 File Extension 1 : jpg
1 File Extension 2 : jpe
2 ContentType : opendocument-text
2 MimeType : application/vnd.oasis.opendocument.text
2 File Extension 0 : odt
3 ContentType : pdf
3 MimeType : application/pdf
3 File Extension 0 : pdf
4 Unknown content-type

– Java Mime Magic Library 결과

0 File Ext : zip
0 File Mimetype : application/zip
1 File Ext : jpg
1 File Mimetype : image/jpeg
2 File Ext : zip
2 File Mimetype : application/zip
3 File Ext : pdf
3 File Mimetype : application/pdf
4 File Ext : ???
4 File Mimetype : ???

위 개별 라이브러리의 파일 타입 인식 결과로, 자바를 사용해서 파일의 포맷을 더 정확하게 확인하고 사용하려면, SimpleMagic 라이브러를 사용해서 파일의 타입을 확인하는게 좋겠다. 이 테스트를 위해서 사용한 소스는 다음과 같다.

import java.io.File;
import java.io.IOException;

import net.sf.jmimemagic.Magic;
import net.sf.jmimemagic.MagicMatch;

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;

public class FileFormatCheckTest {

	static String[] files = null;

	static String docx = "";
	static String jpg = "";
	static String odt = "";
	static String pdf = "";
	static String exe = "";

	static {
		try {
			docx = new java.io.File(".").getCanonicalPath()	+ "/files/docx_file";
			jpg = new java.io.File(".").getCanonicalPath() + "/files/jpg_file";
			odt = new java.io.File(".").getCanonicalPath() + "/files/open_office_odt_file";
			pdf = new java.io.File(".").getCanonicalPath() + "/files/pdf_file";
			exe = new java.io.File(".").getCanonicalPath() + "/files/prgrep_exe_file";

			files = new String[] { docx, jpg, odt, pdf, exe };
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void testA() {
		ContentInfoUtil util = new ContentInfoUtil();
		try {
			ContentInfo info;

			for (int i = 0; i < files.length; i++) {
				info = util.findMatch(files[i]);
				if (info == null) {
					System.out.println(i + " Unknown content-type");
					continue;
				}

				System.out.println(i + " ContentType : " + info.getName());
				System.out.println(i + " MimeType : " + info.getMimeType());

				String[] extensions = info.getFileExtensions();
				if (extensions != null && extensions.length > 0) {
					for (int j = 0; j < extensions.length; j++) {
						System.out.println(i + " File Extension " + j + " : "
								+ extensions[j]);
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void testB() {
		MagicMatch match = null;

		try {
			for (int i = 0; i < files.length; i++) {
				match = Magic.getMagicMatch(new File(files[i]), true, false);

				if (match != null) {
					System.out.println(i + " File Ext  " + " : "
							+ match.getExtension());
					System.out.println(i + " File Mimetype  " + " : "
							+ match.getMimeType());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		// testA();
		testB();
	}

}

프로젝트 다운로드 : FileFormatCheckTest.zip

자바 CountDownLatch를 이용한 동시성 높이기..

각종 환경에서 성능을 높이기 위해서 취하는 방법이 멀티 프로세스와 멀티 쓰레드이다. 자바에서는 성능을 높이기 위해서 기본적으로 멀티 쓰레드의 형태를 취하고 있다. 물론, JNI를 사용해서 Native의 프로세스를 띄울 수 있기도 하다. 자바의 기본 패키지중에서 쓰레드를 사용해서 동시성을 극대화 시키는 각종 자료구조와 클래스들은 java.uti.concurrent 패키지를 확인해 보면 알 수 있다.

수학적인 계산을 제외한, 거의 모든 컴퓨터 성능이슈는 느린 I/O 처리를 어떻게 빠르게 처리하냐가 주된 관심사가 아닐까 한다. 

그래서, 많이 쓰이는 쓰레드의 형태를 살펴보면..
1. 잡을 개별 쓰레드로 위임한다.
2. 잡을 개별 쓰레드가 처리하고 결과를 취합해서, 취합된 결과를 기준으로 처리를 한다.

보통은, 위의 1.번으로 처리를 하는 경우가 많지만, 간혹, 2.번의 형태가 필요한 경우가 발생한다. 이 경우에 보통은 메인 쓰레드에서 잡 쓰레드를 생성하고, 자신은 wait하고 있다가 notify를 받는 구조를 많이 취하게 된다. 바로 이런 형태로 2.번의 상황을 처리할 수 있게 해 주는 클래스가 CountDownLatch이다.

자, 간단하게 예제를 살펴보자.

package net.sjava.example.countdownlatch;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
	static final int max = 10;
	/**
	 * 단일 쓰레드 테스트
	 */
	public static void testSingle() throws Exception {
		long start = System.currentTimeMillis();
		for (long i = 0; i < max; i++) {
			Thread.sleep(1000);
		}

		long elapsedTime = System.currentTimeMillis() - start;
		System.out.println("testSingle elapsed time -> " + elapsedTime);
	}

	/**
	 * CountDownLatch 테스트
	 */
	public static void testCountDownLatch() throws Exception {
		final CountDownLatch latch = new CountDownLatch(max);

		long start = System.currentTimeMillis();
		for (long i = 0; i < max; i++) {
			new Thread(new Worker(latch)).start();
		}

		latch.await();
		long elapsedTime = System.currentTimeMillis() - start;
		System.out.println("testCountDownLatch elapsed time -> " + elapsedTime);
	}

	/**
	 * Job 쓰레드
	 */
	static class Worker implements Runnable {
		private CountDownLatch latch;

		public Worker(CountDownLatch latch) {
			this.latch = latch;
		}

		@Override
		public void run() {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException ex) {
				ex.printStackTrace();
			} finally {
				if (this.latch == null)
					return;

				latch.countDown();
			}
		}
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		testSingle();
		testCountDownLatch();
	}
}

위의 코드의 결과는 아래와 같아서, 너무 극단적이긴 하지만, CountDownLatch의 사용을 위한 쉬운 예가 될 것이다.

testSingle elapsed time -> 10,000
testCountDownLatch elapsed time -> 1,004