퍼사드 패턴에 대한 설명은 깃허브 에 있다.
리팩토링 진행도중에 좀 좋은 구조를 구성해서 적어본다.
우선 Service
레이어에서 Repository
를 너무 많이 의존하는것이 상당히 컸다.
여러군데에서 가져오는 것
사실 이것은 개발하면서 어쩔 수 없는 노릇일 수 있다.
그러나 의존을 너무 많이 가져간다는 것은 유지보수 측면에서도 힘들것이고,
응집도도 낮은 그런 클래스가 되어버린다.
그래서 일단 생각해 낸것은 우리는 객체지향을 사용하고 있다.
어떤 한 클래스는 조합하는것만을 목표로 하는 클래스가 있다면 어떨까🤔 하고 생각했다.
이 클래스를 생각한것은 기존 프로젝트에 테스트 코드를 추가하려고 하다보니까 고민하게 되었다. 🤔
기존 클래스에서 테스트를 하려면 너무 필요없는 의존성까지 추가해줘야해서 자칫하면 혼동을 불러올 수가 있었다.
그래서 생각한 것이 퍼사드 패턴...
이 퍼사드 클래스가 여러개의 Service
레이어를 두면 그것들을 한데 모아서
처리를 해주면 의존은 낮아지면서 응집도는 올릴 수 있을거라고 생각했다.
왜?
각각 역할에 대한 Repository
와 필요한 의존성만 가지고 하나의 Service
로 나눈것을 퍼사드쪽에서
조립해서 써주면 지금과 같은 로직이 되면서 동시에 테스트도 잘 가져갈 수 있을거라 생각했다.
예를 들어 이런 클래스가 있다고 가정하자.
Repository
객체를 두개를 사용하고 있다.
이게 2개일 수 있고 그 이상이 될 수도 있다.
이 구조를 개선하려고 했던 다음 클래스 다이어그램을 보도록 하자.
이런식으로 하위 객체를 의존성을 1개씩만 갖게하고 퍼사드에서는 조합만 해주면 되는게 된다.
이렇게 되면 각 기능에 대한 단위 테스트를 깔끔하게 진행할 수 있고,
문제가 발생하는 로직에 한해서만 유지보수를 가능할 수 있게 되었다.
결국엔 Spring MVC
패턴이 그냥 이 퍼사드 패턴이 적용되어 있다 라고 생각하면 되지만,
아래의 의존성을 덕지덕지 붙여가면서 하나의 서비스를 뚱뚱하게 유지하기는 싫었다.
예시의 코드로 보자면
멤버와 아이템두개를 합쳐서 보여주는게 있다고 하자.
Member.java
public class Member {
private Long id;
private String name;
private String email;
}
Item.java
public class Item {
private Long id;
private String name;
private int price;
//getter, setter 생략
}
Repository
public class MemberRepository extends JpaRepository<Member, Long> {
}
public class ItemRepository extends JpaRepository<Item, Long> {
}
@Service
public class FooService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private ItemRepository itemRepository;
public void getMemberAndItem(int id) {
Member member = memberRepository.findById(id).orElseThrow();
Item item = itemRepository.findById(id).orElseThrow();
//두 값의 비즈니스 로직을 수행
return //두개를 합쳐 반환한다.
}
}
간결하게 두줄만 작성했지만 벌써 두개 조회를 동시에 하고 안에서 로직을 구현하고 있으니 스파게티 코드가 생성될 것이 예상된다.
이 구조를 아래와 같이 변경한다.
기본적으로 생성자 주입으로 변경을 해준다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(fianl MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member findById(final int id) {
Member member = memberRepository.findById(id).orElseThrow();
// member에 대한 로직 수행...
return member;
}
}
public class ItemService {
private final ItemRepository itemRepository;
public ItemService(final ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
public Item findById(final int id) {
Item item = itemRepository.findById(id).orElseThrow();
// item에 대한 로직 수행...
return item;
}
}
일단 서비스를 위와같이 두개로 분리한다. 그러면 각 Repository
에 대해 해야할 일만 명확하게 구분이 되어진다.
이것을 사용할 퍼사드로 넣어주면 되는 것이다 👍
public class TogetherFacade {
private final MemberService memberService;
private final ItemService itemService;
public TogetherFacade(final MemberService memberService, final ItemService itemService) {
this.memberService = memberService;
this.itemService = itemService;
}
public void 전부를_만들어_돌려준다(final int id) {
//이미 비즈니스 로직은 각 서비스에서 실행되었다.
Member member = memberService.findById(id);
Item item = itemService.findById(id);
// 받은 그대로 조합식만 구성해주면 되는 것
//return....
}
}
이렇게 되었을 때, 각 서비스 레이어부터 단위테스트를 잘 구성해서 퍼사드쪽으로 넘어올 수가 있다.
테스트만을 위한 코드작성?
이라고는 볼 수 없을 것 같다.
결국 모든 프로그래밍의 구조를 따진다면 조합해서 놓고보면 쭉 이어진 코드는 맞다.
하지만 객체지향의 의미는 해당 객체에게 메세지를 보내 기능을 수행하도록 구성하는게 바람직하다.
그렇기에 이렇게 분리해주면 오류가 어디서 나는지 명확하게 짚을 수 있다.
그러면서 단일 책임 원칙을 준수하게 된 아래의 레이어들이다. 🤩
여기서 더 어떻게 고쳐야하나? 라는건 나의 남은 숙제다 ㅋㅋㅋㅋㅋ
일단 로직이 더러운것은 놔두고 각자의 역할만 따로따로 분리해서 만들고 보니
로직이 더러운게 아니었다. 그냥 모든 코드가 합쳐져서 한 메소드가
많은 일을 수행하고 있던거지 로직 자체가 이상하게 짜여져있는 것은 아니었다. (물론 이 클래스만 그랬을거다.)
Mocking
을 하면서 Mockito
로 각 서비스 레이어를 테스트하였다.
@Autowired
로 되어있던 것을 생성자 주입으로 바꾸면서 이 테스트도 진행할 수 있게 되었던건데,
이것은 다음 포스트에서 자세하게 다뤄보도록 하겠다.
아무튼 구조를 개선하여 조금 더 깔끔하게 코드를 관리할 수 있게 되었다. 🤣