Post

Item 67 - 최적화는 신중히 하라

최적화 격언 👨‍💻

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

윌리엄 울프

(전체의 97% 정도인) 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다.

도널드 크루스

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

M. A. 잭슨

프로그램 설계 시, 고려해야 할 요소 🧐

(1) 빠른 프로그램보다는 좋은 프로그램을 작성하라. 🐢

  • 성능 때문에 견고한 구조를 해치는 최적화는 하지말자.
  • 좋은 프로그램은 개별 구성 요소의 내부를 독립적으로 설계할 수 있다. - 정보 은닉 원칙
  • 시스템의 나머지에 변화를 주지 않고도 내가 원하는 부분에 변화를 줄 수 있다.

(2) 성능을 제한하는 설계를 피하라.

  • 설계적인 요소는 완성 후 변경하기가 가장 어렵다.
    • API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등이 대표적이다.
    • 완성 후에는 변경이 어렵고, 시스템 성능도 크게 제한할 수 있다.

(3) API를 설계할 때 성능에 주는 영향을 고려하라.

  • public 타입을 가변으로 만들면, 불필요한 방어적 복사를 유발할 수 있다.
    • 메모리 사용이 증가하고, 프로그램 성능이 저하될 수 있다.
    • 가능한 불변 객체를 사용해서 이런 문제를 방지하고, 필요한 경우에만 가변 객체를 사용하자.
  • 컴포지션으로 작성할 수 있는 부분을 상속으로 만들면, 성능제약까지 물려받을 수 있다.
    • 상속은 코드의 재사용성을 높이지만, 상위 클래스의 특성을 물려받아 변경이 어렵다.
    • 반면, 컴포지션을 이용하게 되면 필요한 부분만 선택해서 클래스를 구성할 수 있기 때문에 유연성이 높아지고 성능 최적화에 유리하다.
  • 인터페이스가 아닌 구현 타입을 사용하면, 차후 개선된 구현체를 사용하기 어려워진다.

ex) java.awt.Component 클래스의 getSize 메서드

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Component implements ImageObserver, MenuContainer,
                                           Serializable
{
    public Dimension getSize() {
        return size();
    }

    @Deprecated
    public Dimension size() {
        return new Dimension(width, height); // 방어적 복사 수행 
    }
}
  • getSize 메서드는 Dimension 인스턴스를 반환한다.
1
2
3
4
5
6
7
8
9
10
11
public class Dimension extends Dimension2D
		implements java.io.Serializable {  // 불변이 아니다

    public int width;
    public int height;

    public void setSize(int width, int height) {
          this.width = width;
          this.height = height;
    }
}
  • Dimension 클래스는 가변으로 설계되었다.
  • getSize 메서드를 호출할 때마다 새로운 Dimension 인스턴스가 생성된다. → 성능 저하 유발
1
2
3
4
5
6
7
8
9
public abstract class Component implements ImageObserver, MenuContainer, Serializable {
    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}
  • 자바 2에서 getWidthgetHeigth 메소드를 추가하여 성능 문제를 개선하려 하였다.
  • 이 두 메소드는 각각 너비와 높이의 기본 타입을 반환하므로, getSize를 호출할 때마다 새로운 Dimension객체를 생성하는 것보다 훨씬 효율적이다.

하지만, 기존 클라이언트 코드는 여전히 getSize 메서드를 호출하며, 원래 내린 API 설계 폐해를 감내한다.

(4) 성능을 위해 API를 왜곡하는 건 매우 안 좋은 생각이다.

  • 잘 설계된 API는 성능도 좋은 게 보통이다.
  • API를 왜곡하도록 만든 성능 문제는 플랫폼이나 아랫단 소프트웨어 개선으로 사라질 수 있다.

(5) 최적화 시도 전후로 성능을 측정하라.

  • 시도한 최적화 기법이 성능을 드라마틱하게 높이지 못하거나 심지어 악화시키는 경우가 많다.
  • 주 원인은 프로그램에서 시간을 잡아먹는 부분을 추즉하기가 어렵기 때문이다.
  • 프로파일링 도구를 이용하여 최적화 할만한 부분을 열심히 탐색해보자.
    • 보통 10%의 코드에서 90%의 성능을 사용한다.

자바는 구현 시스템, 릴리즈, 프로세서, 플랫폼 등에 따라 성능 차이가 심하니 잘 측정하면서 최적화 하자.

💡 핵심 정리

  • 좋은 프로그램을 작성하면 보통 성능은 따라온다.
  • 시스템 설계 시에는 성능을 염두에 두자.
    • API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등이 있다.
  • 충분히 빠르면 그걸로 끝이며, 그렇지 않다면 프로파일링 도구를 이용해 원인을 찾자.
  • 알고리즘을 잘 살펴보고, 알고리즘을 변경하는 것을 고려하자.
    • 다른 작은 요소들은 최적화 해봐야 보통 미미한 효과만 갖는다.
This post is licensed under CC BY 4.0 by the author.