해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.



※ 불필요한 객체는 만들지 마라

: 기능이 동일한 객체는 필요할 때 만드는 것보다 재사용하는 게 더 빠르다. 변경 불가능한 객체는 언제나 재사용할 수 있다. 불필요한 객체를 만드는 하나의 예로 String이 있다. String str = new String("str"); 은 정말 불필요하다. 생성자에 전달되는 "str" 자체가 String 객체이다. 만약 위와 같은 문장이 많이 호출되면 쓸데 없는 객체들이 많이 만들어질 것이다. 따라서 String str = "str"로 해야한다. 그리고 String의 경우 플라이웨이트 패턴과 같이 "str" 문자열을 참조하는 객체에 대해서는 모두 같은 참조값을 가진다. 즉, "str" 문자열을 가지는 str1과 str2가 문자열을 참조하는 위치가 같아 객체를 추가로 생성하지 않는다. 

생성자 대신 정적 팩터리 메서드를 사용해도 불필요한 객체를 안 만들 수 있다. 

Boolean a = new Boolean("true"); Boolean b = new Boolean("true"); Boolean c = Boolean.valueOf(true); Boolean d = Boolean.valueOf(true); System.out.println(a==b);

System.out.println(c==d);

위 예제를 보면 a와 b는 생성자를 통해 c와 d는 정적 팩터리 메서드를 통해 객체를 생성했다. 결과를 보면 a와 b는 다르고 c와 d는 같다고 나온다. 이렇게 생성자 대신 정적 팩터리 메서드를 만들면 불필요한 객체를 안 만들 수 있다. 정적 팩터리 메서드 관련해서는 해당 문장의 url을 참고해라.


public class Person{
   private final Date birthDate;
    ......
   public boolean isBabyBoomer(){
     Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
     gmtCal.set(1946, Calendar.JANUARY, 1, 0,0,0);
     Date boomStart = gmtCal.getTime();
     gmtCal.set(1965, Calendar.JANUARY, 1, 0,0,0);
     Date endStart = gmtCal.getTime();
     return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
   }
}

위 isBabyBoomer() 메서드는 특정 사람의 생일 변수 birthDate를 통해 베이비붐 세대(1946~1965)에 태어났는 지 알아보는 코드이다. 위 메서드는 호출될 때마다 쓸데없이 Calendar 객체와 Date 객체를 불필요하게 생성한다. 효율적인 코드는 정적 초기화 블록을 통해 개선하는 것이다. 다음 예가 개선된 코드이다.  

public class Person {
   private final Date birthDate;
   .....
   private static final Date BOOM_START;
   private static final Date BOOM_END;
   static{
     Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
     gmtCal.set(1946, Calendar.JANUARY, 1, 0,0,0);
     BOOM_START = gmtCal.getTime();
     gmtCal.set(1965, Calendar.JANUARY, 1, 0,0,0);
     BOOM_END = gmtCal.getTime();
   }
   public boolean isBabyBoomer(){
     return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
   }

-----------------

public static void main(String[] args){
   Long sum = 0L;
   for(long i=0; i<Integer.MAX_VALUE; i++){
     sum+=i;
   }
   sysout(sum);
}

위 예제에서 잘못된 것을 찾아보면 sum의 자료형이 Long 라는 것이다. 위 코드에서 long i 객체가 sum에 더해질 때마다 Long으로 자동 객체화(autoboxing)이 되고 불필요한 객체가 for문이 수행될 때마다 만들어진다. 따라서 객체 표현형 대신 기본 자료형을 사용하고, 자동 객체화가 발생하지 않도록 유의해야 한다.


직접 관리하는 객체 풀을 만들어 객체 생성을 피하는 기법은 데이터 베이스 연결과 같이 객체 생성 비용이 극단적으로 높지 않다면 사용하지 않는 것이 좋다. 객체 풀을 독자적으로 만들면 코드의 가독성이 떨어지고 메모리 요구량이 증가해 성능도 떨어진다. 최신 JVM은 가벼운 객체에 대해서는 객체 풀보다 월등한 성능을 보여준다.



※ 유효기간이 지난 객체 참조는 폐기하라

public class Stack{
   .....
   public Object pop(){
     if(size == 0)
        throws new EmptyStackException();
     return element[--size];
  }
   ....
}

위 예제는 Stack 클래스를 구현한 일부분이다. pop메서드를 보면 문제점이 하나 있다. element[--size]를 통해 Stack이 참조하는 위치를 바꾸긴 하지만 위치를 바꾸기 전 객체에 대해서는 아무런 조치를 하지 않았다. 해당 객체는 pop 메서드로 인해서 쓸모없는 객체가 되버린다. 따라서 gc에서 처리를 해줘야 하지만 pop 메서드에서 아무런 조치를 해주지 않아 gc에서 해당 메모리를 인식 못 하게 된다. 그래서 메모리 누수가 발생하게 된다. 이 문제는 간단하게 다음과 같이 고칠 수 있다. 

public class Stack{
   .....
   public Object pop(){
     if(size == 0)
        throws new EmptyStackException();
     Object result = elements[--size];
     elements[size] = null;
     return results;
  }
   ....
}

위와 같은 문제가 발생한 이유는 Stack이 자체적으로 메모리 관리를 하기 때문이다. 따라서 자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야 한다. 더 이상 참조하지 않는 객체에 대해서는 반드시 null로 바꿔 주어야 한다.


캐시도 메모리 누수가 흔히 발생한다. 객체 참조를 캐시 안에 넣어 놓고 잊어버리는 일이 많기 때문이다. 이에 대한 해결책으로 WeakHashMap을 가지고 캐시를 구현하는 것이다. WeakHashMap은 오래 사용되거나 사용되지 않는 다고 판단하는 객체를 자동적으로 제거한다. 캐시에 보관되는 객체의 수명은 보통 캐시에 보관된 기간에 따라 결정된다. 이런 작업은 후면 스레드 Timer나 ScheduledThreadPoolExecutor을 사용해 처리할 수 있고 LinkedHashMap과 같이 캐시에 새로운 항목을 추가할 때 처리할 수 있다. LinkedHashMap의 removeEldestEntry가 이를 도와준다. 

캐시 외에 메모리 누수가 자주 발생하는 곳은 리스너 등의 역호출자(callback)다. 예를 들어 해당 블로그의 옵저버 패턴에 나오는 예제를 보자. 옵저버 패턴 포스트는 해당 문장을 클릭하면 볼 수 있다. 옵저버 패턴에 나오는 예제를 보면 Observable인 WeatherData에서 데이터가 변경될 시 dataChanged() 메서드 안 notifyObservers()가 호출되 자동적으로 Observer인 Display 클래스에서 update() callback 메서드가 호출된다. 호출 될려면 Observable에 등록을 해야 하는 데 등록 후 Display 클래스가 더 이상 참조 되지 않는다면 명시적으로 제거해줘야 한다. 그렇지 않으면 메모리 누수가 발생한다. 만약 등록할 때 WeakHashMap을 사용한다면 약한 참조가 이루어져 gc가 자동적으로 참조되지 않은 observer 객체를 삭제해 줄 것이다. 

+ Recent posts