본문 바로가기
JAVA/Effective Java

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

by Garonguri 2022. 4. 12.
728x90

기능적으로 동일한 객체를 매번 생성하는 것비효율적일 수 있다.

->   객체 하나를 재사용   하는 것이 훨씬 적절한 방법이다. 특히, 불변 개체는 언제든지 재사용할 수 있다.

 

불변 객체의 대표적인 예로는 String, Integer, Boolean 등이 있다.

String bad = new String("bad"); // 따라 하지 말 것 
String good = "good";

new로 생성한 코드는 실행될 때마다 String 인스턴스를 새로 만든다. 쓸데없는 String 인스턴스가 여러 개 만들어질 수 있다.

 

* 아래 코드는, 새로운 인스턴스를 매번 만드는 대신 String 인스턴스 하나를 재사용한다.

-> Java의 가상머신 안에서 똑같은 문자열 리터럴에 대해서 동일 코드, 같은 객체를 사용하는 재사용성이 보장된다는 것이다.

 

* 또한, 정적 팩토리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.

생성자 : 호출할 때마다 매번 새로운 객체를 만든다.

팩토리 메서드 : 사용 중 변경되지 않을 것이란 걸 안다면, 언제나 재사용 가능하다.

 

* 생성 비용이 비싼 객체는, 캐싱 해서 재사용하자.

-> 생성 비용이 비싼 경우는 어떤 경우가 있을까?

  1. 메모리, 디스크 등 시스템 자원이 많이 필요할 때
  2. 데이터 크기가 클 때
  3. 객체 내부에 여러 객체가 포함되었을 때 등등

ex) 정규 표현식에 match가 되는지 확인하는 방법인 String.matches 메서드

- 가장 쉬운 방법이긴 하지만, 내부적으로 객체를 만들어 쓰기 위해서는 유한 상태 기계로 컴파일을 해서 사용해야 한다.

( 유한 상태 기계 설명 - 유한 상태 기계는 유한한 개수의 상태를 가질 수 있는 오토마타, 즉 추상 기계라고 할 수 있다. 이러한 기계는 한 번에 오로지 하나의 상태만을 가지게 되며, 현재 상태(Current State)란 임의의 주어진 시간의 상태를 칭한다. 이러한 기계는 어떠한 사건(Event)에 의해 한 상태에서 다른 상태로 변화할 수 있으며, 이를 전이(Transition)이라 한다. )

 

- 따라서, 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})$");
}

public static class RomanNumber {
    
    //재사용하자!
    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();
    }

}

* 불변 객체를 재사용할 때, 안정하게 재사용되는 것이 분명하지 않은 경우

-> 어댑터

  • 어댑터란, 인터페이스를 통해 뒷단 객체로 연결해주는 객체이다. 
    • 뒷단 객체만 관리하는 역할을 담당한다.
    • 따라서, 뒷단 객체 하나 당 어댑터 하나만 만들면 충분하다.
  • ex) Map 인터페이스의 keySet 메서드.
    • 결국, Map 뒷단에 있는 Set 인터페이스의 뷰를 제공하기 때문에, keySet 호출 시마다 같은 객체를 리턴한다.

-> 오토박싱

  • 프로그래머가 기본 타입과 boxing된 기본 타입을 섞어 쓸 때(primitive : int <-> boxing : Integer), 자동으로 변환해주는 것. (boxing, unboxing)
  • 기본 type과 boxing된 기본 type의 경계를 흐리게 만든다. (없애주진 않는다.)

primitive type이 훨씬 시간이 적게 든 것을 확인할 수 있다.

  • 결론 : 불필요한 boxing type의 사용성능을 저하시킬 수 있다.

객체 생성은 비싸니, 언제나 피하는 것이 답일까?

-> NO !

- 프로그램의 명확성, 간결성, 기능을 위해 객체를 생성하는 것은 일반적으로 좋을 수 있다.

- 특히, value object로써 객체 자체를 값으로 사용하는 방식은 thread safe한 방법으로, multi thread 환경에서 안전하다.

- 단순히 객체 생성을 피하고자 만드는 객체 pool은 코드의 가독성과 메모리 효율성을 떨어트릴 수 있다.

- 또한, 방어적 복사가 필요한 상황의 경우에도 객체를 재사용하면 보안에 문제를 줄 수 있다.

 

(방어적 복사란 ? - 생성자의 인자로 받은 객체의 복사본을 만들어 내부 필드를 초기화하거나, getter메서드에서 내부의 객체를 반환할 때, 객체의 복사본을 만들어 반환하는 것이다. 생성자의 인자로 객체를 받았을 때 외부에서 넘겨줬던 객체를 변경해도 내부의 객체는 변하지 않아야 한다. 이럴 때 방어적 복사를 사용한다.)

 

728x90

댓글