10
1221

잘못된 코딩상식..논의.


제가 1.5 버전 교과서로 자바를 배웠습니다. 

학원 학생 시절때 배웠던 것이

String s = "test"+"안녕하세요"+"바이바이"+"오오";

이런식으로 문자를 더하면 좋지 않다 메모리 낭비가 생긴다고 배웠습니다. 


이번에 해당 문제로 기능개선을 하려고 좀더 자세하게 알아본결과

3가지 이슈를 발견하여 논의 하였으면 좋겠습니다. 


1. 예전에는 문자열 +a+b 하는 것이  메모리가 스태틱 영역으로 올라가서 정말 좋지않은 방법이었습니다만..

java 1.7버전부터 문자열이 heap 영역으로 저장이 되어 가비지 대상이 된다고 합니다.

문자열로 인한 아웃오브메모리는 더이상 발생하지 않습니다.


2. 그럼  + + + 연산은 정말 성능이 떨어지는가..

구글링 결과 이것도 자바 1.5 버전부터 StringBuffer append를 사용하도록 자동 컴파일이 된다고 합니다.

그런데 어떤 성능비교 테스트에서 보면 StringBuffer를 사용한 것이 + 연산 한것보다 월등히 좋습니다.

아마도 for문으로 돌면서 append해서 그런 것 같습니다. for문으로 돌아서 나오는 것은 컴파일 시 변환이 안되겠죠.


3. 정말 중요한 성능 비교 문제는 사실 logging때문입니다. 

logger.debug("안녕하세요 {}, 테스트입니다. {}", a, b);
myLogging("안녕하세요 "+a+", 테스트입니다. "+b);

void myLogging(String s) {
    if(memberPram.equals("dev"){
        logger.info(s)
    }
}

위의 1번 줄과 2번줄의 로깅 방법에 대한 성능 비교입니다. 

로그 레벨이 info로 debug는 로깅이 되지 않을때 당연히 1번이 월등이 좋을 것으로 예상을하고

2번은 낭비가 심하다고 생각을 했습니다. 


그런데 실제로 테스트를 해보니 1000만번을 돌려서

1번은 3.5초가 걸렸고

2번은 4초가 걸렸습니다.

하루 로깅 라인이 10만줄이 안되는데.. 의미 없는 성능 비교였던 것 같습니다.


정말 이해할 수 없는건

myLogging 메서드에 인자를 넘겨줄때 ""+""+"" 연산이 발생하는데도 불구하고 별차이가 없었다는 것입니다.

(문자열은 for문을 돌려서 계속 다르게 생성을 하였습니다. )


메서드로 인자를 넘길때 ++가 연산되어 넘어가는가 그렇지 않은가 논의를 해보면

메서드 변수가 스택 영역으로 쌓이는데 연산이 되지 않고 쌓일수는 없다고 생각을 합니다. 

그렇기 때문에 연산되어 넘어가는 것이 맞다고 생각하는데요.

이 문자열 연산이 1000만건에 0.5초의 차이밖에 벌어지지 않습니다.


제가 테스트를 잘못한 것인지 의문입니다만..

정말 성능 이슈가 그렇게 중요한 것인가도 다시한번 고민해봐야 될 것 같습니다.


제가 테스트를 잘못한 것인지, 컴퓨팅 성능이 워낙 좋아져서 이제는 별 차이가 없는 것인지 모르겠네요..


-1
0
  • 댓글 10

  • 제가 속도체크만 해서 다시한번 돌려보니 메모리 사용량 체크는 할줄몰라 눈으로 대강 살펴보니

    1번보다 2번이 10M~이상 더 쓰는 것 같네요. 전체 이클립스 메모리 4096M중에서요. 이것도 의미 없는듯../1000만건당이니..


    게다가 충격적인 것은 실제 로깅 찍을때 는 ++연산한 것이 3만건 기준으로 1.7초대 1.6초로 0.1초 빠르기 까지하네요.;

    성능보다는 편의성을 위주로 개선사항을 정리해야될 것 같습니다.-_-..

    0
  • zepinos
    16k
    2018-08-10 23:25:06

    String s = "test"+"안녕하세요"+"바이바이"+"오오";


    이 문장은 Java 가 class 파일로 precomplie 될 때 아래와 같은 형태로 변경됩니다.


    String s= new StringBuilder().append("test").append("안녕하세요").append("바이바이").append("오오").toString();


    (손코딩이라 문법적으로 안맞을 수도 있습니다)


    테스트 하실 때


    String s = "test";

    s += "안녕하세요";

    s += "바이바이";

    s += "오오";


    와 같이 변경한 뒤에 테스트를 진행해보세요.


    그리고 로깅의 경우, 아래와 같이 변경 후 해보세요.


    if (logger.isDebug())

        logger.debug("안녕하세요 {}, 테스트입니다. {}", a, b);


    물론 컴퓨터의 속도가 매우 빨라져서 크게 의미는 없는 것도 사실입니다.


    이와 유사한 논쟁거리가


    String a = "test";

    String b = "test";

    String c = new String("test");

    if (a == b) System.out.println("a == b");

    else System.out.println("a != b");

    if (a == c) System.out.println("a == c");

    else System.out.println("a != c");


    가 있습니다. JVM 에서 String 에 대한 캐시를 하기 때문에 a == b 가 출력될껀데, 무조건 a == b 가 출력될꺼라곤 보장할 수 없기 때문에 .equals() 을 쓰라고 항상 주입시키고, 저 역시 그게 옳다고 생각합니다. 물론 새로운 객체를 생성하는 형태인 c 는 a != c 가 항상 출력됩니다.





    0
  • lux0425
    103
    2018-08-10 23:30:24

    스트링 + 연산에 대해서 위의 예제 코드로 성능 테스트를 하셨다면

    컴파일러가 알아서 StringBuilder를 사용해줍니다.

    그리고 두번 + 해준다고 해서 문제가 생기지도 않습니다. 성능 이슈가 크게 없습니다.


    문득 궁금해서 찾아보니

    + 연산은 메모리 관리가 안되는 경우의 예제는 아래와 같습니다. 


    tring[] test = new String[1000000000];
    
            for (int i = 0 ; i < test.length; i++) {
                test[i] =  "333232u239847239084729374023874243";
            }
    
            String tmp = "333232u239847239084729374023874243";
            for (String t : test) {
                tmp = tmp + t;
    
            }


    위의 코드의 경우에는 컴파일시 아래와 같이 됩니다


    String[] test = new String[1000000000];
    
            for(int i = 0; i < test.length; ++i) {
                test[i] = "333232u239847239084729374023874243";
            }
    
            String tmp = "333232u239847239084729374023874243";
            String[] var3 = test;
            int var4 = test.length;
    
            for(int var5 = 0; var5 < var4; ++var5) {
                String t = var3[var5];
                tmp = tmp + t;
            }


    요런 상황이라면 문제가 될 요소가 된다고 생각됩니다

    0
  • Kunie
    452
    2018-08-10 23:32:34 작성 2018-08-10 23:35:27 수정됨
    String str = "string";

    String test1 = "";
    String test2 = "";
    String test3 = "";

    long t1_start = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
    test1 += str;
    }
    long t1_end = System.currentTimeMillis();

    long t2_start = System.currentTimeMillis();
    StringBuffer stringBuffer = new StringBuffer();
    for (int i = 0; i < 100000; i++) {
    stringBuffer.append(str);
    }
    long t2_end = System.currentTimeMillis();

    long t3_start = System.currentTimeMillis();
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < 100000; i++) {
    stringBuilder.append(str);
    }
    long t3_end = System.currentTimeMillis();


    System.out.println("test case 1 = " + (t1_end - t1_start));
    System.out.println("test case 2 = " + (t2_end - t2_start));
    System.out.println("test case 3 = " + (t3_end - t3_start));



    이런 경우에 성능차이가 ...

    0
  • zepinos
    16k
    2018-08-10 23:37:21 작성 2018-08-11 03:27:09 수정됨

    그리고 위에서 언급 안하는게 있는데요, StringBuilder 나 StringBuffer 는 모두 기본 생성자에 기본 크기가 존재합니다. 즉, 생성자에 크기를 줄 수 있는데요...이 크기를 벗어나게 되면 자동 증가 되거나 overflow 오류나 뒷부분이 무시되는 경우가 발생할 수 있습니다(후자는 기억이 가물가물)

    즉, for 로 그렇게 우겨넣으려면 최종적으로 들어갈 크기만큼 처음에 생성자로 크기를 넉넉하게 잡고 집어넣어야 제대로 된 테스트가 됩니다. 아니면 용량이 넘칠 때 새로운 영역에 새로 공간을 확보(보통 기존의 2배)하고 기존 내용을 복사하는 작업을 하기 때문에 이 부분에서 테스트 왜곡이 발생할 수 있습니다.

    0
  • 돈까스
    1k
    2018-08-11 11:49:47 작성 2018-08-11 12:19:09 수정됨
    logger.debug("안녕하세요 {}, 테스트입니다. {}", a, b);
    myLogging("안녕하세요 "+a+", 테스트입니다. "+b);

    만약에 스트링의 + 연산에 대한 비용을 비교하시고 싶었다면, 위와 같이 하지 마시고, 아래와 같이 하셔야 보다 동등한 비교이죠.(완벽하다는 이야기는 아닙니다.)

    logger.debug("안녕하세요 {}, 테스트입니다. {}", a, b);
    logger.debug("안녕하세요 "+a+", 테스트입니다. "+b);
    

    지금 테스트하신건 String에 대한 +연산만 테스트하신게 아니고요.

    가상함수에 대한 호출 등에 대한 비용 등이 포함된 경우를 가지고 비교하신 겁니다.


    위의 케이스의 myLogging은 같은 클래스에 있는 메소드를 호출하신 경우잖아요?

    slf4j Logger이라고 생각됩니다만, 실제로 소스를 보시면 Logger는 인터페이스고 실제 구현체는 어떤 클래스일지 환경에 따라 다릅니다.

    나머지 코드의 내용은 모르겠지만(memberParam이 "dev" 인지와 아닌지, 혹시라도 final 라든가),

    a, b가 String인지 그냥 toString() 호출할 것을 기대한 임의의 오브젝트일지도 달라질 수 있고요.

    (왜냐하면 로그를 찍을 때 a 와 b를 String으로만 사용하지는 않잖아요. toString()에서 뭘할지 모르니까)

    아무튼 성능이란 것을 단순히 개념적으로만 판단하시면 안되고요.


    실제 실행 환경에서는 jvm 자체의 런타임 최적화(예를 들자면 method inline 같은 것)가 있을 수 있고,

    cpu / ram / 캐시 레벨에서 data locality 등의 최적화가 들어갈 수 도 있습니다.


    작업 대상이 되는 String 들이 작고, CPU 캐시 안에 다 들어가면 무지 빠른거고,

    안 그런 경우, String 각각의 길이가 무지 긴 길거나, 동일한 String이 아닌 실행할 때마다 캐시 안 되어 있는 것들을 무작위로 연산하는 경우면 무진장 느리겠죠.

    그런 경우에는 연산을 안하고 OutputStream에 바로 쓰는게 더 성능이 낫게 나오겠구요.


    테스트하신 케이스는 a와 b가 String 이나 primitive type일거라고 예상하고요,

    아마도 루프로 돌렸다보니 CPU 캐시 등으로 인해서 굉장히 빠르게 연산이 되었을 것입니다.

    그런 경우 가상 함수에 대한 호출 등에 대한 비용이 더 클 수 있겠죠.


    아무튼 글쓴분께 말씀드리고 싶은 것은, 너무 자바 코드 레벨에서만 단순하게 뭐가 낫다 나쁘다 생각하시면 안되고요.

    실제 컴퓨팅 환경 전체에서 다각도로 보셔야 할 필요가 있다는 것과

    마이크로 레벨의 최적화를 지나치게 고민하는 것은 비효율적이라는 것을 말씀드립니다.

    0
  • 돈까스
    1k
    2018-08-11 12:13:43 작성 2018-08-11 12:19:52 수정됨

    이와 비슷한 케이스로 자주 나오는 케이스가

    어레이에 대해서 각 엘리먼트별로 연산을 할 때, 루프로 돌리는게 나는지 그냥 다 펼쳐서 연산하는게 나은지,

    데이터를 정렬해서 배열을 만들어 바이너리 서치를 해서 찾는 것이 빠른지, 해시 맵을 만들어 찾는게 빠른지,

    DB에서 풀 서치를 할지, 인덱스를 쓸지와 같은 논란거리들이 있죠.


    데이터의 크기가 얼마나 될지, 메모리 안에, CPU 캐시 안에 들어갈 수 있는지,

    전체 데이터 중에 몇 건을 처리해야 하고, 그 접근 순서에 따른 데이터 분포가 어떨지 등에 따라서 결과가 다 다르게 나옵니다.

    best case, worst case, average case가 다 다르고, 다각도로 잘 판단해야죠.

    그게 경력자와 전문가에게 기대하는 모습이고요.

    0
  • 죄송합니다. 이곳엔 워낙 실력자 분들이 많아서 ㅋ.

    Java 1.7 1.8부터 배우셨던 분들은 논란거리도 안되겠지만

    과거의 상식이 시대가 변하면서 비상식이 되는 일들이 많은데 이번에 상식이 비상식이 되었던 문제가 있어서 공유차원에서 글을 올렸습니다.

    이것을 모르는 분들도 많을겁니다. 과거에는 상식이었으니깐요.

    개발자 자질문제 까지 나오니 부끄럽기도 하고 이분들 정말 나보다 실력이 좋은건가 만나보고 싶기도 하네요.

    우선 병렬로 +하는 문자열은 자동으로 SB컴파일되며

    new String 뿐만 아니라 "" 형식도 1.7부터 힙영역에 저장이되고 가비지가 된다는 것.

    로그의 스트링 포멧터 기능은 미미하지만 +연산보단 느리다는 것 다만 로깅을 하지 않을때는 +연산을 안하므로 미세하게 빠르다는 것.

    그리고 이 성능 문제는 극히 차이가 없다는 것 정도입니다.


    이 내용을 모르션던 분들에게 도움이 되었으면 좋겠습니다.

    이런글을 올렸다고 몇몇분들은 개발자 자질 문제제기도 하시고 다 아는 내용을 가르치듯(추가 공유하신 분들제외) 적으시고..

    제가 몇가지 사실을 올렸다고 뭔가를 엄청 모르고 실력없는 개발자가 되었다는게 놀랍네요. 어떻게 그런 추측이 가능한지.. 앞으로 자중하고 더 열심히 하겠습니다. 죄송합니다.~

    (참고는 예제코드는 실운영에 코드를 간략화한 것이지 제가 만든것이 아닙니다. 간략화 과정에서 의미전달이 제대로 안됐을수도 있겠네요.)

    0
  • jja
    1k
    2018-08-12 17:10:17

    이런 고민 자체가 중요한걸 놓치고 있다고 생각함 ㅋㅋㅋ

    0
  • 로그인을 하시면 댓글을 등록할 수 있습니다.