제리 devlog

[이펙티브자바] 최적화는 신중히 하라 본문

Java

[이펙티브자바] 최적화는 신중히 하라

제리 . 2020. 11. 30. 03:09

저자는 최적화를 최대한 지양하라고 한다. 최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다. 

 

성능 때문에 견고한 구조를 희생시켜서는 안된다. 빠른 프로그램보다 좋은 프로그램을 작성해야한다. 좋은 프로그램은 정보 은닉 원칙을 따른다. 그렇기에 개별 구성요소의 내부를 독립적으로 설계할 수 있다. 시스템의 나머지에 영향을 주지않고도 각 요소를 다시 설계할 수 있는 것이다.

 

*정보 은닉 : 캡슐화에서 가장 중요한 장점으로 다른 객체에게 자신을 숨기고 자신의 연산을 통해서만 접근하게한다. 객체의 정보를 직접 꺼내오지 않으므로 유지 보수에 용이해진다. ex) 객체의 필드 명이 바뀐경우 혹은 특정 로직이 바뀐 경우 객체의 값을 꺼내 처리한 부분을 모두 찾아 수정해야함

public class User{
	
    private String userId;
    private String password;
    
    public boolean login(String inputUserId, String inputPassword){
    	if(userId.eqauls(inputUserId) && password.equals(inputPassword)){
        	return true;
        }else{
        	return false;
        }
        
    ...
    
 }
 
 public static void main(String[] args){
 	
    ...
    user.login("user id", "password"); // 좋은 예
    
    if(user.getUserId().eqauls(inputUserId) && user.getPassword().equals(inputPassword)){ //나쁜 예
        	
            ...
    }
    ...
 
 }

 

그렇다고 프로그램을 완성할 때까지 성능 문제를 무시하라는 뜻이 아니다. 구현상의 문제는 나중에 최적화로 해결이 가능하지만 아키텍쳐의 결함이 성능을 제한한다면 시스템 전체를 다시 작성하지 않고서는 해결이 불가능할 수 있다. 완성된 설계의 기본 틀을 변경하는 작업은 유지보수나 개선이 어려운 시스템으로 만들어지기 쉽다. 

 

설계 단계에서 성능을 염두해야한다.

성능을 제한하는 설계를 피하라. 완성 후 변경하기 가장 어려운 설계 요소는 컴포넌트끼리, 혹은 외부 시스템과의 소통 방식이다. API, 네트워크 프로토콜, 영구 저장용 데이터 포멧 등이 대표적이다. 이런 요소는 완성후에는 변경하기 어렵거나 불가능할 수 있으며, 동시에 시스템의 성능을 심각하게 제한할 수 있다.

 

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

public 타입을 가변으로 만들면(내부 데이터를 변경할 수 있게 만들면) 불필요한 방어적 복사를 수없이 유발할 수 있다. 비슷하게 컴포지션으로 해결할 수 있음에도 상속 방식으로 설계한 public 클래스는 상위 클래스에 영원히 종속되며 그 성능 제약까지도 물려 받기 된다. 인터페이스도 있는데 굳이 구현 타입을 사용하는 것 역시 좋지 않다. 특정 구현체에 종속되게 하여 나중에 더 빠른 구현체가 나오더라도 이용하지 못하게한다.

 

컴포지션

컴포지션은 기존의 클래스가 새로운 클래스의 구성요소에 사용되는 것이다. 이렇게 되면 새로운 클래스는 기존 클래스의 영향이 적어지고 기존 클래스 안에 새로운 메소드가 추가되도 안전하다. 상속의 경우 부모 클래스를 수정하면 자식 클래스도 같이 수정해야하는 경우가 발생하기 쉽다.

public class B extend A{  //상속

	...
    
}

public class B{  //컴포지션

    private A a;

}

 

불변 클래스 vs 가변 클래스

불변 클래스 가변 클래스
String 클래스
String 클래스와 같이 인스턴스가 한 번 생성되면 그 값을 변경할 수 없는 클래스를 불변 클래스라 말한다.
StringBuffer 클래스
StringBuffer 클래스와 같이 자유롭게 인스턴스의 값을 변경할 수 있는 클래스
append()나 insert() 메소드와 같은 값을 변경하는 메소드는 없다. append()나 insert() 메소드와 같이 값을 변경하는 set()메소드가 존재한다.

 

방어적 복사란?

높이와 넓이를 인자로 받는 Box 클래스와 Box를 인자로 받는 Pain클래스가 있다고 가정해보자

public class Box {
  int width;
  int height;
  public Box(int width, int height){
    this.width = width;
    this.height = height;
  }

  public void setSize(int width, int height){  //가변
    this.width = width;
    this.height = height;
  }
}

public class Paint {

  private Box box;

  public Paint(Box box) {
    this.box = box;
  }

  public void paintBox() {  //출력
    System.out.println("width : " + box.width);
    System.out.println("height : " + box.height);
  }
}

Box는 set메서드를 사용해 값을 수정할 수있는 가변 클래스이다. Paint의 paintBox는 인자로 전달 받은 box의 높이와 넓이를 출력한다.

public class Main {

  public static void main(String[] args) {
    Box box = new Box(10, 10);
    Paint paint = new Paint(box);
    paint.paintBox();
    box.setSize(20, 20);
    paint.paintBox();
  }
}

box의 값이 변하자 paintBox의 출력 값도 변했다. 10 X 10 크기의 상자만 페인트를 칠하면됐지만 갑자기 전달 받은 박스의 크기가 2배로 커진 것이다. 이때 방어적 복사가 필요하다.

public class Paint {

  private Box box;

  public Paint(Box box) {
    this.box = new Box(box.width, box.height); //방어적 복사
  }

  public void paintBox() {
    System.out.println("width : " + box.width);
    System.out.println("height : " + box.height);
  }
}

다시 출력해보면 박스의 크기를 수정해도 결과가 변화지않는다. 이것을 방어적 복사라고한다.

 

본론으로 돌아와서 API 설계가 성능에 영향을 주는 예시는 다음과 같다.

java.awt.Component클래스의 getSize메서드가 있다. API 설계자는 getSize()가 호출되면 새로운 Dimension인스턴스를 반환하게 설정했다.

여기에 Dimension은 가변으로 설계했으니 getSize를 호출하는 모든 곳에서 인스턴스를 (방어적으로 복사하느라) 새로 생성해야한다. Dimension을 불변으로 만드는게 가장 이상적이지만 getSize()를 getWidth()와 getHeight()로 나누는 방법도 생각할 수 있다. 실제로 이러한 메서드가 추후에 나왔지만 기존 클라이언트는 여전히 getSize()를 호출하며 원래 API설계의 폐해를 감내하고 있다.

 

잘 설계된 API는 성능도 좋은 게 보통이다. 그러니 성능을 위해 API를 왜곡하는 건 매우 안 좋은 생각이다. 성능 문제는 다음 버전에서 사라질 수는 있겠지만 왜곡된 API와 이를 지원하는데 고통은 지속될 것이다.

 

신중하게 설계하여 깨끗하고 명확하고 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 수 있다. 물론 성능에 만족하지 못할 경우에 한정된다. 각각의 최적화 시도 전후로는 성능을 측정해야한다. 예상과달리 최적화 기법이 잘 적용되지 않는 경우가 많다. 느릴거라고 짐작한 부분은 사실 성능에 별다른 영향을 주지 않는 경우가 있을 수 있다. 프로파일링 도구를 사용해서 최적화 노력을 어디에 집중해야 할지 도움을 받자. 개별 메소드 소비 시간과 호출 횟수 같은 런타임 정보를 제공받을 수 있다. 

 

자바 언어는 성능 모델이 덜 정교하다. 다양한 기본 연산에 드는 상대적인 비용을 덜 명확하게 정의하고 있어 최적화 시도 전후의 성능 측정은 필요하다. 다시말해 프로그래머가 작성하는 코드와 CPU에서 수행하는 명령 사이의 추상화 격차가 커서 최적화로 인한 성능 변화를 일정하게 예측하기가  어렵다. 자바 성능 모델은 정교하지 않을 뿐 더러 구현 시스템, 릴리스, 프로세서마다 차이가있어 여러 하드웨어에서 구동한다면 각각 측정해야한다. 

 

결론

최적화 보단 초기 API설계에 집중하라. 좋은 프로그램을 작성하면 성능은 따라오기 마련이다. 단, 시스템을 설계할 때, 특히 API, 네트워크 프로토콜, 영구 저장용 데이터 포멧을 설계할 때는 성능을 염두에 두어야한다. 시스템 구현을 완료했으면 성능을 측정해봐라. 적당히 빠르면 그걸로 끝이다. 그렇지 않다면 프로파일링 도구로 문제의 원인이 되는 지점을 찾고 가장먼저 어떤 알고리즘을 사용했는지 확인하자. 알고리즘을 잘못 골랐다면 다른 저수준 최적화는 아무리해도 큰 소용이 없다. 

 

Comments