자바 의존성 주입 디자인 패턴은 우리에게 하드 코딩된 종속성을 제거하고 응용 프로그램을 느슨하게 결합되고 확장 가능하며 유지 보수 가능하게 만들어줍니다. 우리는 자바에서 의존성 주입을 구현하여 컴파일 시간에서 의존성 해결을 런타임으로 이동시킬 수 있습니다.
자바 의존성 주입
자바 의존성 주입은 이론적으로 이해하기 어려워 보일 수 있으므로 간단한 예제를 살펴보고 애플리케이션에서 느슨한 결합과 확장 가능성을 달성하기 위해 의존성 주입 패턴을 어떻게 사용할지 살펴보겠습니다. 예를 들어 우리가 이메일을 보내기 위해 EmailService
를 사용하는 애플리케이션이 있다고 가정해 봅시다. 보통은 아래와 같이 이를 구현할 것입니다.
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver){
//이메일 보내기 로직
System.out.println("Email sent to "+receiver+ " with Message="+message);
}
}
EmailService
클래스는 수신자 이메일 주소로 이메일 메시지를 보내는 로직을 포함하고 있습니다. 애플리케이션 코드는 아래와 같을 것입니다.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec){
//일부 메시지 유효성 검사, 조작 로직 등 수행
this.email.sendEmail(msg, rec);
}
}
MyApplication
클래스를 사용하여 이메일 메시지를 보내는 클라이언트 코드는 아래와 같을 것입니다.
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
}
MyApplication
클래스는 이메일 서비스를 초기화하고 그것을 사용하는 것을 담당합니다. 이로 인해 하드 코딩된 의존성이 발생합니다. 앞으로 다른 고급 이메일 서비스로 전환하려면 MyApplication 클래스의 코드를 변경해야 합니다. 이로 인해 우리의 애플리케이션을 확장하기가 어려워지며 이메일 서비스가 여러 클래스에서 사용된다면 더 어려워집니다.- 만약 애플리케이션을 추가적인 메시징 기능인 SMS 또는 Facebook 메시지를 제공하도록 확장하려면 다른 애플리케이션을 작성해야 합니다. 이는 애플리케이션 클래스와 클라이언트 클래스의 코드 변경을 필요로 합니다.
- 애플리케이션을 테스트하는 것은 매우 어려울 것입니다. 왜냐하면 우리의 애플리케이션은 직접적으로 이메일 서비스 인스턴스를 생성하기 때문입니다. 우리의 테스트 클래스에서는 이러한 객체들을 목 객체로 만들 방법이 없습니다.
누군가는 MyApplication
클래스에서 이메일 서비스 인스턴스 생성을 제거하여 생성자를 통해 이메일 서비스를 인수로 요구함으로써 이야기할 수 있습니다.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = null;
public MyApplication(EmailService svc){
this.email=svc;
}
public void processMessages(String msg, String rec){
//일부 메시지 유효성 검사, 조작 로직 등을 수행합니다
this.email.sendEmail(msg, rec);
}
}
그러나 이 경우에는 클라이언트 애플리케이션 또는 테스트 클래스가 좋지 않은 디자인 결정인 이메일 서비스를 초기화하도록 요청하고 있습니다. 이제 위의 구현에 대한 모든 문제를 해결하기 위해 자바 의존성 주입 패턴을 어떻게 적용할 수 있는지 살펴보겠습니다. 자바 의존성 주입에는 최소한 다음이 필요합니다:
- 서비스 구성 요소는 기본 클래스 또는 인터페이스로 설계되어야 합니다. 서비스에 대한 계약을 정의하는 인터페이스나 추상 클래스를 사용하는 것이 좋습니다.
- 소비자 클래스는 서비스 인터페이스를 기반으로 작성되어야 합니다.
- Injector 클래스는 서비스를 초기화한 다음 소비자 클래스를 초기화합니다.
Java 의존성 주입 – 서비스 구성 요소
우리의 경우, MessageService
를 가질 수 있습니다. 이는 서비스 구현을 위한 계약을 선언합니다.
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
이제 위의 인터페이스를 구현하는 이메일 및 SMS 서비스가 있다고 가정해 봅시다.
package com.journaldev.java.dependencyinjection.service;
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
//이메일 보내는 논리
System.out.println("Email sent to "+rec+ " with Message="+msg);
}
}
package com.journaldev.java.dependencyinjection.service;
public class SMSServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
//SMS 보내는 논리
System.out.println("SMS sent to "+rec+ " with Message="+msg);
}
}
의존성 주입 자바 서비스가 준비되었으므로 이제 소비자 클래스를 작성할 수 있습니다.
Java 의존성 주입 – 서비스 소비자
소비자 클래스에 대한 기본 인터페이스가 필요하지는 않지만 소비자 클래스에 대한 계약을 선언하는 Consumer
인터페이스가 있습니다.
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
소비자 클래스 구현은 아래와 같습니다.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(MessageService svc){
this.service=svc;
}
@Override
public void processMessages(String msg, String rec){
//일부 메시지 유효성 검사, 조작 논리 등
this.service.sendMessage(msg, rec);
}
}
저희 응용 프로그램 클래스는 서비스를 사용할 뿐입니다. 서비스를 초기화하지 않으므로 “역할 분리”가 더욱 용이해집니다. 또한 서비스 인터페이스의 사용은 MessageService를 mocking하고 서비스를 컴파일 시간이 아닌 런타임에 바인딩하여 응용 프로그램을 쉽게 테스트할 수 있게 합니다. 이제 우리는 서비스와 소비자 클래스를 초기화할
자바 종속성 주입 – 인젝터 클래스
를 작성할 준비가 되었습니다. MessageServiceInjector
라는 메서드 선언이 포함된 인터페이스를 만들어봅시다.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
이제 모든 서비스에 대해 아래와 같이 인젝터 클래스를 생성해야 합니다.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new EmailServiceImpl());
}
}
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.SMSServiceImpl;
public class SMSServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new SMSServiceImpl());
}
}
이제 간단한 프로그램으로 응용 프로그램을 사용하는 클라이언트 애플리케이션을 살펴보겠습니다.
package com.journaldev.java.dependencyinjection.test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.injector.EmailServiceInjector;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.injector.SMSServiceInjector;
public class MyMessageDITest {
public static void main(String[] args) {
String msg = "Hi Pankaj";
String email = "[email protected]";
String phone = "4088888888";
MessageServiceInjector injector = null;
Consumer app = null;
//이메일 보내기
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//SMS 보내기
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
응용 프로그램 클래스는 서비스 사용에만 책임이 있음을 알 수 있습니다. 서비스 클래스는 인젝터에서 생성됩니다. 또한 페이스북 메시징을 허용하기 위해 응용 프로그램을 확장해야 하는 경우, 서비스 클래스와 인젝터 클래스만 작성하면 됩니다. 따라서 의존성 주입 구현은 하드코딩된 종속성 문제를 해결하고, 응용 프로그램을 유연하고 확장하기 쉽게 만들어줍니다. 이제 인젝터와 서비스 클래스를 mocking하여 응용 프로그램 클래스를 쉽게 테스트할 수 있는지 살펴보겠습니다.
Java 의존성 주입 – 익명 클래스와 모의 인젝터 및 서비스를 사용한 JUnit 테스트 케이스
package com.journaldev.java.dependencyinjection.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplicationJUnitTest {
private MessageServiceInjector injector;
@Before
public void setUp(){
// 익명 클래스로 인젝터를 모의화합니다
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
// 메시지 서비스를 모의화합니다
return new MyDIApplication(new MessageService() {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Mock Message Service implementation");
}
});
}
};
}
@Test
public void test() {
Consumer consumer = injector.getConsumer();
consumer.processMessages("Hi Pankaj", "[email protected]");
}
@After
public void tear(){
injector = null;
}
}
위에서 보듯이, 익명 클래스를 사용하여 인젝터와 서비스 클래스를 모의화하고 애플리케이션 메서드를 쉽게 테스트할 수 있습니다. 위의 테스트 클래스에서 JUnit 4를 사용하므로 프로젝트 빌드 경로에 포함되어 있는지 확인하십시오. 응용 프로그램 클래스에서 의존성을 주입하기 위해 생성자를 사용했습니다. 또 다른 방법으로는 응용 프로그램 클래스에서 의존성을 주입하기 위해 setter 메서드를 사용하는 것입니다. setter 메서드 의존성 주입을 위해 아래와 같이 응용 프로그램 클래스를 구현합니다.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(){}
//setter 의존성 주입
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec){
//메시지 유효성 검사, 조작 로직 등 수행
this.service.sendMessage(msg, rec);
}
}
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
MyDIApplication app = new MyDIApplication();
app.setService(new EmailServiceImpl());
return app;
}
}
하나의 최고의 setter 의존성 주입 예는 Struts2 Servlet API Aware 인터페이스입니다. 생성자를 기반으로 하는 의존성 주입을 사용할지, setter를 기반으로 하는지는 설계 결정이며 귀하의 요구 사항에 따라 달라집니다. 예를 들어, 서비스 클래스 없이 애플리케이션이 전혀 작동하지 않는 경우 생성자 기반 DI를 선호할 것이고, 그렇지 않으면 실제로 필요할 때만 사용하려고 setter 기반 DI를 선택할 것입니다. 자바 의존성 주입은 컴파일 시간에서 객체 바인딩을 런타임으로 이동하여 애플리케이션에서 제어의 역전 (IoC)를 달성하는 방법입니다. IoC는 팩토리 패턴, 템플릿 메소드 디자인 패턴, 전략 패턴 및 서비스 로케이터 패턴을 통해 달성할 수 있습니다. 스프링 의존성 주입, Google Guice 및 Java EE CDI 프레임워크는 Java Reflection API와 자바 어노테이션의 사용을 통해 의존성 주입 프로세스를 용이하게 합니다. 필드, 생성자 또는 setter 메소드를 주석으로 표시하고 구성 xml 파일이나 클래스에서 구성만 해주면 됩니다.
Java 의존성 주입의 이점
Java에서 의존성 주입을 사용하는 장점 중 일부는 다음과 같습니다:
- 관심사의 분리
- 어플리케이션 클래스에서의 보일러플레이트 코드 감소, 모든 종속성을 초기화하는 작업이 주입기 구성 요소에 의해 처리되기 때문입니다
- 구성 가능한 구성 요소는 어플리케이션을 쉽게 확장 가능하게 만듭니다
- 모의 객체를 사용하여 단위 테스트가 쉽습니다
Java 의존성 주입의 단점
Java 의존성 주입에는 몇 가지 단점이 있습니다:
- 과도하게 사용하면 변경 사항의 효과가 실행 시에 알려져 유지 관리 문제로 이어질 수 있습니다.
- Java에서 의존성 주입은 컴파일 시간에 감지될 수 있었던 런타임 오류를 발생시킬 수 있는 서비스 클래스 의존성을 숨깁니다.
이것으로 Java의 의존성 주입 패턴에 대한 설명이 끝났습니다. 서비스를 제어할 때 알고 사용하는 것이 좋습니다.