스프링에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리 하는 것이 아닌 Spring Container에 모두 맡긴다.
즉, 개발자가 main문이 있는 자바객체에서 생성하는 방식 -> 프레임워크, Ioc 컨테이너가 모두 객체를 관리하는 방식으로, 제어의 객체 관리의 권한이 넘어 갔음으로 제어의 역전
이라고 한다.
객체들의 의존관계 방향성
만 따진다면, SOLID 원칙 중 DIP 원칙과 밀접한 관련을 갖는다. DIP
는 상위객체가 new 연산자를 쓰면서 하위 객체에 의존성을 갖게하는 기존의 방식과 달리, interface를 둠으로써 하위 객체가 오히려 상위 객체에 의존성을 갖게하는 방식을 말한다.
개발자가 직접 생성하는 방향
상위객체가 하위객체(구현클래스)에 의존한다.
프레임워크가 객체를 생성하는 방향
인터페이스(매개체)를 통해서 하위객체(Impl)가 상위객체에 의존하게 한다
: DIP(의존관계 역전)
충족
인터페이스의 구현클래스가 확장한다 할지라도 인터페이스에 의존하는 클라이언트 객체는 변화에 닫혀있게 된다.
: OCP(개방-폐쇄)
충족
IoC의 기술, 의존성 주입
이라고 한다. 정확하게 표현 의존관계 주입이다. 외부에서 주입하는 방식이란 뜻인데, 컨테이너가 관심사의 분리
(생성과 사용의 분리)로 객체의 의존관계를 연결해주는 기술이라는 것이다.
보통, XML, Annotation 등 으로 구현된다.
▶ 관심사의 분리(생성과 사용의 분리)
ApplicationContext 객체가 생성
하고
클라이언트 객체는 그 객체를 사용
해서 호출만 하면 된다.
▶ 의존성 주입 원칙
1) 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다.
2) 상위, 하위 둘다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 한다.
▶ DI 장점
느슨한 결합
-> 런타임시 의존관계가 결정
됨.참고) 느슨한 결합
강한 결합
객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다. A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.
느슨한 결합
객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.
SOLID 원칙에서 O 에 해당하는 Open Closed Principle 을 지키기 위해서 디자인 패턴 중 전략패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략패턴을 사용하게 된다.
▶ 스프링 DI 구현
XML, Annotation(@Component
, @bean
, @Configuration
, @Autowired
, @Inject
등)으로 spring에서 관리하는 객체, @bean
으로 싱글톤 객체
로 등록시킨다.
@bean
: 직접 bean으로 등록할 때 사용
@Configuration
: 여러가지 bean을 등록할 때 사용
ex.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
▶ 사용자는 어떻게 bean 객체를 가져올 수 있나?
사실, 스프링에서는 ApplicationContext
환경에서 bean을 관리하는 것이다. bean 팩토리
라고도 불린다.
실제로 개발자가 직접 접근할려면 beanFactory 기능을 상속 받은 ApplicationContext
객체(인터페이스)에서 getContext()
, getBean()
메서드로 불러온다.
✔ 실제 ApplicationContext 객체
ApplicationContext context = ApplicationContextProvider.getContext();
MyClass myClass = context.getBean(myClass.class);
실제예시
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new
AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService =
applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
▶ 실제 많이 사용하는 방식은 @Autowired
방식(프로퍼티 생성방식)과 @RequiredargsConstructor
(생성자 생성방식- 스프링부트4.0부터 밀고 있는 방식 )이다.
▶ 어노테이션 DI 방식의 주의점
생성자주입방식, @RequiredArgsConstructor
을 이용해서 편리하게 의존성 주입하는 방법을 알아봤다. 추가적으로 Lombok 어노테이션이(@Getter 혹은 @Setter 등) 사용할땐 편하지만, 단점도 있다는 것을 알아야 한다.
setter 메서드가 필요없는 필드에 대해서도 setter 메서드를 강제로 생성하게 되니, 필드 값이 변경될 위험이 생기게 된다.
이런 부분들은 Lombok을 사용하게될 경우 리팩토링이 힘들어지는 부분도 있으니 너무 무분별하게 사용하지 않는 것이 좋다.
▶ 생성자 주입을 사용하는 이유! 순환참조 방지
개발하다보면 여러 서비스들 간에 의존관계가 생기게 되는 경우가 있다. 서로서로 주거니 받거니 호출을 반복하면서 끊임없이 호출하다가 결국 StackOverflowError 를 발생시키고 죽는다 경우가 생기는데 이게 순환참조 문제
이다.
이 순환참조의 문제는 실제 코드가 호출이 되기 전까지는 오류가 있다는 것을 알지 못한다는 것
이다!
그런데 생성자 주입 방식에서는 이를 방지할 수 있게 해준다!
실제 생성자주입 방식을 사용할 때, 순환 참조가 생길시 나오는 오류!
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| courseServiceImpl defined in file [/Users/yaboong/.../CourseServiceImpl.class]
↑ ↓
| studentServiceImpl defined in file [/Users/yaboong/.../StudentServiceImpl.class]
└─────┘
빈 생성시 아래와 같은 로직이 수행되면서 어떤 시점에 스프링이 그것을 캐치해서 순환참조라고 알려주는 것 같다.
new CourseServiceImpl(new StudentServiceImpl(new CourseServiceImpl(new ...)))
이처럼 생성자 주입을 사용하면 객체 간 순환참조를 하고 있는 경우에 스프링 애플리케이션이 구동되지 않는다.
왜? 컨테이너가 빈을 생성하는 시점에서 객체생성에 사이클관계가 생기기 때문이다!
반대로, 수정자 주입
을 사용하면 아주 잘 구동되고 순환참조를 하고 있는 부분에 대한 호출이 이루어질 경우 StackOverflowError
를 뱉기 때문에, 오류를 뱉을 수 밖에 없는 로직을 품고 애플리케이션이 구동되는 것이다!
생성자 방식을 꼭 사용하자!