Language/Java

[이펙티브자바] 아이템 4, 5, 6 요약 정리

anxi 2024. 5. 29. 22:13

아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라

저번 블로그에 작성했었던 유틸 클래스의 경우 정적 메서드와 정적 필드만을 갖고 있습니다.

이러한 유틸 클래스는 인스턴스를 생성해 사용하는 것이 아닌 유틸 클래스의 정적 메서드와 정적 필드를 사용합니다.

그런데 Util이란 단어가 클래스에 붙지 않고, 그냥 클래스와 똑같이 생겨서 외부에서 인스턴스화 하여 사용할 수 있습니다.

왜냐하면 생성자가 없이 정적 필드와 메서드만 있어도 기본 생성자가 컴파일러에 의해 자동으로 생성되기 때문입니다.

public class Util {
	
    // 컴파일러가 생성해주는 기본 생성자
    public Util() {}

 

기본 생성자의 접근 제어자는 Public이기 때문에 외부에서 인스턴스화할 수 있습니다.

 

이를 방지하기 위해선 추상 클래스로 만드는 사람들도 있습니다. 하지만 추상 클래스로 막는다 할지라도 하위 클래스를 생성해서 상속하여 인스턴스화할 수 있습니다. 

 

따라서 가장 올바른 방법은 private 기본 생성자를 생성하는 것입니다.

public class Util {
	
    private Util() {
    	throw new Exception();
    }

 

private 기본 생성자를 만들면 외부에서 인스턴스화 하는 것을 막을 수 있고, 추가로 상속 또한 막을 수 있습니다.

 

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

많은 클래스가 하나 이상의 자원에 의존한다.

예시) 한글 사전, 영어 사전, 중국어 사전 등등 언어에 따라 사전의 기능을 달라지게 하고 싶다면?

 

1. 정적 유틸리티 클래스

public class Dictionary {
	
    public static final Korean dictionary = new ~;
    
    public static boolean isValid(String word) { ... }
}

 

2. 싱글턴

public class Dictionary {
	
    public final Korean dictionary = new ~;
    
    public static final Dictionary dictionary = new Dictionary(...);
    public static boolean isValid(String word) { ... }
}

 

유틸 클래스와 싱글턴 방식 클래스는 한 언어의 사전을 사용할 때는 괜찮을 수 있어도 여러 사전을 사용하기는 쉽지 않다.

사용하는 자원에 따라 동작이 달라지는 클래스에서는 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 의존 객체 주입을 사용하면 된다.

 

public interface Dictionary {}

public class KoreanDict implements Dictionary {}
public class EnglishDict implements Dictionary {}
public class ChinaDict implements Dictionary {}

public class SpellChecker {
	
    private final Dictionary dictionary;
    
    public SpellChecker(Dictionary dictionary) {
    	this.dictionary = dictionary;
    }
}

// DI 적용시
KoreanDict koreanDict = new KoreanDict();
SpellChecker spellChecker = new SpellChecker(koreanDict);

 

한국어, 중국어, 영어 사전을 의존성 주입을 통해 외부에서 관계를 결정할 수 있다.

 

클래스가 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 DI를 사용한다.

장점(2)

- 유연성

- 테스트 용이성

 

단점(1)

- 의존성이 많을 시 코드 가독성 저해 (Spring 같은 의존 객체 주입 프레임워크 사용하면 해소 가능)

 

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

생성자 대신 정적 팩터리 메서드를 사용하여 불필요한 객체 생성 회피

// 생성자 : 호출할 때마다 새로운 객체 생성
new Boolean(String s);

// 정적 팩터리 메서드 : 재사용
Boolean.valueOf(String s);

 

생성 비용이 비싼 객체를 반복해서 필요할 경우, 캐싱을 이용하자

하기는 주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드이다.

public boolean isRomanNumeric(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 메서드를 사용하는데, 

matches는 정규표현식용 Pattern 인스턴스를 생성하고 한번 쓰고 바로 버려진다. -> 객체 생성 비용 높다.

 

만약 isRomanNumeric() 메서드를 자주 이용하여 검증한다면 무수히 많은 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 isRomanNumeric(String s) {
        return ROMAN.matcher(s).matches();
    }
}

 

Pattern.compile을 통해 주어진 정규표현식으로 Pattern 인스턴스를 생성한다.

비싼 객체인 만큼 한번 생성하고 그 이후에는 재사용함으로써 성능 향상이 가능하다.

 

불필요한 객체 생성 : 오토박싱 (auto boxing)

오토박싱 : 프로그래머가 기본 타입(long)과 박싱된 기본 타입(Long)을 섞어 쓸 때 상호 변환해주는 기술

 

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
    	// 무수한 오토박싱
        sum += i;
    return sum;
}

private static long sum() {
    long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

 

위와 같이 Long으로 sum을 지정한다면, i가 더해질 때 마다 sum += (Long) i; 이런 식으로 오토박싱이 이뤄진다.

즉, Long 인스턴스가 i만큼 계속 생성되며 성능상 이슈가 발생한다.

 

하지만 아래와 같이 long인 기본 타입으로 만들어주면 발생하지 않는다.

 

따라서 박싱된 기본 타입 보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 해야한다.

 

객체 생성은 비싸니 생성을 피하자 ! (X)

최근 JVM은 작은객체를 생성하고 회수하는 일이 큰 부담이 되지 않는다.

오히려 프로그램의 명확성, 간결성, 기능을 위해선 객체를 추가로 생성하는 것은 좋다.

하지만, 아주  무거운 객체가 아닌 이상 객체풀을 생성하지 말자 !

객체풀 : 자주 생성, 파괴되는 객체의 경우 계속 생성, 파괴로 인한 자원 낭비를 막기 위해 일정량의 객체를 미리 생성하고 재사용 하는 방법

하지만, 객체풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨린다.