이펙티브 자바 - 아이템 5 & 6
이 글은 몇몇 크루들과 이펙티브 자바 스터디를 하며 정리한 내용입니다. 🙌
🌩 [아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
하나의 클래스에서 다른 자원에 의존하는 경우가 많다. 또한 해당 클래스가 유틸리티 클래스라면 싱글톤이나 정적 클래스로 사용되는 경우가 많다.
정적 클래스로 구현한 경우
public class SpellChecker {
private static final Lexicon dictionary = ....;
priavte SpellChecker() {}
public static boolean isValid(String word) {
...
}
...
}
싱글톤으로 구현한 경우
public class SpellChecker {
private final Lexicon dictionary = ....;
public static SpellChecker INSTANCE = new SpellChecker(...);
priavte SpellChecker() {}
public static boolean isValid(String word) {
...
}
...
}
그런 경우 여러 단점이 발생한다.
- 유연하지 않다.
- 안에 의존하고 있는 객체를 런타임 시점에 바꾸거나 조작하기가 어렵다.
- 여러 다른 사전들을 이용하고 싶을 때 변경에 자유롭지 못하다.
- 테스트하기 어렵다.
- 정적으로 의존 객체를 내부에서 생성하므로 모킹하거나 해당 객체에 대한 조작으로 테스트를 하기 어렵다.
의존성 주입(DI)을 통해 해결하기
위 문제에 대한 가능한 해결방법이 DI 말고 하나가 더 있다.
- 변경에 대한 유연성을 부여하기 위해
final
을 제거하고 여러 사전들을 바꿔서 사용할 수 있도록 한다.- 이렇게 할 경우 멀티스레드 환경에 취약하고 오류를 내기가 쉽다.
따라서 DI를 통해서 문제를 해결해본다.
DI란 클래스에서 필요한 자원을 내부에서 생성하는 것이 아니라 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식으로 구현하는 것이다.
public class SpellChecker {
private final Lexicon dictionary;
priavte SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNul(dictionary);
}
public static boolean isValid(String word) {
...
}
...
}
- 여러 자원에 대한 의존 관계에 자유로우며 유연하다.
- 불변을 보장하여 여러 클라이언트에 대한 공유에 안정적이다.
추가적으로 특정 자원을 직접 넘겨주기보다 팩터리를 넘겨주어 필요시 자원을 클래스 내부에서 직접 생성할 수 있도록 할 수도 있다. 이때 Supplier<T>
처럼 한정적 와일드카드 타입(타입 안정성을 지원하는 Generic을 사용)을 통해 팩터리 매개변수를 넘겨서 필요한 자원을 생성하도록 한다.
요약
- 클래스가 다른 자원에 의존한다면 그것을 직접 싱글톤이나 정적 클래스로 구현하는 것은 좋은 방법이 아니다.
- 또한 그 자원들을 그 클래스가 직접 생성하도록 하지 않는 것이 더 좋다.
- 대신 의존 객체 주입으로 외부에서 해당 자원이나 팩터리를 생성자나 정적 팩터리에 넘겨주도록 하자.
🌩 [아이템 6] 불필요한 객체 생성을 피하라
현재 JVM의 성능이 좋아져 객체 생성이 이전만큼 비싼 작업은 아니지만 여전히 매번 생성하기에 무겁거나 지나치게 반복적으로 생성하여 성능에 안좋은 영향을 미치는 경우가 있다.
따라서 객체는 (특히 불변 객체는) 재사용할 수 있다면 재사용하는 것이 좋다.
String을 재사용 하자
new String("woowa")
는 만들때마다 heap영역에 새로운 객체를 생성한다. 만일new String("woowa")
를 두번 호출한다면 두개의 객체가 생성된다.- 반면
String s = "woowa"
를 호출한다면 heap영역 내부에 String constant pool에 객체가 생성되고 동일한 스트링이라면 재사용 한다.
불변 클래스의 정적 팩터리 메서드로 객체를 재사용 하자
예를 들어 Boolean(String)
보다 Boolean.valueOf(String)
을 사용하여 불변 객체를 재사용 하는 것이 더 빠르고 세련된 구현 방식이다.
객체 생성이 비싼 객체라면 재사용 하자
String.matches()
내부에서 사용되는 Pattern
객체는 매우 생성이 비싼 객체이다. 이 메서드를 반복해서 사용하면 내부에서 위 객체를 계속 생성하여 사용한다. 따라서 필요하다면 비교하고자 하는 정규식에 대한 Pattern
을 미리 만들어두고 그것을 재사용하는 형식으로 사용하는 것이 훨씬 성능이 좋다.
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("정규식 ..");
public static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
오토박싱으로 생성되는 불필요한 객체를 주의하자
오토박싱으로 기본타입을 자동적으로 박싱된 타입으로 변환시켜준다. 하지만 명시적으로 객체를 생성하는 부분이 개발자에게 노출되지는 않기 때문에 예기치 않게 굉장히 많은 객체를 생성하고 있을 수도 있다. 따라서 성능에 큰 영향을 미칠수도 있다.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
예를 들어 위와 같은 코드는 sum 이라는 변수가 박싱타입으로 선언되어 있으므로 누적되는 i
는 매번 Long
으로 오토박싱되며 새로운 객체가 2^31개나 생성되는 일이 발생한다.
이런 의도치 않은 오토박싱을 주의하고 되도록 기본타입을 사용하기를 추천한다.
주의할 점
객체 생성을 최대한 안하기 위해서 지나친 재사용을 하라는 뜻이 아니다. JVM에서 객체를 생성하는 것이 대부분 크게 부담되는 일이 아니기 때문에 프로그램의 안정성, 명확성, 간결성을 위해서 객체를 자주 생성하는 것은 바람직한 것이다.
객체를 재사용하기 위해 무리하여 pool을 생성하는 것을 지양하자. 코드에서 직관적이지 않고 오히려 메모리 사용량을 늘릴 수 있다. 다만 엄청 비싼 자원이라면(DB 커넥션과 같이) pool을 사용하는 것이 적합하다.
객체를 새로 생성하는 것이(예를 들면 방어적 복사) 프로그램 안정성 측면에서 훨씬 좋을 수 있다. 이럴때는 주저없이 객체를 재사용하기보다 새로 생성하는 것이 훨씬 좋다.
느낀 점
불필요하다고 명확히 느낄 때만 객체 생성을 지양하라는 것이지 무조건 객체를 재사용하기 위해서 무리하라는 것이 아니다. 마지막 부분에서 저자가 강조한 것처럼 트레이드 오프를 고려하고 지혜로운 결정을 내리는 것이 매우 중요하다.
가끔 개발을 하다가 보면 “어디서 이렇게 하라고 했다.” 라고 이야기하면서 특정 방식을 적용하자고 하는 말을 자주 듣곤 한다. 하지만 대부분 그 지령은 절대 진리는 아니다. 상황에 따라서 적합한 것을 선택하는 것이 매우 중요한 개발자 역량이라고 생각한다.