당니의 개발자 스토리
관심사의 분리 본문
관심사의 분리
이번 시간에는 관심사의 분리라는 제목으로 되게 중요한 이야기를 해드리겠습니다.


애플리케이션을 하나의 공연이라고 생각해봅시다. 공연을 하나 잘 만들어서 띄우는 거랑 개발을 잘 해서 이제 release 하는 거랑 어쩌면 비슷한 점이 많은 것 같아요. 각각의 인터페이스를 배역이라고 생각합시다. 그런데 실제 이 배역에 맞는 배우를 선택하는 거는 누가 해야될까요?
예를 들어서, 로미오와 줄리엣이라는 공연을 하면 로미오 역할을 누가 할지, 줄리엣 역할을 누가 할지를 배우들이 정하나요? 아니죠.
그거는 공연 기획자나 기획팀에서 하는 거지 실제 배우가, 즉 구체적인 배우가 "여주인공 섭외하고 올게요~" 하고 섭외해오는 게 아니잖아요.

자 그래서 우리가 봤던 이전 코드는 로미오 역할을 인터페이스로 보고, 레오나르도 디카프리오가 구현체로 봤을 때, 레오나르도 디카프리오 라는 배우가 직접 줄리엣 역할을 하는 여자 주인공을 직접 초빙하는 거랑 똑같은 코드인 겁니다.
다시 말해서,

OrderServiceImpl은 OrderService와 관련된 로직만 해야 되는데, 얘가 "DiscountPolicy는 FixDiscountPolicy로 해야돼~" 하고 자기가 직접 선택하는 경우인 거에요. 굉장히 구체적인 것까지 직접 선택한 거죠.
즉, OrderServiceImpl이 DiscountPolicy 라는 객체를 직접 생성하고 구체적인 할인 정책까지 선택을 해서,

이 discountPolicy에다가 할당하는 거예요.
다시 공연으로 돌아가서, 이런 경우에는 디카프리오는 공연도 해야 되고, 동시에 여자 주인공도 공연에 직접 초빙해야 하는 굉장히 다양한 책임을 가지게 됩니다. 벅차겠죠. 우리가 봤던 이전 코드가 딱 그런 코드에요.
따라서 관심사를 분리를 해야됩니다.

배우는 본인의 역할인 배역을 수행하는 것에만 딱 집중해서 해야돼요. 딱 대본 보고, 아무 사람이라도 같이 연습을 할 수 있어야됩니다.
그리고 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임은 그것만 담당하는 별도의 공연 기획자가 나와야 돼요. 하나씩 담당하는 거죠.
그래서 이제 공연 기획자가 나올 시점입니다. OrderServiceImpl이 직접 할인 정책을 선택하는 게 아니라, 대신 선택을 해줄 기획자가 있어야되는 거죠.
정리하면, 애플리케이션도 공연이랑 마찬가지로 이렇게 개발을 해야됩니다.
실제 실행되는 객체들은 본인의 역할만 수행하게 해줘야 돼요. 우리가 역할과 구현을 분리했잖아요. 그 인터페이스에 어떤 구현체들이 들어갈지, 할당될지는 공연 기획자를 해야됩니다.
자 이걸 이해한 상태에서 공연 기획자를 만들어보겠습니다.
여기서는 기획자를 AppConfig 라고 할 건데요, 지난 스프링 입문 강의에서의 SpringConfig와 조금 닮았습니다. 그런데 여기서는 스프링을 안 쓰는 거죠.

AppConfig는 애플리케이션의 전체 동작 방식을 구성(Config)한다고 해요. Configuration이라고 그러죠. 구성이라는 단어는 설정이라고 볼 수도 있습니다.
그래서 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만드는 겁니다.
지금부터 만들어볼게요.
이 설정 클래스는 애플리케이션 전반에 대한 운영을 책임지는 거라고 보시면 돼요.

hello.core 밑에다가 AppConfig 라는 클래스를 만들겠습니다. 나의 애플리케이션 전체를 설정하고 구성한다는 뜻이에요.
일단 먼저 코드를 짜고 보여드릴게요.

먼저, MemberService를 만들거에요.
이전에는 MemberServiceImpl 안에서 MemberRepository 객체를 생성하고, 구현체로 뭘 쓸건지까지 직접 했었죠.

이렇게 MemoryMemberRepository를 누가 지정해줬나요? MemberServiceImpl이 해줬단 말이에요. 이건 마치 배우가 직접 상대 배우를 섭외하는 거랑 똑같은 거에요. 그래서 어떻게 해결하냐?
이제 이런 거는 AppConfig한테 맡기는 거에요.

우리 애플리케이션에 대한 환경 구성에 대한 건 AppConfig에서 다 하는 겁니다.

일단 MemberServiceImpl이 해줬던 MemoryMemberRepository 생성을 AppConfig에서 해야하므로,

MemberServiceImpl에 있는 MemoryMemberRepository를 지웁니다. 그리고 생성자를 만들어줄 거에요.

이렇게 해서, 이 memberRepository에 구현체가 뭐가 들어갈지를 생성자를 통해서 선택을 합니다.
MemberServiceImpl이 직접 하는 게 아니라, 외부에서 파라미터로 주입해주는 거죠.
그럼 AppConfig 에서는

구현체를 생성자의 파라미터로 전달해주면 되겠죠.
이러면, 어디선가 AppConfig를 통해서 memberService()를 불러다가 쓰겠죠. 그러면, MemberServiceImpl 즉, 구현체가 생성되는데 MemoryMemberRepository가 여기서 들어갑니다.

그럼 여기에 MemoryMemberRepository가 들어가서,

이 memberRepository에 할당 됩니다.

이제 더 이상 MemberServiceImpl 안에 MemoryMemberRepository 대한 코드가 없죠.
오로지 MemberRepository 인터페이스만 있죠. MemberServiceImpl이 드디어 추상화에만 의존하는 거에요. DIP를 지키는 겁니다. 구체적으로 MemoryMemberRepository가 존재하는지도 MemberServiceImpl은 전혀 몰라요. AppConfig 가 알아서 넣어줬기 때문이죠.
이렇게 생성자를 통해서 객체가 들어간다고 해서 생성자 주입이라고 합니다.
그리고 이제 OrderService도 마찬가지로 해줍니다.

OrderService에 대한 구체적인 것도 AppConfig에서 선택을 하는거죠. OrderServiceImpl을 선택하고, 여기서도 생성자 주입을 할 거예요.

그런데 OrderService는 사용하는 필드가 두 개죠. MemberRepository랑 DiscountPolicy 2개 다 필요하죠.
OrderService의 new 부분을 둘 다 지워주고, 생성자를 만들어줍니다.

final이 있으면, 기본으로 할당하든, 생성자를 통해서 할당하든 무조건 할당이 되어야 합니다. 생성자를 보면, 두 개 다 받는 걸 볼 수 있어요.
다시 AppConfig 로 돌아가서,

이렇게 넣어주면 됩니다.
누군가 AppConfig를 통해서 orderService()를 조회하면, OrderServiceImpl 객체가 생성되고 반환되는데, 거기에는 MemoryMemberRepository랑 FixDiscountPolicy이 들어가요.

얘는 지금 생성자를 통해서 MemoryMemberRepository랑 FixDiscountPolicy이 각각 memberRepository와 discountPolicy에 할당이 되겠죠.
OrderServiceImpl은 DIP를 준수하고 있을까요? 철저하게 DIP를 지키고 있습니다. 인터페이스에만 의존하잖아요. 구체적인 클래스에 대해서 전혀 몰라요. OrderServiceImpl 입장에서는

누군가가 여기에 MemoryMemberRepository를 넣어줄지, DbMemberRepository를 넣어줄지, JdbcMemberRepository를 넣어줄지 모르는 거예요. 또 DiscountPolicy가 FixDiscountPolicy가 들어올지 RateDiscountPolicy가 들어올지 전혀 모르는 거죠.
아무튼 AppConfig를 완성했습니다.

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성합니다.
예전에는 내가 직접 new해서 생성을 하거나, 아니면 MemberServiceImpl 안에서 new 했죠. 이제 그런게 다 사라진 거에요.
AppConfig에서 필요한 구현 객체를 다 생성을 합니다. AppConfig의 역할이 이거인거죠.
그 다음에 AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입해줍니다. 주입은 연결이라고 할 수 있는데요.
이게 무슨 말이냐면,

cmd + e 해서 히스토리를 보고, AppConfig로 들어가보면,

MemberServiceImpl 같은 경우에 new 해서 MemoryMemberRepository 객체를 생성해서,

이거에 대한 참조 값을 여기에다가 넣어주게 되죠.
파라미터로 들어가는 걸 주입(Injection)해준다고 표현합니다. 생성자를 통해서 주입한다고 해서, 생성자 주입이라고 합니다.
정리해보면,


설계 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않죠. 이제 DIP를 지키는 거에요.
단지 MemberServiceImpl은 MemberRepository 라는 인터페이스에 의존을 하고 있습니다.
그리고 MemberServiceImpl 입장에서 생성자를 통해서 어떤 구현 객체가 들어올지, 뭐가 주입이 될 지는 전혀 알 수 없는 거에요. 단지 다형성에 의해서 뭔가가 들어오는 거죠.
MemberServiceImpl의 생성자를 통해서 어떤 구현 개체가 주입될지는 오직 외부에서 결정이 돼요. 옛날에는 이MemberServiceImpl 안에서 결정이 됐죠. 이제는 공연 기획자, 우리 애플리케이션의 환경을 구성하는 기획자인 AppConfig를 통해서 결정이 되는 거예요.
그래서 MemberServiceImpl은 이제부터 의존 관계에 대한 고민은 다 외부에 맡기는 거예요.
그리고 MemberServiceImpl은 이제 실행에만 집중하면 돼요.

실행에만 집중한다는 것은 save 호출하는 것과 같이 "난 뭔지 모르겠고~ 그냥 MemberRepository에 있는 save만 호출할 거야. 나는 멤버 리포지토리에 뭐가 있는지 몰라도, findById로 메모리에서 가져오든, db에서 가져오든 이제 더 이상 나의 관심사가 아니야. 나는 그냥 이 인터페이스에 맞춰서 이 기능만 호출할 거야!" 하고 개발하면 되는거죠. 그래서 실행에만 집중을 하면 됩니다.
이제 클래스 다이어그램을 보겠습니다.

이 MemberService 인터페이스를 구현한 게 MemberServiceImpl 입니다. 그리고 MemberServiceImpl은 MemberRepository 라는 인터페이스에 의존하게 됩니다. 여기까지는 이전 그림과 똑같죠.
어디에서 차이가 있냐면, 이제 AppConfig가 새로 등장을 합니다. MemberServiceImpl이라는 객체를 AppConfig가 생성을 하죠. 그리고 원래는 MemberServiceImpl이 생성했던 MemoryMemberRepository를 이제는 AppConfig가 대신 생성을 합니다.

그렇게 해서 객체의 생성과 연결은 AppConfig가 담당을 하는 거에요.
그래서 이제 DIP가 완성이 돼요. MemberServiceImpl은 MemberRepository인 추상에만 의존하면 돼요. 이제 더 이상 구체 클래스를 몰라도 됩니다.
그렇게 해서 관심사가 분리가 되는 거예요. 객체를 생성을 하고 연결하는 이 역할과 실행하는 역할이 명확하게 분리가 됐어요.

AppConfig가 객체를 생성하고 연결하고 "아 여기 인터페이스에는 뭐가 들어가야 돼요", "MemberServiceImpl 너는 MemoryMemberRepository를 써야 돼." 하고 이러한 결정을 전부 AppConfig가 다 하는 겁니다.
그리고 실제 로직이 돌아가는 건, MemberServiceImpl은 실행하는 것만 고민하면 되는 거예요. MemberServiceImpl은 그냥 인터페이스가 저장하라고 했으니 저장만 하면 되는 거예요.
자 그래서 이거를 또 조금 더 풀어서 설명을 드릴게요.


자 그래서 객체 인스턴스 다이어그램을 그리면, AppConfig가 MemberServiceImpl 객체를 만들 때 MemoryMemberRepository 객체도 생성을 했죠.
MemberServiceImpl을 생성을 할 때 이 MemoryMemberRepository의 참조 값 x001번을 생성자에다가 같이 넘깁니다.
그래서 이 MemberServiceImpl은 생성한 MemoryMemberRepository에 대한 값을 주입받게 되는거죠.

그래서 AppConfig 객체는 MemoryMemberRepository 객체를 생성하고 그 참조 값을 MemberServiceImpl을 생성하면서 생성자로 전달합니다.
그리고 클라이언트인 MemberServiceImpl 입장에서 보면, 의존관계를 마치 주입해주는 것 같다고 해서 이거를 Dependency Injection(DI) 이라고 합니다. 우리말로 번역을 하면, 의존관계 주입 또는 의존성 주입이라고 해요.
의존관계 주입이 조금 더 와닿는 것 같아요. 왜냐하면 의존이라는 것은 관계가 진짜 중요하거든요. 그래서 아까 객체는 서로가 협력하는 관계를 이루고 있다고 했잖아요.
그 다음에 OrderServiceImpl에 대해서 설명을 해드리겠습니다.

설계 변경으로 OrderServiceImpl은 더이상 FixDiscountPolicy를 의존하지 않아요.

얘는 순수한 인터페이스에만 의존을 하죠. DIP를 만족하는 거에요. 단지 DiscountPolicy 인터페이스에만 의존을 하는거죠.
OrderServiceImpl 입장에서 생성자를 통해서 어떤 구현 객체가 들어올지, 어떤 인스턴스가 들어올지는 전혀 알 수가 없어요.
그리고 OrderServiceImpl의 생성자를 통해서 어떤 구현 객체가 주입이 될지는 오직 외부인 AppConfig, 우리 애플리케이션 전체에 대한 구성을 담당하는 AppConfig에서 결정을 합니다. 그래서 OrderServiceImpl은 이제부터 실행에만 집중을 하면 됩니다.
자 그래서 결과적으로 이 OrderServiceImpl에는 MemoryMemberRepository와 FixedDiscountPolicy 라는 객체 의존관계가 최종적으로 주입이 됩니다.
이제 AppConfig를 한번 실행 해볼게요.
그 전에 MemberServiceImpl 들어가서

이걸 누르면,

어떤 문제가 있는지 알려줍니다.컴파일 오류난 데를 고치면 됩니다.
그리고 MemberApp 들어가서,

기존에는 이렇게 memberService에서 직접 객체를 생성했는데, 이제는 AppConfig가 그 역할을 하고 있으므로, AppConfig를 이용해서 애플리케이션을 만들어 보겠습니다.
먼저, AppConfig를 무조건 만들어줘야 합니다.

그리고 memberService를 AppConfig 통해서 만들어야합니다.

이렇게 AppConfig 에서 MemberService를 달라고 하면, 이 MemberService 인터페이스를 줍니다. 그리고 memberService 안에는 MemberServiceImpl이 들어가 있을 겁니다. 이렇게 코드를 수정하면 나머지 로직은 똑같습니다.
그리고 위에서 MemberService를 달라고 하면,

MemberServiceImpl 객체를 생성하면서, "내가 만든 MemberServiceImpl은 MemoryMemberRepository를 사용할 거야" 하면서 주입을 해줍니다.
이제 MemberApp을 실행해보겠습니다.

OrderApp에서 에러가 났습니다.

임시방편으로 null을 넣어서 다시 돌려보면,


잘 되는걸 확인할 수 있습니다.
이제 OrderApp으로 넘어가겠습니다.
얘도 AppConfig를 써야합니다.

그리고 AppConfig에서 memberService, orderService를 꺼냅니다.

OrderService를 들어가보면, 생성자로 MemoryMemberRepository와 FixedDiscountPolicy 두 개를 넘깁니다.

그렇게 해서 OrderServiceImpl이 MemoryMemberRepository와 FixedDiscountPolicy 객체를 참조(의존)하도록 그림을 완성시키고, 완성된 OrderServiceImpl 객체를 반환하는 거에요.
돌려보면,

잘 되는걸 볼 수 있습니다.
이러면, OrderApp도 더 이상 구체 클래스에 의존할 필요가 없는거죠.
그리고 테스트 코드 오류도 수정해줘야합니다.

얘도 수정을 해줘야합니다.
먼저, MemberServiceTest에 들어가서

AppConfig를 사용하도록 수정해야 합니다. 좀 다르게 바꿔보겠습니다.
@BeforeEach를 이용할 건데,

얘는 각 테스트를 실행하기 전에 무조건 실행이 되는거에요.

이렇게 하면, 각각의 테스트를 실행하기 전에 AppConfig를 만들고, memberService를 할당해줍니다. 그리고나서, 이 테스트가 도는 겁니다.
그 다음은 OrderServiceTest도 수정해보겠습니다.
얘는 필드가 2개 필요합니다. cmd + e 해서 이전 클래스를 열어서 @BeforeEach를 복붙할 겁니다.

이렇게 해서 테스트들을 다 돌려보면,

잘 돌아갑니다.
자 이렇게 해서 테스트까지 다 바꿔봤고 관심사를 분리를 해봤습니다. 이제 DIP를 지키게 된 거죠.

정리해보면, AppConfig를 통해서 관심사를 확실하게 분리했죠.
그래서 AppConfig는 공연 기획자로 보시면 돼요. 우리 애플리케이션이 어떤 식으로 동작해야 될지를 기획하는 공연 기획자로 보시면 됩니다.
그래서 이제부터는 AppConfig가 구체 클래스를 직접 선택을 해요. 배역에 맞는 담당 배우를 AppConfig가 선택을 해서 할당을 해줍니다. 그래서 애플리케이션이 어떻게 동작할지에 대한 전체 구성, Configuration을 책임집니다.
자 이제부터 각 배우들은 본인의 담당 기능을 실행하는 책임만 지면 됩니다.
그리고 OrderServiceImpl은 의존관계를 부여하고 객체 생성하고, 이런 거에 대해서는 멀어지는 거에요. 그냥 딱 실행만 고민하면 돼요. 대본 가지고 연기하는 것만 고민하면 되는 거에요.
그리고 OrderServiceImpl은 기능을 실행하는 책임만 딱 지면 되죠.
OrderServiceImpl를 다시 보면,

뭐를 내놔라! 하는 구체적인 요구가 전혀 없죠.
그리고 실행하는 부분에는 인터페이스만 고민하고, 이 인터페이스를 보고 그냥 개발하면 되는거에요. 구체 클래스에 대해서 뭐가 될지 고민할 필요가 전혀 없어요. 실행만 고민하면 됩니다.
여기까지 애플리케이션 전체 구성을 책임지는 담당자(AppConfig)를 만들어 봤구요.
기존에 너무 많은 역할과 책임이 OrderServiceImpl이나 MemberServiceImpl에 있었죠. 이제 이것들을 역할과 책임을 적절하게 잘 분리를 했습니다. 단일 책임 원칙을 지킨거죠.
이제 다음 시간에는 이 AppConfig를 조금 리팩토링을 해볼게요. 왜냐하면 지금은 이 AppConfig를 보면 뭔가 크게 다가오지가 않아요.
그래서 이 AppConfig의 문제점과 더 나은 방향으로 리팩토링 하는 거에 대해서 설명을 드리겠습니다.
'스프링 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
| 새로운 구조와 할인 정책 적용 (0) | 2024.01.18 |
|---|---|
| AppConfig 리팩터링 (0) | 2024.01.18 |
| 새로운 할인 정책 적용과 문제점 (0) | 2024.01.18 |
| 새로운 할인 정책 개발 (0) | 2024.01.18 |
| 주문과 할인 도메인 실행과 테스트 (0) | 2024.01.17 |