ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] 스파르타) Project3 마무리 (Dependency Injection)
    TIL 2024. 5. 7. 21:19

     

     > 프로젝트를 진행하면서 발생했던 문제점과 관련있는 부분들을 간단하게 알아봤다.

     

    학습 키워드: Java, Javascript, dependency injection, DI, assembler

     

    1. Dependency Injection

    1) What is it?:

     - Dependency Injection (DI, 의존성 주입)은 프로그래밍에 널리 쓰이는 기법으로, 직접적인 class 사용을 피하고 interface 등의 사용으로 여러 버전의 클래스(혹은 함수)를 유동적으로 바꿔가며 쓸 수 있도록 하여 코드의 재사용성을 챙기고, 그렇게 함으로써 refactoring 및 testing을 용이하게 하여 유지/보수까지 신경쓰는 디자인 패턴의 한 종류다.

     

    2) How does it work?:

     - Wikipedia의 DI의 구현 예시들을 간단하게 살펴보자.

    public class Client {
        private Service service;
    
        Client() {
            // The dependency is hard-coded.
            this.service = new ExampleService();
        }
    }

     - 위는 DI가 적용되지 않은 방식의 코드다. Client의 constructor에 ExampleService() 클래스의 생성자가 hard-code 되어있다. 이런 식의 구현은 후에 service의 종류를 다른 class로 교체하거나, 테스트를 할 시 코드를 일일이 수정해줘야 하는 불편함이 생긴다.

     

    public class Client {
        private Service service;
    
        // The dependency is injected through a constructor.
        Client(Service service) {
            if (service == null) {
                throw new IllegalArgumentException("service must not be null");
            }
            this.service = service;
        }
    }

     - 처음 코드와 같은 hard-coding을 피하는 가장 보편적인 방법으로는 constructor injection이 있다. Service 객체를 파라미터로 받아 처리하면, service에 변동사항이 생기더라도 Client 클래스를 반복 수정할 일이 없어진다.

     

    public class Client {
        private Service service;
    
        // The dependency is injected through a setter method.
        public void setService(Service service) {
            if (service == null) {
                throw new IllegalArgumentException("service must not be null");
            }
            this.service = service;
        }
    }

     - 다음은 Setter Injection 방식이다. 단순히 constructor만을 이용하는 방식에서 벗어나 setter method를 이용하여 Service를 설정할 수 있게 됨으로써 언제든지 새로운 Service로 변경할 수 있게 되어 좀 더 유동적으로 사용이 가능하지만, 그렇기 때문에 Client가 사용하기 전에 Service의 validity 및 availiablity를 보다 섬세하게 신경써야 한다는 사소한 번거로움이 생긴다.

     

    public interface ServiceSetter {
        void setService(Service service);
    }
    
    public class Client implements ServiceSetter {
        private Service service;
    
        @Override
        public void setService(Service service) {
            if (service == null) {
                throw new IllegalArgumentException("service must not be null");
            }
            this.service = service;
        }
    }
    
    public class ServiceInjector {
    	private final Set<ServiceSetter> clients = new HashSet<>();
    
    	public void inject(ServiceSetter client) {
    		this.clients.add(client);
    		client.setService(new ExampleService());
    	}
    
    	public void switch() {
    		for (Client client : this.clients) {
    			client.setService(new AnotherExampleService());
    		}
    	}
    }
    
    public class ExampleService implements Service {}
    
    public class AnotherExampleService implements Service {}

     - Interface Injection 방식은 Client가 interface를 상속받아 해당 interface의 setService method를 override하는 식으로 작성된다. 그 다음, assembler가 여러 Client들을 인터페이스로 한데 묶어서 관리하며 여러 method들을 통해 client에 의존성을 주입한다. 이 코드에서 assembler는 ServiceInjector 클래스를 말한다.

     

    public class Program {
    
        public static void main(String[] args) {
            // Build the service.
            Service service = new ExampleService();
    
            // Inject the service into the client.
            Client client = new Client(service);
    
            // Use the objects.
            System.out.println(client.greet());
        }	
    }

     - Assembly 방식은 가장 단순한 방식의 DI 구현이다. 보통 프로젝트의 root가 되는, 즉 execution이 시작되는 구간에서 Service와 Client를 생성하고 넘겨주는 작업을 해주는 식으로 구현된다. 이 작업을 해주는 클래스를 따로 만드는 경우 해당 클래스를 Assembler 라고 칭한다.

     

    3) Why use it?:

     - 앞서 말했듯이 DI를 사용하면 크게 세 가지 장점이 생긴다. 다른 코드와의 연결, 즉 의존성이 사라져 코드를 재사용하기가 쉬워지고, 의존성을 주입받기 때문에 주입되는 클래스의 특징에 변화가 생기더라도 주입받는 클래스를 수정할 필요가 없어져 코드의 유지/보수가 간편해진다. 또한 같은 방식으로 테스트용 클래스를 주입할 수도 있어서 테스트 환경 조성에도 유리하다.

     

    2. 이번 프로젝트에서의 DI

    1) Assembler:

     - 이번 프로젝트가 이전 개인과제 (Project 2)에서 확장시킨 팀 과제인데, 개인 과제 진행 당시에 module을 설계하면서 각각의 모듈이 서로를 import하는 상황이 생겼었다. 그래서 이를 해결할 방법으로 다른 하나의 모듈을 더 만들어서 해당 모듈에서 나머지 모듈을 import하여 조립할 수 있도록 설계를 바꿔나갔었다. 팀 과제의 마무리 단계에서 DI를 살펴보다가 이게 가장 기초적인 DI인 Assembler의 한 형태라는 것을 알게 됐다.

     

    2) Passing function as parameter:

     - Assembler 방식으로 script를 채워 나가다가 팀 과제가 좀 진행되자 card.js의 addEventListener 에서 다른 모듈의 함수들을 사용해야하는 상황이 발생했다. Javascript에서는 함수가 parameter로 전달될 수 있다는 것을 배웠기 때문에 card.js에 let으로 mountedFunction 함수(실제로는 다른 이름)를 null로 생성해두고, mountFunction(...func)을 export해서 assembler가 필요한 함수들을 전달할 수 있도록 설계했다.

     

    3) Issue with passing multiple functions:

     - 2) 의 연장선에서 생긴 문제인데, spread operator를 사용해서 여러개의 함수를 전달하는 것 까지는 좋았지만, 만들다보니 parameter 형태가 다른 함수들을 같은곳에 mount하는 방법이 없을지 고민하게 되었다. 예를 들면, 영화 포스터 card를 클릭하면 addEventListener 작업에서 추가한 총 3가지의 함수를 실행해야 하는데, 이 중 하나의 함수의 인자가 다른 경우였다. 처음 두 개의 함수는 영화의 id를 전달받기 때문에 문제가 없었으나, 나머지 한 개의 함수 (div container의 css class를 toggle하는 함수)가 인자로 css class의 이름을 전달받는 구조여서 mountFunctions(...functions)의 형태로 전달받으면 이를 실행하기가 애매해지는 것이다. 결국 또 다른 mount 함수를 작성하여 해결했지만, DI를 구현했다기에는 뭔가 유지/보수가 매우 까다로워 보여서 실패한 시도라고 생각했다. 프로젝트 제출 전까지 더 자세히 알아보고, 해결 방안이 생기면 기록해야겠다.

     

     

    --

     

    REFERENCES:

     

     

    Dependency injection - Wikipedia

    From Wikipedia, the free encyclopedia Software programming technique Dependency injection is often used alongside specialized frameworks, known as 'containers', to facilitate program composition. In software engineering, dependency injection is a programmi

    en.wikipedia.org

     > Wikipedia, Dependency Injection

     

    728x90
Designed by Tistory.