Skip to content

Latest commit

 

History

History
126 lines (106 loc) · 6.27 KB

불필요한_객체_생성을_피하라.md

File metadata and controls

126 lines (106 loc) · 6.27 KB

아이템 06 불필요한 객체 생성을 피하라

재사용은 빠르고 세련된 것이다.

  • 똑같은 기능의 객체를 매번 생성하기 보다는 재사용하는 편이 나을 것
  • 특히 불변 객체는 언제든 재사용할 수 있다.

극단적 예시

String s = new String("bikini");
  • 위의 문장은 실행될 때마다 String 인스턴스를 만듦
  • 위의 문장이 반복문 안에서 100만번 호출된다면?
  • 쓸데 없는 String 인스턴스가 백만개일수도..
String s = "bikini";
  • 이 코드는 위 코드의 개선 버전
  • 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.
  • 불필요한 객체 생성을 피할 수 있음
  • (객체임에도 위의 문법으로 재사용 가능한 이유는 상수 풀과 관련이 있다.)

생성 비용이 비싼 객체를 대하는 태도

  • 생성 비용이 아주 비싼 객체도 있다.
  • 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
  • 캐싱도 불필요한 객체 생성을 피한다는 맥락과 관련이 있는 것이다.

생성 비용이 비싼 객체의 캐싱 - 예시

   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})$");
  • 위의 코드에서 String.matches를 사용하는 부분을 보자.
  • String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법
  • String.matches 동작 중 내부적으로 정규표현식용 Pattern 인스턴스를 만든다.
  • But 패턴 인스턴스 생성이 무거운 작업이기에 성능이 중요한 상황에 쓰기 쉽지 않다.
  • 이러한 메서드가 사용될때마다 패턴 인스턴스가 생성되고 삭제된다면 그 비용은 만만치 않을 것
public class RomanNumerals {
    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();
    }
}
  • 위 코드는 개선 버전
  • Pattern이 불변이라는 점을 이용해서 캐싱해두었다.
  • 그리고 나중에 이 메서드가 호출될 때마다 인스턴스가 재활용되니 우아하다.

불변 객체와 재사용의 연관성

  • 객체가 불변이라면 재사용해도 안전함이 명백하다.
  • 위의 예시에서 패턴 인스턴스를 재활용한 것도 정적으로 선언후에 변하지 않을 것임을 믿기 때문
  • 이러한 개념은 우리의 직관으로 충분히 받아들일 수 있다.
  • 하지만 불변하다는 특성이 매번 재사용하기에 안전하다는 특성으로 귀결되지는 않는다.
  • 오히려 반대인 상황도 있음

직관에 반대되는 예시

  • 어댑터를 생각해보자
  • 어댑터는 실제 작업은 뒷단 객체만 관리
  • 뒷단 객체 외에는 관리할 대상 없음 (불변)
  • 이렇게 보면 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분해보임
public  class UsingKeySet {
    public static void main(String[] args) {
	    Map<String, Integer> menu = new HashMap<>(); 
	    menu.put("Burger", 8); 				   
	    menu.put("Pizza", 9);
	    Set<String> names1 = menu.keySet(); 
	    Set<String> names2 = menu.keySet(); 
	    names1.remove("Burger"); 
	    System.out.println(names1.size()); // 1 
	    System.out.println(names2.size()); // 1 
	} 
}
  • 실제로 Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다.
  • KeySet은 어댑터처럼 오직 하나, 불변한 자신 뒤의 Map만 바라보기에 KeySet을 개발한 개발자는 재사용을 고려하였을지도 모르겠다. (실제로 재사용)
  • 그런데 사용하는 개발자들은 keySet을 호출할 때다마 새로운 Set 인스턴스가 만들어지리라고 순진하게 생각할 수도 있음
  • 하지만 그렇지 않다
  • 매번 같은 Set 인스턴스를 반환함 (재사용)
  • 그리고 위처럼 변경에 취약하게 되었다.
  • keySet이 생성하는 셋뷰는 하나의 대상만을 바라보기에 어댑터와 마찬가지로 하나의 인스턴스만 필요할 것 처럼 사료된다.
  • 하지만 직관적이지 않고 불필요한 동작을 담아내는 안전하지 않은 특성으로 귀결되는 현상을 확인할 수 있다.

불필요한 객체를 만들어내는 또 다른 예, 오토박싱

  • 오토 박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려준다.
  • 하지만 완전히 없애주는 것은 아니다.
  • 의미상으로는 별다를 것 없지만 성능에서는 그렇지 않다.
private static long sum() {
	Long sum = 0L;
	for (long i = 0; i <= Integer.MAX_VALUE; i++) {
		sum += i;
	}
	return sum;	
}
  • 이 프로그램이 정확한 답을 내기는 한다.
  • 하지만 제대로 구현했을 때보다 훨씬 느리다.
  • sum 변수를 Long으로 선언해서 불필요한 Long 인스턴스가 약 2^31개나 만들어진 것..
  • 결론은? 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

객체 생성은 비싸니 피하라는 소리가 아니다.

  • 요즘의 jvm에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
  • 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.
  • 거꾸로 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 객체 풀을 만들지는 말자. (별도의 얘기로 풀링과 캐싱의 차이점은 한번씩 찾아보시길)
  • 이번 아이템은 기존 객체를 재사용할 수 있다면 재사용하라는 이야기
  • 하지만 아이템 50은 새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라다.
  • 사실 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가 더 크다. (버그와 보안구멍으로 이어짐)
  • 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 주니 참고하라

참고

https://deveric.tistory.com/123
이펙티브 자바 (조슈아 블로크) 아이템 06 불필요한 객체 생성을 피하라