[Effective Java] 최적화는 신중히 하라

들어가며

맹목적인 어리석음을 포함해 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다. 심지어 효율을 더 높이지도 못하면서

자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다.

최적화를 할 때는 다음 두 규칙을 따르라.

첫번째, 하지마라.
두번째, 전문가를 한정해서 아직 하지마라, 다시 말해 완전히 명백하고 최적화되지 않은 해법을 찾을 때 까지는 하지 마라.

위 격언들은 자바가 탄생하기 전에 나온 말로 최적화의 어두운 진실을 이야기 해준다. 최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다. 빠르지도 않고 제대로 동작하지도 않으면서 수정하기 어려운 소프트웨어를 만드는 것이다. 빠른 프로그램보다는 좋은 프로그램을 작성하라. 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있다. 따라서 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있다. 프로그램을 완성할 때까지 성능 문제를 무시하라는 뜻은 아니다. 구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고는 해결하기 불가능할 수 있다. 따라서 설계 단계에서 성능을 반드시 염두에 두어야 한다.

성능을 제한하는 설계를 피하라. 완성후 변경하기 어려운 부분이 컴포넌트끼리 혹은 외부 시스템과의 소통 방식이다.

API를 설계할 때 성능에 주는 영향을 고려하라. public 타입을 가변으로 만들면 불필요한 방어적 복사를 수없이 유발하게 된다. 컴포지션으로 해결할 수 있음에도 불구하고 상속을 통해 설계하면 하위 클래스는 상위 클래스에 영원히 종속되며 성능 제약도 물려받게 된다. 또한 인터페이스를 놔두고 구체 타입을 통해 사용하게 되면 특정 구현체에 종속되어 나중에 더 빠른 구현체가 나오더라도 이용하지 못하게 한다. 이는 현실적인 문제다. java.awt.Component의 getSize() 메소드가 그러하다. 매번 Dimension 인스턴스를 리턴하기 위해 방어적 복사를 수행한다. getSize, getHeight를 통한 방법도 있으니 그러한 부분은 고려하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

/**
* Returns the size of this component in the form of a
* <code>Dimension</code> object. The <code>height</code>
* field of the <code>Dimension</code> object contains
* this component's height, and the <code>width</code>
* field of the <code>Dimension</code> object contains
* this component's width.
* @return a <code>Dimension</code> object that indicates the
* size of this component
* @see #setSize
* @since JDK1.1
*/
public Dimension getSize() {
return size();
}

/**
* @deprecated As of JDK version 1.1,
* replaced by <code>getSize()</code>.
*/
@Deprecated
public Dimension size() {
return new Dimension(width, height); //방어적 복사 수행
}

잘 설계된 API는 성능도 좋은게 보통이다. 그러니 성능을 위해 API를 왜곡하는건 좋지 않은 생각이다. 신중하게 설계하여 깨끗하고 명확하고 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 차례가 된다. 물론 성능에 만족하지 못할 경우에 한정된다.

각각의 최적화 시도 전후로 성능을 측정하라. 시도한 최적화 기법이 성능을 눈에 띄게 높이지 못하는 경우가 많고, 심지어 더 나빠지게 할 때도 있다. 주요 원인은 프로그램에서 시간을 잡아먹는 부분을 추측하기가 어렵기 때문이다. 프로파일링 도구는 최적화 노력을 어디에 집중해야 할지 찾는데 도움을 준다. 시스템 규모가 커질수록 프로파일러는 중요해진다. jmh도 프로파일러는 아니지만 자바 코드의 상세한 성능을 알기 쉽게 보여주는 프레임워크다.

최적화 시도는 C나 C++에서도 중요하지만 성능 모델이 덜 정교한 자바에서는 더욱 중요하다. 자바는 기본 연산에 드는 상대적인 비용을 덜 명확하게 정의하고 있다. 즉, 코드와 CPU 연산의 추상화 격차가 커서 최적화로 인한 성능 변화를 일정하게 예측하기가 그만큼 더 어렵다. 자바의 성능 모델은 정교하지 않을뿐더러 구현 시스템, 릴리즈, 프로세서마다 차이가 있다. 그래서 프로그램을 여러가지 자바 플랫폼이나 하드웨어 플랫폼에서 구동한다면 최적화 효과를 그 각각에서 측정해야 한다. 그러다 보면 다른 구현 혹은 하드웨어 플랫폼 사이에서 성능을 타협해야 하는 상황도 마주하게 된다. 자바는 지속적으로 발전하고 있어서 프로그램의 성능 예측이 더욱 어려워지고 있다. 그에 비례해서 측정의 중요성도 매우 커지고 있다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/21/Languages/Effective%20JAVA/item67/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.