의존성 주입 (Dependency Injection)
소프트웨어 개발에서 코드의 유지보수성과 확장성을 높이기 위해 "의존성 주입(Dependency Injection, DI)"이라는 개념이 자주 사용된다. 특히 Java에서는 DI를 활용하면 객체 간 결합도를 낮추고 코드의 유연성을 극대화할 수 있다. DI를 활용하면 객체 간의 직접적인 의존성을 제거하고, 대신 외부에서 객체를 주입받도록 설계할 수 있다.
이 포스팅에서는 Java에서 의존성 주입을 어떻게 구현하는지, 그리고 DI를 활용했을 때 얻을 수 있는 장점에 대해 알아보겠다.
1. 의존성 문제와 기존 방식의 한계
기존 방식의 문제점
아래 예제는 EmailService 클래스를 활용하는 애플리케이션을 구현한 코드다.
public class EmailService {
public void sendEmail(String message, String receiver) {
System.out.println("Email sent to " + receiver + " with Message=" + message);
}
}
이 EmailService 클래스를 사용하는 MyApplication 클래스를 구현해보자.
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec) {
this.email.sendEmail(msg, rec);
}
}
그리고 클라이언트 코드도 작성해보자.
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "pankaj@abc.com");
}
}
이 코드에서 MyApplication 클래스는 EmailService 인스턴스를 직접 생성하고 있다. 이는 다음과 같은 문제를 초래한다.
- 하드코딩된 의존성 - EmailService에 강하게 결합되어 있어, 다른 메시지 서비스로 변경하려면 코드를 수정해야 한다.
- 유연성 부족 - SMS나 Facebook 메시지 같은 기능을 추가하려면 새로운 클래스를 만들어야 한다.
- 테스트 어려움 - EmailService가 직접 생성되므로, 단위 테스트 시 Mock 객체를 활용하기 어렵다.
이 문제를 해결하기 위해 의존성 주입을 적용해보자.
2. 의존성 주입 적용하기
서비스 인터페이스 정의
먼저, 메시지 전송 기능을 추상화하는 인터페이스를 만들자.
public interface MessageService {
void sendMessage(String msg, String rec);
}
구체적인 서비스 구현
이제 MessageService를 구현하는 EmailServiceImpl과 SMSServiceImpl 클래스를 작성하자.
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Email sent to " + rec + " with Message=" + msg);
}
}
public class SMSServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("SMS sent to " + rec + " with Message=" + msg);
}
}
서비스 소비자 (Consumer)
이제 서비스 인터페이스를 이용하는 MyDIApplication 클래스를 구현하자.
public interface Consumer {
void processMessages(String msg, String rec);
}
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);
}
}
이제 MyDIApplication 클래스는 MessageService 인터페이스에 의존하므로, 특정 구현체(EmailServiceImpl, SMSServiceImpl)에 직접 의존하지 않는다.
인젝터 인터페이스
public interface MessageServiceInjector {
Consumer getConsumer();
}
인젝터 클래스
이제 각 서비스별 인젝터 클래스를 작성하자.
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new EmailServiceImpl());
}
}
public class SMSServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new SMSServiceImpl());
}
}
이제 애플리케이션에서 이메일과 SMS를 전송하는 기능을 실행할 수 있다.
public class MyMessageDITest {
public static void main(String[] args) {
String msg = "Hi Pankaj";
String email = "pankaj@abc.com";
String phone = "4088888888";
MessageServiceInjector injector = new EmailServiceInjector();
Consumer app = injector.getConsumer();
app.processMessages(msg, email);
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
이제 MyDIApplication은 MessageService 인터페이스를 통해 서비스를 이용하며, 실제 서비스 객체는 인젝터에서 주입받는다. 덕분에 새로운 메시지 서비스를 추가할 때 MessageService를 구현한 클래스와 해당하는 인젝터 클래스만 추가하면 된다.
단위 테스트 코드
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
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", "pankaj@abc.com");
}
@After
public void tearDown() {
injector = null;
}
}
이제 JUnit 테스트에서 mock을 활용하여 DI가 적용된 애플리케이션을 쉽게 검증할 수 있다.
3. 의존성 주입의 장점과 단점
장점
- 관심사의 분리: 서비스 객체를 직접 생성하지 않으므로, 애플리케이션 코드가 보다 깔끔해진다.
- 유연성 증가: 새로운 메시지 서비스를 쉽게 추가할 수 있다.
- 테스트 용이성: Mock 객체를 사용하여 단위 테스트가 가능해진다.
단점
- 구현 복잡성 증가: 단순한 애플리케이션이라면 굳이 DI를 사용할 필요가 없다.
- 런타임 오류 가능성: 컴파일 타임에 발견할 수 있는 오류가 런타임까지 지연될 수 있다.
4. 결론
의존성 주입은 애플리케이션의 확장성과 유지보수성을 높이는 강력한 디자인 패턴이다. Java에서 DI를 적용하면 객체 간 결합도를 낮추고, 인터페이스 기반으로 설계할 수 있어 테스트가 용이해진다. Spring Framework와 같은 DI 컨테이너를 사용하면 DI를 더욱 편리하게 적용할 수도 있다.
다만, 모든 프로젝트에 DI가 반드시 필요한 것은 아니다. 필요에 따라 적절한 디자인 패턴을 선택하는 것이 중요하다. 하지만, 복잡한 애플리케이션을 개발할 때 DI를 도입하면 코드의 유연성과 확장성이 크게 향상될 것이다.
'JAVA' 카테고리의 다른 글
자바 자료형 변환 정리 (0) | 2022.12.08 |
---|---|
@AuthenticationPrincipal 사용시 LazyInitializationException (2) | 2022.11.19 |
Java compareTo(), compare()의 차이와 내림차순, NaN값 처리 (0) | 2022.10.27 |