Dependancy Injectoin(DI)

Dependancy Injectoin(DI)

  • 의존성 주입(Dependancy Injection)이란 객체가 객체 스스로의 의존성 객체를 직접 생성하지 않고 이러한 의존성 객체들을 클래스 프로퍼티(필드)에 선언하는 소프트웨어 기술이다.
  • 클래스에 선언된 종속성 객체들은 외부로부터 (스프링 프레임워크) 주입된다

DI의 장점

  • 코드 재사용성 및 유지보수 측면에서 효율적
  • 코드 가독성의 증가
  • 테스트 하기 용이하다
  • 결합도를 낮추고 응집도를 증가시킨다

의존성 주입 예시 - EmployeesSalariesReportService

  1. None Dependancy Injection

    class EmployeesSalariesReportService {
        void generateReport() {
            EmployeeDao employeeDao = new EmployeeDao();
            List<Employee> employees = employeeDao.findAll();
    
            EmployeeSalaryCalculator employeeSalaryCalculator = new EmployeeSalaryCalculator();
            List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);
    
            PdfSalaryReport pdfSalaryReport = new PdfSalaryReport();
            pdfSalaryReport.writeReport(employeeSalaries);
        }
    }
    
    public class Runner {
        public static void main(String... args) {
            EmployeesSalariesReportService employeesSalariesReportService = new EmployeesSalariesReportService();
    
            employeesSalariesReportService.generateReport();
        }
    }
    • 서비스 구현체에서 필요한 모든 인스턴스를 직접 생성한다
  2. Manual Dependancy Injection

    class EmployeesSalariesReportService {
        private final EmployeeDao employeeDao;
        private final EmployeeSalaryCalculator employeeSalaryCalculator;
        private final SalaryReport salaryReport;
    
        EmployeesSalariesReportService(EmployeeDao employeeDao, EmployeeSalaryCalculator employeeSalaryCalculator, SalaryReport salaryReport) {
            this.employeeDao = employeeDao;
            this.employeeSalaryCalculator = employeeSalaryCalculator;
            this.salaryReport = salaryReport;
        }
    
        void generateReport() {
            List<Employee> employees = employeeDao.findAll();
            List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);
    
            salaryReport.writeReport(employeeSalaries);
        }
    }
    
    public class Runner {
        public static void main(String... args) {
            EmployeesSalariesReportService employeesSalariesReportService = new EmployeesSalariesReportService(
                    new EmployeeDao(),
                    new EmployeeSalaryCalculator(),
                    new PdfSalaryReport()
            );
    
            employeesSalariesReportService.generateReport();
        }
    }
    • 서비스 구현체에서 필요한 모든 인스턴스를 선언하고 생성자를 통해 주입받도록한다
    • 프로그램을 실행하기 위한 메인 메서드에서 프로그래머가 직접 의존성 객체들을 생성하고 주입한다
  3. Dependancy Injection By Spring (Application Context)

    @ComponentScan
    public class Configuration {}
    
    @Service
    class EmployeesSalariesReportService {
        private final EmployeeDao employeeDao;
        private final EmployeeSalaryCalculator employeeSalaryCalculator;
        private final SalaryReport salaryReport;
    
        EmployeesSalariesReportService(EmployeeDao employeeDao, EmployeeSalaryCalculator employeeSalaryCalculator, SalaryReport salaryReport) {
            this.employeeDao = employeeDao;
            this.employeeSalaryCalculator = employeeSalaryCalculator;
            this.salaryReport = salaryReport;
        }
    
        void generateReport() {
            List<Employee> employees = employeeDao.findAll();
            List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);
    
            salaryReport.writeReport(employeeSalaries);
        }
    }
    
    public class Runner {
        public static void main(String... args) {
            AnnotationConfigApplicationContext context = getSpringContext("pdf-reports");
    
            EmployeesSalariesReportService employeesSalariesReportService = context.getBean(EmployeesSalariesReportService.class);
            employeesSalariesReportService.generateReport();
    
            context.close();
        }
    
        private static AnnotationConfigApplicationContext getSpringContext(String profile) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            context.getEnvironment().setActiveProfiles(profile);
            context.register(Configuration.class);
            context.refresh();
            return context;
        }
    }
    • 어노테이션(@Service )을 통해 서비스 구현체를 컴포넌트로 등록한다
    • 서비스 구현체에서 필요한 모든 인스턴스를 선언하고 생성자를 통해 주입받도록한다
    • 스프링 컨텍스트를 생성
      • Configuration을 읽어 컴포넌트 스캔하도록 한다
      • Profile을 설정하여 동일한 인터페이스로 구현된 객체들을 구분한다
    • 컴포넌트 스캔을 통해 클래스패스 내에 등록된 모든 컴포넌트를 빈으로 생성되고 의존관계에 따라 자동으로 주입된다.

의존성 주입 방식

  • @Autowired 어노테이션을 사용하여 의존성을 주입하며 어노테이션 선언 위치에 따라 세가지 방식으로 의존성 주입이 가능하다
    1. 필드 주입
    2. 생성자 주입
      • 스프링 프로젝트 팀에서 사용 권장
      • 컴포넌트(빈) 으로 선언되고 생성자가 하나라면 @Autowired 어노테이션 생략 가능
    3. 수정자 주입
  • @Autowired(required = false) 옵션을 통해 의존성 객체가 존재 하지 않는것을 허용 가능(nullable dependancy)

생성자 주입이 권장되는 이유

  1. 필드 주입의 문제점
    • 필드 주입시 외부에서 수정이 불가 ⇒ 테스트 시 의존성 객체를 수정할 수 없다

      테스트 시 문제점
      유닛 테스트 환경에서는 스프링 애플리케이션 컨택스트를 직접적으로 생성하지 않을 수 있고 이러한 경우에는 의존성 객체가 주입되지 않아 NullPointerException 런타임 에러가 발생하게 된다.

    • 필드주입의 경우에는 의존성이 숨겨지기 때문에 해당 객체를 로드 할 시점이 되어서야 순환 참조 에러(런타임 에러)가 발생할 수 있다
  2. 수정자 주입의 단점
    • Setter의 경우 public으로 구현하기 때문에, 관계를 주입받는 객체의 변경 가능성이 열려 있다(muttable 속성을 보장할 수 없음) ⇒ 다른 곳에서 임의로 객체를 변경할 수 있기 때문에 에러가 발생할 위험이 높음
  3. 객체의 불변성 확보 (final 키워드 사용)
    • 객체 생성 시 1회만 호출된다는 게 보장된다. 즉 의존성 객체가 불변객체임을 보장한다
    • 의존성 객체가 final 로 선언되었기 때문에 의존성 객체가 반드시 주입 되었음을 보장한다
      • 컴파일 시점에 개발자의 실수로 인한 에러를 발견할 수 있다
      • 같은 이유로 순환참조 관계에 있을 경우 컴파일 시점에 예외가 발생한다

자바 & 스프링에서 인터페이스 기반 설계의 장점

  • 테스트가 용이하다
  • JDK 다이나믹 프록시 사용을 가능하도록 한다
  • 빈의 변경이 용이하다(느슨한 결합, 여러 방식의 구현이가능, ⇒ 확장에 열려있는 설계가 가능)

Interface Based Bean Injection & Dynamic Wirering

인터페이스 빈

  • 인터페이스 기반으로 의존성을 주입할 경우 IOC(Application-Context)에서 해당 인터페이스의 구현체를 자동으로 주입한다
  • 만약 해당 인터페이스를 구현한객체가 없거나 해당 인터페이스를 구현한 객체가 2개 이상이고 어떤 구현체가 선택되어야 할지에 대한 명시가 없으면 컴파일 시점에 예외가 발생한다
  • 스프링 프레임워크는 @Profile, @Qualifier 어노테이션을 지원함으로써 인터페이스로 선언된 의존성의 경우 런타임 시점에 동적으로 인터페이스 구현체를 주입하도록 하여 확장에 열려있는 소프트웨어 설계가 가능하도록 한다
  • **@Profile : 구현체가 특정 프로파일(실행환경) 에서만 생성되도록 설정할 수 있다**
    • 인터페이스만을 지원하기 위해 사용하는 것은 아니며 프로파일 설정에 따라 다른 빈들을 매핑할 수 있도록 하는 어노테이션

    • 애플리케이션 컨텍스트에서 하나 이상의 프로파일이 지정하고 구현체에 @Profile 어노테이션을 통해 프로파일을 명시하면 특정 프로파일마다 매핑될 구현체를 선택할 수 있다.

      
      @Component
      @Profile("pdf-reports")
      public class PdfSalaryReport implements SalaryReport {
          public void writeReport(List<EmployeeSalary> employeeSalaries) {
              System.out.println("Writing Pdf Report");
          }
      }
  • @Qualifier
    • 스프링 컨테이너가 하나의 인터페이스로 구현된 여러개의 빈을 찾았을 때, 추가적으로 어떤 구현체가 선택되어야 할지에 대한 정보를 명시한다
    • 구현체에는 이름을 할당한다 e.g) @Componet("find-name")
    • 의존성을 주입하는 코드(**@Autowired**)와 함께 @Qualifier("찾는이름")을 작성해야 한다

0개의 댓글