결론
클래스는 다른 클래스를 의존합니다.
이때 해당 클래스가 다른 클래스를 직접 생성하게 해서는 안됩니다. 그럴 경우, 수정이 번거롭고 테스트가 하기가 어렵습니다.
대신, 필요한 의존관계를 (혹은 그 클래스 객체를 생성하는 팩토리를) 생성자에 (혹은 정적 팩토리나 빌더에) 넘겨 주도록 합시다.
'의존 객체 주입'이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해줍니다.
의존 객체 주입 패턴 - 생성자 (p. 29)
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexion distionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
}
설명
많은 클래스는 하나 이상의 자원에 의존합니다.
이에 따른 예시로는 맞춤법 검사기는 사전(dictionary)에 의존합니다.
이런 클래스를 정적 유틸리티 클래스로 구현하는 모습을 드물지 않게 볼 수 있습니다.
맞춤법 검사기 - 정적 유틸리티 (p. 28)
public class SpellChecker {
private static final Lexicon dictionary = new ...;
//객체 생성 방지
private Spellchecker() {}
public static boolean isValid(String word) {...}
}
- 해당 클래스는 dictionary 자원을 의존하고 있습니다.
- 또한, dictionary 자원을 직접적으로 생성하고 있기 때문에, 수정이 번거롭고 테스트하기가 어렵습니다.
비슷하게, 싱글턴으로 구현하는 경우도 흔합니다.
맞춤법 검사기 - 싱글톤 (p. 28)
public class SpellChecker {
private final Lexicon dictionary = new ...;
//객체 생성 방지
private SpellCheck(...) {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public boolean isValid(String word) {...}
}
- 정적 유틸리티 클래스와 마찬가지로 dictionary 자원을 의존하고 있으며, 직접적으로 생성하고 있기 때문에 수정이 번거롭고 테스트하기가 어렵습니다.
문제점: 두 방식 모두 사전을 단 하나만 사용한다.
실전에서는 사전이 언어별로 따로 있고, 특수 어휘용 사전을 별도로 두기도 합니다. 심지어 테스트용 사전도 필요할 수 있습니다.
사전 하나로 이 모든 쓰임에 대응할 수 있기를 바라는 건 너무 순진한 생각입니다.
해결: SpellChecker 가 여러 사전을 사용하도록 만들어보자.
방법 1. 간단히 dictionary 필드를 교체하는 메서드(setter)를 추가한다.
간단히 dictionary 필드의 final 한정자를 제거하고, 다른 사전으로 교체하는 메서드(setter)를 추가할 수 있습니다.
하지만 이 방식은 어색하고 오류를 내기 쉽습니다.
또한, 멀티스레드 환경에서는 쓸 수 없습니다.
참고 멀티스레드 환경에서 setter 메서드를 통한 의존성 변경이 문제가 발생할 수 있는 이유 - 스레드 안전성 (Thread Safety) 관련 문제
여러 스레드가 동시에 객체의 setter 메서드를 호출하면, 예상치 못한 결과가 나올 수 있습니다.
한 스레드가 setter 메서드를 호출하면서 객체의 상태를 변경하는 도중, 다른 스레드가 동시에 같은 setter 메서드를 호출하여 객체의 상태를 변경할 수 있습니다. 이로 인해 의도치 않은 상태로 객체가 변할 수 있으며, 프로그램의 동작이 일관성 없게 될 수 있습니다.
따라서, 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나, 싱글턴 방식이 적합하지 않습니다.
방법 2. 의존 객체 주입 패턴 - 권장
인스턴스를 생성할 때, 생성자에 필요한 자원을 넘겨주는 방식입니다.
의존 객체 주입 패턴 (p. 29)
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexion distionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
}
SpellChecker 객체를 생성할 때, dictionary에는 어떤 사전이든 Lexion 타입의 하위타입(다형성) 이기만 하면 매개변수에 넣어 원하는 객체로 의존관계를 주입할 수 있습니다.
장점
- 유연성과 테스트 용이성을 높여줍니다. (원하는 객체로 손쉽게 변경이 가능)
- 예에서는 dictionary 딱 하나의 자원만 사용하지만, 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 작동합니다.
- 또한, 불변을 보장하여 (같은 자원을 사용하려는) 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있기도 합니다. (setter는 불변성을 보장하지 않습니다.)
- 의존 객체 주입은 위와 같은 생성자이외에도, 정적 팩터리, 빌더 모두에 똑같이 적용할 수 있습니다.
참고 정적 팩터리
public class Grade { ... private static Grade of(int takenSemester) { if (0 < takenSemester && takenSemester <= 2) { return new Freshman(); } if (2 < takenSemester && takenSemester <= 4) { return new Sophomore(); } if (4 < takenSemester && takenSemester <= 6) { return new Junior(); } if (6 < takenSemester &&<takenSemester <= 8){ return new Senior(); } ... } ... }
참고 빌더(Builder)
public static class Builder { // 필수 매개변수 private final int servingSize; private final int servings; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } ... }
변형. 생성자에 자원 팩토리를 넘겨주는 방법
팩토리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말합니다.
즉, 팩토리 메서드 패턴을 구현한 것입니다.
자바 8에서 소개한 Supplier<T> 인터페이스가 팩터리를 표현한 완벽한 예입니다.
Supplier<T>
public interface Supplier<T> {
T get(); //추상 메서드. 매개변수가 없고, 단순히 객체를 반환한다.
}
호출
public static void main(String[] args) {
Supplier<String> helloSupplier = () -> "Hello ";
System.out.println(helloSupplier.get() + "World");
}
출력 결과
Hello world
Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입(bounded wildcard type)을 사용해 팩터리의 타입 매개변수를 제한해야 합니다. 이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있습니다.
다음 코드는 클라이언트가 제공한 팩터리가 생성한 타일(Tile)들로 구성된 모자이크(Mosaic)를 만드는 메서드입니다.
Mossaic create(Supplier<? extends Tile> tileFactory) { ... }
참고 와일드 카드 타입
모든 타입을 대신할 수 있는 와일드카드 타입<?> 입니다. 정해지지 않은 unknown type이기 때문에 Collection<?>로 선언함으로써 모든 타입에 대해 호출이 가능해졌습니다. 중요한 점은 any type이 아니라 unknown type 이라는 점입니다.
문제 발생 X
void printCollection(Collection<?> c { for(Object e : c) { System.out.println(e); } }
문제 발생 O
@Test void genericTest() { Collection<?> c = new ArrayList<String>(); c.add(new Object()); //컴파일 에러 }
와일드 카드의 경우 unknown type이므로 Integer, String 또는 개발자가 추가한 클래스까지 될 수 있는 범위가 무제한입니다. 와일드카드의 경우 add로 넘겨주는 객체가 unknown 타입의 자식이여야 하는데, 정해지지 않았으므로 자식 여부를 검사할 수 없습니다.
이러한 상황이 발생하는 이유는 와일드카드가 any 타입이 아닌 unknown 타입이기 때문입니다.
반면에, get으로 값을 꺼내는 작업은 와일드카드로 선언되어 있어도 문제가 없습니다.
왜냐하면 값을 꺼낸 결과가 unknown 타입이어도 우리는 해당 타입이 어떤 타입의 자식인지 확인이 필요하지 않으며, 적어도 Object의 타입임을 보장할 수 있기 때문입니다.
참고 한정적 와일드 카드 타입
Java에서 제공하는 한정적 와일드 카드는 특정 타입을 기준으로 상한 범위와 하한 범위로 지정함으로써 호출 범위를 확장 또는 제한할 수 있습니다.
의존 객체 주입의 단점과 프레임워크
의존 객체 주입이 유연성과 테스트 용이성을 개선해주기도 하지만, 의존성이 수천 개나 되는 큰 프로젝트에서는 오히려 코드를 어지럽게 만들기도 합니다.
스프링(Spring), 대거(Dagger), 주스(Guice) 와 같은 의존 객체 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있습니다.
출처
https://www.yes24.com/Product/Goods/65551284
'책 > effective java' 카테고리의 다른 글
[effective java] item 7. 다 쓴 객체 참조를 해제하라 (0) | 2023.07.25 |
---|---|
[effective java] item 6. 불필요한 객체 생성을 피하라 (0) | 2023.07.25 |
[effective java] item 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2023.07.23 |
[effective java] item 3. 싱글톤임을 보장하기 위해 private 생성자 또는 열거 타입을 사용해라 (0) | 2023.07.17 |
[effective java] item 2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.07.17 |