개발관련/(과거)메모

Java Performance를 올리는 코딩 Best Practice

동팡 2020. 5. 20. 20:49

옛날 자료 + 추가 정리해서 올림.

 

일단 Performace를 고려한 코딩을 하기 위해서는 우리의 마음가짐을 다음과 같이 한다.

 - 메모리 사용에 대한 생각

 - 시간 복잡도에 대한 생각

 

위와 같은 마음 가짐으로 진행한다.

각설하고 시작한다.

 

 

문자열의 연접 방식

  • String 객체는 불변 객체이다. 한번 초기화한 객체는 불변성을 갖고 있다. , 변경을 하지 못한다. 때문에, 문자열을 연접할 때 매번 메모리 초기화 작업을 진행한다. 문자열 연접을 도와주는 클래스 중, StringBufferStringBuilder가 존재한다.
  • StringBuffer의 경우, append 메소드에 synchronized가 되어있기 때문에, StringBuilderappend보다 느리다. 그런데! Java5 이상부터의 컴파일러는 String Concat 연산을 StringBuilder 연산으로 최적화(치환) 작업을 진행한다(그러나, 반복문은 얘기가 다르다. 매번 StringBuilder 객체를 생성한다.).
  • 나쁜 예
    • String retStr = str1 + str2 + str3;
  • 좋은 예
    • String retStr = new StringBuilder( str1 ).append( str2 ).append( str3 ).toString();

 

자료형의 기본형 사용

  • Java에서의 자료형은 기본형과 래퍼(객체형) 타입이 존재한다. 기본형 타입의 경우, 값을 직접 제어하기 때문에 Stack 메모리에서 바로 제어할 수 있다. 반면, 래퍼 타입의 자료형은 Heap 메모리에 저장한다. 해당 Heap메모리에서 값을 제어할 때 순서는 다음과 같다. 1) Stack 메모리에서 Heap 메모리의 참조 값 조회 2) 참조 값을 통해 Heap 조회. 때문에, 메모리 효율성은 기본형 타입의 사용이 좋다.

 

로그 레벨의 확인

  • 로그 레벨을 통해 PrintStream을 선택적으로 사용할 수 있다. 예를 들어, Info일 때 debug, trace와 같은 log PrintStream 작업을 진행하지 않는다. 그러나, 아래와 같은 로그문은 예외이다.
  • 그 레벨이 Info가 설정되어 있어도 String concat 작업을 진행한다. 때문에 아래와 같이 변경한다.

변경 전

log.debug(“User [” + userName + “] called method X with [” + i + “]”);

변경 후

if (log.isDebugEnabled()) {

	log.debug(“User [” + userName + “] called method X with [” + i + “]”);         

}

 

String.replace()

  • Java 8이하(8포함) String.replace() 보다는 Apache Common Lang’s StringUilts.replace()를 사용해야한다. 내장 replace보다 Common Langreplace가 더 빠르다. 해당 사항은 Java9에서 개선되었다.
  • JMH를 통해 밴치마킹 하면 아래와 같이 확연하게 확인할 수 있다. 테스트모드는 throughput이며, 노란색 부분의 스코어가 높은 것이 좋은 것이다.

 

반복문

1. indexed for vs enhanced for

  • 위와 같이 for문은 2개가 있다. 예전부터 우리가 써왔던 index for문과 Java5에 나온 enhanced for문이다. 일단 아래의 조건이 충족되면 enhanced for문을 사용하자.
    • 증감연산(i++)을 통한 반복 처리
    • Iterable을 구현한 객체에 대한 반복문(array, Collection)
    • Index(I )을 핸들링하지 않을 때
  • Enhanced for문을 사용하는 이유는 당연 속도가 더 빠르기 때문이다. i값에 대한 indexing이 없기 때문에 Enhanced for문이 더 빠를 수밖에 없다. 

2. indexed for문 사용할 때

  • Indexed for문에서 for문 탈출 조건을 아래와 같이 설정하면 안된다.

나쁜 예

 

for ( int i=0; i < list.size(); i++ ) {}

좋은 예

int size = list.size();
for ( int i=0; i < size; i++ ) {}

또는 

for ( int i=0, size=list.size(); i < size; i++ ) {}

 

 

EntrySet()의 사용

  • Map 컬렉션에 대해 for문을 이용할 때 가 많다. 단순 key, value를 조회 및 사용할 때 전자보다는 후자를 사용한다. map.get( key )에서 map 객체에서 key에 상응하는 value 값을 찾는 lookup 작업때문에 후자보다 느리다.

나쁜 예

for ( K key : map.keySet() ) { V value = map.get(key); }

 

좋은 예

for ( Entry<K, V> entry : map.entrySet() ) { K key = entry.getKey(); V value = entry.getValue() }

 

EnumMap 또는 EnumSet의 사용

  • Map, Set 인터페이스의 구현체로 보통 HashMap/Set을 사용한다. EnumMap/Set Enum과 함께 사용된다. HashMap/Set 보다 EnumMap/Set을 꼭 사용해야할 경우는 key 값이 동적이지 않을 때이다. 대표적으로 AppConfig.properties 같은 어플리케이션 initializing할 때 사용하는 설정 값이다. 여기서 사용하는 Key는 설정 이름이며, 바뀌지 않는다.
  • HashMap은 힙메모리를 더 적게 사용하지만, valuelookup 할 때 key에 대해 hashCode(), equals()를 실행한다. 반면, EnumMap의 경우, 내부적으로 Enum의 특성을 활용한다. , key.ordinal()를 통해 배열 조회를 실시한다. , key 값이 정적/고정일 때 EnumMap이 더 빠르다.
  • 구현 상세는 다음의 링크를 참고한다.
  • https://www.baeldung.com/java-enum-map

 

Collection의 구현체 선택

우리는 Collection 인터페이스를 정말 많이 사용한다. Collection 인터페이스의 구현체는 여러가지 존재한다. 그러나, “보통의 경우 다음과 같은 구현체를 사용하는 것을 권고한다. 구현체 상호간 JMH 테스트를 진행하면, 아래의 구현체들이 성능적인 측면에서 우위를 점한다.

  • Set -> HashSet
  • List -> ArrayList
  • Map -> HashMap
  • Queue -> LinkedList
  • Deque -> ArrayDeque

 

Collection 생성자/addAll/add

Set과 같은 Collection 인터페이스의 구현체에 Data를 삽입할 때의 경우이다. 다음의 상황은 다음의 방법을 할 것을 권고한다.

  • Data 한번 삽입(한번 초기화): addAll 보다 생성자를 통해 삽입한다.
  • 후자를 택하라
for (inti=0; i<1000; i++) { list.add( arr[i] ); }

VS

list.addAll( Arrays.asList( arr ) );

 

불필요한 객체 생성의 최소화

  • 딱히 할말이 없다. 불필요한 new 연산은 최대한 간소화한다. 예는 다음과 같다. 아래와 같이 하지말자.
Map<String, Object> returnMap = new HashMap<>();

returnMap = selectUserInfoList( UserVo vo );

 

변수의 재활용

  • 코드의 가독성을 해하지 않는 선에서 변수를 재활용한다. 예는 아래와 같다.

적용 전

int totalNum = getATotalNum() + getBToTalNum();
int maxNum = getATotalNum() + getBToTalNum();

적용 후

int num = 0;

num = getATotalNum() + getBToTalNum();  vo.setTotalNum( num );
num = getAMaxNum() + getBMaxNum();  vo.setMaxNum( num );

 

싱글턴의 사용

  • 1개의 인스턴스만 사용하는 방식이다. 싱글톤으로 사용할 수 있는 객체일 경우, 싱글톤의 사용을 권고한다. 싱글턴의 사용으로 인해, 객체 생성 비용 감소, 메모리의 효율성을 확보할 수 있다. 싱글턴을 생성할 때 상태 값을 사용하지 말아야한다(클래스 맴버 변수/쓰레드 세이프하지 않음). 여러 쓰레드가 사용할 수 있기 때문이다.

 

Null “” 예외 처리

예외 처리할 때 아래와 같은 코드를 많이 봤을 것이다.

If ( str == null || str.equals( “” ) ) { throw new IllegalArgumentException( " invalid input." ); }

   아래와 같이 변경한다.

if ( str == null || str.isEmpty() ) { throw new IllegalArgumentException( " invalid input." ); }

 

여기까지 읽은 독자의 경우, 여러 생각할 수 있다. 1) 굳이? 2) 하드웨어 좋아짐 3) 변태같음

근데 중요한 사항이 있다. 고성능 어플리케이션은 성능적인 관점의 조각들이 합쳐졌을 때 탄생한다. , 퍼즐과 같다. 위에서 언급한 항목들은 퍼즐 조각 중 한 개다.

다른 퍼즐 조각들은 서버의 튜닝, JVM의 튜닝, WAS의 튜닝, DBMS의 튜닝, 네트워크의 튜닝, 프로토콜의 튜닝, 캐시의 사용 등이 있다.

 

결론은 퍼즐 조각 하나라도 챙겼으면 한다.

 

 

참고문헌

 

https://gist.github.com/benelog/b81b4434fb8f2220cd0e900be1634753

https://www.javaworld.com/article/2150208/a-case-for-keeping-primitives-in-java.html

https://blog.jooq.org/2017/10/11/benchmarking-jdk-string-replace-vs-apache-commons-stringutils-replace/

https://stackoverflow.com/questions/11555418/why-is-the-enhanced-for-loop-more-efficient-than-the-normal-for-loop

https://blog.jooq.org/2015/02/05/top-10-easy-performance-optimisations-in-java/

https://help.semmle.com/wiki/display/JAVA/Inefficient+use+of+key+set+iterator

https://docs.oracle.com/javase/tutorial/collections/implementations/summary.html

https://stackoverflow.com/questions/3321526/should-i-use-string-isempty-or-equalsstring