[아이템 6] 불필요한 객체 생성을 피하라

gang_shik·2022년 2월 19일
0

Effective Java 2장

목록 보기
6/9
  • 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많음, 재사용은 빠르고 세련됨, 불변 객체는 언제든 재사용이 가능함

  • String s = new String("bikini"); 이와 같은 방식은 실행될 때마다 인스턴스를 새로 만드는데 이런 방식은 하면 안됨

  • 이를 그나마 개선하면 String s = "bikini";임, 하나의 String 인스턴스를 사용함

  • 이 방식을 사용하면 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체재사용함이 보장됨

같은 가상 머신 안에서 똑같은 문자열 리터럴을 사용하는?

String Constant Pool

여기서 위의 방식에서 new 연산자를 사용하면 내용이 같아도 개별적인 객체로 쓰임을 알 수 있음

그래서 new 연산자 방식을 하지 말라고 한 것인데 그러면서 문자열 리터럴을 사용하면 이것이 해결 될 수 있다고 했는데 그 말은 문자열 리터럴로 생성하면 해당 String값은 Heap 영역 내 String Constatnt Pool에 저장되어 재사용 됨을 의미함, 아래와 같이

그래서 이를 확인하기 위해서 String에서 intern()이라는 메서드를 사용할 수 있는데 여기서 이 메서드를 통해서 Pool에 있다면 해당 주소를 없다면 생성한 주소를 반환함으로써 이를 확인할 수 있음

@DisplayName("이미 Pool에 값이 있다면 같은 주소를 바라본다.")
@Test
public void test2_1(){
    String s = new String("aaa");
    String b = "aaa";
    String c = s.intern();

    assertThat(b == c);
}

그래서 문자열 리터럴을 사용하는 모든 코드가 같은 객체재사용함을 보장하는 것임


  • 생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있음

  • 생성 비용이 아주 비싼 객체의 경우 캐싱하여 재사용하는걸 권장함

  • 여기서 문자열 유효 체크를 하는 상황에선 정규표현식으로 이를 해결할 수 있음, 아래와 같이

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은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성비용이 높음

  • 이런 상황에서 성능을 개선하기 위해서 필요한 정규표현식을 표현하는 Pattern 인스턴스클래스 초기화(정적 초기화)과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral메서드가 호출될 때마다 이 인스턴스를 재사용함

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 인스턴스static final필드로 끄집어내고 이름을 지어주어 코드의 의미가 훨씬 잘 드러남

  • 여기서 이 메서드를 한 번도 호출하지 않으면 이 ROMAN 필드는 쓸데없이 초기화됨, 그렇다고 지연 초기화를 쓰면 오히려 성능개선도 안되고 코드만 복잡해짐

  • 객체가 불변이라면 재사용해도 안전함이 명백하지만 훨씬 덜 명확하거나 직관에 반대가 될 수도 있음

  • 여기서 어댑터의 경우 뒷단 객체만 관리하면 됨, 뒷단 객체 하나당 어댑터 하나씩만 만들어서

  • Map 인터페이스의 KeySet 메서드 역시 Set 인스턴스가 반환한 인스턴스는 기능이 다 똑같음, 이 반환된 객체 하나만 수정해도 다른 모든 객체가 따라서 바뀜 모두가 똑같은 Map 인스턴스를 대변하므로, 그래서 이 KeySet이 뷰 객체를 여러개 만들어도 상관없지만 어떠한 이득도 없음

유한 상태 머신?

유한 상태 머신

유한 상태 머신에 대한 정의를 보면 장치나 모델이 가질 수 있는 유한개의 상태정의하고, 조건에 맞는 이벤트가 발생되면 해당 상태변경되는 방식으로 동작하는 것을 의미함

이 부분과 관련해서 Pattern을 입력받은 정규표현식에 해당하는 유한 상태 머신을 만든다고 하였는데, 이는 정규표현식으로 문자열 형태를 확인하는 자체가 확인을 위한 범주에 있어서 유한 상태 머신으로써 매칭을 하고 비교하기 때문에 그렇다고 볼 수 있음


  • 오토박싱기본 타입박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술임

  • 오토박싱의 경우도 불필요한 객체를 만들어내는 또 다른 예시임

  • 오토박싱기본타입과 그에 대응하는 박싱된 기본 타입구분을 흐려주지만, 완전히 없애주는 것은 아님

private static long sum() {
		Long sum = 0L;
		for (long i = 0; i <= Integer.MAX_VALUE; i++)
				sum += i;
		
		return sum;
}
  • 위와 같이 sum 변수를 long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스를 만들어냄

  • 이 때 알 것은 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의해야 하는 것임

  • 여기서 중요한 것은 객체 생성은 비싸니 피해야 한다는 관점이 아님, 작은 객체를 생성하고 회수하는 건 큰 부담이 없고 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것은 좋은 일임

  • 하지만 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 나만의 객체 풀만들지 말자

  • 데이터베이스 연결 같은 경우 재사용하는 편이 낫긴 하지만 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘려 성능을 떨어뜨림

객체 생성을 피하고자 객체 풀을 만드는 것?

여기서 객체 풀을 만드는 것 자체가 객체가 필요할 때 풀에 요청을 하고 반환하고 일련의 작업수행하는 Object Pool Pattern이 존재함

이를 통해서 미리 생성된 풀을 통해서 객체를 받아올 수 있음 즉, Pooling 할 객체 클래스가 존재하면 이 객체를 Pooling할 팩터리를 만들고 이 팩터리를 통해서 객체를 Pooling하면서 만들 수 있음

하지만 이 경우 위에서 설명했듯이 Pool하는 방식 자체가 꽤 과정이 있는데 이걸 굳이 단순히 객체 생성을 피하고자 쓰지 말라는 것임


profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글