Facade 패턴 이해하기
Facade 패턴
파사드(Facade) 패턴은 복잡한 클래스 구조에 대해 사용하기 편하게 간편한 인터페이스(API)를 구성하기 위한 구조 패턴이다. 예시를 들자면, 라이브러리의 클래스와 메서드가 목적과 동작을 이해하기 어려워 사용하기에 난이도가 높을 때, 적절한 네이밍과 정리를 통해 사용자가 쉽게 라이브러리를 다룰 수 있는 인터페이스를 만드는 것과 같다.
우리가 만드는 프로그램은 기능을 추가하고 서비스를 업데이트 할수록 점점 커지고 서로 관계를 맺으며 복잡해진다. 어떤 요청을 처리하는 과정을 개별적으로 제어하는 것이 아니라, 상호 관련된 클래스들을 적절히 제어하는 일종의 창구를 통해 중계한다면 복잡한 절차 없이 원하는 결과를 얻을 수 있다.
이처럼 파사드(Facade) 패턴을 통해 시스템의 전체를 알지 못하더라도 원하는 결과를 가져올 수 있고, 복잡한 클래스를 정리하여 의존성을 낮출 수 있다.
Facade 패턴 구조
•
Facade
◦
여러 시스템과 상호 작용하는 복잡한 로직을 재정리한 높은 레벨의 인터페이스이다.
◦
서브 시스템의 많은 역할들을 묶어 처리하는 단순한 창구의 역할을 맡는다.
◦
Facade를 통해 클라이언트와 서브 시스템의 의존도를 낮추어 준다.
•
Additional Facade
◦
Facade 패턴에 파사드 클래스를 반드시 한 개만 둘 필요가 없고, 재귀적으로 파사드 클래스를 호출하여 처리하게 위임할 수 있다.
•
SubSystem
◦
실질적 동작을 수행하는 수십 가지 라이브러리나 클래스들
파사드 패턴은 전략 패턴이나 팩터리 패턴과 같은 디자인 패턴과는 달리 클래스 구조가 정형화 되어있지 않다. 클래스를 어디에 두고 어떤 형식으로 위임해야하고 하는 제약이 없고, 파사드 클래스를 만들어 적절히 기능 집약과 의존성 분리를 해주면 파사드 패턴이 되는 것이다.
그렇기 때문에 시스템이 너무 복잡하거나 시스템의 결합도가 너무 높아 의존성을 낮추고 싶을 때 주로 사용된다.
Facade 패턴 장점
•
하위 시스템의 코드를 분리하여, 외부에서 해당 시스템을 사용하기 쉬워진다.
•
하위 시스템 간의 의존성을 낮추고, 의존성을 한 곳을 집약하여 관리하기 용이해진다.
•
하위 시스템의 이해가 부족하더라도, Facade 클래스의 이해만으로 시스템을 사용하는 것이 가능해진다.
Facade 패턴 단점
•
파사드 클래스가 모든 하위 클래스가 결합된 God 객체가 될 수 있다.
•
파사드 클래스 자체가 서브 시스템에 의존성을 가지기 때문에, 의존성 자체를 없앨 수는 없다.
•
파사드 클래스로 인해 코드 자체가 늘어나기 때문에, 유지보수 측면에서 공수가 더 많이 들게 된다.
Cabi의 구조와 문제점
현재 Cabi는 Controller - Service - Repository에 더해, Facade 패턴과 OptionalFetcher를 추가해서 Controller - FacadeService - Service - OptionalFetcher - Repository 구조를 가지고 있다.
사실 여기에는 문제가 있다. Facade 패턴의 주된 사용 목적은 그 이름처럼 비즈니스 로직 앞 단에서 클라이언트가 해당 비즈니스 로직을 신경쓰지 않고 가져오는 것과, 결합도를 낮추고 가독성을 향상하는데에 있다. 하지만 Cabi의 구조에서는 CQRS 개념을 도입하여, Controller에서 Read의 동작은 FacadeService에서 직접 수행하고, CUD 동작은 하위의 Service를 호출해서 수행한다. 이는 Facade 패턴의 사용 목적과도 부합하지 않기도 하고, CUD의 경우에는 FacadeService 계층이 하는 일이라곤 그저 데이터를 전달하는 통로에 불과하다.
두 번째 문제는 OptionalFetcher에 있다. OptionalFetcher의 경우에는 도입하게 된 이유가 Service가 Repository에서 데이터를 불러왔을 때 데이터가 꼭 있어야 하는 경우 exception을 던지는 걸 묶어서 처리하기 위함이였다. 때문에 초기에는 이름도 ExceptionHandler 였으며, 이름을 바꾸며 기능을 명확히 한 이후에도 단순히 Optional 형태의 데이터를 받아와 get과 find로 분리하여 get의 경우에는 Optional이 null인 경우 exception을 던지고 find는 null을 그대로 반환한다. 말 그대로 Optional 형태의 데이터를 꺼내주는 역할 밖에 없었다는 의미이다.
사실 이러한 기능은 Repository 자체에서도 충분히 구현 가능하다. Spring Data JPA의 경우에는 Optional이 아닌 데이터를 직접 받는 것이 가능하고, 그련 경우에 데이터가 없으면 null을 넣어준다. 다만 서비스에서 null을 직접 확인하고 exception을 던져야 하는 번거로움이 있을 뿐이다. 또한 List 형태로 데이터를 여러 개 받아오는 경우에는 OptionalFetcher는 그저 데이터를 전달하는 통로 역할 밖에 하지 않는다.
최근에 이러한 OptionalFetcher의 기능적 부실함과 모호함으로 인해, OptionalFetcher 이름을 바꾸고 여러 데이터를 조합해서 서비스에서 데이터를 가져다가 사용할 수 있도록 하는 Repository의 상위 계층 개념으로 사용하자는 의견이 있었다. 이번에 Redis 관련해 리팩토링을 하던 중, Redis를 OptionalFetcher처럼 사용해야하나 아니면 OptionalFetcher 안으로 넣어서 사용해야하나 고민을 하게 되었다. 그러던 중 이런 Cabi의 구조를 생각해보다가 이런 구조적 문제점들을 발견하게 되었고 이 글을 쓰게 되었다.
새로운 구조 제안하기
이러한 두 가지 문제를 해소하기 위해 고민하던 중 한 가지 방안이 떠올랐다.
먼저 현재의 FacadeService에서 CUD만 Service를 호출하고 Read는 OptionalFetcher를 직접 호출하는 구조에서, Facade 패턴의 목적에 맞게 FacadeService - Service 구조를 아예 분리하는 것이다. CQRS가 필요하다면 Service를 CommandService와 QueryService로 분리하고, FacadeService는 목적에 따라 CommandService와 QueryService에서 필요한 기능을 호출하여 사용하는게 맞는 구조라 생각한다.
이렇게 FacadeService를 분리를 하고자 생각을 하고 나니, Facade와 Service의 목적을 분명하게 생각할 수 있었다.
•
QueryService : 필요한 데이터를 조회해오기
•
CommandService : 요청한 동작(CUD)을 수행하기
•
FacadeService : 하위 Service에 데이터 처리나 조회를 요청하기 + 조회해온 데이터로 응답에 필요한 데이터 조합하기
물론 이렇게 목적을 분명히하고 기능을 나눈다고 하여도, 요청 로직에 따라 Facade가 단순한 통로 역할만을 수행하게 되는 경우도 분명히 있을 것이다. 하지만 이후 해당 조회나 동작 수행에 추가적인 검증 로직이 필요하다면 Facade에 검증하는 Service의 기능만 추가하면 되니, 확장성과 유지보수 측면에서 훨씬 개선될 것으로 예상된다.
다만 이와 같이 구조를 작성하기 위해서는 Service 기능을 단순히 데이터를 받아오는데 그치지 않고, 기능의 단일 책임과 재사용성을 고려해 Service의 기능이 명확해야 할 것 같다.
여기까지 생각하고 나니, OptionalFetcher가 있어야 하는 의미를 잃어버린 것 같다. 현재의 OptionalFetcher의 값이 없을 때 null을 넣거나 exception을 던지는 기능은, JPA와 Service에서 대체할 수 있다. 다만 OptionalFetcher가 없어지는 경우의 문제는 값이 null일 때 던져야 하는 exception이 변경되는 경우 모든 코드를 찾아보며 바꾸어주어야 한다는 점이 있지만, 현재 Cabi에서 exception은 예외 발생 계층에 따라 항상 DomainException을 던진다는 점과 이후 세분화가 필요하다면 ExceptionHandler를 따로 두고 전체적으로 Exception을 관리해주는 책임을 가지는 별개의 객체를 두는 게 더 나아보인다.
결론을 내리자면, Facade 패턴의 목적에 맞지 않는 사용과 OptionalFetcher의 모호함을 근거로 Controller - FacadeService - (QueryService / CommandService) - Repository의 구조로 변경하는 것을 제안한다는 것이다.
리팩토링 구조
위 구조도와 같이 Service 계층으로 FacadeService + (QueryService / CommandService) 구조로 나누어 가져간다. Facade 패턴의 목적에 맞게 해당 패키지의 Controller는 해당 FacadeService만 바라보고, FacadeService에서 여러 기능을 구현한 QueryService와 CommandService들을 호출해 데이터를 조합하거나 삽입, 수정, 삭제를 수행한다.
위 구조도는 유사한 예시일 뿐 정확히 일치하지 않는다.
리팩토링 가이드