일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 몽고 인덱스
- langgraph
- ipo 매매자동화
- 누적합
- AWS
- 완전탐색
- 쿠키
- jwt 표준
- spring event
- next-stock
- piplining
- ai agent
- 추천 검색 기능
- 검색어 추천
- JPA
- 이분탐색
- 아키텍쳐 개선
- 결제서비스
- 프로그래머스
- gRPC
- 트랜잭샨
- 구현
- BFS
- 백준
- 카카오
- 셀러리
- 디버깅
- docker
- 크롤링
- 레디스 동시성
- Today
- Total
코딩관계론
스프링의 IoC(Inversion of Control)과 DI(Dependency Injection) 본문
제어의 역전 IoC(Inversion of Control)
예를 들면 아래와 같은 코드를 개발자가 작성했다고 하면 createOrder 함수의 실행주기는 개발자가 정하는 것이 아니라, 프레임워크가 결정하게 되는 것입니다. 또한 memberRepository, discountPolicy와 같은 인터페이스들도 프레임워크가 무엇을 객체화하는지에 따라서 달라지게 됩니다.
이러한 설정을 개발자가 담당하는 것이 아니라 프레임워크가 담당하게 되면 제어가 역전됐다고 합니다.
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
System.out.println("member = " + member);
int discount = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discount);
}
프레임워크 vs 라이브러리
프레임워크는 내가 작성한 코드를 제어하고 , 대신 실행하면 그것은 프레임워크가 맞다
내가 작성한 코드가 직접 제어의 흐름을 담당하면 그것은 라이브러리다.
의존관계 주입 DI(Dependency Injection)
DI(Dependency Injection, 의존성 주입)는 객체 지향 프로그래밍에서 객체 간의 의존성을 외부에서 주입해 주는 디자인 패턴입니다. 이를 통해 객체 간의 결합도를 낮추고 코드의 유연성과 재사용성을 높일 수 있습니다.
의존관계에는 동적인 의존관계와 정적인 의존관계가 있습니다. 이에 대한 차이점을 알아보겠습니다.
정적 의존관계
정적 의존관계는 코드 상에서 import 문을 통해 어떤 클래스나 인터페이스를 의존하고 있는지를 알 수 있는 관계를 의미합니다. 이는 소스 코드를 읽는 것만으로도 파악이 가능합니다. 제시된 코드에서 OrderServiceImpl 클래스는 MemberRepository와 DiscountPolicy 인터페이스에 의존하고 있습니다. 이를 정적인 의존관계라고 합니다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
동적 의존관계
동적인 의존관계는 프로그램 실행 시점에 실제 어떤 구현 객체가 주입될지를 의미합니다. 즉, 인터페이스를 구현한 구체적인 클래스가 런타임에 결정되는 것을 말합니다.
MemberRepository와 DiscountPolicy는 인터페이스이기 때문에, 이를 구현한 다양한 클래스가 존재할 수 있습니다. 프로그램이 실행될 때 생성자를 통해 이 인터페이스를 구현한 실제 객체가 주입됩니다.
아래의 제시된 코드를 보시면 Main이 실행되는 시점에서 실제 생성자 객체를 넣어주고 있습니다.
MemberRepository memberRepository = new MemoryMemberRepository();
DiscountPolicy discountPolicy = new RateDiscountPolicy();
OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
DI 사용이유
Ioc와 DI가 무엇인지를 알아봤습니다. 하지만 이를 왜 사용하게 될까요? 객체지향의 원칙을 상기해보면 개방폐쇄의 원칙과, 상위 클래스들은 추상화된 인터페이스에 의존해야 한다고 배웠습니다.
그럼 아래의 코드를 보고 생각해봅시다. 참고로 MemberRepository, DiscountPolicy는 모두 Interface입니다. 결론적으로 개방폐쇄원칙과 의존 역전 원칙을 지키지 못했습니다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
System.out.println("member = " + member);
int discount = discountPolicy.discount(member, itemPrice); //SIP 원칙의 예제임
return new Order(memberId, itemName, itemPrice, discount);
}
}
그 이유는 다음과 같습니다
- 개방 폐쇄 원칙은 확장에는 열려있고, 변경에는 닫쳐있습니다. 만약 MemberRepository가 다른 구현체로 변경되려고 하면 코드 수정이 필요하게 됩니다. 이렇게 작성하면 의존 역전 원칙도 위배되게 됩니다.
- 의존 역전 원칙은 상위 클래스가 특정 구현체에 의존하지 않아야 합니다. 하지만 OrderServiceImple은 MemoryMemberReposiy 클래스와, RateDiscountPolicy 클래스를 직접 의존하고 있습니다.
그럼 이런 객체지향 원칙을 지키기 위해선 어떻게 변경해야 할까요? 바로 DI를 사용해서 생성자를 통해서 구체적인 객체를 주입받게 만들어줘야 헙니다. 아래의 코드처럼 생성자를 통해서 주입 받게 만들어주면 코드의 변경 없이 기능의 확장 및 인터페이스에만 의존하게 됩니다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
IoC 사용이유
아래의 글을 읽다가 보면 DI만으로 객체지향의 원칙을 달성할 수 있다고 생각할 수 있는데 그것은 엄청난 착각입니다.(필자도 그랬다...)
아래의 코드를 보면 DI를 통해서 객체를 생성했지만, 여기서도 다른 구현체로 변경된다면 실행 코드의 수정이 필요해지게 됩니다.
MemberRepository memberRepository = new MemoryMemberRepository();
DiscountPolicy discountPolicy = new RateDiscountPolicy();
OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
이를 제어의 역전을 통해서 해결할 수 있습니다. 제어의 역전은 호출 시기가 개발자에 의해서 정해지는 것이 아니라, 프레임워크에 의해서 정해지는 것이기 때문에 객체의 생성과 설정이 분리되기 때문입니다.
아래의 AppConfig 클래스는 메서드들이 객체를 생성하지만, 실제로 이 메서드들을 호출하는 것은 스프링 컨테이너입니다. 개발자는 객체 생성의 제어권을 갖지 않으며, 스프링 컨테이너가 이를 대신하게되고, 코드의 변경 없이 구현체를 변경할 수 있게 됩니다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
}
@Bean
public MemoryMemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy getDiscountPolicy() {
return new RateDiscountPolicy();
}
}
최종적으로는 아래와 같은 코드로 변경없이 코드를 실행할 수 있습니다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = applicationContext.getBean(OrderService.class);
[참고자료]
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보
www.inflearn.com
'개발 > Java' 카테고리의 다른 글
AOP의 필요성 및 개념 (0) | 2024.07.16 |
---|---|
Bean 의존관계 주입 방법 - 생성자 주입을 사용하자 (0) | 2024.07.14 |
디자인 패턴 - 팩토리 패턴 (0) | 2024.07.08 |
디자인패턴 - 어댑터 패턴 (0) | 2024.07.08 |
디자인 패턴 - 싱글톤 패턴 (0) | 2024.07.07 |