ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체지향 프로그래밍 입문 강의 (5) - 의존과 DI
    et cetera/TIL 2022. 1. 2. 17:59
    반응형

    의존

    기능 구현을 위해 다른 구성 요소를 사용하는 것.

    ex) 객체 생성, 메서드 호출, 데이터 사용

     

    - 의존은 변경이 전파될 가능성을 의미

    - 의존하는 대상이 바뀌면 덩달아 바뀔 가능성이 높아진다.

    ex) 호출하는 메서드의 파라미터가 변경

    호출하는 메서드가 발생할 수 있는 익셉션 타입이 추가

    ⇒ 그 메서드를 사용하는 코드도 덩달아 바뀌게 된다.

     

     

    순환 의존

    순환 의존 → 변경 연쇄 전파 가능성

    A가 B에 의존하고, B가 C에 의존하고, C는 다시 A에 의존하고 있다고 치자.

    이렇게 되면 A의 변경이 C에 영향을 주고, C의 변경은 B에, B의 변경은 다시 A에 영향을 준다.

    ⇒ 클래스, 패키지 모듈 등 모든 수준에서 순환 의존이 없도록 해야한다.

     

    의존하는 대상이 많다면?

    A가 의존하고 있는 대상들(B, C, D, E, F)이 각자의 이유로 바뀌게 되면, 그만큼 A도 바뀔 가능성이 높아진다.

    그래서 의존하는 대상은 적을수록 좋다.

     

     

    의존 대상이 많은 경우

    대표적인 케이스가 바로 한 클래스에서 많은 기능을 제공하는 경우.

    public class UserService {
        public void register(RegReq regReq) {
            // ...
        }
        
        public void changePW(ChangeReq changeReq) {
            // ...
        }
        
        public void blockUser(String id, String reason) {
            // ...
        }
        
        // ...
    }

    - 각 기능마다 의존하는 대상이 다를 수 있다.

    - 한 기능 변경이 다른 기능에 영향을 줄 수 있다.

    - 테스트하기가 더욱 힘들어진다.

    한 기능만 테스트하고 싶어도 나머지 기능들에서 필요한 의존성 대상들까지

    함께 초기화해야 하는 불상사가 발생할 수도 있다.

     

    따라서, 한 클래스가 제공하는 기능이 많으면 이를 기능 별로 분리하는 것을 고려해보는 것이 좋다.

    public class UserRegisterService {
        public void register(RegReq regReq) {
            // ...
        }
    }
    
    public class ChangePwService {
        public void changePw(ChangeReq changeReq) {
            // ...
        }
    }
    
    public class UserBlockService {
        public void blockUser(String id, String reason) {
            // ...
        }
    }

    기능 별로 클래스를 분리하면 클래스의 갯수는 늘어나지만,

    각 클래스마다 필요로 하는 의존이 줄어들게 되고, 

    한 기능을 수정할 때 다른 기능과 관련된 코드를 수정하는 일이 발생하지 않게 된다.

    더불어, 개별 기능을 테스트하는 것도 조금 더 수월해지게 된다.

     

    ※ 의존 대상을 줄이는 또 다른 방법

    추상화를 통해 여러 의존 대상을 단일 기능으로 묶을 수 있는지 검토해보는 것.

     

    ※ 의존 대상 객체를 직접 생성하면?

    - 생성 클래스가 바뀌면 의존하는 코드도 바뀐다. (추상화에서 언급)

    - 의존 대상 객체를 직접 생성하지 않는 방법?

        - Factory, Builder

        - 의존성 주입(Dependency Injection, DI)

        - 서비스 로케이터(Service Locator)

     

     

     의존 주입(Dependency Injection, DI)

    의존하는 대상을 직접 생성하지 않고, 외부에서 생성자나 메소드를 이용해서 전달받는 방식

    public class ScheduleService {
        private UserRepository repository;
        private Calculator cal;
        
        public ScheduleService(UserRepository repository) {
            this.repository = repository;
        }
        
        public void setCalculator(Calculator cal) {
            this.cal = cal;
        }
    }
    
    // 초기화 코드
    UserRepository userRepo = new DbUserRepository();
    Calculator cal = new Calculator();
    
    ScheduleService schSvc = new ScheduleService(userRepo);
    schSvc.setCalculator(cal);

    위의 코드를 보면 ScheduleService라는 생성자와 setCalculator 메소드를 통해

    의존하는 객체를 전달받고 전달받은 객체를 필드에 할당하고 있다.

    그리고 생성자와 메소드를 통해서 객체를 전달하는 것은 초기화 코드에서 진행한다.

    프로그램을 초기화하는 코드에서 알맞은 객체를 생성하여 전달하는 것.

     

    이게 DI의 전부이다.

    용어가 어려워보이지만, 사실 간단한 개념이다.

     

     

     조립기(Assembler)

    프로그램을 시작하는 메인 메서드에서 의존 객체를 생성하고 주입할 수도 있지만,

    보통은 조립기(Assembler)를 이용해서 객체를 생성하고 의존 주입을 처리한다.

    ex) Spring Framework(대표적인 조립기)

     

    스프링은 다음과 같이 객체를 생성하고 의존 대상을 주입하는 코드를 설정으로 작성하고,

    그 설정 코드를 이용해서 ApplicationContext라고 불리는 조립기를 생성한다.

    @Configuration
    public class Config {
        @Bean
        public ScheduleService scheduleSvc() {
            ScheduleService svc = new ScheduleService(repo());
            svc.setCalculator(expCal());
            return svc;
        }
        
        @Bean
        public UserRepository repo() { ... }
        
        @Bean 
        public Calculator expCal() { ... }
    }
    
    // 초기화
    ctx = new AnnotationConfigApplicationContext(Config.class);
    
    // 사용할 객체 구함
    ScheduleService svc = ctx.getBean(ScheduleService.class);
    
    // 사용
    svc.getSchedule(...);

    상단의 AnnotationConfigApplicationContext가 바로 스프링에서 제공하는 조립기에 해당한다.

    조립기를 초기화하는 시점에 설정 클래스를 이용해서 객체를 만들고, 의존 주입이 발생하게 된다.

     

    초기화 이후에는 조립기에서 필요한 객체를 구하고, 그 객체를 사용하면 되는 것.

     

    DI의 장점

    1. 의존 대상이 바뀌면 조립기(설정)만 변경하면 된다.

    2. 의존하는 객체의 실제 구현이 없어도, 대역 객체를 사용하여 테스트하기가 쉽다.

    ex) 실제 DB 연동을 구현한 객체를 사용하지 않고도, 메모리를 이용해서 구현한 객체로 테스트할 수 있다.

    (DB가 없어도 DB와 연동하는 기능을 테스트할 수 있다)

    또한, 대역 객체를 사용하면 원하는 대로 상태를 초기화할 수 있기 때문에

    다양한 경우의 수를 테스트하기가 훨씬 수월해진다.

     

    DI는 습관처럼 사용하기!

    의존 객체는 항상 주입받도록 코드를 작성하는 습관을 들이도록 하자.

     

    반응형

    댓글

Designed by Tistory.