스프링 프레임워크의 대표적인 기술중 하나인 DI(Dependency Injection) 에 대해서 알아보자.
DI라는 기술은 왜 등장한것인지? 그리고 사용하면 어떤 이점을 얻을 수 있는지에 대해서 정리한 글이다.
이해를 돕기 위해 위의 클래스 다이어그램을 참고하여 각 클래스의 상세 기능구현보다는 코드의 핵심 부분을 설명하겠다.
public class MemberServiceImpl implements MemberService {
MemberRepository memberRepository = new JdbcMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
}
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 위반
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) 개념이 등장한 것이다.
DI의 목적은 관심사를 분리
하는 것이다.
클라이언트 코드 내부에서 의존관계 설정을 하지 않고 해당 관심사를 외부로 분리시킨다는 뜻이다.
앞의 문제를 해결할 새로운 클래스를 만들어보자.
애플리케이션 전체 동작 방식을 구성하기 위한, 구현객체를 생성하고 연결하는 책임과 역할을 가진 클래스이다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new JdbcMemberRepository();
}
}
AppConfig라는 클래스를 만들었다.
다음으로 클라이언트 코드인 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);
}
}
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();
}
}