OOP

OOP - DI(Dependency Injection)와 서비스 로케이터

🤖 Play with Android 🤖 2023. 2. 26. 14:28
728x90


📌  애플리케이션 영역과 메인 영역

로버트 C 마틴(Robert C. Martin) 은 소프트웨어를 아래의 2가지 영역으로 구분하고 있다.

  • 애플리케이션 영역
    • 고수준 및 저수준의 구현을 포함한 영역
  • 메인 영역
    • 어플리케이션 영역에서 사용될 객체를 생성 / 설정 / 실행한다
    • 각 객체간의 의존 관계를 설정한다.
    • 애플리케이션을 실행한다.

 

메인 영역과 애플리케이션 간의 의존관계

메인 영역과 애플리케이션 간의 의존은 다음 그림과 같게 되는데, 여기서 알 수 있는 점은 모든 의존은 메인 영역에서 애플리케이션 영역으로 향한다는 것이다. 즉, 반대의 경우인 애플리케이션 -> 메인 으로의 의존은 존재하지 않는다.
이는 메인 영역을 변경하더라도 애플리케이션 영역은 변경되지 않는다는 것을 뜻하며 따라서 애플리케이션에서 사용할 객체를 교체하기 위해 메인 영역의 코드를 수정하는 것은 애플리케이션 영역에는 어떠한 영향을 주지 않는 효과를 얻을 수 있다.

 

그리고 애플리케이션에서 사용될 객체를 제공하는 책임을 갖는 객체를 서비스 로케이터(Service Locator)라고 부른다.

 

서비스 로케이터 방식은 로케이터를 통해서 필요한 객체를 직접 찾는 방식인데, 이 방식에는 몇가지 단점이 존재한다. 따라서 서비스 로케이터를 사용하기보다는 외부에서 사용할 객체를 주입받는 DI(Dependency Injection) 방식을 사용하는 것을 추천한다.

 

 

 

📌  DI(Dependency Injection)을 이용한 의존 객체 사용

DI는 필요한 객체를 외부에서 넣어주는 방식으로 사용할 객체를 전달받을 방법을 제공하면 DI 준비는 끝난다.

생성자 생성 방식

public Class Player {
	
	private final BettingMoney money;

	public Player() {
		this.money = new BettingMoney();
        ...
    }
    ...
}
  • 콘크리트 클래스를 직접 사용해서 객체를 생성하게 되면 의존 역전 원칙을 위반하게 되며, 결과적으로 확장 폐쇄 원칙을 위반하게 된다.
  • 이런 단점을 보완하기 위한 방법이 DI이다.
  • DI는 필요한 객체를 직접 생성하거나 찾지 않고 외부에서 넣어 주는 방식이다.

 

public Class Player {

	private final BettingMoney money;
    
	// 구체적으로 어떤 체스 기물인지를 외부에서 주입받고 있다.
	public Player(BettingMoney money) {
		this.money = money;
        ...
    }
    ...
}
  • 위 코드는 DI을 사용하여 다음과 같이 개선할 수 있다. 

DI(Dependency Injection: 의존 주입)은 위에서 언급되었던 단점을 극복하기 위한 방법으로 필요한 객체를 직접 생성하거나 찾지 않고, 외부에서 넣어주는 방식을 말한다. 위에서 보았다시피 DI 구현 자체는 매우 간단한데, 사용할 객체를 주입받을 수 있는 방법을 제공하면 된다. 

 

수정된 Player 클래스를 보면 생성자를 호출할 때 외부에서부터 금액(BettingMoney) 객체를 전달받고 있다. 이들 자체에서는 스스로 의존하고 있는 객체를 찾거나 생성하지 않고, 외부로부터 주입(injection)을 받고 있다.

 

 

 

설정 메소드 방식

public class Player {
	
    private BettingMoney money;
    
    public Player() {
    }
    
    public void setBettingMoney(BettingMoney money) {
    	this.money = money;
    }
    
    public void someMethod() {
    	money.method();
        ...
    }
 	...   
}

설정 메서드 방식은 객체를 생성한 이후에 의존 객체를 설정할 수 있기 때문에, 어떤 이유로 인해 의존할 객체가 나중에 생성된다면 설정 메서드 방식을 사용해야 한다.

 

 

Player를 생성한 이후 setBettingMoney() 메서드를 호출하지 않은 상태에서 someMethod()를 실행

설정 메서드 방식은 몇 가지 주의해야 할 점이 있다.

  • money.method() 호출과정에서 NullPointerException 예외가 발생한다.
  • 아직 money에 대한 할당이 제대로 이루어지지 않았기 때문이다.
  • 즉, Player 객체를 사용하는 클라이언트 입장에서는 생성자 호출 이후 setter를 먼저 호출한 이후에 여러 기능(메서드)들이 실행가능하며 제대로 기능한다는 것을 알아야만이 Player를 제대로 사용할 수 있게 된다.

 

 

생성자 생성 방식 vs 설정 메소드 방식

DI 프레임워크가 의존 객체 주입을 어떤 방식까지 지원하느냐에 따라 달라지겠지만, 생성자 방식과 설정 메서드 방식 중에서 생성자 방식을 더 선호되는 편이다. 그 이유는 생성자 방식은 객체를 생성하는 시점에 필요한 모든 의존 객체를 준비할 수 있기 때문이다. 생성자 방식은 생성자를 통해서 필요한 의존 객체를 전달받기 때문에, 객체를 생성하는 시점에서 의존객체가 정상인지 확인할 수 있다. 생성 시점에 의존 객체를 모두 받기 때문에, 한 번 객체가 생성되면 객체가 정상적으로 동작함을 보장할 수 있게 된다.

 

 

 

 

 

 

📌 서비스 로케이터를 이용한 의존 객체 사용

서비스 로케이터 방식이란?

프로그램 개발 환경이나 사용하는 프레임워크의 제약으로 인해 DI 패턴을 적용할 수 없는 경우가 있다. 예를 들어, 안드로이드 플랫폼을 개발하는 모바일 앱의 경우 화면을 생성할 때 Activity 클래스를 상속받도록 하는데 이 경우에는 DI처리를 할 수 없다.

 

이러한 경우 우리는 의존 객체를 찾는 다른 방법을 모색해야 하는데, 어플리케이션에서 필요로 하는 객체를 제공하는 책임을 가지는 서비스 로케이터(Service Locator)가 바로 그 예이다.

 

서비스 로케이터는 다음과 같이 의존 대상이 되는 객체별로 제공 메서드를 정의하고, 의존 객체가 필요한 코드에서는 ServiceLocator가 제공하는 메서드를 활용해서 알맞은 객체를 의존한 뒤 기능을 수행할 수 있다.

public class ServiceLocator {
    public 작업 get작업() {...}
    public 의존객체 get의존객체() {...}
}

서비스 로케이터가 올바르게 동작하기 위해서는 서비스 로케이터 스스로 어떤 객체를 제공해야할지에 대해서 알고 있어야 하는 것이 중요하다. 

 

 

  • DI를 사용할 때 메인 영역에서 객체를 생성했던 것과 비슷하게 서비스 로케이터를 사용하는 경우에도 메인 영역에서 서비스 로케이터가 제공할 객체를 초기화해 준다.
  • 서비스 로케이터는 애플리케이션 영역의 객체에서 직접 접근하기 때문에, 애플리케이션 영역에 위치하게 된다.

 

서비스 로케이터의 단점

서비스 로케이터의 단점은 ISP(인터페이스 분리 원칙)을 위반한다는 것이다. 만약 위의 코드에서 "작업"이라는 인스턴스만 필요한 경우에도 "의존 객체"라는 객체에 대한 의존이 함께 발생하게 된다.

 

만약 Car라는 클래스와 Diesel 엔진 인터페이스, Gasoline 엔진 인터페이스 이렇게 3가지가 존재한다고 해보자.

  • 예를 들어 Car가 Diesel 엔진만 필요하여도, 서비스 로케이터가 가진 Gasoline 엔진에도 접근할 수 있게 된다.
  • 동일한 의존 객체를 여러 클래스에서 사용해야 할 경우, 제공 메서드를 각 객체 수만큼 준비해야 한다.
    • 같은 기능을 하는 중복된 코드가 늘어나게 된다.
  • 의존성에 문제가 있어도 컴파일 타임에 확인하는 것이 불가능하다.