[객체지향] DI와 서비스 로케이터

Hyebin Lee·2022년 2월 25일
0

JAVA

목록 보기
4/6
post-thumbnail

서비스 로케이터

어플리케이션 영역과 메인 영역

  • 어플리케이션 영역: 고수준 정책과 저수준 구현을 포함한 코드
  • 메인 영역: 어플리케이션이 동작하도록 각 객체들을 연결해 주는 영역

A와 B 인스턴스를 작동시키는 worker 클래스가 있다고 가정해보자. A와 B의 저수준 구현에 따라 worker는 아무튼 A와 B의 객체를 필요로 하기 때문에 코드가 변한다. 고수준 코드인 worker가 저수준의 구현 코드에 의존하는 것은 의존 역전 원칙에 어긋난다.

이 때 해결할 수 있는 방안이 바로 서비스 로케이터이다.

서비스 로케이터란?

서비스 로케이터란 위와 같은 상황에서 저수준의 객체를 필요로 하는 고수준의 클래스들을 위해 객체를 제공하는 기능을 갖고 있는 클래스를 말한다.

따라서 로케이터의 필드에는 A와 B의 객체가 들어가게 되며 getInstance().getA()형태로 worker는 그때그때 달라지는 콘크리트 클래스를 코드 변화 없이 가져올 수 있다.

그렇다면 이러한 로케이터의 필드(생성자)는 누가 설정해줄까? 그 역할을 바로 메인 영역에서 하는 것이다. 결국 메인 영역은 어플리케이션 영역의 객체를 생성하고 설정하고 실행하는 책임을 갖기 때문에 어플리케이션 영역에서 사용할 하위 수준의 모듈을 변경하고 싶다면 메인 영역을 수정하게 된다. 따라서 모든 의존은 메인 영역에서 어플리케이션 영역으로 향한다.

상속을 통한 서비스 로케이터

위와 같이 getter 형태의 서비스 로케이터는 메인 영역에서 생성자를 설정해주게되면 결국 생성자의 입력값인 콘크리트 클래스가 고수준 클래스에게도 접근이 용이해져서 고수준 클래스가 콘크리트 클래스를 접근할 위험성이 크다는 단점이 생긴다.

그 단점을 보완하기 위해 상속을 통한 서비스 로케이터를 활용할 수 있다. 그 방법은 아래와 같다.

  • 객체를 구하는 추상 메서드를 제공하는 상위 타입 구현
  • 상위 타입을 상속받은 하위 타입에서 사용할 객체 설정

상속 방식 서비스 로케이터 구현의 상위 타입

public abstract class ServcieLocator{

	public abstract JobQueue getJobQueue();
	public abstract Transcoder getTranscoder();

	protected ServiceLocator(){
		ServiceLocator.instance = this;
	}

..

상속 방식 서비스 로케이터 구현의 하위 타입

public class MyServiceLocator extends ServiceLocator{

	private FileJobQueue jobQueue;
	private FfmpegTranscoder transcoder;

	public MyServiceLocator(){

	super();
	this.jobQueue = new FileJobQueue();
	this.transcoder = new FfmpegTranscoder();
}

@Override
public JobQueue getJobQueue(){
return jobQueue();
}

@Override
public Transcoder getTranscoder(){
return transcoder;
}

제네릭을 이용한 서비스 로케이터 (인터페이스 분리)

서비스로케이터의 단점은 인터페이스 분리 원칙을 위반한다는 점이다. JobCLI 클래스가 사용하는 타입은 JobQueue 뿐인데 ServiceLocator를 사용함으로써 Transcoder 타입에 대한 의존이 함께 발생하게 된다.

이러한 문제는 자바에서 제공하는 제네릭을 통해 해결할 수 있다.

제네릭 기반 객체 등록 방식 서비스 로케이터 구현

public class ServiceLocator{
	private static Map<Class<?>,Object> objectMap = new HashMap<Class<?>,Object>();

	public static <T> T get(Class<T> klass){
	return (T) objectMap.get(klass);
	}

	public static void regist(Class<?> klass, Object obj){
		objectMap.put(klass, obj);
}
}

메인 영역에서는 ServiceLocator.regist() 메서드를 이용해서 객체를 등록해준다.

public static void main(){
ServiceLocator.regist(JobQueue.class, new FileJobQueue());...}

ServiceLocator를 사용하는 코드는 다음과 같이 작성한다.

public class JobCLI{

	public void interact(){

		//JobQueue에만 의존
		JobQueue jobQueue = ServiceLocator.get(JobQueue.class);
..}

서비스 로케이터의 가장 큰 단점은 동일 타입의 객체가 다수 필요할 경우, 각 객체 별로 제공 메서드를 만들어 주어야 한다는 점이다.

DI(Dependency Injection)

콘크리트 클래스를 직접 사용해서 객체를 생성하게 되면 의존 역전 원칙을 위반하며 결과적으로 확장 폐쇄 원칙을 위반하게 된다. 앞서 언급한 서비스 로케이터도 몇가지 단점이 있어서 이를 보완한 것이 DI이다.

DI는 외부에서 객체를 넣어주는 방식이다.

생성자를 통한 DI

DI의 가장 간편하면서도 대표적인 방법은 생성자를 통해 객체를 주입받는 것이다.

예를 들어 앞에서 언급한 Worker 클래스에 A와 B 객체를 전달받아서 필드로 설정할 수 있도록 생성자를 만들어두면 나중에 외부에서 해당 생성자에 객체를 직접 생성해서 넣어주기만 하면 되므로 코드 변경 없이 기능을 수행할 수 있게 된다.

설정을 통한 DI

흔히 우리가 setter라고 알고 있는 것이다. 하지만 단순히 하나의 파라미터만을 받는 setter 뿐만 아니라 필요에 따라 여러개의 입력을 받아 설정하는 setter도 만들 수 있다.

생성자를 통한 DI는 생성됨과 동시에 필요한 객체들을 모두 주입받기 때문에 오류 위험성이 적다는 장점이 있는 반면에 설정을 통한 DI는 나중에 생성된 객체도 쉽게 주입해줄 수 있고 주입해야 하는 객체가 많은 경우 가독성이 좋게 확인 가능하다는 장점이 있다.

스프링에서의 DI 활용

스프링 프레임워크에서 유독 이 DI 활용이 잘된다는 강점이 있는데 스프링에서 DI를 활용하는 방법은 크게 xml과 configuration 활용이다. 요즘은 후자를 많이 쓰고 있는 추세이다.

  • xml: 생성자, 설정, 조립을 하는 코드가 외부 코드로 나와있어 실제로 자바 코드를 컴파일하고 배포하는 과정 없이 xml 파일만 간단하게 조작하여 객체 의존 여부를 변경할 수 있어 가볍다.
  • configuration : xml은 오타에 취약한데 반면 configuration은 자바 파일이라 오류를 쉽게 찾을 수 있다. 하지만 수정 사항이 있는 경우 컴파일부터 배포까지 다시 해주어야 한다.
@Configuration
public class TranscoderConfig{
	@Bean
	public JobQueue fileJobQueue(){
		return new FileJobQueue();
	}

....
}

0개의 댓글