Skip to content

Latest commit

 

History

History
94 lines (66 loc) · 4.99 KB

File metadata and controls

94 lines (66 loc) · 4.99 KB

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

  • 똑같은 기능의 객체를 매번 생성하기보다, 객체 하나를 재사용하는게 나을 때가 있다.
  • 특히, 불변 객체는 언제든지 재사용할 수 있고, 생성이 비싼 객체들을 따로 객체 pool 관리하며 재사용 할 수 있다.
String s = new String("pearl"); // 계속 스트링 객체 생성, 사용 x
String s = "pearl"; // 하나의 스트링 인스턴스 사용
  • 이전에 Item 01 에서 정적 팩토리를 통해 객체를 새로 생성하지 않고 이미 생성된 객체를 반환해주는 것을 보았다.
  • 정적 팩토리를 활용해 매번 객체를 생성하지 않게 만들 수 있음을 기억하자.

생성 비용이 비싼 객체

  • 비싼 객체가 반복해서 필요할 때, 캐싱해서 사용하는 것이 좋다.
  • 자신이 생성한 객체가 비싼 객체인지, 항상 주의해야 한다.
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 메서드를 사용하는 문제점이 있다.
  • 이 메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져 GC 대상이 된다.
  • Pattern은 입력받은 정규 표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.
  • 그러나 이 메서드는 로직 상 여러 번 호출될 수 있기 때문에, Pattern 인스턴스를 직접 생성해 캐싱해두고, 메서드가 호출될 때 마다 캐싱된 인스턴스를 재사용하는 것이 좋다.
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();
  }
}
  • 이렇게 개선하면 isRomanNumeral 메서드가 빈번히 호출되는 상황에서 성능을 상당히 높일 수 있다.

불변 객체 재사용 시 안전함 보장

  • 어댑터
  • Map 인터페이스의 keySet 메서드

불필요한 객체를 만들어 내는 경우

Auto Boxing

  • 프로그래머가 원시 타입과 박싱된 기본 타입을 섞어서 쓸 때, 자동으로 변환해주는 기술이다.
  • Auto Boxing 은 원시 타입과 박싱된 타입의 구분을 흐리게 만들지만, 완전히 없애주지는 않는다.
  • 특히, 자동으로 wrapping 해주다가 성능 문제를 만드는 경우가 있다.
private static long sum() {
    Long sum = 0L;
    for (long i=0; i <= Integer.MAX_VALUE; i++){
        sum += i;
    }
    return sum;
}
  • 여기서 sum 변수를 원시 타입인 long 이 아니라 wrapping 된 Long으로 사용하고 있다.
  • 이 문제 때문에 불필요한 Long 인스턴스가 반복문의 범위인 Integer.MAX_VALUE 만큼 생성된다.
  • 따라서, boxing 되어있는 기본 타입보다, 원시 타입을 사용하고, 코드 중에 의도치 않은 auto boxing 이 숨어들지 않도록 해야한다.

+ 주의: 객체 생성을 무조건 피해야하는 것은 아니다.

  • 가벼운 객체라면 몇 번 새로 생성해서 쓰는게 오히려 GC에 부담되지 않는다. (비용이 크지 않음)
  • 객체 재사용은 무거운 객체, 생성 비용이 비싼 객체를 대상으로 고려하는게 일반적이다.
  • 또한, 방어적 복사가 필요한 상황에서, 객체를 복사하려는 생성을 피해선 안된다.
    • 기존 객체를 변경해서 사용하는 것보다, 방어적 복사를 통해 복사한 객체를 변경해야하는 경우 (기존 객체의 동작을 해칠 위험이 있거나, 안전성 보장이 필요할 때)
    • 이런 방어적 복사의 개념은 Kotlin 에서 더 명확하게 언어적 차원으로 다룬다. (data classcopy() 메서드)

+ 추가: 유한 상태 머신과 Pattern.compile() 이 하는 일

  • 유한 상태 머신이 무엇인지?
    • FSM은 "입력의 흐름에 따라 정해진 상태를 바꾸며 동작하는 시스템"을 추상화한 모델
    • 정규식을 처리하기 위한 내부 구조에서, 입력 문자열을 한 글자씩 읽으면서 현재 상태에서 다음 상태로 전이하는 방식으로 일치 여부를 판단한다. (FSM 개념)
    • 복잡한 정규식일수록 더 많은 상태와 전이 조건이 필요해져 구성 비용이 비싸진다.
  • Pattern.compile() 을 하면 무슨 일이 생기는지?
    • Pattern.compile() 은 정규식을 FSM으로 바꾸어 효율적인 매칭을 가능하게 한다.
    • 문자열로 된 정규식을 분석(파싱)하여 FSM으로 컴파일한다.
    • 이 과정에는 정규식 파싱, 트리 생성, 상태 머신 구성 등 복잡한 작업이 포함되기 때문에 캐싱해서 재사용하는게 효율적이다.