본문 바로가기

Reading/Effective Java

[Effective-Java] Item 66, 67.네이티브 메소드는 신중히 사용하라, 네이티브 메소드는 신중히 사용하라

[아이템66 - 네이티브 메소드는 신중히 사용하라]


JNI(자바 네이티브 인터페이스; Java Native Interface): 자바 프로그램이 네이티브 메소드를 호출하는 기술

네이티브 메소드(Native Method): C, C++ 같은(여기에 국한되는건 아니다) 네이티브 프로그래밍 언어로 작성한 메소드

 

네이티브 메소드의 주요 쓰임


1. 레지스트리 같은 플랫폼 특화 기능

자바 버전이 올라가며 OS같은 하부 플랫폼의 기능들을 점차 흡수하기에 네이티브 메소드 사용 필요는 점차 줄어든다.

자바9 부터는 process API를 추가해 OS 프로세스에 접근할 수 있어졌다. [참고] / [참고2] 

추가로 대체할 자바 라이브러리가 없는 네이티브 라이브러리를 사용해야할 때도 사용한다.

 

2. 네이티브 코드로 작성된 기존 라이브러리를 사용할 때

ex) 레거시 데이터를 사용하는 레거시 라이브러리.

 

3. 성능 개선 목적으로 결정적인 영향을 주는 영역만 따로 작성

거의 권장하지 않는 방법이다. 대부분 작업에서 지금의 자바는 다른 플랫폼에 견줄만한 성능을 보인다.

java.math가 처음 추가된 자바 1때나 BigInteger는 C로 작성한 고성능 라이브러리에 의지햇지만, 자바 3부터는 순수 자바로 구현하며 원래의 네이티브 구현보다도 빨라졌다. 그 후 자바 8까지 곱셈 성능 개선 외에 큰 변화는 없었다.

 

추가로 네이티브 라이브러리 쪽은 GNU 다중 정밀 연산 라이브러리(GMP)를 필두로 개선 작업이 계속 돼왔다.[참고]

고성능 다중 정밀 연산이 필요하다면 네이티브 메소드를 통해 GMP를 사용하는 것을 고려하자.

 

네이티브 메소드의 단점


  • 네이티브 언어가 안전하지 않아 네이티브 메소드를 사용하는 application도 메모리 훼손 오류에 안전하지 않다.
  • 네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성이 낮고 디버깅이 어려워 오히려 속도가 느려질 수 있다.
  • 가비지 컬렉터가 네이티브 메모리를 자동 회수하지 못하고 추적조차 할 수 없다.
  • 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용이 추가된다.
  • 네이티브 메소드와 자바 코드 사이의 접착코드(glue) 작성으로 귀찮고 가독성이 떨어진다.

정리


네이티브 메소드가 성능을 개선해주는 일은 거의 없으니 저수준 자원이나 네이티브 라이브러리 사용을 제외하곤 최소한으로 사용하고 테스트하자. 네이티브 코드의 버그는 애플리케이션 전체로 버그가 퍼질 수 있다.

단순하게 JNI를 이용하는 예제들은 많았지만 책에서 말한 반드시 사용해야만 하는 상황은 만들어 보지 못했다. [JNI 활용 예제]

 

 

 

[아이템 67. 최적화는 신중히 하라]


1. 빠른 프로그램보다 좋은 프로그램을 작성하자

성능 때문에 견고한 구조를 희생하지 말자. 정브 은닉 원칙을 잘 따른 좋은 프로그램은 개별 구성요소의 내부를 독립적으로 설계할 수 있다.

따라서 시스템 나머지에 영향을 주지 않고 각 요소를 다시 설계할 수 있다.

 

2. 성능을 제한하는 설계를 피하자

설계 요소인 컴포넌트 간 소통방식과 외부 시스템과의 소통방식은 완성 후 변경이 어렵거나 불가능할 수 있고 시스템 성능도 심각하게 제한될 수 있다. ex) API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등

 

3. API 설계시, 성능에 주는 영향을 고려하자

public 타입을 가변으로 만들면 불필요한 방어적 복사를 수없이 유발할 수 있고, 컴포지션으로 해결할 수 있는 것도 상속 방식으로 설계한 public 클래스는 상위 클레스에 영원히 종속되며 성능의 제약도 물려받는다. 

 

java.awt.Component 클래스가 불필요한 방어적 복사를 수행하는 대표적인 예다.

 

java.awt.Component

 

Dimension이 불변이 아니라서 getSize()를 호출하는 모든 곳에서 방어적 복사를 수행해 인스턴스를 새로 생성해야한다. Dimension을 불변으로 설계하는게 가장 이상적이지만 getSize를 getWidth와 getHeight로 나눠 객체의 기본 타입 값을 따로 반환할 수도 있다. 그렇기 때문에 Component 클래스에 두 메소드가 추가됐긴 하지만 getSize 메소드는 계속 호출 가능해 API 설계 결정의 폐해를 감내하고있다.

 

자바 2부터 추가된 Component class

 

잘 설계된 API는 성능도 좋은게 보통이기 때문에 성능을 위해 API를 왜곡하는 건 좋지 않다.

API를 왜곡하도록 만든 성능 문제는 해당 플랫폼, 아랫단 소프트웨어의 다음 버전에서 사라질 수도 있지만 왜곡된 API와 이를 지원하는데 따르는 고통은 영원히 계속된다. 신중하게 설계해 깨끗하고 명확한 구조를 갖춘 프로그램을 완성한 뒤 최적화를 고려하자(성능이 안좋다면)

 

Dimension은 public 필드에 setter까지 제공해 불변이 아니다. 

 

4. 각각의 최적화 시도 전후로 성능을 측정하자

최적화는 좋은 결과보다 해로운 결과로 이어지기 쉽다. 시도한 최적화 기법이 성능을 크게 높이지 못하는 경우가 많고, 더 나빠질 수도 있다. 프로그램에서 시간을 잡아먹는 부분을 추측하기 어렵고 느릴거라 생각한 부분이 성능에 별 영향을 주지 않는다면 시간만 날린 꼴이다.

 

5. 프로파일링 도구(profiling tool) [참고1/참고2]

최적화 노력을 어디에 집중해야 할지 찾는데 도움을 준다. 개별 메소드 소요시간, 호출 횟수 같은 런타임 정보를 제공해 집중할 곳과 알고리즘 변경 사실을 알려주기도 한다. n^2성능을 가지는 알고리즘이 있다면 효율적으로 교체해야 하고, 다른 튜닝을 안해도 문제는 없어진다.

프로파일러는 아니지만 자바 코드의 상세한 성능을 알기 쉽게 보여주는 마이크로 벤치 마킹 프레임 워크(jmh)도 있다.[JMH 참고]

 

성능 모델이 덜 정교한 자바에서는 성능 측정의 필요가 필수적이다. 기본 연산에 드는 상대적인 비용을 덜 명확하게 정의하고 있고, 이 말은 작성 코드와 CPU에서 수행하는 명령 사이의 추상화 격차가 커서, 최적화로 인한 성능 변화를 일정하게 예측하기 어렵다.

 

그리고 자바는 구현 시스템, 릴리즈, 프로세서마다 성능 차이가 있어 여러 자바 플랫폼이나 여러 하드웨어 플랫폼에서 구동한다면 최적화의 효과를 각각에서 측정해야하며 다른 구현 또는 하드웨어 플랫폼 사이에서 성능을 타협해야할 수 있다.

 

자바는 시간이 지날수록 프로세서부터 가상머신, 라이브러리, 자바가 수행하는 하드웨어 종류도 다양해지고 있어 더 복잡해졌다. 이는 성능 예측을 더 어렵게하기 때문에 성능 측정의 중요성은 아주 커졌다.

 

6. 정리

좋은 프로그램을 작성하다 보면 성능은 따라온다. 단, API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 설계는 성능을 염두에 두자. 그 후 성능을 측정해두고 빠르면 그냥 두고, 그렇지 않다면 문제가 되는 알고리즘이 있는지 찾아보고, 없다면 저수준으로 넘어가서 프로파일러를 사용해 문제 원인 지점을 최적화하자

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.