1. DIP의 개념와 위반 예시
DIP란 SOLID 원칙 중 하나로 "프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다"는 원칙이다. 즉, 프로그래머는 인터페이스에 의존해야지 인터페이스를 구현한 클래스에 의존하면 안된다는 것이다. 아래 코드를 살펴보자.
public class OrderServiceImpl implements OrderService{
...
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
...
}
OrderServiceImpl 클래스에서는 DiscountPolicy 인터페이스 객체를 선언하고 구현 클래스로 FixDiscountPolicy를 선택했다. 만약 이 상황에서 다른 구현 클래스로 변경하고 싶다면 아래와 같이 코드를 변경할 것이다.
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
이렇게 변경을 위해 기존의 코드를 수정하는 행위는 OCP 원칙에 위반한다. (OCP란 "소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀야한다"는 원칙이다.) 이러한 문제가 발생하는 이유는 DIP를 어겼기 때문이다. 위 상황을 그림으로 나타내면 다음과 같이 OrderServiceImpl이 DiscountPolicy 인터페이스만 의존한다고 생각할 수 있다.
하지만 OrderServiceImpl에서 직접 구현 클래스 객체를 생성하고 주입시키고 있기 때문에 사실 DiscountPolicy와 FixDiscountPolicy, RateDiscountPolicy를 모두 의존한다.
DIP의 원칙을 지키기 위해서 우리는 인터페이스만 의존해야하므로 아래와 같이 수정하면 문제를 해결할 수 있다.
private final DiscountPolicy discountPolicy;
하지만 코드가 이렇게 되면 기존에 discountPolicy를 사용하던 메서드에서 NullPointerException이 발생한다. 위와 같은 문제를 해결하기 위해서는 누군가가 DiscountPolicy의 구현 객체를 대신 생성하고 OrderServiceImpl에 주입시켜줘야 한다.
2. AppConfig의 등장
이전에 DIP 원칙을 위반하여 OCP를 위반한 코드를 보면 OrderServiceImpl이 OrderService와 관련된 로직만 해야하는데 DiscountPolicy를 "직접 선택"하기 때문에 발생하는 문제다. 즉, 하나의 클래스가 "다양한 책임"을 가지게 된다. (하나의 클래스는 하나의 책임을 가져야하는 SRP 원칙에 위반된다.) 따라서 같이 DiscountPolicy 구현 클래스를 생성하고 알맞은 객체를 넣어주는 클래스는 따로 만들어야 한다. 따라서 우리는 애플리케이션(App)의 전체 동작 방식을 구성(Config)하기 위해 "구현 객체를 생성하고 연결하는 책임"을 가지는 별도의 설정 클래스를 만들게 된다. AppConfig를 작성하기 전에 먼저 기존 코드를 아래와 같이 인터페이스에만 의존하도록 수정하자.
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;
}
...
}
위와 같이 앞으로는 인터페이스 객체에 생성자를 통해 구현 클래스 객체를 주입시킬 것이다. AppConfig는 아래와 같이 구성할 수 있다.
public class AppConfig {
...
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy);
}
...
}
위와 같이 AppConfig는 애플리케이션의 동작에 필요한 구현 객체를 생성하고 생성한 객체 인스턴스를 생성자를 통해 연결한다. 이와 같이 생성자를 통해 주입하는 방식을 "생성자 주입"이라고 부른다.
- AppConfig를 사용하면 이처럼 객체를 생성하고 연결하는 역할과 실행하는 역할을 명확히 분리할 수 있다.
- OrderServiceImpl입장에서는 memberRepository와 discountPolicy에 대해 전혀 모른다. 이런 것을 의존관계가 마치 외부에서 주입되는 것같다고 하여 DI(Dependency Injection, 의존관계 주입, 의존성 주입)이라고 한다.
- 이제는 AppConfig를 변경하면 원하는 구현 클래스로 변경할 수 있기 때문에 클라이언트 코드는 변경하지 않아도 된다. 즉, 외부에서 어플리케이션을 구성하는 역할인 AppConfig 같은 녀석들만 고치면 되므고, 사용 영역인 클라이언트 코드는 변경하지 않아도 된다는 것이다. 따라서 OCP 원칙에 만족한다.
AppConfig는 아래와 같이 main문이나 다른 클래스에서 사용할 수 있다.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
...
}
}
3. [맛보기] AppConfig에 스프링 적용
AppConfig에 스프링을 적용하면 아래와 같이 코드를 작성할 수 있다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
- @Configuration은 AppConfig처럼 애플리케이션에서 구성을 담당하는 클래스 위에 붙인다.
- @Bean을 사용하면 각 메서드가 스프링 컨테이너라는 곳에 등록된다.
이전에 AppConfig를 사용하는 방식은 아래와 같이 변경할 수 있다.
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);
...
}
}
- 스프링은 모두 ApplicationContext로 시작한다. 이것이 스프링 컨테이너 다. 이것이 @Bean과 같은 객체들을 관리해준다. ApplicationContext를 생성할 때는 위와 같이 "new AnnotationConfigApplicationContext({애플리케이션 구성에 사용할 클래스})"를 사용해야한다. 그러면 @Configuration이 붙은 AppConfig를 구성 정보로 활용한다. 이때 안에 있는 @Bean이 붙은 것들(스프링 빈)을 모두 스프링 컨테이너에 집어넣고 관리해준다.
- getBean()을 사용하면 위와 같이 생성하여 컨테이너에 넣었던 객체를 불러올 수 있다. 이때 getBean에는 이름과 반환받을 클래스 타입을 전달해야 한다. 이때 이름은 @Bean이 붙은 메서드의 이름이다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 스프링 컨테이너와 스프링 빈 (0) | 2023.08.20 |
---|---|
[Spring] IoC, DI, 컨테이너 (0) | 2023.08.20 |
[Spring] test 코드 작성 기초 (0) | 2023.08.18 |
[Spring] 좋은 객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2023.08.17 |
[Spring] 좋은 객체 지향 프로그래밍 (0) | 2023.08.09 |