Item 6. 불필요한 객체 생성을 피하라

다람·2025년 2월 22일
0

Effective Java

목록 보기
6/13
post-thumbnail

1. 객체를 반복 생성하지 말고 재사용하자

  • 같은 기능을 하는 객체를 매번 생성하는 것보다 이미 생성된 객체를 재사용하는 것이 성능 면에서 유리하다.
  • 불변(Immutable) 객체는 언제든 재사용할 수 있으며, 대표적인 예 String 클래스가 있다.

String을 생성할 때 new 연산자를 사용하면 안 되는 이유

String s1 = new String("bikini");  // 새로운 객체 생성 (비효율적)
String s2 = "bikini";              // 기존 문자열 재사용 (권장)
  • "bikini"new String("bikini")로 생성하면 매번 새로운 객체가 생성되어 메모리 낭비가 발생한다.
  • "bikini"라는 문자열이 이미 존재하면 s2같은 객체를 재사용한다.
  • 리터럴 방식("bikini")을 사용하면 같은 JVM 내에서는 항상 동일한 객체를 재사용한다.(String pool)
    간단한 String pool 구조

String Pool이란?

  • String PoolJVM이 문자열 리터럴을 저장하고 재사용하는 메모리 공간이다.
  • String s = "bikini";와 같은 리터럴 방식으로 생성된 문자열은 String Pool에 저장되고, 같은 문자열이 요청되면 기존 객체를 재사용한다.
  • 반면에 new String("hello")는 String Pool을 사용하지 않고 새로운 객체를 생성한다.
    JVM과 String Pool 구조
  • String Pool은 정확하게는 Method Area 내부에 위치한다고 한다. 내가 이해한 대로 JVM의 메모리 구조에 맞춰서 각각 어떻게 저장되는지 그려보았다.

2. 정적 팩터리 메서드를 활용하여 불필요한 객체 생성을 방지할 수 있다

  • 정적 팩터리 메서드는 객체를 매번 새로 생성하지 않고 기존 객체를 반환하여 재사용할 수 있다.
  • 대표적인 예 : Boolean.valueOf(String)
Boolean b1 = Boolean.valueOf("true"); // 기존 객체 재사용
Boolean b2 = new Boolean("true");   // 새로운 객체 생성 (비효율적)
  • new Boolean("true")는 매번 새로운 객체를 생성하지만, Boolean.valueOf("true")미리 캐싱된 인스턴스를 반환한다.

캐싱(Caching)*이란?

캐싱이란 자주 사용하는 데이터를 미리 저장해두고 재사용하는 기법을 의미한다.

3. 생성 비용이 큰 객체는 캐싱하여 재사용하라

  • 생성 비용이 비싼 객체를 반복해서 사용해야 한다면, 캐싱을 활용해 재사용하는 것이 좋다.
  • 예를 들어, 정규 표현식을 사용할 때 String.matches()를 직접 쓰면 매번 Pattern 객체를 새로 생성하게 되어 비효율적이다.

비효율적인 정규표현식 사용 (객체 반복 생성)

static boolean isRomanNumeral(String s) {
	return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
					+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
  • 내부적으로 Pattern.compile()이 호출되며, 매번 새로운 Pattern 인스턴스가 생성된다.
  • isRomanNumeral이 자주 호출되면 불필요한 Pattern 객체가 계속 생성되어 성능이 저하된다. 아래의 이미지는 matches 메서드가 어떻게 작성되어있는지 확인하기 위해서 자바 코드를 캡쳐해온 것이다.
    String 클래스
    Pattern 클래스

Pattern 인스턴스를 캐싱하여 재사용한다.

성능을 개선하기 위해서 불변인 Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해두고, 메서드를 호출할 때 인스턴스를 재사용하도록 한다.

static boolean isRomanNumeral(String s) {
	private static final Pattern ROMAN = Pattern.compile(
					"^(?=.)M*(C[MD]|D?C{0,3})"
					+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
	
	static boolean isRomanNumeral(String s) {
			return ROMAN.matcher(s).matches();
	}
}
  1. Pattern.compile(regex)가 클래스 로딩 시 한 번만 실행되게 된다.
    • static final 필드이므로 JVM이 한 번만 초기화하고 이후에는 재사용함.
    • isRomanNumeral()을 여러 번 호출해도 새로운 Pattern 객체를 만들지 않게 된다.
  2. 객체 생성을 줄여 성능이 향상된다.
    • 기존 방식에서는 isRomanNumeral()이 호출될 때마다 Pattern.compile()이 실행되는 구조였다.
    • 인스턴스를 캐싱하여 재사용하도록 한 방식은 이미 생성된 Pattern 객체를 재사용하므로 불필요한 객체 생성을 줄여 성능이 향상되게 된다.
  3. 코드의 의미가 훨씬 잘 드러난다.
    • Pattern 객체를 static final 필드로 분리함으로써 정규 표현식이 어떤 역할을 하는지 명확해지게 되었다.

4. 불변 객체가 아닌 경우에도 재사용할 수 있다.

  • 불변 객체는 언제든 안전하게 재사용할 수 있다.
  • 하지만 불변이 아닌 객체라도 상태가 변하지 않는다면 재사용이 가능하다.

어댑터(Adapter) 패턴을 활용한 객체 재사용

// Target 인터페이스
interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Adaptee 클래스 (기존 기능 제공)
class AdvancedMediaPlayer {
    void playMp3(String fileName) {
        System.out.println("Playing MP3 file: " + fileName);
    }
}

// Adapter 클래스(Target을 상속)
class MediaAdapter implements MediaPlayer {
    private final AdvancedMediaPlayer advancedMediaPlayer; // 기존 기능 포함
    
    public MediaAdapter() {
        this.advancedMediaPlayer = new AdvancedMediaPlayer(); // 기존 객체 재사용
    }
    
    @Override
    public void play(String audioType, String fileName) {
        if ("mp3".equalsIgnoreCase(audioType)) {
            advancedMediaPlayer.playMp3(fileName); // 기존 기능 내부 호출
        }
    }
}

// 클라이언트 코드
public class AdapterPatternExample {
    public static void main(String[] args) {
        MediaPlayer player = new MediaAdapter(); // 어댑터 사용
        player.play("mp3", "song.mp3"); // 내부에서 기존 AdvancedMediaPlayer를 사용
    }
}
  • MediaAdapterMediaPlayer 인터페이스를 구현하고, AdvancedMediaPlayer 객체를 내부에서 호출하는 방식이다.
  • 이렇게 어댑터 패턴을 이용하면 기존 AdvancedMediaPlayer 기능을 재사용하면서도 MediaPlayer 인터페이스를 지원할 수 있다. MediaPlayer 인터페이스만 보고 사용하면 내부에서 어떤 객체를 쓰는지 신경 쓸 필요 없게 된다.
  • 위의 내용을 토대로 어댑터 패턴이 item6과 관련된 이유는 아래와 같이 생각해보았다.
    • 기존 AdvancedMediaPlayer를 새로 만들지 않아도 된다.
    • Adapter 클래스를 사용하면 새로운 버전이 나온다고 하더라도 MediaPlay 구현체를 별도로 만들 필요 없이 기존의 클래스를 재사용 할 수 있게되어서 하나의 어댑터만 있으면 기능 확장이 가능하다.

7. 객체 풀을 직접 만들면 안된다.

  • 객체 생성 비용이 크다고 해서 불필요한 객체 생성을 피하려고 직접 객체 풀(Object Pool)을 만들면 안 된다.
  • JVM의 가비지 컬렉션이 자동으로 메모리 관리를 해주기 때문에 더 효율적이라서 직접 객체 풀을 관리하면 오히려 성능이 나빠질 수 있다.
  • 예외 : 데이터베이스 연결 풀(DB Connection Pool)과 같은 경우는 예외적으로 필요하다.

8. 결론

  1. 기존 객체를 재사용할 수 있다면 새로운 객체를 만들지 말자.
  2. 불필요한 객체 생성을 피하기 위해 정적 팩터리 메서드와 캐싱을 활용하자.
  3. 박싱된 기본 타입보다 기본 타입을 사용하자.
  4. 객체 생성을 피하려고 직접 객체 풀을 만들지 말자(JVM의 GC가 있다는 것을 잊지 말자).
profile
개발하는 다람쥐

0개의 댓글