이펙티브 자바의 첫 시작은 객체를 생성하고 파괴하는 것에 대한 고찰이다.
"2장 - 객체의 생성과 파괴" 는 다음과 같은 기준으로 맥락을 잡고 있다.
- 객체를 만들어야 할 때는 언제인가
- 객체를 만들지 말아야 할 때는 언제인가
- 올바른 객체 생성 방법은 무엇인가
- 객체의 불필요한 생성을 피하는 방법은 무엇인가
- 객체를 제 때에 파괴시키는 방법은 무엇인가
- 파괴 전에 수행해야 할 정리 작업을 관리하는 요령이 있는가
위와 같은 맥락을 계속 기억하며 공부하자.
객체 지향 프로그래밍을 하다보면, 다른 객체쪽에서 자원을 빌려쓰는 경우가 많다.
예를 들어 A라는 기능을 구현하는 클래스를 만들고 싶은데,
이를 위해서는 a, b, c 세개의 자원이 필요하다고 가정해보자.
해당 자원들을 가지고 있는 곳이 이미 있을텐데 굳이 A에서 또 자원을 넣어주는건 비효율적이다
그냥 마치 이웃끼리 서로 상부상조 하는것처럼 빌려쓰면 그만이지 않을까?
이런 방식으로 만약 A라는 클래스가 B라는 자원을 가져와서 쓰고 있다면,
우리는 이때 "A가 B에 의존한다." 고 말하게 된다.
예를 들어보자.
맞춤법 검사 기능을 하는 클래스를 만들고 싶다고 했을때 어떤 자원이 필요할까?
기본적으로 그 맞춤법을 확인해볼수 있는 사전 (Dictionary)이 필요할 것이다.
그럼 어떤 방식으로 맞춤법 검사기가 사전이라는 자원을 가져와 쓰는지,
직접 코드로 확인해보자.
<정적 유틸리티를 사용한 코드>
public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() { } public static boolean isValid(String word) { ... { public static List<String> suggestions(String type) { ... } }
<싱글턴을 사용한 코드>
public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() { } public static SpellChecker INSTANCE = new SpellChecker(...); public static boolean isValid(String word) { ... { public static List<String> suggestions(String type) { ... } }
왜 위와 같은 예시를 들었을까?
실제로 이렇게 정적 유틸리티 클래스나 싱글턴으로 구현하는 경우가 흔하다 한다.
Comment.
문득 "어째서 사람들은 이런 상황에서 정적 유틸리티 클래스나 싱글턴을 흔하게 사용하게 됬을까?" 라는 생각이 들게된다.
조금 일상적인 예시를 들어보면,
내가 레고 블럭을 쌓기 위해서 재료를 가져왔는데,
그 블럭이 형태를 유지하지 못하고 계속 흐물흐물 거린다면 어떨까?완성하는거 자체가 불가하다.
다시 말해 재료로 가져온 것은 그 자체로 더이상 형태가 변하지 않는 상태여야 한다는 것이다.그래서 사람들은 클래스에서 필요한 자원을 가져오기 위해,
final 변수를 사용하게 되지 않았을까?그런데 인스턴스에 final을 붙이는 가장 일반적인 예시가 바로..
정적 유틸리티 클래스와 싱글턴이였을 것이다.
문제는 앞선 Item에서 배웠듯,
정적 유틸리니 클래스는 더이상 인스턴스가 생성되지 못하게 하고,
싱글턴은 인스턴스가 무조건 하나만 생성되게 한다.
즉, 둘 모두 사전을 단 하나만 사용한다고 가정하고 있다.
실제로는 어떤가.
언어별 사전이 따로 있는것은 물론이거니와, 특수 어휘용 사전도 따로 있다.
이렇게 하면 여러 상황에서 유연하게 대처할 수가 없다.
그래서 사용하는 자원에 따라 동작이 달라지는 클래스에서는,
정적 유틸리티 클래스나 싱글턴 방식이 부적합하다.
여러 상황에서도 능동적으로 대처할 수 있는 방안이 필요하다.
의존 객체 주입 방식 이 바로 위 문제의 해결법이 될 수 있다.
원리는 간단하다.
자원을 가져와서 고정시켜놓고 사용하는 것이 아닌,
인스턴스를 생성할 때 상황에 맞는 필요한 자원을 생성자에 넘겨주는 방법이다.
바로 예제 코드를 보자.
public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid (String word) { ... } public List<String> suggetions (String type) { ... { }
위와 같이 하면,
인스턴스를 생성할 때 그때에 맞는 필요한 자원을 넘겨줄 수가 있다.
일반 사전이든, 특수용어 사전이든 다 가능하다는 말이 된다.
사실 이 방법은 단순하기도 하고 이미 많은 개발자들이 사용해오고 있던 방식이다.
다만 이것이 하나의 방법론이고 이름까지 있다는걸 모를 뿐이다.
어쨋든 이 방법에서는 자원이 몇 개던 의존 관계가 어떻던 간에,
문제없이 잘 동작한다.
그만큼 유연하며 테스트 용이성을 높여준다.
Comment
테스트 용이성은 왜 높아질까?
간단하다. 테스트라는 것은 여러가지 테스트케이스를 만들어 예외가 없는지 넣어보는 과정이다.
이 방법이 여러 상황에서도 대처가 가능한 만큼,
테스트 할 수 있는 적절한 환경을 만들어준다고 할 수 있다.
하지만 역시나 단점도 존재하기 마련인데,
의존성이 수천개나 되는 큰 프로젝트라면 오히려 코드를 어지럽게 만들기도 한다.
그런데 그렇게 큰 프로젝트라면,
스프링(Spring) 같은 의존 객체 프레임워크를 쓰는게 더 올바르다.
<의존 객체 주입 방법의 장점>
- 유연성이 높다.
- 테스트가 용이하다.
<의존 객체 주입 방법의 단점>
- 의존성이 수천개나 되는 프로젝트에서는 코드가 오히려 어지럽다.
이번 글에서는 의존 객체 주입 방법에 대해 알아봤다.
필자의 코멘트를 마지막으로 글을 마친다.
Item5 정리
- 클래스가 내부적으로 하나 이상의 자원에 의존하고,
그 자원이 클래스 동작에 영향을 준다면,
싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
- 이 자원을 직접 클래스에서 만드는건 더 안된다.
- 대신 필요한 자원을 생성자에 넘겨주자.
- 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.