[영상후기] [10분 테코톡] 오찌, 야호의 DI와 IoC

박철현·2023년 4월 27일
1

영상후기

목록 보기
111/161

movie

IoC란?

  • IoC(Inversion of Control) : 제어의 역전
    • 제어 : 객체 생명주기나 메서드의 호출을 직접 제어(관리)한다.
      • 직접 객체를 생성하여 코드를 제어
public class A {
	private B b;
    
    public A() {
    	this.b = new B();
    }
 }
  • 제어의 역전 : 프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 관리하는 것
    • 인자로 받아 주입 -> 외부에서 관리
public class A {
	private B b;
    
    // 제어 흐름을 A가 직접 하는 것이 아닌 A 외부에서 관리
    public A(B b) {
    	this.b = b;
    }
}

IoC 왜 필요할까?

예제1 (IoC 미적용)

  • A가 샌드위치 주문, BMT 샌드위치
  • 샌드위치 가게는 정해진 BMT 샌드위치 레시피대로 제작
  • A는 옵션을 변경하고 싶었으나 반영이 안됨

public class ItalianBMT {
	WhiteBread whiteBread;
    MozzarellaCheese mozzarellaCheese;
    ChiliSauce chiliSauce;
    MayonaiseSauce mayonaiseSauce;
    
    public ItalianBMT() {
    	this.whiteBread = new WhitedBread();
        this.mozzarellaCheese = new MozzarellaCheese();
        this.chiliSauce = new ChiliSauce();
        this.mayonnaiseSauce = new MayonnaiseSauce();
    }
}

예제2 (IoC 적용)

  • A가 샌드위치 가게에서 요구한 재료로 구성된 샌드위치 주문
  • 샌드위치 가게에서 요구한 재료로 만들어줌

public class ItalianBMT {
// 어떤 종류든 받을 수 있는 Interface 각각 구성
	Bread bread;
    Cheese cheese;
    List<Sauce> sauces;
    
    public ItalianBMT(Bread bread, Cheese cheese, List<Sauce> sauces) {
    this.bread = bread;
    this.cheese = cheese;
    this.sauces = sauces;
  }
}

결과

  • 객체 내부에서 재료의 종류를 제어해 변경에 자유롭지 못하던 코드가 외부에서 제어를 받으며 변경에 자유로워짐
  • 재료에 대한 제어권을 기존에는 ItalianBMT가 가졌었지만 변경된 코드에서는 A가 갖도록 함

그래서 왜 필요할까?

  • 객체지향 원칙을 잘 지키기 위해!

    • 역할과 관심을 분리해 응집도를 높이고 결합도를 낮추며, 변경에 유현한 코드를 작성할 수 있는 구조가 될 수 있기 때문
    • 이는 곧 객체지향 원칙을 지키는 코드가 됨
  • Hollywood Principle(할리우드 법칙)

    • Don't call us, we'll call you (우리에게 연락하지마쇼 알아서 불러줄테니)
    • 우리가 어떤것을 주도하여 호출하는 것이 아닌 주도권은 빼앗기고 호출 당하기를 기다리는 모습이 IoC와 비슷해서 GoF 디자인 패턴에서 Hollywood Principle 이라 불리기도 함

DIP

  • DIP와 IoC는 함께 사용하길 권장받는 관계

  • DIP(Dependency Inversion Principle) : 의존 역전 법칙

    • 상위 모듈은 하위 모듈에 의존하지 않고 둘다 추상화에 의존
      • ex) Italian BMT(고수준 모듈) -> 빵은 whiteBread(저수준 모듈) 이어야 했음
      • 상위 모듈이 하위 모듈을 의존했던 현상
      • 빵 종류를 변경하면 whiteBread를 의존하는 Italian BMT 코드에도 연쇄적으로 영향이 일어남
  • DIP를 활용하여 문제 해결

    • ex) Italian BMT(고수준 모듈) -> Bread(저수준) (Interface) 의존
      • 고수준, 저수준 모두 추상화에 의존하여 제어 역전됨
        • (기존) 이탈리안 비엠티(고수준) -> White 빵(저수준)
        • (변경) 이탈리안 비엠티(고수준) -> Bread InterFace <- 빵들..
      • Bread : 고수준 모듈인 이탈리안 비엠티 입장에서 생성
        • 저수준이 고수준 의존하는 현상 (메뉴 -> 빵 결정)
          • 기존 : 메뉴(고수준) -> 빵(저수준) 고정
          • 변경 : 메뉴 -> Bread <- 빵(저수준)
        • 역전됨을 알 수 있음
          • 고수준 모델인 Italian BMT도 추상화(BREAD) 의존
          • 저수준 모델인 (White BREAD)도 추상화(BREAD) 의존 하기에 DIP 만족
  • 예제2 코드도 고수준 모듈인 ItalianBMT가 저수준 모듈인 재료들에 의존하지 않고 추상화 재료에 의존 => 이미 DIP 적용

IoC와 DIP의 관계?

  • 클래스 간 결합을 느슨히 하기 위한 같은 목적을 가짐
    • 한 클래스의 변경에 따른 다른 클래스들의 영향을 최소화
    • 애플리케이션을 지속가능하고 확정성 있게 만든다

IoC와 DIP 모두 Principle(원칙) 이다.

  • 같은 목적을 가졌지만 아래와 같이 다름
    • IoC (제어의 역전) : 제어권 내부에서 외부로 역전
      • new 키워드로 생성하지 않고 주입받아서 넣기
      • ItalianBMT 생성 시 유통기한 지난 Bread가 들어오는지 모른다
    • DIP (의존 방향 역전) : 필드가 인터페이스로 구현되어 있어 변경이 자유로움
      • 위 예시에서 이탈리안 비엠티 빵 종류
        • 화이트 Bread -> Bread로 추상화 -> 화이트 bread가 아니여도 됨
      • 고수준모듈 입장에서 만들어진 인터페이스에 저수준이 의존하게 됨(고, 저수준 모두 추상화에 의존)

IoC 원칙 구현 방법?

  • DI(Dependency Injection - 의존성 주입), 팩토리, 템플릿 메소드 등의 방법이 있으나 영상에서는 DI설명

DI란?

의존성 주입

  • 의존성 : 클래스 간 의존 관계가 있다는 것
    • 한 클래스가 바뀔 뛰 다른 클래스가 영향을 받는다는 것
    • 의존성 주입 : 의존성을 외부에서 주입해주는 것

의존성 주입 패턴 3가지

생성자 주입

  • 필요한 의존성이 필요한 모든 생성자를 만들고, 생성자를 통해 의존성 주입

Setter 주입

  • 의존성을 입력받는 Setter 메서드들을 만들고 메서드를 호출해서 의존성 주입

Interface 주입

  • 의존성을 주입하는 메서드를 포함하는 인터페이스를 작성하고, 인터페이스를 구현하면서 의존성을 주입한다.
public class ItalianBMT implements RecipeInjection{
	WhiteBread whiteBread;
    MozzarellaCheese mozzarellaCheese;
    ChiliSauce chiliSauce;
    MayonaiseSauce mayonaiseSauce;
    
    @Override
    public void inject(WhiteBread whiteBread, ...) {
    	this.whiteBread = whiteBread;
        this.mozzarellaCheese = mozzarellaCheese;
        this.chiliSauce = chiliSauce;
        this.mayonnaiseSauce = mayonnaiseSauce;
    }
}

interface RecipeInjection {
 	void inject(WhiteBread whiteBread, ....);
}
  • setter 주입처럼 외부에서 호출해줘야 하는 것은 비슷하나, 의존성 주입 메서드를 빠트릴 수 있는 setter와 다르게 Override를 통해 메서드 구현을 강제할 수 있는 차이가 있음

의존성 분리

  • DIP를 이용해 의존 관계를 분리시킨다.

  • 의존성을 주입하는 방법을 알아봤지만, 각각의 속성들을 변경하고 싶을때는 어떻게 해야할까?

    • ex) 화이트브레드 -> 다른 빵으로 변경
    • DIP 적용해야 함(원칙이지만 어떤 방안으로)
      • DI를 이용하되 DIP 원칙은 따라야 하는 형태로 해야 변경에 유연
  • IoC와 DIP는 원칙 / DI는 IoC를 달성하는 디자인 패턴 중 하나

Spring DI 방법

자동주입

  • 스프링 빈으로 등록되면 스프링이 자동으로 생성, 필요한 의존성도 주입해준다.
@Controller
public class MemberController {
	private final MemberService memverService;
    
    public MemberController(MemberService memberService) {
    	this.memberService = memberService;
    }
}

Spring의 의존성 주입 방법

@Autowired 어노테이션 활용

  • 여기에 의존성을 주입해줘! 하는 의미
  • Spring이 자동으로 적절한 의존성 주입해줌
@Controller
public class MemberController {
	@Autowired
	private MemberService memverService;
}
  • 주입 받을 MemberService 도 Spring Bean 이어야 함
@Autowired 의존성 주입 방법
  • 필드주입
    • Spring 적용하지 않은 DI 방법 중 없던 방법
    • 불가능한 주입을 프레임워크의 힘을 빌려서 주입
    • 주입 받고자 하는 필드 위에 @Autowired 어노테이션을 붙이면 스프링은 의존성을 직접 주입
@Controller
public class MemberController {
	@Autowired 
	private MemberService memverService;
}
  • 테스트 등의 이유로 자동이 아닌 수동 의존성 주입을 하고 싶어도 생성자 및 setter가 없으므로 사용자가 직접 의존성을 넣어줄 수가 없음
    • 의존성이 프레임워크에 강하게 종속한다는 문제점이 있음
Setter주입
  • setter 메소드에 @Autowired 어노테이션 붙이면 자동으로 의존성 주입
@Controller
public class MemberController {
	private MemberService memberService;
    
    @Autowired
    public void setMemberService(MemberService memberService) {
    	this.memberService = memberService;
    }
}
  • 객체의 속성이 없는 빈 객체를 만들고 Setter로 의존성 주입

    • 빈 생성자 또는 빈 정적 팩토리 메서드 필요
    • final 필드를 가질 수 없고, 의존성의 불변을 보장할 수 없음
      • 불변성 보장 불가 이유 : java에서 final 키워드의 경우 필드 선언 또는 생성자에서만 값 지정 가능
      • Setter 주입의 경우 매개변수가 빈 생성자를 활용해 객체를 생성하고 속성을 설정하는 방식으로 객체를 주입하기 때문에 final 키워드 불가
  • 그럼 Setter 주입은 왜 존재하지?

    • RunTime에 Setter 호출하면 의존성 변경 가능하니, 의존성을 변경한다거나 의존성을 선택적으로 주입할 때 사용
생성자주입
@Controller
public class MemberController {
	private final MemberService memverService;
    
    // @Autowired , Spring 4.3 이후 생성자 1개면 생략 가능
    public MemberController(MemberService memberService) {
    	this.memberService = memberService;
    }
}
  • 객체의 최초 생성 시점에 Spring이 의존성 주입

  • Spring에서 공식적으로 추천하는 방법

    • Spring 4.3 이후 버전 부터 생성자가 하나뿐이 없을 경우 해당 생성자에 Spring이 자동으로 @Autowired를 붙여주어 생략 가능
  • 공식 문서 상 생성자 주입과 Setter 주입은 혼용할 수 있지만 스프링은 생성자 주입을 추천함

    • Setter 주입은 선택적 의존성을 사용해야 함
    • 이유 : 생성자 주입된 컴포넌트들이 완전히 초기화된 상태로 클라이언트에 반환되기 때문
  • 생성자 주입 사용 시

    • 필드를 final 가능, 의존성 주입이 생성자 호출 시 최초 1회만 이루어 짐
    • 의존 관계 불변 가능
    • NullPointerException 방지 가능
      • 다른 방식의 경우 빈 객체를 만들고 런타임시에 객체를 넣음
      • 반면 생성자 주입 방식은 객체를 생성할 때 넣어줘야 하기 때문에 방지해줌
    • 순환참조 문제 방지 : 순환참조시 종료시켜버림 (2.6 버전부터 나머지 방식도 방지)
  • 생성자가 여러개라면?

    • 의존성을 자동으로 주입하는데 사용할 생성자에 @Autowired
    • @Autowired가 여러 개 있을 경우 가장 많은 의존성을 주입할 수 있는 생성자 사용
    • @Autowired가 붙은 모든 생성자가 사용 불가능한 경우 또는 어떤 생성자에도 @Autowired가 없을 경우 기본 생성자를 호출
      • 기본 생성자 조차 없을 경우 컴파일 에러
    • 생성자 방식 -> 필드 방식 -> setter 방식 순으로 의존성 주입됨

의존성 주입 대상이 여러 개일 때

  • 타입 검색 후 여러개라면 bean의 이름을 기준으로 의존성 주입

예시

@Controller
public class PayController {
	private final PayService payService;
    
    public PayController(PayService payService) {
    	this.payService = payService;
    }
}
  • 결제 담당하는 PayService Interface가 있고, 구현체로 네이버/카카오가 모두 Bean으로 등록되어 있다면 PayController 에서는 도대처 어떤 Pay를 주입받아야 할까?
    • Application 시작 시 프로그램 실행 안됨
    • 2개의 Bean 등록되었다 메시지

주입 대상이 여러개일때

  • 스프링은 의존성 주입 대상 찾을 때 정의되어있는 타입 기준으로 찾음
    • 주입 대상 : private final PayService payService;
  • 네이버페이, 카카오페이 모두 PayService의 구현체
  • 타입을 기준으로 여러 Bean 검색되었다면 Spring은 그 다음으로 Bean의 이름을 기준으로 의존성 주입
    • 주입하는데 사용하는 메서드의 매개변수 명과 등록된 Bean의 이름이 일치하는지 체크
      • 생성자 매개변수명을 naverPayService로 변경하면 자동으로 naverPayService Bean 주입
@Controller
public class PayController {
	private final PayService payService;
    
    // 생성자 매개변수명 변경
    public PayController(PayService naverPayService) {
    	this.payService = payService;
    }
}

위 방식의 단점

  • 그러나 이러면 naver만 주입됨
    • 수동으로 카카오페이를 넣어줘야할때 등 헷깔림 및 번거로움

@Qualifier 어노테이션 활용

  • 해당 빈의 구분자 지정 가능
    • 추가)@Component("abc") 이런식으로 이름 지정도 가능
@Service
@Qualifier("mainPayService") // 구분자 지정
public class NaverPayService implements PayService {}
@Controller
public class PayController {
	private final PayService payService;
    
    // 주입받을 구분자 입력
    public PayController(@Qualifier("main{ayService") PayService payService) {
    	this.payService = payService;
    }
}

@Primary 어노테이션 활용

  • 해당 타입으로 의존성 검색할 때 우선적으로 주입(기본 Bean)
  • @Qualifier와 @Primary 둘다 있다면 @Qualifier가 우선순위를 가짐
    • Qualifier가 특정 지정값을 넣는 것이니 당연한 것

결론

  • 의존성 검색 기준 : 타입 -> @Qualifier -> @Primary -> 변수명
profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글