Spring DI (Dependency Injection) 사용이유

janjanee·2021년 8월 26일
1

Spring

목록 보기
1/2
post-thumbnail

스프링 프레임워크의 대표적인 기술중 하나인 DI(Dependency Injection) 에 대해서 알아보자.
DI라는 기술은 왜 등장한것인지? 그리고 사용하면 어떤 이점을 얻을 수 있는지에 대해서 정리한 글이다.

DI란?

  • 의존관계 주입, 의존성주입
  • 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 알려주는 것
  • "주입"은 의존성(서비스)을 사용하려는 객체(클라이언트)로 전달하는 것을 의미

DI 등장배경

이해를 돕기 위해 위의 클래스 다이어그램을 참고하여 각 클래스의 상세 기능구현보다는 코드의 핵심 부분을 설명하겠다.

public class MemberServiceImpl implements MemberService {

    MemberRepository memberRepository = new JdbcMemberRepository();

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

}
  1. MemberServiceImpl에서 Member를 저장하는 Repository를 JDBC로 사용중이다.
  2. 갑자기 JDBC -> JPA로 요구사항이 변경됐다.
  3. 이런 상황을 대비해서 인터페이스를 잘 설계한 자신에게 흡족해하며 새로운 JpaMemberRepository 구현클래스를 만들고 적용했다.
public class MemberServiceImpl implements MemberService {

    MemberRepository memberRepository = new JpaMemberRepository();

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

}

다형성을 활용하여 구현체만 JpaMemberRepository로 변경했고, 아무런 문제 없이 잘 동작하는 것 같다!
그러나 해당 코드에 문제점이 존재한다.

객체지향 설계 원칙에 어긋나는 코드

객체지향 설계 원칙 중 SRP, OCP, DIP에 위반한 상황이다.

간단하게 위 원칙들에 대해 설명하자면

  • SRP 단일 책임 원칙 (Single Responsibility Principle)
    • 한 클래스는 하나의 책임만 가져야 한다.
  • OCP 개방-폐쇄원칙 (Open/Closed Principle)
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀있어야 한다.
  • DIP 의존관계 역전 원칙 (Dependency Inversion Principle)
    • 구체화에 의존하지 말고 추상화에 의존해야 한다.
    • 클라이언트 코드가 구현클래스에 의존하지 말고, 인터페이스에 의존해야한다.

그럼 위의 코드 어느 부분이 OCP, DIP, SRP를 위반했는지 자세히 살펴보자.

MemberRepository memberRepository = new JdbcMemberRepository();
MemberRepository memberRepository = new JpaMemberRepository();
  • SRP 위반

    • 클라이언트 코드가 본인의 로직 실행 이외에 의존관계 책임까지 신경쓰고있다.
  • DIP 위반

    • 추상(인터페이스)와 함께 구체(구현)클래스에도 의존하고 있다.
      • 추상 의존: MemberRepository
      • 구체 의존: JdbcMemberRepository, JpaMemberRepository
  • OCP 위반

    • 새로운 요구사항에 맞춰 기능이 확장됐고, 동시에 클라이언트 코드에 변경이 일어났다.
      • new JdbcMemberRepository() -> new JpaMemberRepository()

정리해보자면 클라이언트 코드가 너무 많은 책임을 갖고 있으며 인터페이스 뿐만 아니라 구체 클래스도 알고있기 때문에 기능 확장이 발생하면 소스 코드의 변경도 함께 일어나는 것이다.

어떻게 문제를 해결할까???

정답은 인터페이스만 의존하도록 설계를 변경해야한다.

public class MemberServiceImpl implements MemberService {

    MemberRepository memberRepository;
    
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }
}

구현체 new JpaMemberRepository()를 없애고 인터페이스만 의존하도록 설계를 변경했다.
컴파일은 성공하지만 실행하면 당연히 구현체가 없기 때문에 NPE(null pointer exception)가 발생한다.

그럼 인터페이스만 의존하도록 어떻게 설계 해야하는걸까?

누군가 클라이언트 MemberServiceImpl에 MemberRepository 구현체를 대신 생성하고 주입 해주어야 한다.

바로 이러한 상황 때문에 DI(Dependency Injection) 개념이 등장한 것이다.

관심사의 분리 - (AppConfig)

DI의 목적은 관심사를 분리 하는 것이다.
클라이언트 코드 내부에서 의존관계 설정을 하지 않고 해당 관심사를 외부로 분리시킨다는 뜻이다.

앞의 문제를 해결할 새로운 클래스를 만들어보자.
애플리케이션 전체 동작 방식을 구성하기 위한, 구현객체를 생성하고 연결하는 책임과 역할을 가진 클래스이다.

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public MemberRepository memberRepository() {
        return new JdbcMemberRepository();
    }

}

AppConfig라는 클래스를 만들었다.

  • 구현 객체를 생성
    • MemberServiceImpl
    • JdbcMemberRepository
  • 생성한 객체 인스턴스 참조를 생성자를 통해서 주입
    • MemberServiceImpl -> JdbcMemberRepository

다음으로 클라이언트 코드인 MemberServiceImpl에서 의존성 주입을 받을 생성자를 만들자.

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }
}
  • MemberServiceImpl(클라이언트 코드) 입장에서 어떤 구현체가 주입될 지 모른다.
  • 생성자를 통해 어떤 구현체를 주입할지는 AppConfig에서 결정된다.
  • MemberServiceImpl(클라이언트 코드)는 이제부터 의존관계 고민 없이 오로지 실행(비즈니스 로직)에만 집중
  • 참고 : final 키워드는 선언과 함께 초기화되는 점과 재할당을 막는 이유로사용
class MemberServiceTest {

    MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

    @Test
    void join() {
        Member member = new Member(1L, "lee");
        memberService.join(member);
        ...
    }
}

테스트 코드는 다음과 같다. AppConfig를 통해서 의존성을 주입받을 수 있다.

이전 요구사항처럼 JDBC -> JPA로 변경된다면?
AppConfig만 변경 하면된다.

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public MemberRepository memberRepository() {
        return new JpaMemberRepository();
    }

}

References

profile
얍얍 개발 펀치

0개의 댓글