본문 바로가기
스프링

스프링 - 디자인 패턴 - 의존성 주입 #2

by deep-dev 2020. 9. 24.

자바 의존성 주입 패턴

개요

의존성 주입을 사용하면 하드 코딩 된 의존성을 제거하여 애플리케이션을 느슨하게 결합하고 확장 및 유지 관리할 수 있습니다.

컴파일 타임에서 런타임으로 의존성 해결을 이동하기 위해 자바에서 의존성 주입을 구현할 수 있습니다.

스프링 프레임워크는 의존성 주입 원칙으로 구현되었습니다.

자바 의존성 주입

자바 의존성 주입은 이론으로 이해하기 어렵기 때문에, 간단한 예를 들어 본 다음 의존성 주입 패턴을 사용하여 애플리케이션에서 느슨한 결합 및 확장성을 달성하는 방법을 살펴 보겠습니다.

이메일을 보내기 위해 EmailService 를 사용하는 애플리케이션이 있다고 가정해 보겠습니다.

일반적으로 아래와 같이 구현합니다.

package com.designpattern.di.legacy;

public class EmailService {

    public void sendEmail(String message, String receiver){

        System.out.println("Email sent to " + receiver + " with Message = " + message);
    }
}

EmailService 클래스에는 수신자 이메일 주소로 이메일 메시지를 보내는 로직이 있습니다.

우리의 애플리케이션 코드는 다음과 같습니다.

package com.designpattern.di.legacy;

public class MyApplication {

    private EmailService email = new EmailService();

    public void processMessages(String msg, String rec){

        this.email.sendEmail(msg, rec);
    }
}

클라이언트 코드는 MyApplication 클래스를 사용하여 아래와 같이 이메일 메시지를 보냅니다.

package com.designpattern.di.legacy;

public class MainLegacy {

    public static void main(String[] args) {

        MyApplication app = new MyApplication();
        app.processMessages("Hi Gildong", "Gildong@gmail.com");
    }
}

처음에는, 위의 구현에 아무런 문제가 없는 것 같습니다.

그러나 위의 코드는 특정 제한이 있습니다.

MyApplication 클래스는 이메일 서비스를 초기화 한 후 사용합니다.

이로 인해 하드 코딩 된 의존성이 발생합니다.

나중에 다른 고급 이메일 서비스로 전환하려면, MyApplication 클래스에서 코드를 변경해야 합니다.

이것은 애플리케이션을 확장하기 어렵게 만들고 이메일 서비스가 여러 클래스에서 사용되는 경우 더 어려울 것입니다.

SMS 또는 Facebook 메시지와 같은 추가 메시징 기능을 제공하도록 애플리케이션을 확장하려면 이를 위한 다른 애플리케이션을 작성해야 합니다.

여기에는 애플리케이션 클래스와 클라이언트 클래스의 코드 변경도 포함합니다.

애플리케이션이 이메일 서비스 인스턴스를 직접 생성하기 때문에 테스트가 매우 어려울 것입니다.

테스트 클래스에서 이러한 객체를 모의할 수 있는 방법이 없습니다.

이메일 서비스가 필요한 생성자에 인수로 전달하여 MyApplication 클래스에서 이메일 서비스 인스턴스 생성을 제거 할 수도 있습니다.

package com.designpattern.di.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);
    }
}
package com.designpattern.di.legacy;

public class MainLegacy {

    public static void main(String[] args) {

        MyApplication app = new MyApplication(new EmailService());
        app.processMessages("Hi Gildong", "Gildong@gmail.com");
    }
}

그러나 이 경우, 이메일 서비스를 초기화 하도록 클라이언트 애플리케이션 또는 테스트 클래스에 요청하고 있으며 좋은 설계 결정이 아닙니다.

이제 위 구현의 모든 문제를 해결하기 위해 자바 의존성 주입 패턴을 적용하는 방법을 살펴 보겠습니다.

자바의 의존성 주입에는 최소한 다음이 필요합니다:

서비스 컴포넌트는 기본 클래스 또는 인터페이스로 설계 되어야 합니다.

서비스 업무을 정의하는 인터페이스나 추상 클래스가 좋습니다.

사용자 클래스는 서비스 인터페이스 측면에서 작성 되어야 합니다.

주입자 클래스는 서비스를 초기화 한 다음 사용자 클래스를 초기화 합니다.

자바 의존성 주입 - 서비스 컴포넌트

서비스 구현을 위한 업무를 선언하는 MessageService 가 있습니다.

package com.designpattern.di.service;

public interface MessageService {

    void sendMessage(String msg, String rec);
}

이제 위의 인터페이스를 구현하는 이메일 및 SMS 서비스가 있다고 가정해 보겠습니다.

package com.designpattern.di.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.designpattern.di.service;

public class SMSServiceImpl implements MessageService {

    @Override
    public void sendMessage(String msg, String rec) {

        System.out.println("SMS sent to " + rec + " with Message = " + msg);
    }
}

의존성 주입 자바 서비스를 준비하였으며 이제 사용자 클래스를 작성합니다.

자바 의존성 주입 - 서비스 사용자

사용자 클래스에 대한 기본 인터페이스는 필요하지 않지만 사용자 클래스의 업무를 선언하는 Consumer 인터페이스가 있습니다.

package com.designpattern.di.consumer;

public interface Consumer {

    void processMessage(String msg, String rec);
}

사용자 클래스 구현은 아래와 같습니다.

package com.designpattern.di.consumer;

import com.designpattern.di.service.MessageService;

public class MyDIApplication implements Consumer {

    private MessageService service;

    public MyDIApplication(MessageService svc) {
        this.service = svc;
    }

    @Override
    public void processMessage(String msg, String rec) {

        this.service.sendMessage(msg, rec);
    }
}

애플리케이션 클래스는 단지 서비스를 사용하고 있습니다.

서비스를 초기화 하지 않으므로 더 좋은 "관심사의 분리" 를 달성합니다.

또한 서비스 인터페이스를 사용하면 MessageService 를 모의하여 애플리케이션을 쉽게 테스트하고 컴파일 시간이 아닌 런타임에 서비스를 바인딩 할 수 있습니다.

이제 우리는 의존성 주입자 클래스를 작성하여 서비스와 사용자 클래스를 초기화 할 준비가 되었습니다.

자바 의존성 주입 - 주입자 클래스

Consumer 클래스를 반환하는 메서드를 가진 MessageServiceInjector 인터페이스를 만들어 보겠습니다.

package com.designpattern.di.injector;

import com.designpattern.di.consumer.Consumer;

public interface MessageServiceInjector {

    public Consumer getConsumer();
}

이제 모든 서비스에 대해 아래와 같은 주입자 클래스를 만들어야 합니다.

package com.designpattern.di.injector;

import com.designpattern.di.consumer.Consumer;
import com.designpattern.di.consumer.MyDIApplication;
import com.designpattern.di.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

    @Override
    public Consumer getConsumer() {
        return new MyDIApplication(new EmailServiceImpl());
    }
}
package com.designpattern.di.injector;

import com.designpattern.di.consumer.Consumer;
import com.designpattern.di.consumer.MyDIApplication;
import com.designpattern.di.service.SMSServiceImpl;

public class SMSServiceInjector implements MessageServiceInjector {

    @Override
    public Consumer getConsumer() {
        return new MyDIApplication(new SMSServiceImpl());
    }
}

이제 클라이언트 애플리케이션이 이것을 사용하는 방법을 살펴 보겠습니다.

package com.designpattern.di;

import com.designpattern.di.consumer.Consumer;
import com.designpattern.di.injector.EmailServiceInjector;
import com.designpattern.di.injector.MessageServiceInjector;
import com.designpattern.di.injector.SMSServiceInjector;

public class MainMessageDI {

    public static void main(String[] args) {
        String msg = "Hi Gildong";
        String email = "Gildong@gmail.com";
        String phone = "12345678";
        MessageServiceInjector injector = null;
        Consumer app = null;

        //Send email
        injector = new EmailServiceInjector();
        app = injector.getConsumer();
        app.processMessages(msg, email);

        //Send SMS
        injector = new SMSServiceInjector();
        app = injector.getConsumer();
        app.processMessages(msg, phone);
    }
}

보시다시피 애플리케이션 클래스는 서비스 사용에만 책임이 있습니다.

서비스 클래스는 주입자에서 생성합니다.

또한 페이스북 메시지를 보내도록 애플리케이션을 확장해야 한다면, 서비스 클래스와 주입자 클래스만 작성하면 됩니다.

따라서 의존성 주입 구현은 하드 코딩 된 의존성 문제를 해결하고 애플리케이션을 유연하고 확장하기 쉽게 만드는 데 도움이 되었습니다.

이제 주입자 및 서비스 클래스를 모의하여 애플리케이션 클래스를 얼마나 쉽게 테스트 할 수 있는지 살펴 보겠습니다.

자바 의존성 주입 - JUnit 테스트 사례 (모의 주입자 및 서비스 사용)

package com.designpattern.di.test;

import com.designpattern.di.consumer.Consumer;
import com.designpattern.di.consumer.MyDIApplication;
import com.designpattern.di.injector.MessageServiceInjector;
import com.designpattern.di.service.MessageService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class MyDIApplicationJUnitTest {

    private MessageServiceInjector injector;
    @Before
    public void setUp(){
        //mock the injector with anonymous class
        injector = new MessageServiceInjector() {

            @Override
            public Consumer getConsumer() {
                //mock the message service
                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 Gildong", "Gildong@gmail.com");
    }

    @After
    public void tear(){
        injector = null;
    }
}

보시다시피 익명 클래스를 사용하여 주입자 및 서비스 클래스를 모의하고 애플리케이션 메서드를 쉽게 테스트 할 수 있습니다.

위 테스트 클래스에 JUnit 4 를 사용하고 있으므로, 위 테스트 실행 시 프로젝트 빌드 경로에 있는지 확인하세요.

생성자를 사용하여 애플리케이션 클래스에 의존성을 주입했습니다.

또 다른 방법은 setter 메서드로 애플리케이션 클래스에 의존성을 주입하는 것입니다.

setter 메서드 의존성 주입의 경우 애플리케이션 클래스는 다음과 같이 구현합니다.

package com.designpattern.di.consumer;

import com.designpattern.di.service.MessageService;

public class MyDIApplication implements Consumer {

    private MessageService service;

    public MyDIApplication() { }

    public void setService(MessageService svc) {
        this.service = svc;
    }

    @Override
    public void processMessages(String msg, String rec) {

        this.service.sendMessage(msg, rec);
    }
}
package com.designpattern.di.injector;

import com.designpattern.di.consumer.Consumer;
import com.designpattern.di.consumer.MyDIApplication;
import com.designpattern.di.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 기반을 사용할지 여부는 설계 결정이며 요구 사항에 따라 다릅니다.

예를 들어, 애플리케이션이 서비스 클래스 없이 전혀 작동하지 않는 경우 생성자 기반 의존성 주입을 사용하고, 실제로 필요할 때만 사용하기 위해 setter 메서드 기반 의존성 주입을 사용합니다.

자바의 의존성 주입은 컴파일 타임에서 런타임으로 객체 바인딩을 이동하여 애플리케이션에서 제어의 역전 ( IoC : Inversion of Control ) 을 달성하는 방법입니다.

팩토리 패턴, 템플릿 메서드 패턴, 전략 패턴 그리고 서비스 로케이터 패턴을 통해 IoC 를 달성 할 수 있습니다.

스프링 의존성 주입, 구글 Guice 및 Java EE CDI 프레임워크는 Java Reflection API 및 자바 주석을 사용하여 의존성 주입 프로세스를 쉽게 합니다.

필요한 것은 필드, 생성자 또는 setter 메서드에 주석을 달고 구성 xml 파일이나 클래스에서 구성하는 것입니다.

자바 의존성 주입의 이점

자바 의존성 주입을 사용하면 다음과 같은 이점이 있습니다.

관심사를 분리합니다.

의존성을 초기화하는 모든 작업을 주입자 컴포넌트가 처리하므로 애플리케이션 클래스의 표준 코드가 감소합니다.

구성 가능한 컴포넌트로 애플리케이션을 쉽게 확장합니다.

모의 객체로 단위 테스트가 쉽습니다.

자바 의존성 주입의 단점

자바 의존성 주입에는 몇 가지 단점도 있습니다.

과도하게 사용 시 런타임에 변경 효과를 알 수 있기 때문에 유지 관리 문제가 발생 할 수 있습니다.

자바의 의존성 주입은 서비스 클래스의 의존성을 가려서 컴파일 타임에 포착 가능한 런타임 오류를 유발합니다.

마무리

자바의 의존성 주입 패턴이었습니다.

서비스를 제어할 때 알고 사용하는 것이 좋습니다.

댓글