이 시리즈는 인프런 강의(김영한 님의 ‘스프링 핵심 원리 - 기본편’)로 공부하며 혼자 기록하고, 사람들과도 공유할 수 있도록 작성하는 글이다. 최대한 추가적인 정보는 공식 홈페이지, 문서를 보며 얻을 예정이다.
(개인적인 생각과 이해가 들어가 있기 때문에 저의 ‘무식함’이 있을 수 있습니다😜 혹시라도 이 글을 보게 되시는 분이 계시다면 잘못된 부분 댓글로 많이 알려주시면 너무 감사하겠습니다!!)
GitHub Repository : https://github.com/jcw1031/spring-core-study
처음에 관심사의 분리라는 말만 들었을 때 무슨 의미인지 몰랐다. 강의에서 예시를 들어 이해하기 쉽게 설명해 주셔서 해당 예시로 설명해 보도록 하겠다.
애플리케이션의 하나의 공연이고, 각각의 인터페이스를 배역이라고 생각하자. 구현체들은 배역을 수행하는 배우들이다. B 배역의 배우를 A 배역의 배우가 직접 선택하지 않는다. 하지만 우리의 이전 코드는 배우가 상대 배역의 배우를 선택하고 초빙하는 것과 같다. A 배역의 배우는 상대 배역의 배우를 선택하고, 직접 초빙까지 해야 하는 다양한 책임을 갖고 있다. 이 내용은 굉장히 중요한 내용이다!!
배우는 본인의 역할인 배역을 잘 수행하는 것에 집중해야 한다. 상대 배역의 배우가 어떤 배우든지 연기를 할 수 있어야 한다. 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 가진 별도의 공연 기획자가 필요하다. 이 전 시간 마지막에 얘기했던 그 ‘누군가’가 바로 공연 기획자, AppConfig
이다.
애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고 연결하는 책임을 가진 별도의 설정 클래스를 만들어 보자.
core
패키지에 AppConfig
클래스를 생성해 놓는다.
그리고 MemberServiceImpl
로 한 번 가보자.
이 전에는 회원 저장소 인터페이스의 객체를 생성하고 어떤 구현체를 할당할지를 회원 서비스 구현체 MemberServiceImpl
이 직접 선택하고 생성하였다. 이제는 회원 저장소 인터페이스에만 의존하도록 코드를 변경해야 한다.
인터페이스 객체를 선언하고, 생성자를 통해 구현체를 받아서 인터페이스 객체에 할당하는 방식이다. 생성자에서 받아오는 구현체도 모두 MemberRepository
인터페이스의 구현체이기 때문에 MemberRepository
로 받아온다. 이렇게 하면 MemberServiceImpl
은 MemberRepository
인터페이스에만 의존하게 된다.
MemberServiceImpl
에MemoryMemberRepository
에 대한 코드는 전혀 찾아볼 수 없다!
다시 AppConfig
로 돌아가 보자. MemberServiceImpl
의 생성자를 통해 MemberRepository
의 구현체를 할당하고 있다. MemberService
는 구현체가 어떤 것이 들어올지 알지 못한다. 하지만 MemoryMemberRepository
든 DbMemberRepository
든 MemberRepository
역할을 구현하는 것이기 때문에 문제가 되지 않는다.
이제 OrderServiceImpl
도 똑같이 변경해 보자. 회원 저장소와 할인 정책이 필요하니 MemberRepository
인터페이스 객체와 DiscountPolicy
인터페이스 객체를 선언한다. 그리고 생성자를 통해 MemberRepository
구현체와 DiscountPolicy
의 구현체를 받아와 할당시킨다.
AppConfig
에도 코드를 추가하도록 하자. OrderServiceImpl
의 생성자를 통해 MemberRepository
구현체와 DiscountPolicy
구현체를 할당하고 있다.
이제 AppConfig
가 애플리케이션의 실제 동작에 필요한 구현 객체를 생성하도록 하였다. 생성한 객체 인스턴스의 참조를 생성자를 통해 주입해준다.
MemberServiceImpl
→MemoryMemberRepository
OrderServiceImpl
→MemoryMemberRepository
,FixDiscountPolicy
아래의 구조로 변경된 것이다.
이로써 DIP를 준수할 수 있게 되었다. MemberServiceImpl
은 MemberRepository
인터페이스에만 의존하면 되고 구현체 클래스를 몰라도 되기 때문이다. 객체를 생성하고 연결하는 역할(AppConfig
)과 실행(xxxServiceImpl
)하는 역할이 명확하게 분리되었다!!
클라이언트인
MemberServiceImpl
입장에서는 마치 의존관계를 외부에서 주입해 주는 것 같다고 하여 이를 DI(Dependency Injection), 우리말로는 의존관계 주입 또는 의존성 주입이라고 한다.
이제 AppConfig
를 사용해 의존관계를 주입해주도록 하여 실행을 해보도록 하자. 기존의 main
메서드가 있는 클래스들의 코드를 수정하여 실행해 보자.
기존에 MemberService
의 구현체를 직접 생성하여 할당했던 코드는 지운다. 이제 AppConfig
객체를 생성하고, MemberService
객체에 memberService()
메서드(AppConfig
에 만들었던 메서드)를 통해 구현체를 할당한다. 나머지 코드는 동일하다.
AppConfig
의memberService()
메서드가 기억이 나지 않으면 위로 가서 한 번 보고 오자.
실행을 해보기 전에, OrderApp
에서 생성자 오류가 발생하기 때문에(기본 생성자가 없기 때문에) 일단은 null
을 할당하자.
다시 MemberApp
으로 와서 실행을 해보자. 역시 문제없이 잘 작동한다.
OrderApp
도 수정한다. MemberApp
과 마찬가지로 기존에 직접 구현체를 생성하여 할당하던 코드는 과감히 지우고, AppConfig
객체를 생성하여 memberService()
와 orderService()
를 통해 구현체를 할당한다.
역시 OrderApp
도 잘 동작한다.
기존에 작성했던 테스트 코드도 수정해 주어야 한다.
MemberServiceTest
의 기존에 있던 구현체를 생성하여 할당하던 코드를 지우고, 인터페이스 객체를 선언하는 코드를 추가한다. 그리고 인터페이스 객체에 구현체를 할당해 주기 위해 @BeforeEach
어노테이션을 사용한다.
@BeforeEach
어노테이션은 각 테스트가 시작되기 전에 무조건 실행되는 부분을 나타낸다.
@BeforeEach
어노테이션 아래에 메서드를 만들고, AppConfig
를 생성하여 memberService()
를 사용해 인터페이스 객체에 구현체를 할당한다.
OrderServiceTest
도 동일하게 수정하면 된다.
테스트를 실행해 보면, 모두 초록빛이다 ✅
AppConfig
를 통해 관심사를 확실하게 분리하였다. 애플리케이션의 실제 동작에 필요한 구현 객체를 생성하고, 이를 주입해 주고 있다. xxxServiceImpl
은 인터페이스에만 의존하고, 자신의 기능을 수행하는 책임만 가지게 되었다.
지금까지 ‘구현체를 변경하였을 때 클라이언트에 변경이 있으면 안 된다.’라는 주제를 꽤 오랜 시간 다룬 것 같다. 그만큼 중요하다는 의미인 것 같다. 이제 다음 시간에는 AppConfig
를 리팩터링 하고 정액 할인 정책에서 정률 할인 정책으로 변경했을 때 클라이언트에도 영향이 미치는지, 그리고 지금까지의 전체적인 흐름을 한 번 훑어보도록 하자.