Java2015. 1. 15. 00:54
반응형

 Java 이용해서 개발을 진행하다 보면 잠재적으로 문제를 일으킬 수 있는 경우 소스코드의 경우 컴파일시점에 경고메시지를 출력합니다. 이번 포스팅에서는 그 중에서 Java7부터 추가된 'Type safety: Potential heap pollution via varargs parameter' 에 대해서 알아보도록 합시다. 직역하면 '타입 안전성 : 가변 매개변수를 통한 잠재적인 힙(heap) 오염' 정도로 번역할 수 있을듯 합니다.


public static <T> void test(List<T>... variable);
public static <T> void test(T... variable);

※위처럼 제너릭 타입을 가변 매개변수로 선언했을 경우, 경고 메시지가 출력됩니다.


 일반적인 가변 매개변수의 경우에는 경고가 출력되지 않는것과 비교하여 제네릭의 어떤 특성이 경고를 발생시키는지 알아보기 위해 먼저 제너릭에 대해서 간단하게 알아보도록 합시다. 일반적으로 제너릭(generics)을 사용하는 간단한 예제코드는 아래와 같습니다.


List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();

class Sample<T> {
...	
}

 위의 코드는 컴파일 타임에 형(Type)을 검사한후 형 소거자(Type erasure)에 아래와 같이 변환되는 과정을 거쳐서 컴파일 됩니다. 이러한 메커니즘을 통해 기존 코드(제너릭스 도입전의 코드와의 하위 호환성을 유지합니다.) 런타임시에는 타입이 소거됨으로서 하위 호환성을 유지할 수 있는지만 타입 안정성 보장에는 어느정도 손해를 감수할 수 밖에는 없습니다.

List list1 = new ArrayList();
List list2 = new ArrayList();

class Sample {
...	
}


 위에서 언급한 타입 안정성 보장에는 어느정도 손해를 감수할 수 밖에는 없다는 것은 어떤 경우일까요? 바로 형변환(Casting)을 할때 입니다. 그럼 아래의 코드를 통해서 확인해보도록 합시다.


Object to List

Object obj = new Object();
List<String> list = (List<String>)obj;


List to List

List<String> list1 = Arrays.asList(new String[]{"A"});
List<Integer> list2= (List<Integer>)(Object)list1;

for (Integer integer : list2) {
    System.out.println(integer);
}


 두번째 예제 코드는 약간 억지스러운 예제라고 생각하시는 분들도 계실지 모르겠지만 위의 두가지 예제 코드는 정상적으로 컴파일되는 코드입니다. 먼저 Object를 List로 변환하는 첫번째 예제의 경우 런타임에 바로 ClassCastException이 발생하게 됩니다. 두번째 예제 코드는 어떨까요? 두번째 코드의 경우는 1, 2라인만 컴파일하고 실행했을때는 예외가 발생하지 않지만 마지막까지 컴파일후 실행하면 첫번째 코드와 마찬가지로 ClassCastException이 발생합니다.


 첫번째 예제코드는 대입 연산(2번째 라인)에서 바로 예외가 발생하였지만 두번째 예제코드는 1, 2라인만 실행했을 경우는 형 소거자에의해 제너릭 타입이 제거되므로 둘다 List타입으로 변환됨으로서 실제의 타입은 다르더라도 둘다 List형이므로 대입이 가능합니다. 하지만 실제의 타입으로 형변환이 필요한 4라인에 와서 예외가 발생합니다. 물론 위의 코드는 'Type safety: Unchecked cast' 경고를 유발하므로 작성시에 충분히 대응이 가능합니다.






 이제 본 포스팅의 초점인 매개변수에 있어서 나오는 경고에 대해서 살펴보도록 합시다. 위에서 제너릭의 형 소거자가 타입 안정성 보장에 틈을 가져온다고 언급했습니다. 매개변수의 경우 아래와 같은 변환 과정을 거쳐 컴파일이 진행됩니다.


//콜렉션의 경우
public static <T> void test(List<T>... variable);
 ↓
public static <T> void test(List<T>[] variable);
 ↓
public static <T> void test(List[] variable);

//비콜렉션
public static <T> void test(T... variable);
 ↓
public static <T> void test(T[] variable);


 위의 매개변수의 경우 어떻게 타입 안정성을 해칠수 있는지 살펴보도록 합시다.


public static <T> List<T> test(List<T>... variable) {
    Object[] objArr = variable;
    //오브젝트의 배열로 업캐스팅 된 상태이므로 다른 제너릭 타입의 리스트도 대입이 가능합니다.
    objArr [0] = Arrays.asList(new Integer[]{Integer.valueOf(1),Integer.valueOf(2)});
    ...
}
public static <T> void test2(T... variable) {
    //T가 List<String>일 경우 아래처럼 힙 오염이 될 가능성이 있습니다.
    List<Integer> list = (List<Integer> )variable[0];
    list.add(Integer.valueOf(1));
}


 위의 예제에서 매개변수로 전달된 리스트가 메소드 내부에서 오염되었음을 알 수 있습니다. 매개변수가 아닐 경우에 출력되는 'Type safety: Unchecked cast' 경고와 본질적으로는 같은 경고라고 생각되지만 매개변수의 참조 혹은 메소드 호출을 통해서 메소드 외부에도 영향을 미친다는 점에서 별도의 경고를 출력하는 것으로 생각됩니다. 이 경고를 억제하기 위해서는 @SuppressWarnings("unchecked") 혹은 @SafeVarargs라는 1.7에서 추가된 별도의 어노테이션이 사용 가능합니다만 @SafeVarargs어노테이션의 경우는 메소드 외부에 영향을 끼친다는 점이 해소되었다는 것을 강제하기 위해서 static메소드 이거나 final 메소드의 경우에만 어노테이션을 기술할 수 있습니다. 메소드 재정의(Override)를 통한 우회가 가능하기 때문입니다.



이클립스 어노테이션 기술

※이클립스(eclipse)에서 final로 선언된 메소드의 경우 @SafeVarargs어노테이션을 기술할 수 있도록 출력



 이번 포스팅에서 소개한 경고 이외에도 Java컴파일러는 다양한 경고를 출력합니다. 이런 경고의 원인을 잘 이해하고 개발을 진행하는 것이 중요하다고 생각됩니다. 또한 @SuppressWarnings, @SafeVarargs처럼 경고를 억제하는 어노테이션은 단순히 경고 출력을 억제할 뿐이므로 해당 어노테이션을 추가하기 전에 개발자 스스로 런타임에 문제가 없다는 것을 확인한 후 추가하도록 합시다.



Posted by Reiphiel
Java2014. 11. 27. 00:28
반응형

 이전 포스팅에서 Apache httpclient를 이용하여 http요청을 보내는 방법에 대한 포스팅을 올린적이 있습니다. 사실 http는 평이한 구조의 프로토콜 이므로 요청 내용을 살펴보는 것만으로도 디버깅이 수월하게 할 수 있습니다. 이러한 요청 내용을 살펴보는 방법에는 여러가지 방법이 있을수 있겠지만 본 포스팅에서는 httpclient에서 제공하는 로깅기능을 이용하여 직접 출력하는 방법과 프록시(proxy)를 이용하여 서버와 클라이언트 중간에서 요청을 중개하면서 확인하는 방법에 대해서 소개하도록 하겠습니다.



Proxy를 통한 httpclient 요청 디버깅

	try {
			RequestConfig.Builder requestBuilder = RequestConfig.custom();
			requestBuilder.setProxy(new HttpHost("127.0.0.1", 8888, "http"));
			HttpClientBuilder builder = HttpClientBuilder.create();
			builder.setDefaultRequestConfig(requestBuilder.build());
			HttpClient client = builder.build();

			HttpGet httpGet = new HttpGet(new URI("요청을 보낼 URL"));

			HttpResponse response = client.execute(httpGet);

			System.out.println(EntityUtils.toString(response.getEntity()));
		} catch (URISyntaxException e) {
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}


 기본적인 http 요청 코드에 프록시를 설정하는 메소드를 호출해서 프록시호스트를 전달해 주는 것으로서 디버깅을 할 수 있습니다. 위의 코드는 잘 알려진 http 디버깅 툴인 fiddler의 기본 설정인 8888번 포트에 프록시 설정을 하는 코드 입니다. 위의 코드를 실행하게 되면 아래와 같이 fiddler에서 요청을 확인할 수 있습니다. 요청하는 소스코드에 단 한줄만 추가하는 것만으로 쉽게 요청 내용을 확인할 수 있으므로 매우 편리하게 사용할 수 있습니다.









Log4J의 로거를 이용한 httpclient 요청 디버깅


헤더 / 상태 출력용 로거 설정 예
log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%c] %m%n

log4j.logger.org.apache.http=DEBUG
log4j.logger.org.apache.http.wire=ERROR


 위의 log4j 설정은 Apache httpclient 4.3 Logging Guide에서 발췌한 내용으로 디버깅 시에 가장 추천하는 형태의 로거 설정입니다. 위의 설정후 출력되는 로그를 확인하면 아래와 같습니다.


DEBUG [org.apache.http.client.protocol.RequestAddCookies] CookieSpec selected: best-match
DEBUG [org.apache.http.client.protocol.RequestAuthCache] Auth cache not set in the context
DEBUG [org.apache.http.impl.conn.PoolingHttpClientConnectionManager] Connection request: [route: {}->####접속URL####][total kept alive: 0; route allocated: 0 of 2; total allocated: 0 of 20]
DEBUG [org.apache.http.impl.conn.PoolingHttpClientConnectionManager] Connection leased: [id: 0][route: {}->####접속URL####][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20]
DEBUG [org.apache.http.impl.execchain.MainClientExec] Opening connection {}->####접속URL####
DEBUG [org.apache.http.conn.HttpClientConnectionManager] Connecting to ####접속URL####/####접속IP####
DEBUG [org.apache.http.impl.execchain.MainClientExec] Executing request GET / HTTP/1.1
DEBUG [org.apache.http.impl.execchain.MainClientExec] Target auth state: UNCHALLENGED
DEBUG [org.apache.http.impl.execchain.MainClientExec] Proxy auth state: UNCHALLENGED
DEBUG [org.apache.http.headers] http-outgoing-0 >> GET / HTTP/1.1
DEBUG [org.apache.http.headers] http-outgoing-0 >> Host: ####접속호스트명####
DEBUG [org.apache.http.headers] http-outgoing-0 >> Connection: Keep-Alive
DEBUG [org.apache.http.headers] http-outgoing-0 >> User-Agent: Apache-HttpClient/4.3.2 (java 1.5)
DEBUG [org.apache.http.headers] http-outgoing-0 >> Accept-Encoding: gzip,deflate
DEBUG [org.apache.http.headers] http-outgoing-0 << HTTP/1.1 200 OK
DEBUG [org.apache.http.headers] http-outgoing-0 << Server: nginx
DEBUG [org.apache.http.headers] http-outgoing-0 << Date: Wed, 26 Nov 2014 14:41:31 GMT
DEBUG [org.apache.http.headers] http-outgoing-0 << Content-Type: text/html; charset=UTF-8
DEBUG [org.apache.http.headers] http-outgoing-0 << Transfer-Encoding: chunked
DEBUG [org.apache.http.headers] http-outgoing-0 << Connection: close
DEBUG [org.apache.http.headers] http-outgoing-0 << Cache-Control: no-cache, no-store, must-revalidate
DEBUG [org.apache.http.headers] http-outgoing-0 << Pragma: no-cache
DEBUG [org.apache.http.headers] http-outgoing-0 << P3P: CP="CAO DSP CURa ADMa TAIa PSAa OUR LAW STP PHY ONL UNI PUR FIN COM NAV INT DEM STA PRE"
DEBUG [org.apache.http.headers] http-outgoing-0 << X-Frame-Options: SAMEORIGIN
DEBUG [org.apache.http.headers] http-outgoing-0 << Content-Encoding: gzip


 아시다시피 Log4J의 로거는 카테고리구조로 되어있으며 위의 설정은 org.apache.http 카테고리는 debug모드로 하위의 org.apache.http.wire는 error레벨로 지정했습니다. 위의 예에서 org.apache.http.wire의 로거 설정을 제거하면 전체가 debug모드가 되면서 전체 컨텍스트가 출력되게 됩니다만 알아보기 힘든 문자들이 출력되므로 디버깅에 그리 좋은 형태는 아닙니다. 따라서 일반적인 경우라는 위의 추천 로거 설정을 사용하면 될 듯 합니다.


※위의 로그에서 []사이의 카테고리를 참조하시면 필요한 로그 정보만 출력하도록 로거를 구성할 수 있습니다. 또한 로거에 대한 자세한 내용은 Apache HttpComponents 홈페이지에서 확인할 수 있습니다.


참조 : https://hc.apache.org/httpcomponents-client-4.3.x/logging.html


Posted by Reiphiel
Java2014. 11. 19. 01:40
반응형

 Java8에서 새로 추가된 기능중에 Base64 인코더와 디코더가 있습니다.(이제와서!?!?) 사실 여타의 프로그래밍 언어에는 대체적으로 언어 내부에 구현체가 존재하거나 사실상 표준의 라이브러리들이 존재해왔으나 Java만은 이상하게도 지금까지 제공하지 않았습니다.(Oracle/sun의 JDK에 포함된 구현체와 같이 특정 벤더에 종속적인 API는 여기서 언급하지 않겠습니다.) 따라서 Apache재단의 commons-codec라이브러리를 사용하는 것이 가장 일반적인 선택이 아니었나 싶습니다. 하지만 Java8부터 표준API가 제공되기 시작함으로서 외부 라이브러리에 의존하는 일도 이제 다 지난일이 될 듯 합니다. 자 그럼 Java8의 Base64 API에 대해서 알아보도록 합시다.



Java8 Base64 문자열 변환 소스코드

    public static String fileToBase64String(final File target)
    throws IOException {
		Encoder encoder = Base64.getEncoder();
		BufferedInputStream bis = null;
		ByteArrayOutputStream baos = null;
		try {
			bis = new BufferedInputStream(new FileInputStream(target));
			baos = new ByteArrayOutputStream();

			byte[] buffer = new byte[8192];
			int readSize = 0;
			while ((readSize = bis.read(buffer)) > -1) {
				baos.write(buffer, 0, readSize);
			}

			return encoder.encodeToString(baos.toByteArray());
		} finally {
			try {
				bis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				baos.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}


Java8 Base64 문자열 to File 변환 소스코드

	public static void base64StringToFile(final String encoded, final File output)
	throws IOException {
		Decoder decoder = Base64.getDecoder();
		BufferedOutputStream bos = null;
		try {
			bos = new BufferedOutputStream(new FileOutputStream(output));
			bos.write(decoder.decode(encoded));
		} finally {
			try {
				bos.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}


 위에서 소개한 소스코드는 인자로 받은 파일을 Base64인코딩 하고 인코딩된 문자열을 다시 디코딩하는코드입니다. 각각 소스코드 상단에 Base64클래스의 팩토리 메소드를 호출해서 인코더와 디코더를 취득한 것을 볼 수 있는데 이러한 팩토리 메소드에는 아래와 같이 세종류가 있습니다.



  • RFC4648 Base64인코딩

Encoder encoder = Base64.getEncoder();
Decoder decoder = Base64.getDecoder();


  • RFC4648 Base64URL인코딩

Encoder encoder = Base64.getUrlEncoder();
Decoder decoder = Base64.getUrlDecoder();


  • RFC2045(Mime)

Encoder encoder = Base64.getMimeEncoder();
Decoder decoder = Base64.getMimeDecoder();



 위처럼 세가지 타입의 팩토리 메소드가 있으며 기본적으로 패딩처리(3바이트 단위로 끊어지지 않는 경우 나머지 부분을 채움 '='문자로 채움)가 활성화 되있으며 마임타입 인코딩의 경우 한 라인의 문자수가 디폴트로 76문자에 개행문자로 CRLF로 지정되어있다. RFC표준에 충실하게 구현되어있는 느낌입니다. 물론 아래처럼 간단하게 다른 수치를 지정할 수 있다.


Encoder encoder = Base64.getEncoder().withoutPadding(); //패딩 실행 안함
Encoder encoder = Base64.getMimeEncoder(80, new byte[]{'\n'}); //라인당 문자수 80,  개행문자 LF



 



Java8 Base64 API와 commons-codec과의 차이점


 사실 직전 포스팅에서 Apache commons-codec 라이브러리의 API에 대해서 알아보았었으므로 각각의 차이점에 대해서 언급하지 않을 수 없습니다. Java8의 경우는 RFC4648규약과, RFC2045규약을 각각 지원하고 있었고 commons-codec의 경우는 RFC2045만을 지원하고 있었습니다. 그러나 사실 RFC4648규약이 하위 규약을 표준화하는 성격의 규약인 관계로 인코더/디코더를 교차적으로 사용해도 특별히 문제되는 것 같지 않았습니다.


※위에서 소개한 파일을 Base64 인코딩하고 디코딩하는 소스로 Java8과 commons-codec의 API이용하여 교차적으로 인코딩 디코딩 테스트를 해본 결과 특별히 문제없는듯 했습니다. 물론 간단한 결과로 문제없다고 단정짓기는 힘드므로 실제 적용하실 경우엔 주의가 필요합니다.


 위에서 소개한대로 Java8의 경우는 각각의 규약별로 별도의 인코더, 디코더 인스턴스를 제공하는 팩토리 메소드를 제공하고 있어서 동일한 메소드 호출로 처리할 수 있으나 commons-codec의 경우는 별로도 제공하고 있지 않습니다. 따라서 필요에따라 직접 관련 인자를 메소드에 제공해서 호출하거나 별도의 메소드를 호출해야 합니다.


Java8의 MimeEncoder에 대응하는 commons-codec의 API

byte[] org.apache.commons.codec.binary.Base64.encodeBase64Chunked(byte[] binaryData)


 위의 메소드는 Java8의 MimeEncoder에 대응하는 메소드로서 위를 실행하여 문자열로 변환후에 출력하면 Java8과 동일하게 행별로 76문자 + CRLF로이루어진 문자열이 출력됩니다. 다만 미묘하게 틀린점 하나는 가장 마지막 라인에 개행문자가 한번 더 출력된다는 점입니다.



Java8의 UrlEncoder에 대응하는 commons-codec의 API

String org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(byte[] binaryData)


 위의 메소드가 마찬가지로 Java8의 UrlEncoder에 대응하는 메소드입니다. 역시나 약간의 차이가 있는데 commons-codec의 구현체는 padding이 없으므로 동일한 결과를 원한 경우는 위에서 소개한 withoutpadding메소드를 통해 인코드를 취득해야 합니다.



 마지막으로 성능면에서 간단한 테스트 결과 Java8의 구현체가 commons-codec의 구현체에 비해 약 3-4배정도 차이가 났습니다만 어찌보면 당연한 결과일 듯 합니다. 표준API로 포함되어있으므로 컴파일러 최적화나 이런 부분에 있어서 어느정도 유리한 위치에 서있기 때문입니다.



 지금까지 Java8에 추가된 Base64관련 표준 API에 대해서 살펴보았습니다. 표준API인 만큼 하위 호환성을 고려해야하는 상황이 아닌 경우에는 선택에 주저할 필요가 없을 듯 합니다. 물론 국내 환경상 Java8이 주류가 되기까지는 어느정도 시간이 필요하겠지만 말입니다.



Posted by Reiphiel