JAVA/JAVA | Spring 학습기록

[호호 스터디] DI와 서비스 로케이터 _객체지향과 디자인 패턴 Chapter 6

kth990303 2022. 3. 27. 19:56
반응형

호호 스터디에서 Chapter 6: DI와 서비스 로케이터를 듣기 전에, 미리 책을 읽고 공부한 내용을 기록한 포스팅이다.

 

DI와 서비스 로케이터

각 객체들을 사용하기 위해선 어떤 방법이 좋을까?

아무 생각없이 객체를 생성하고 의존하게 될 경우, Chapter 5에서 배웠던 DIP(의존 역전 원칙), OCP(개방 폐쇄 원칙), SRP(단일 책임 원칙) 등 SOLID 원칙을 어기게 되고, 변경에 유연하지 못한 코드가 만들어질 확률이 높다.

특히, 순환 의존이 발생할 경우, 요구사항이 수정되거나 변경될 경우 모든 객체의 코드를 수정해야 될 수도 있다.

 

이번 시간에는 DI(Dependency Injection), 서비스 로케이터를 이용한 객체 사용 방법에 대해 알아보도록 하겠다.


Service Locator

예전에 꽤나 자주 쓰였던 방법이라 한다.

최근에는 서비스 로케이터를 이용한 방법이 가지는 몇 가지 단점들로 인해 특정 경우를 제외하곤, 거의 사용하지 않는 분위기라 한다. 

서비스 로케이터는 어플리케이션에서 필요로 하는 객체를 제공하는 책임을 갖는 객체이다.

 

public class Participants {

    private final Dealer dealer;
    private final List<Player> players;

    public void run() {
        ServiceLocator serviceLocator = ...; // 클래스 혹은 정적 메서드로 구해준다.
        
        dealer = serviceLocator.getDealer();
        players = serviceLocator.getPlayers();
    }
}

위 코드처럼 의존 객체를 주입하는 방법들을 서비스 로케이터에서 관리하는 것이다.

 

이 방법에는 몇 가지 단점이 존재한다.

첫 번째로, ISP (인터페이스 분리 원칙)을 위반하게 된다.

서비스 로케이터의 정의를 생각해보면 당연한 것이다. 특정 의존 객체에 대한 요구사항이 추가되거나 변경될 경우, 서비스 로케이터에 요구사항 해당 메서드를 추가해야 되며, 이는 서비스 로케이터에서 가지고 있는 다른 클래스들에게 변경의 여지를 유발하게 된다.

 

이 문제를 해결하는 방법은, 의존 객체마다 서비스 로케이터를 작성하는 방법이 있다.

하지만, 이러한 경우 동일 구조의 서비스 로케이터 클래스를 만들어 생산성을 낮추는 문제를 야기할 수 있게 된다.

다행히 JAVA에선 아래와 같은 방법으로 위 문제를 어느정도 해결할 수 있다.

public class ServiceLocator {
    private static Map<Class<?>, Object> objectMap = new HashMap<>();
    
    public static <T> T get(Class<T> targetClass) {
        return (T) objectMap.get(targetClass);
    }
    
    public static void register(Class<?> targetClass, Object obj) {
        objectMap.put(targetClass, obj);
    }
}

위와 같이 제네릭과 Map 자료구조를 이용하여 객체를 등록하고 관리할 수 있어 

ISP를 위반하지 않으면서, 중복된 구조를 줄이면서, 의존 객체를 제공할 수 있는 서비스 로케이터를 만들 수 있긴 하다.

하지만, 이후에 서술할 두 번째 단점은 여전히 존재한다.

 

두 번째로, 동일 타입의 객체가 다수 필요할 경우, 각 객체 별로 제공 메서드를 만들어 주어야 한다.

public class ServiceLocator {
    
    // Participant는 Dealer, Player를 하위클래스로 가진다.
    public Partipant getParticipant1() { ... }
    public Partipant getParticipant2() { ... }
    public Partipant getParticipant3() { ... }
}

위와 같이 인터페이스 또는 추상클래스를 구현/상속하는 하위 클래스가 생겨날 때마다 메서드를 매번 만들어주어야 한다.

 

만약 하위 클래스가 추가될 경우, ServiceLocator 수정은 물론, Participant를 의존 객체로 가지는 클래스의 코드도 ServiceLocator에서 수정된 메서드를 사용해주는 것으로 변경해주어야 하기 때문에 OCP(개방 폐쇄 원칙)을 위반하게 된다!


DI (Dependency Injection)

DI에는 두 가지 방법이 존재한다.

  • 생성자 주입 방식
  • 설정 메서드 방식

설정 메서드 방식부터 먼저 살펴보자.

public class Participants {

    private final Dealer dealer = new Dealer();
    private final List<Player> players = new ArrayList<>();
    
    public Participants() {}

    public void setDealer(Dealer dealer) {
        this.dealer = dealer;
    }
    
    public void setPlayers(List<Player> players) {
        this.players= players;
    }
}

위와 같이 setter method를 이용하여 의존 객체를 주입해주는 방식을 설정 메서드 방식이라고 한다.

 

설정 메서드 방식은 명확하게 set이라는 네이밍을 가진 메서드로 상태를 주입해주기 때문에, 후에서 서술할 생성자 주입에 비해선 다소 가독성이 높고 명확해보일 수 있다는 장점이 존재한다.

 

하지만, 설정 메서드 방식은 치명적인 단점이 존재한다.

바로, 우리 개발자들을 괴롭히는 NPE (NullPointerException)을 유발할 위험성이 높다는 것이다.

설정 메서드 방식은 주입할 객체를 생성하지 않고 사용을 가능케 하므로 NPE가 발생할 수 있다. 즉, 의존 객체를 설정하지 못한 상태에서 객체를 사용할 수 있기 때문에 발생하는 문제이다.


생성자 주입은 설정 메서드 방식에서 존재하는 단점을 예방할 수 있다.

public class Participants {

    private final Dealer dealer;
    private final List<Player> players;

    public Participants(Dealer dealer, List<Player> players) {
        this.dealer = dealer;
        this.players = players;
    }
}

설정 메서드 방식과 다르게, 생성자에서 파라미터로 상태를 주입받는다.

따라서 Participants 클래스를 사용하기 위해선, 의존 객체가 먼저 생성이 돼있어야 하므로 NPE가 발생하지 않게 된다.

 

다만, 특정 이유로 의존할 객체가 나중에 생성돼야 한다면, 생성자 주입 방식은 사용할 수 없다.

그렇지만, 특정 클래스가 객체를 의존한다는 것 자체가 그 객체의 기능을 사용하기 위함이므로, 웬만해선 미리 의존 객체 생성을 먼저 해주고 클래스를 만들어주도록 하자.

 

또한, 앞에서 설정 메서드의 장점으로 얘기한 가독성이 증가한다는 부분도 한 번 다시 고민해보아야 한다.

의존할 객체가 많을 경우, 설정 메서드 방식, 또는 서비스 로케이터 방식이 가독성이 더 좋을 수는 있겠지만, 의존할 객체가 많다는 것 자체가 SRP (단일 책임 원칙)을 잘 지키고 있는지, 그리고 변경에 유연하지 못한 의존성이 높은 코드를 작성한 게 아닌지 고민해볼 필요가 있다.


개인적으로는 실수할 여지가 더 많은 건 컴퓨터(IDE 등)가 아닌 사람이라 생각하기 때문에,

NPE를 유발할 수 있는 설정메서드 방식보단, 생성자 주입 방식을 자주 사용할 듯 하다 :)

반응형