본문 바로가기

OOP, FP/이펙티브 자바

이펙티브 자바 04. private 생성자, 불필요한 객체

private 생성자만 존재하는 클래스

자바에는 private 기본 생성자와 정적 메소드나 필드만으로 구성된 클래스가 존재한다.

이러한 클래스는 생성자를 다른 곳에서 호출할 수 없으므로 하위 클래스를 만들 수 없다.


Arrays와 같이 기본 자료형이나 배열에 적용되는 메소드를 제공하는 클래스를 사용해본 적이 있을 것이다.

이런 클래스는 객체를 만들어 제공하는 목적이 아닌, 유틸성 기능을 제공하는 목적으로, 정적 메소드나 필드만으로 구성된다.

객체를 지향하는 자바에서 객체를 생성할 수 없고 상속, 구성을 활용할 수 없기 때문에 악명이 높다.


하지만, 이런 클래스들도 분명 필요할 때가 있다.

Java.lang.Math나 java.util.Arrays처럼 기본 자료형 또는 배열에 적용되는 메소드를 한군데 모아놓을 때 유용하다.

우리는 이러한 클래스가 제공하는 메소드를 이용해서 많은 작업을 줄일 수 있다.

다른 예로, 특정 인터페이스를 구현하는 객체를 만드는 팩토리 메소드등의 정적 메소드를 모아놓을 때도 유용하다.

java.util.Collections가 좋은 예인데, 컬렉션 요소를 정렬한다던가 컬렉션의 불변객체를 생성한다던가 하는 기능을 제공한다.

마지막으로, final 클래스에 적용할 메소드들을 모아놓을 때도 활용할 수 있다.

생성자를 호출할 수 없으니 계승 또한 불가능하기 때문이다.


불필요한 객체

기능적으로 동일한 객체는 매번 생성하기보다 재사용하는 편이 낫다.

변경 불가능한 객체는 언제든지 재사용될 수 있다.

아래 코드는 책에서 언급된 절대로 피해야 될 극단적 예이다.


1
String s = new String("string");
cs

위의 문장은 실행될 때마다 새로운 String 객체를 만든다.

String의 생성자로 전달되는 "string"은 그 자체로 String의 인스턴스이다.

프로그램 여기저기에서 "string"이라는 문자열을 사용하는데, 개별로 메모리에 올라가게된다.

만약, 위의 코드가 반복문에서 실행되면 문제는 커진다. 그냥 아래처럼 하는 것이 바람직하다.


1
String s = "string";
cs

이렇게 하면 실행될 때마다 객체를 만드는 대신, 동일한 String 객체를 사용한다.

게다가 같은 가상 머신에서 실행되는 모든 코드가 "string" 객체를 재사용하게 된다.


변경 불가능 클래스

생성자와 정적 팩토리 메소드를 같이 제공하는 변경 불가능 클래스의 경우,

생성자 대신 정적 팩토리 메소드를 이용하면 불필요한 객체 생성을 피할 수 있다.

생성자는 호출될 때마다 새로운 객체를 생성하지만, 정적 팩토리 메소드는 그렇게 하지 않아도 되기 때문이다.


변경 가능 클래스

변경 불가능 클래스는 언제든 재사용할 수 있다.

변경 가능 클래스도 변경할 일이 없다면 재사용할 수 있다.


하지만 변경 가능 클래스를 재사용할 때 주의할 점이 한가지 있다.

한번 만든 다음에는 바꿀 일이 없는 객체를 여러번 생성하지 않아야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person {
 
    private final Date birthDay;
    
    public Person(Date birthDay) {
        this.birthDay = birthDay;
    }
    
    public boolean isBabyBoomer() {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946,  Calendar.JANUARY, 1000);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965,  Calendar.JANUARY, 1000);
        Date boomEnd = gmtCal.getTime();
        
        return birthDay.compareTo(boomStart) >= 0 &&
                birthDay.compareTo(boomEnd) < 0;
    }
}
cs

위의 코드는 한번 만들면 변경할 필요가 없는 Date 객체에 관련된 코드이다.

Person의 객체가 베이비 붐 세대에 속하는지 아닌지를 알려주는 isBabyBoomer 메소드(1946 ~ 1964년생일 경우 true)를 구현한다.


그런데 이 코드는 문제가 있다.

호출될 때마다 Calendar 객체 하나, TimeZone 객체 하나, Date객체 두 개를 쓸데없이 만들어 낸다.

어느 도시의 인구수만큼 Person 인스턴스를 만들어 베이비 붐 세대의 비율을 구한다면, 그 도시의 인구수만큼 계속해서 불필요한 객체를 생성할 것이다.

이렇게 비효율적인 코드는 정적 초기화 블록을 이용하여 개선하는 것이 좋다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Person {
 
    private final Date birthDay;
    
    private static final Date BOOM_START;
    private static final Date BOOM_END;
    
    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946,  Calendar.JANUARY, 1000);
        BOOM_START = gmtCal.getTime();
        
        gmtCal.set(1965,  Calendar.JANUARY, 1000);
        BOOM_END = gmtCal.getTime();
    }
    
    public Person(Date birthDay) {
        this.birthDay = birthDay;
    }
    
    public boolean isBabyBoomer() {
        return birthDay.compareTo(BOOM_START) >= 0 &&
                birthDay.compareTo(BOOM_END) < 0;
    }
}
 
cs

정적 초기화 블록을 이용함으로써 코드는 조금 길어졌지만 BOOM_START와 BOOM_END가 상수라는 사실이 분명하게 드러나 좀 더 명확해졌다. 또한 호출될 때마다 생성하던 Calendar, TimeZone 그리고 Date 객체를 클래스가 초기화되는 시점에 한 번만 만들게 되었다.


이제 도시의 베이비 붐 세대 비율을 구할 때 이전보다 훨씬 빠르고 안전하게 구할 수 있다.

책에서는 isBabyBoomer를 10만 번 호출하면 32,000ms (약 32초) 걸렸지만, 개선된 코드는 130ms(약 0.13초) 걸리면서 성능이 250배나 좋아졌다고 한다.


이러한 성능 개선은 분명 효과적이지만, 고려해야할 사항이 있다.

만일 개선된 Person 클래스가 초기화된 다음에 isBabyBoomer 메소드가 한 번도 호출되지 않는다면 BOOM_START, BOOM_END 필드는 쓸데없이 초기화 되었다고 봐야 한다. 즉, 사용 빈도와 성능을 같이 고려해야한다는 것.

이런 상황은 초기화 지연기법을 사용하면 피할 수 있지만, 초기화를 지연시키면 구현이 복잡해지면서 성능 또한 크게 개선하기 어렵다.

 

불필요한 객체 - 뷰(어댑터)

지금까지 살펴본 String 객체와 변경되지 않는 Date와 같은 객체는 초기화 이후 변경되지 않으므로 재사용이 가능하다.

뷰라고 부르는 어댑터 패턴의 경우를 살펴보자.

어댑터 패턴이란 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환하는 패턴이다.

이를 이용하여 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

어댑터의 경우 실제 수행을 위임할 후면 객체가 관리하는 것 이외의 정보는 따로 저장하지 않으므로 특정 객체에 대한 어댑터를 하나 이상 만들 필요는 없다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("age""26");
    map.put("name""foo");
    
    Set<String> keySet1 = map.keySet();
    Set<String> keySet2 = map.keySet();
    
    System.out.println(keySet1 == keySet2);
    System.out.println(keySet1.equals(keySet2));
    
    System.out.println(keySet1);
    System.out.println(keySet2);
    
    System.out.println("age 삭제");
    keySet1.remove("age");
    System.out.println(keySet1);
    System.out.println(keySet2);
}
 
결과
true
true
[name, age]
[name, age]
age 삭제
[name]
[name]
cs

Map 인터페이스의 keySet메소드는 Map객체의 key 목록이 담긴 Set 뷰를 반환한다.

keySet 을 호출할 때마다 새로운 Set객체가 반환될 것 같지만, 같은 Map에 keySet을 여러 번 호출하면 실제로는 같은 Set객체가 반환된다.

다시 말해, 여러번 호출하여 반환된 객체 가운데 하나가 변경되면 다른 객체들도 변경된다.

후면 객체인 Map이 전부 같기 때문이다. 따라서 keySet으로 뷰 객체를 여러 개 만들 필요가 없다.

 

불필요한 객체 - 오토박싱

JDK 1.5 이후 기본 자료형과 래퍼 클래스를 섞어 사용할 수 있는 오토박싱 기능이 추가되었다.

 

1
2
3
4
5
6
7
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i; // (Long)sum 에 (long)i가 추가될 때마다 객체 생성
    }
    System.out.println(sum);
}
cs

위의 코드를 돌려보면 계산 결과는 장확히 나오지만, 성능이 한참 떨어진다.

sum은 long이 아니라 Long으로 선언되어 있는데, 이 때문에 sum에 i가 더해질 때마다 객체가 생성된다.

결국 2^31개의 쓸데없는 객체가 만들어지면서 성능이 저하되는 요인이 되었다.

sum의 자료형을 Long에서 long으로 바꿔보자. 성능이 눈에띄게 향상된다.

저자의 컴퓨터에서는 43초에서 6.8초로 줄어들었다고 한다.

 

오토박싱을 주의하자.

기본 자료형과 객체 표현형(래퍼 클래스)를 같이 사용할 때, 생각지도 못한 자동 객체화가 발생하지 않도록 주의해야 한다.

 

불필요한 객체 - 객체 풀

객체 생성 비용이 극단적으로 높지 않다면 객체를 재사용하기 위해 직접 객체 풀을 만들어 객체 생성을 피하는 기법은 피하는 것이 좋다.

객체 풀을 만드는 비용이 정당화될 만한 예로는 데이터베이스 커넥션 객체가 있다.

테이터베이스 접속하는 비용이 충분히 높으므로 이런 객체들은 한번 생성하고 재사용하는 것이 마땅하다.

또한 라이선스 정채에 따라 커넥션 수가 제한될 수도 있다는 점을 고려해야 하므로 재사용하는 것이 유리하다.

 

일반적으로는 독자적으로 관리되는 객체 풀을 만들면 코드가 어지러워진다.

뿐만 아니라 메모리 요구량이 증가하면서 성능도 저하된다.

최신 JVM은 최적화된 가비지 컬렉터를 갖고 있어서 가벼운 객체라면 객체 풀보다 월등한 성능을 제공한다.

 

정리

이번 규칙의 명백한 교휸은 불필요한 객체를 생성하지 말라는 것이다.

하지만, 객체를 만드는 비용이 높으니 무조건 피하라는 것은 아니다.

생성자 안에서 하는 일이 작고 명확하면 객체의 생성과 반환은 신속하게 이루어진다.

그러므로 객체를 만들어서 코드의 명확성과 단순성을 높이고 성능을 향상시킬 수 있다면 만드는 것이 좋다.