DI를 사용하는 이유

Posted by sJun on November 07, 2020 ·

스프링을 공부하다보면 첫번째 난관이 DI 라고 봅니다.

@Autowired이나 @ComponentScan으로 외부에서 의존관계를 주입해주는 ‘방법’은 알겠는데 도대체 이것을 ‘왜’ 쓰는지는 모르는 경우가 많습니다.

DI를 알려면 올바른 객체 지향의 설계가 무엇인지에서부터 출발합니다.

상당히 모호할 수 있는데, 로버트 마틴이라는 분이 이미 객체지향 설계에 대한 5가지 지침을 소개한 적이 있습니다.

일명 SOLID라고 불리는 것들이죠.


S : SRP / 한 클래스에는 하나의 책임만

O : OCP / 확장에는 열리고 변경에는 닫혀있다.

L : LSP / 객체는 프로그램의 정확성을 깨뜨리지 않고 하위 타입의 인스턴스로 바꿀 수 있어야함. (다형성에서 하위 클래스는 인터페이스의 규약을 다 지켜야한다는 것)

I : ISP / 범용 인터페이스보다 특정 클라이언트를 위한 인터페이스 여러개가 더 낫다.

D : DIP / 구체화에 의존하지말고 추상화에 의존해라.


이렇게 있는데 중요한 것은 OCP와 DIP 입니다.

일단, 기존 순수 자바의 다형성으로는 이 OCP와 DIP를 지킬 수가 없습니다.

이해를 돕기 위해 MemberRepository 라는 인터페이스가 있다고 가정합시다.

Repository와 DB를 연결하기 위한 방법을 정하기 위해 한 가지 방법을 정해야하는데 현재 클라이언트의 요구가 모호한 상황입니다.

MemberRepository // 인터페이스

MemoryMemberRepository // 자체 메모리 사용, DB 사용안함.
JdbcMemberRepository // JDBC를 이용해서 직접 DB와 연동
JpaMemberRepository // Jpa를 이용한 DB 연동
SpringMemberRepository // 스프링 API를 이용한 DB연동

그래서 프로그래머는 MemberRepository 라는 인터페이스를 만들고 MemoryMemberRepository를 사용해 Service를 구현하고 ‘다형성’을 이용해서 클라이언트의 요구변경이 일어나면 그 때 리포지토리와 DB를 잇는 방법을 바꾸기로 합니다.

// MemberService

MemberRepository memberRepository = new MemoryMemberRepository();

.
.
.

다형성을 이용해서 이런 식으로 요구사항 변경에 맞춰서 리포지토리 구현이 가능합니다.

그런데 뭔가 이상합니다. 객체 지향 설계 5가지 원칙 중에서 위 코드는 OCP와 DIP를 전혀 지키지 않고 있습니다.

변경에는 닫혀있고 확장시에만 열려있어야 하는 코드가 구현 객체를 변경하려니 코드를 직접 변경해줘야합니다.

다형성을 이용했는데 OCP가 지켜지지 않습니다.

DIP는 어떨까요?

추상화에 의존해야하는데 위 코드는 누가봐도 구현체에 의존하고 있습니다. 그리고 Service 뿐만 아니라 MemberRepository를 사용하는 모든 곳의 코드를 일일히 변경해줘야합니다.

분명 다형성을 이용했는데 어느 하나 지켜지는 것이 없습니다.

이런 것들을 해결하기 위해서는 객체를 생성하고, 연관관계를 대신 맺어주는 무언가가 필요해보입니다.

이제 단순히 추상화에만 의존하기 위해 과감히 다음 코드를 지웁니다.

MemberRepository memberRepository; // = new MemoryMemberRepository();

인터페이스에만 의존하기 위해 생성자도 추가해줍니다.

public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
 }

그런데 구현체가 없습니다. 그래서 구현객체를 누군가가 대신 ‘생성’해주고 ‘주입’ 해주어야합니다.

다음과 같이 구현 객체를 생성하고 주입해주는 별도의 설정 클래스를 만듭니다.

@Configuration
public class SpringConfig {

 @Bean
 public MemberService memberService() {
    return new MemberService(memberRepository());
    }

 @Bean
 public MemberRepository memberRepository() {
    return new MemoryMemberRepository();
    // 이 코드만 바꾸면 된다.
    // return new JdbcMemberRepository();
    // return new JpaMemberRepository();
    }
}

MemberService의 생성자가 호출 될 때 memberRepository()를 리턴하고 MemberRepository가 생성될 때 MemoryMemberRepository()를 리턴합니다.

프로그래머는 이제 Repository가 어떤 방식이든, 별도의 설정 파일 내부의 MemoryMemberRepository() 부분만 바꿔주면 됩니다.

이 외부 설정파일의 코드의 한 줄의 수정만으로 이 인터페이스에 의존하는 모든 구현 클래스들의 동작 방식을 바꿀 수 있습니다.

이 경우 실제 MemberService 구현 클래스는 단순히 MemberRepository라는 추상화에만 의존하고, 변경할 필요가 없으니 변경에는 닫혀있으나 확장에는 열려있습니다.

외부에서 생성하고 주입해줌으로써 이제 OCP와 DIP 원칙 두개가 완전히 지켜졌네요.

또한 이제 클라이언트의 요구사항의 변경에도 유연하게 대응할 수 있겠네요.