본문 바로가기

Language & Framework/Java

이펙티브 자바 (4) 자원을 직접 명시하지 말고 의존 객체를 주입해서 사용하라 (+ item4)

 

해당 파트는 사실 item4가 아니라 item5인데, item4 "인스턴스화를 막으려거든 private 생성자를 활용하라" 부분이 별달리 정리할 내용이 없으며 item5도 스프링을 배울 때 공부할만한 내용의 반복이라 길게 작성할 필요가 없을 것 같아 한 번에 포스팅하게 되었다.

 

item4의 경우 다들 상식적으로 알고 있을만한 부분이지만 한 번 더 확인해보자.

 

대부분의 경우 반드시 어떤 객체의 인스턴스화를 막아야할 이유는 없지만, 우리는 종종 오직 유틸리티 용도로만 사용하기 위한 클래스를 작성할 때가 있다. 대표적으로 자바 내부에서 그런 클래스를 찾아보자면 "Math"가 있다.

 

 

Math 클래스는 내부의 모든 멤버가 static이다.

애초에 인스턴스화 같은 건 전혀 고려하지 않고 만들어진 클래스라는 것이다.

 

물론 Math를 인스턴스화한다고 프로그램이 망가지거나 사회가 무너지고 북극의 얼음이 녹지는 않지만, 그래도 누군가가 무의미하게 유틸리티 클래스를 인스턴스를 생성해서 사용한다거나 상속받아서 사용하는 일은 아예 차단해버리는 것이 좋을 것 같다.

 

아래는 잘못된 예시를 보여준다.

 

 

1
2
3
4
5
6
7
8
9
public abstract class AbstractUtilityClass {
    public static void print() {
        System.out.println("print");
    }
 
    public static void main(String[] args) {
        AbstractUtilityClass utilityClass = new ChildUtilityClass();
    }
}
cs

 

 

인스턴스화를 막겠다고 추상 클래스를 선언하는 것은 아주 무의미한 행동이다.

첫 번째로, 추상 클래스를 상속받아서 인스턴스를 생성할 수 있다.

두 번째로, 추상 클래스로 만들어놓으면 이것이 원래 상속 받아서 사용하라고 만들어놓은 클래스가 아닌가 하는 잘못된 생각을 불러 일으킬수 있다.

 

따라서 인스턴스화를 막고 싶다면 private 생성자를 활용하여 인스턴스화 및 상속을 모두 방지할 수 있다.

만약 Reflection API나 클래스 내부에서까지 막아버리고 싶다면 어떻게 할까?

지난 글을 읽어봤으면 알겠지만 생성자에서 예외까지 던져버리면 완벽하게 막을 수 있다.

 

 

1
2
3
4
5
public class UtilityClass {
    private UtilityClass() {
        throw new RuntimeException();
    }
}
cs

 

 

그다지 이해하기 어려운 내용은 아니라고 생각한다.

 

이제 다음으로 item 5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 부분을 다루겠다.

이 부분도 사실 스프링을 사용하고 있다면 모두 반 강제적으로(..) 지키고 있는 부분이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SpellChecker {
    private final static Dictionary dictionary = new Dictionary();
    private SpellChecker() {};
    public static boolean isValid(String word) {
        return dictionary.getContent().contains(word);
    }
}
 
@Getter
public class Dictionary {
    private String content;
}
 
 
cs

 

 

위의 SpellChecker 클래스는 뭐가 잘못된 것일까?

바로 Dictionary 구현체를 필드 내부에서 직접적으로 생성하고 있다는 것이다.

 

이렇게 될 경우 문제는 다음과 같다.

1. 테스트가 어려워진다. SpellChecker만 테스트하고 싶은데 Dictionary라는 직접적인 구현체에 의존하고 있기 때문이다.

2. 확장성이 떨어진다. 만약 Dictionary와 비슷하지만 세부 사항이 다른 객체가 필요하다면? 기존 코드를 모조리 수정해야할 것이다.

 

final을 제거하고 setDictionary같은 걸 사용하면 안되냐고 할 수도 있지만, set을 활용하는 방식은 오류를 내기 쉬우며 멀티스레드 환경에서는 쓸 수가 없다. 

( 항상 정해진 인터페이스를 통해서만 객체와 통신할 때 최대한의 안정성이 보장된다는 사실을 기억하자. )

 

그러면 어떻게 해야할까?

아주 간단하다. SpellChecker의 인스턴스를 생성하는 시점에 Dictionary의 구현체를 정해주고, 그 전까지는 interface만 가지고 있으면 된다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface DictionaryInterface {
    String getContent();
}
 
public class KoreanDictionary implements DictionaryInterface{
    private String content;
 
    @Override
    public String getContent() {
        return this.content;
    }
}
 
public class NewSpellChecker {
    private final DictionaryInterface dictionary;
    public NewSpellChecker(DictionaryInterface dictionary) {
        this.dictionary = dictionary;
    }
    public boolean isValid(String word) {
        return dictionary.getContent().contains(word);
    }
}
 
 
cs

 

 

음.. 아주 편안해졌다.

이제 만약 Mockito 같은 프레임워크를 사용하지 않는다고 하더라도, 우리는 DictionaryForTest 같은 클래스를 만들어서 손쉽게 유닛테스트를 할 수 있을 것이다.

 

그리고 KoreanDictionary EnglishDictionary 등 기본적인 틀은 같으나 세부 요구사항이 다를 때에도 유연하게 대처할 수 있다.

즉, NewSpellChecker의 코드는 전혀 수정할 필요가 없는데 확장성은 증가했다는 것이다.

 

이게 바로 객체지향 SOLID 원칙 중 (내가 생각하기에) 가장 중요하게 여겨지는 OCP(Open-Closed Principls)에 해당한다.

그리고 우리는 이 형태를 계속해서 사용하고 있다. 어디에서?

스프링에서.

 

스프링은 우리가 클래스들을 @Component나 @Bean으로 등록해놓으면 컴파일 시점에 적절한 구현체를 찾아서 필요한 객체에 주입해주는 역할을 대신하고 있다. 참 편리한 일이다.

 

추가적으로 책에서 팩터리 메서드 패턴을 언급하고 있는데, 이전에 작성한 글이 있으니 궁금하면 해당 글을 읽어보자.

아래에는 간단한 예시만 올리겠다.

 

 

https://7357.tistory.com/197?category=1077082 

 

디자인 패턴 (2) Factory-Method Pattern : 팩토리 메서드 패턴

Factory Method Pattern? 팩토리 메서드 패턴은 인스턴스를 만드는 책임을 추상 클래스(혹은 추상 인터페이스)의 책임으로 감싼 것이다. 그렇다.. 이렇게 말하면 무슨 말인지 알 수가 없다. 지식 자랑글

7357.tistory.com

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DefaultDictionaryFactory implements DictionaryFactory {
    @Override
    public DictionaryInterface getDictionary() {
        return new KoreanDictionary();
    }
}
 
public interface DictionaryFactory {
    DictionaryInterface getDictionary();
}
 
public class FactorySpellChecker {
    private final DictionaryInterface dictionary;
    public FactorySpellChecker(DictionaryFactory dictionaryFactory) {
        this.dictionary = dictionaryFactory.getDictionary();
    }
}
 
cs

 

팩토리 메서드 패턴을 활용한다면 이제는 정말 스프링 없이도 외부적으로는 아무런 코드 변경 없이 factory만으로 모든 것을 해결할 수 있을 것이다.이전에 본 것이 @Component를 사용할 때의 모습이였다면 (물론 자바에서는 직접 주입해줘야 하지만), factory method pattern을 활용한 것은 마치 @Configuration과 비슷하게 느껴지기도 한다.

 

책에서 마지막으로 소개하는 방식은 Supplier를 생성자 매개 변수로 사용하는 것이다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SupplierSpellChecker {
    private final DictionaryInterface dictionary;
 
    public SupplierSpellChecker(DictionaryInterface dictionary) {
        this.dictionary = dictionary;
    }
 
    public SupplierSpellChecker(Supplier<extends DictionaryInterface> dictionarySupplier) {
        this.dictionary = dictionarySupplier.get();
    }
 
    public boolean isValid(String word) {
        return dictionary.getContent().contains(word);
    }
}
 
/*        SupplierSpellChecker spellChecker = new SupplierSpellChecker(() -> new KoreanDictionary());
        SupplierSpellChecker spellChecker = new SupplierSpellChecker(KoreanDictionary::new);*/
cs

 

이런 식인데, 이게 왜 필요하냐고 생각할 수도 있지만 생각보다 우리가 자주 사용하는 코드에서도 이런 형태를 찾아볼 수 있다.

대표적으로 (Constructor는 아니고 Static Factory Method지만) Optional의 orElseThrow()를 볼 수 있겠다.

 

 

 

 

그렇다..

이제 다 썼는데 이번 글은 뭔가 평소보다 정성이 덜 들어가서(기존에 알던 내용들이다보니) 그런지 뭔가 찝찝하고 어떻게 끝내야할지 모르겠다.

아무튼 끝이다.