코드의 여백

[이펙티브 자바] 객체 파괴

by rowing0328

※ 책 내용을 바탕으로 제 관점에서 풀어 쓴 글입니다. 일부 내용이 다를 수 있습니다.

 

아이템 6 - 불필요한 객체 생성 금지

 // Boxing type
public static long sum() {
	Long sum = 0L;
    for(long i = 0; i <= Integer.MAX_VALUE; i++) {
    	sum += i;
    }
    return sum;
}

// Primitive Type
public static long sum() {
	long sum = 0L;
    for(long i = 0; i <= Integer.MAX_VALUE; i++) {
    	sum += i;
    }
    return sum;
}
  • Boxing type 대신 Primitive Type 을 권장한다.
    반복문이나 연산에서는 성능 저하를 유발할 수 있으므로 항상 Primitive Type을 우선적으로 사용하자.

 

public class PhonePatternUtil {
	
    private final String pattern;
    
    public boolean isValid(String phone) {
    	...
    }

}
  • Util Class 에서 또한 Primitive type을 권장한다.
    참 거짓을 response 하는데 Boxing type을 사용하는 것은 낭비일 수 있다.

 

// Primitive Type 사용
int price;  // 가격이 0원 (예: 증정품)

// Boxing Type 사용
Integer price = null; // 가격이 아직 정해지지 않음
  • 상황에서는 Boxing Type(Wrapper Class)이 더 적합할 수 있다. 
    • Primitive Type (int)
      • 사용 조건 : 기본값이 명확하고, 성능 최적화가 필요한 경우
      • 예 : 가격이 0원으로 설정된 증정품
    • Boxing Type (Integer)
      • 사용 조건 : null 값을 통해 설정되지 않은 상태를 명확히 표현해야하는 경우
      • 예 : 가격이 아직 정해지지 않은 상태

 

// 비효율적인 코드: Pattern instance가 매번 생성됨
static boolean isEmailValid(String s) {
    return s.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$");
}

// String.matches 내부 구현(String.java)
public boolean matches(String regex) {
    return Pattern.matches(regex, this);
}

// Pattern.matches 내부 구현 (Pattern.java)
public static boolean matches(String regex, CharSequence input) {
    Pattern p = Pattern.compile(regex); // Pattern 객체 생성
    Matcher m = p.matcher(input);
    return m.matches();
}

// 효율적인 코드: Pattern instance를 한 번만 생성
public class EmailUtil {

    private static final Pattern EMAIL = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$");

    static boolean isEmailValid(String s) {
        return EMAIL.matcher(s).matches();
    }
    
}
  • 주의해야 할 내장 Method
    자주 사용하는 내장 메서드는 성능과 메모리 사용을 고려해, 재사용 가능한 구조로 변경하자.

 

 

아이템 7 - 다 쓴 객체 참조를 해제하라

Map<Object, String> cache = new HashMap<>();
Object key = new Object();
cache.put(key, "Value");

// key가 더 이상 필요 없음
key = null; // 하지만 cache에는 여전히 key와 value가 남아 있음
  • 메모리 누수 발생
    사용이 끝난 객체가 참조 상태로 남아 있으면 GC가 해당 객체를 회수하지 못한다.
  • 성능 저하
    불필요한 객체가 메모리를 차지하면서 시스템 성능이 떨어진다.
  • OutOfMemoryError 발생 위험
    메모리 누수가 쌓이면 자원이 부족해지고, 심각한 경우 애플리케이션이 멈출 수 있다.

 

import java.util.WeakHashMap;

public class WeakHashMapExample {
    
    public static void main(String[] args) {
        WeakHashMap<Object, String> cache = new WeakHashMap<>();
        Object key = new Object();
        cache.put(key, "Value");

        key = null; // 참조 제거
        System.gc(); // GC 실행 요청

        System.out.println("Cache size: " + cache.size()); // 0
    }
    
}
  • 명시적 참조 제거
    객체 사용이 끝난 후 컬렉션에서 제거해야한다.
  • 약한 참조 활용
    WeakHashMap으로 다 쓴 객체를 자동으로 정리할 수 있다.
  • 캐시 관리
    유효 기간을 설정해 오래된 항목을 자동으로 제거한다.
    불필요한 항목은 주기적으로 정리하는게 좋다.

 

 

아이템 8 - finalizer, cleaner를 피하라

public class Item8 {

    @Override
    protected void finalize() {
        System.out.println("call finalize");
    }
    
}

public class MainApplication {
    
    private void run() {
        Item8 item8 = new Item8();
    }

    public static void main(String[] args) {
        MainApplication mainApplication = new MainApplication();
        mainApplication.run();
        System.gc(); // GC를 강제로 트리거 (힌트 제공)
    }
    
}
  • Finalizer의 문제점
    • 예측 불가능 : GC 실행 시점을 알 수 없어, 정리 작업이 지연될 수 있다.
    • 성능 저하 : GC 성능에 영향을 미쳐 시스템 효율성을 떨어뜨린다.
    • 안전성 문제 : finalize()에서 예외 발생 시 자원 누수가 발생할 수 있다.
  • System.gc()의 한계
    • 강제성이 없음 : JVM에 GC를 실행하라는 힌트일 뿐이며, 실제 실행 여부는 JVM의 구현과 상황에 따라 달라진다.
    • 비권장 : GC 타이밍은 JVM이 최적화하도록 맡겨야하며, System.gc()에 의존하는 것은 비효율적이다.

 

import java.lang.ref.Cleaner;

public class CleanObject implements AutoCloseable {
    
    private static final Cleaner cleaner = Cleaner.create();

    private static class CleanData implements Runnable {
        @Override
        public void run() {
            System.out.println("Cleaning resources...");
        }
    }

    private final CleanData cleanData;
    private final Cleaner.Cleanable cleanable;

    public CleanObject() {
        this.cleanData = new CleanData();
        this.cleanable = cleaner.register(this, cleanData);
    }

    @Override
    public void close() {
        cleanable.clean(); // 명시적으로 자원 정리
        System.out.println("CleanObject closed.");
    }
    
}
  • GC 의존성
    Cleaner는 GC에 의존적으로 동작하므로 자원의 해제가 GC 실행 시점에 좌우된다.
  • 즉시성 부족
    Cleaner는 비동기적으로 실행되기 때문에 정리 작업이 즉시 이루어질 것이라는 보장이 없다.

 

 

아이템 9 -  try-finally 대신 try-with-resources

static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[100];
            int n;
            while ((n = in.read(buf)) > 0) {
                out.write(buf, 0, n);
            }
        } finally {
            out.close(); // 자원 수동 해제
        }
    } finally {
        in.close(); // 자원 수동 해제
    }
}

  • 코드 복잡성 증가
    try-finally를 사용할 경우, 자원을 명시적으로 닫아야 하므로 코드가 장황해지고 가독성이 떨어진다.
  • Stack Trace 어려움
    예외가 발생하면 스택 트레이스에서 원인 추적이 어려워질 수 있다.

 

public class Resource implements AutoCloseable {
    
    @Override
    public void close() throws Exception {
        throw new Exception("inside Resource exception");
    }
    
}

try (Resource r1 = new Resource(); 
	 Resource r2 = new Resource()) {
    throw new Exception("Main exception");
}

  • 코드 간결화
    AutoCloseable을 구현한 객체를 사용하여, 자원을 자동으로 해제한다.
  • Stack Trace 개선
    여러 예외를 다룰 때도 스택 트레이스에서 예외의 원인을 명확히 추적할 수 있다.

 

 

마무리

이번 장을 통해, GC가 모든 메모리 문제를 자동으로 해결해주는 것은 아니라는 사실을 인지했다.

편의성에만 안주하지 않고, 코드의 동작을 깊이 이해하며 책임감 있게 관리하는 것이 얼마나 중요한지 다시 한번 깨달았다.

블로그의 정보

코드의 여백

rowing0328

활동하기