[Java] Lambda(Stream) 는 항상 좋을까?

[원글] http://www.javacodegeeks.com/2015/12/3-reasons-shouldnt-replace-loops-stream-foreach.html

Java 1.8이 출시된 후 많은 블로그 및 홈페이지에서 Lambda 를 소개하는 글들이 많이 올라와 있습니다. 그렇지만 대부분의 글에 Lambda의 장점들에만 치중하는 내용이 대부분인데 오늘 RSS 피드에 괜찮은 내용이 있어 요약해 봅니다.

1. 성능

Lambda(Stream) 와 For-loop 방식의 처리에는 성능의 차이가 발생된다.

ArrayList, for-loop : 6.55 ms
ArrayList, seq. stream: 8.33 ms
int-array, for-loop : 0.36 ms
int-array, seq. stream: 5.35 ms

위의 성능의 차이는 어찌 보면 큰 차이는 아닌것 같지만 대량의 데이터를 처리해야 하는 로직에 포함된 Stream API라면 많은 성능 차이를 일으킬 수 있습니다.

2. 가독성

저도 간단하게 Stream API를 이용하여 Filter및 Collector를 이용하면서 소스 코드가 확연히 줄어든 경험이 있지만 가독성은 Stream API를 사용하기 전보다 떨어지는 것을 느꼈습니다.

단순 리스트에서 동일한 값을 뽑아내는 형태의 것은 문제가 없다고 생각하지만 Filter를 이용해서 값을 추출하고 추출한 값을 다시 가공하고 연산하여 데이터를 취합하는 형태를 구현 한다면 훨씬 복잡하고 이해하기 힘든 코드가 될 것이라는 생각입니다.

또한 다수의 사용자가 동일한 코드를 작업하는 환경이라면 모든 사용자가 익숙하지 않다면 Stream API 사용을 자제하는 것이 좋다고 생각됩니다.

3. 유지보수

대부분의 코드는 단발성이 아닌 수정이 빈번하게 발생되는 경우가 있습니다. Stream API를 사용하는 경우 StackTrace에 불필요한 내용이 출력되어 오류 분석에 어려움이 있을 수 있습니다.

[For-loop]

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:13)

[Lambda]

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.lambda$1(Test.java:18)
    at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
    at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:557)
    at Test.lambda$0(Test.java:17)
    at java.util.Arrays$ArrayList.forEach(Arrays.java:3880)
    at Test.main(Test.java:16)

이상입니다.

[Java] Dropbox API(Upload, Download, List)

Dropbox API 사용법

iOS Ad-hoc 버전 릴리즈 및 팀의 파일 공유를 위해 Dropbox를 사용하는데 이번에 CI(continuous integration)를 하려고 마음을 먹어서 단계별 기능을 확인하고 있다.

Dropbox API 사용법

https://www.dropbox.com/developers-v1/core/start/java 링크에 상세하게 설명되어 있다.

위의 단계에서 주의해야 할 사항은 AccessToken을 얻는 부분이다. 예제대로 따라 하면 AccessToken을 매번 새로 생성하기 때문에 재사용이 안된다.

그렇기 때문에 App Home – Settings – Generated access token 을 이용하여 AccessToken을 생성한다.

[DropboxAPI.java]

import com.dropbox.core.DbxClient;
import com.dropbox.core.DbxEntry;
import com.dropbox.core.DbxException;
import com.dropbox.core.DbxWriteMode;

import java.io.*;

public class DropboxAPI {   


    public DbxEntry.File uploadFile(DbxClient client, String fileName, long fileLength, byte[] fileBytes) throws IOException, DbxException {
        ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
        DbxEntry.File uploadedFile = client.uploadFile("/CoozplzFile/" + fileName, DbxWriteMode.add(), fileLength, bais);
        return uploadedFile;
    }


    public DbxEntry.WithChildren listAllFiles(DbxClient client) throws DbxException {
        DbxEntry.WithChildren children = client.getMetadataWithChildren("/CoozplzFile");
        return children;
    }


    public DbxEntry.File downloadFile(DbxClient client, String sourceFile, File downloadedPath) throws IOException, DbxException {
        FileOutputStream fos = new FileOutputStream(downloadedPath);
        DbxEntry.File downloadedFile = client.getFile("/CoozplzFile/"+ sourceFile, null, fos);
        fos.close();
        return downloadedFile;
    }
}

[DropboxAPITest.java]

import com.dropbox.core.*;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.io.*;
import java.util.List;
import java.util.Locale;

public class DropboxAPITest {

    public static String ACCESS_TOKEN = "ACCESS_TOKEN";

    DbxClient client;


    @Before
    public void setup() {
        DbxRequestConfig config = new DbxRequestConfig("Sample/1.0", Locale.getDefault().toString());
        client = new DbxClient(config, ACCESS_TOKEN);
    }


    @Test
    public void testListFiles() throws DbxException {

        DropboxAPI dropboxAPI = new DropboxAPI();
        DbxEntry.WithChildren children = dropboxAPI.listAllFiles(client);
        Assert.assertNotNull(children);
        List<DbxEntry> list = children.children;
        System.out.println(list);
        Assert.assertNotNull(list);
    }


    @Test
    public void testUpload() throws IOException, DbxException {
        DropboxAPI dropboxAPI = new DropboxAPI();


        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        InputStream in = DropboxAPITest.class.getResourceAsStream("101-N101_c.ai");

        byte[] buf = new byte[1024];
        while (true) {
            int readSize = in.read(buf);
            if (readSize < 0) {
                break;
            }
            baos.write(buf, 0, readSize);
        }
        DbxEntry.File uploadedFile = dropboxAPI.uploadFile(client, "101-N101_c.ai", baos.size(), baos.toByteArray());
        Assert.assertEquals(baos.size(), uploadedFile.numBytes);
        System.out.println("Numbers of bytes = " + uploadedFile.numBytes);
        baos.close();
    }


    @Test
    public void testDownload() throws IOException, DbxException {
        DropboxAPI dropboxAPI = new DropboxAPI();

        DbxEntry.File file = dropboxAPI.downloadFile(client, "101-N101_c.ai", new File("D:/101-N101_c.ai"));

        String url = client.createShareableUrl(file.path);
        System.out.println(url);

        Assert.assertTrue(url.contains("https://www"));

    }


}

Apache POI 를 이용한 PPT to Image 결과

  1. 목표: HTML5 기반 PPT 문서 공유 프로그램
  2. 개발:
    1. PPT 슬라이드를 이미지 파일로 변경한다. (Apache POI, java.graphics)
    2. 발표자가 이미지를 변경하면 해당 이미지 파일명을 파라미터로 받아 파일 시스템에서 읽어 구독자에 전달한다.
  3. 결과
    1. Apache POI와 Java Graphic 을 이용한 이미지 전환 작업의 결과가 좋지 않다.

 

아래 ‘Original’ 이란 부분은 PPT를 이미지 캡처한 화면이고 ‘PPT to image’ 는 Apache POI를 통해 변환된 이미지를 캡처한 화면이다.
대략적인 의미 전달은 문제가 없겠지만 내용이 많은 PPT는 문제가 있을 것 같다.(실망) ^^

 

프로세스 스냅샷을 이용한 Java 어플리케이션 분석


사이트에 장애가 발생되면 처음 하는 일이  로그 분석입니다.

로그 분석 후 원인을 못 찾게 되면 다음 하는 일은 Thread Dump 입니다.

포스팅 내용은 ThreadDump 를 이용하여 무한 루프에 빠진 스레드 정보를 찾는 내용입니다.

하단 이미지는 제가 우분투 환경에서 가상으로 무한 루프를 돌리고 테스트를 한 내용입니다. (top command 이용)

coozplz@localhost:~$ top



전체 CPU 사용량은 노란색 테두리 부분을 합산한 내용으로 보시면 됩니다.(38+28)

붉게 표시된 부분의 CPU% 는 (CPU 사용량 / CPU Core 수) 를 하시면 됩니다. 저는 테스트 환경이 쿼드 코어에서 200%가 사용중이므로 즉 50%가 사용중입니다. (무한루프를 두번 만들었습니다.)

Thread 덤프를 통해 분석을 해야 하는데 Thread Dump에는 무한 루프에 빠졌다는 정보는 없습니다.


coozplz@localhost:~$ ps -mo pid,lwp,pcpu -C java

출력결과

20058     –  192

 

   – 20097  0.0

   – 20098  0.0

   – 20099  0.0

   – 20100  0.0

   – 20298  0.0

   – 20301  0.0

   – 20302  0.0

   – 20303  0.0

   – 20304  0.0

   – 20305  0.0

   – 20306  0.0

   – 20307  0.0

   – 20308  0.0

   – 20309  0.0

   – 20310  0.0

   – 20311  0.0

   – 20312  0.0

   – 20313  0.0

   – 20314  0.0

   – 20315  0.0

   – 20318 99.9

   – 20381 99.8

위의 내용중 20318’ 스레드와 ‘20381’ 스레드가 CPU 를 각각 99% 사용하고 있다는 정보가 확인 되었습니다.

‘20318’ HexString 값으로 변경하면 4f5e 라는 값이 나옵니다.

0x4f5e” 값으로 ThreadDump에서 분석을 해보면 어떤 스레드에서 무한 루프가 돌고 있는지 확인할 수 있습니다.


ThreadDump 명령어(Eclipse Console 또는 catalina.out 에 출력됨)

coozplz@localhost:~$ kill -3 20058


“Thread-118” daemon prio=10 tid=0x0000000001563000 nid=0x4f5e runnable [0x00007f0610f23000]

  java.lang.Thread.State: RUNNABLE

   at sun.nio.ch.FileDispatcherImpl.read0(Native Method)

   at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)

   at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)

   at sun.nio.ch.IOUtil.read(IOUtil.java:197)

   at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:379)

   – locked <0x00000007db9e04f0> (a java.lang.Object)

   at coozplz.test.presense.NetworkSocket.read(NetworkSocket.java:153)

   at coozplz.test.presense.NetworkSocket.recvMessage(NetworkSocket.java:216)

   at coozplz.test.presense.NetworkSocket.recvMessageHead(NetworkSocket.java:355)

   at coozplz.test.presense.NetworkSocket.recvMessage(NetworkSocket.java:372)

   at coozplz.test.presense.ServerThread.run(ServerThread.java:33)


Dump내용중이 coozplz.test.presense.ServerThread 소스를 분석해보면 무한 루프를 찾을 수 있을 겁니다.


TDA(ThreadDump analiyzer) 를 이용한 스레드덤프 분석

순서

  1. 쓰레드 덤프를 5~7초 간격으로 3~5회 정도 추출한다.
    jstack 또는 kill -3 명령을 이용

  2. TDA(ThreadDump analyzer)를 실행한다.

  3. 파일 – 열기 선택 후 스레드 덤프 파일을 선택한다.

  4. 분석 시작


주의사항

쓰레드 덤프를 간격을 두고 여러번 추출하는 이유는 여러번 추출하는 동안 같은 부분에 BLOCK이 되어 있다면 로직 처리 시간이 15초가 넘어가는 경우기 때문에 병목 현상을 예상할 수 있습니다.


위의 이미지를 보면 덤프가 출력된 시간을 볼 수 있습니다. 약 7초간격으로 반복적으로 쓰레드 덤프를 출력 했습니다.

위의 상황을 Oracle JDBC에서 대기 상태로 남는 문제가 발생되는 것을 확인할 수 있습니다. 링크된 부분을 클릭하면 LOCK 된 객체때문에 대기중인 스레드 정보도 확인할 수 있습니다.



양방향 암호화

importjava.security.Key;
importjavax.crypto.Cipher;importjavax.crypto.spec.SecretKeySpec;
publicclassAESCrypto {
   private String use = “Y”;
   private String salt;

   public String getUse() {
       return use;
   }

   publicvoidsetUse(String use) {
       this.use = use;
   }

   public String getSalt() {
       return salt;
   }

   publicvoidsetSalt(String salt) {
       this.salt = salt;
   }

   publicstatic Key generateKey(String algorithm, byte[] keyData) {
       SecretKeySpec keySpec = new SecretKeySpec(keyData, algorithm);
       return keySpec;
   }

   public String Encrypt(String src) throws Exception {
       if (!“Y”.equals(use)) return src;
       Key key = generateKey(“AES”, toBytes(salt, 16));
       String transformation = “AES”;
       Cipher cipher = Cipher.getInstance(transformation);
       cipher.init(Cipher.ENCRYPT_MODE, key);
       byte[] plain = src.getBytes();
       byte[] encrypt = cipher.doFinal(plain);
       return toHexString(encrypt);
   }

   public String Decrypt(String hex) throws Exception {
       if (!“Y”.equals(use)) return hex;
       Key key = generateKey(“AES”, toBytes(salt, 16));
       String transformation = “AES”;
       Cipher cipher = Cipher.getInstance(transformation);
       cipher.init(Cipher.DECRYPT_MODE, key);
       byte[] encrypt = toBytesFromHexString(hex);
       byte[] decrypt = cipher.doFinal(encrypt);
       returnnew String(decrypt);
   }

   publicstaticbyte[] toBytes(String digits, int radix)
       throws IllegalArgumentException, NumberFormatException {
           if (digits == null) {
               returnnull;
           }
           if (radix != 16 && radix != 10 && radix != 8) {
               thrownew IllegalArgumentException(“For input radix: \”” + radix
                       + “\””);
           }
           int divLen = (radix == 16) ? 2 : 3;
           int length = digits.length();
           if (length % divLen == 1) {
               thrownew IllegalArgumentException(“For input string: \”” + digits
                       + “\””);
           }
           length = length / divLen;
           byte[] bytes = newbyte[length];
           for (int i = 0; i < length; i++) {
               int index = i * divLen;
               bytes[i] = (byte) (Short.parseShort(
                           digits.substring(index, index + divLen), radix));
           }
           return bytes;
       }

   publicstaticbyte[] toBytesFromHexString(String digits)
       throws IllegalArgumentException, NumberFormatException {
           if (digits == null) {
               returnnull;
           }
           int length = digits.length();
           if (length % 2 == 1) {
               thrownew IllegalArgumentException(“For input string: \”” + digits
                       + “\””);
           }
           length = length / 2;
           byte[] bytes = newbyte[length];
           for (int i = 0; i < length; i++) {
               int index = i * 2;
               bytes[i] = (byte) (Short.parseShort(
                           digits.substring(index, index + 2), 16));
           }
           return bytes;
       }

   publicstatic String toHexString(byte[] bytes) {
       if (bytes == null) {
           returnnull;
       }

       StringBuffer result = new StringBuffer();
       for (byte b : bytes) {
           result.append(Integer.toString((b & 0xF0) >> 4, 16));
           result.append(Integer.toString(b & 0x0F, 16));
       }
       return result.toString();
   }
}