개요
최근 좋은 기회로 새로운 팀에 합류하게 되었다🚀 사이드 프로젝트를 진행하기 위해 백엔드 개발 방식을 논의하던 중, 아래와 같은 제안을 마주했다.
Facade 컴포넌트..? Facade 패턴을 책에서 읽긴 했지만, 스프링 부트 프로젝트를 하면서 직접 사용해 본 적은 없어서 감이 쫌 안잡혔다. Interface와 Implementation으로 나누는 거랑 크게 다른건가? 🤔
그래서 오늘은 퍼사드 패턴에 대해 다시 한 번 복기하고, SRP 원칙과 연관하여 살펴보려고 한다 🚀
퍼사드 패턴 (Facade Pattern)
일단 스프링에 퍼사드를 적용하기 전에 퍼사드 패턴이 뭔지부터 알아보자. 퍼사드 패턴의 정의는 아래와 같다.
서브 시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶는 것.
고수준 인터페이스를 통해 클라이언트가 서브 시스템을 보다 편리하게 사용할 수 있게 만든다.
즉 인터페이스를 단순하게 만들고, 클라이언트와 서브 시스템을 분리하는 디자인 패턴이라고 정리할 수 있다.
왜 이런 패턴을 만들었을까? 퍼사드 패턴은 서브 시스템 (= 하위 시스템)이 많아질수록, 그리고 복잡해질수록 각 객체간 의존성이 강해져 코드를 유지 관리하기가 어려워지는 문제를 해결하기 위해 등장한 디자인 패턴이다.
퍼사드의 등장 배경에 대해 더 자세히 알아보자. 하나의 시스템이 동작하기 위해선 수많은 서브 시스템들이 각각의 비즈니스 로직을 가지고 동작해야한다. 하지만 이런 복잡한 비즈니스 로직을 클라이언트가 알아야 할 필요가 있을까? (여기서 클라이언트란 클라-서버 모델의 클라가 아니라, 특정 객체를 호출하는 객체를 의미!)
클라이언트가 하위 시스템을 직접 호출한다면, 또는 하위 시스템이 또 다른 하위 시스템과 계층간 연관성을 가진다면 특정 하위 시스템에 변경사항이 생길 때마다 연관된 모든 코드들이 영향을 받을 것이다.
@GetMapping("/users/{userId}/bookmarks}")
public List<BookmarkRes> getBookmarksById(@PathVariable Long userId) {
if (userService.existsById(userId)) {
throw new UserNotFoundException();
}
return bookmarkService.findAllByUser(userId);
}
@GetMapping("/users/{userId}/comments")
public List<CommentRes> getCommentsById(@PathVariable Long userId) {
if (userService.existsById(userId)) {
throw new UserNotFoundException();
}
return commentService.findAllByUser(userId);
}
이렇게 생긴 Controller들이 있다고 가정하자. 만약 userService.existsById의 메서드 시그니처가 바뀐다면 어떨까? userService.existsById가 사용되는 컨트롤러 코드를 모두 찾아 코드를 수정해야 한다. 지금은 예시라서 컨트롤러가 두 개지만, 해당 메서드를 사용하는 컨트롤러가 수십개라면? 로직이 좀 더 까다롭다면? Service 레이어에서 다른 도메인의 서비스를 호출하는 경우도 마찬가지다.
이런 식으로 객체들이 직접적인 의존성을 더 많이 가질수록 유지관리성, 가독성은 점점 더 나빠진다. 그럼 이 강결합 문제를 어떻게 해결할 수 있을까?
우리가 전화로 문의를 넣는 상황을 생각해보자. 우리 (=클라이언트)는 문제가 생기면 CS 상담을 통해 이를 해결한다. 문제가 해결이 된 후에도 우리는 실제 업체의 내부 판매 프로세스 (=하위 시스템)을 전혀 알 수 없고, 그저 상담원과의 통화 기록만이 남을 뿐이다.
이런 실제 상황을 통해, 우리는 클라이언트-하위 시스템 간의 강결합 문제를 해결할 수 있는 방법을 하나 생각할 수 있다.
🤔 아! 클라이언트가 하위 시스템과 바로 통신하게 만들지 말고, CS 상담원같은 중간 계층을 하나 두면 결합도가 떨어지지 않을까?
그래서 객체계의 CS 상담원, 퍼사드가 등장한 것이다 😎
정리하면?
- Facade 패턴은 객체간 의존성을 줄이기 위해 단순화된 인터페이스를 이용해 클라이언트와 하위 시스템을 분리하는 디자인 패턴이다.
- Facade는 클라이언트-하위시스템, 하위시스템-하위시스템 사이에 위치할 수 있다.
- Facade는 아래 상황에 추천한다.
- 클라이언트가 하위 시스템을 쓰기 쉽게 개선할 필요가 있을 때
- 비즈니스 계층과 프레젠테이션 계층의 상호 의존성을 줄여야 할 때
- 레거시 코드, 비즈니스 로직 등을 클라이언트에게서 숨겨야 할 때
단일 책임 원칙과 Facade
Facade 패턴은 SOLID의 단일 책임 원칙 (SRP)와도 관련이 있다. SRP는 단일 모듈은 단 하나의 변경 원인을 가져야 한다는 객체지향 원칙이다. 여기서 변경 원인이란 액터 (동일한 변경을 요청하는 한 명 이상의 사람들)을 의미한다. 따라서 SRP를 한 번 더 정리하자면, 하나의 모듈은 단 하나의 액터에 대해서만 책임져야 한다는 원칙이라고 정리할 수 있겠다.
좀 더 풀어서 살펴보자. 모듈은 단순히 함수와 데이터로 구성된 응집된 집합을 가리킨다. 모듈은 왜 단 하나의 액터에 대해서만 책임을 가져야 할까? 모듈에 대해 서로 다른 액터가 존재할 경우, 해당 모듈은 변경될 수 있는 가능성을 액터의 수 만큼 갖게 된다.
코드의 변경 가능성이 높다는 것은 코드를 수정할 일이 많아진다는 것이고, 이는 곧 코드에 오류가 생길 가능성이 높아진다는 것으로 해석할 수 있다. 시스템이 크고, 코드가 서로 얽혀있을 수록 변경 가능성은 자연히 높아진다. 그래서 우리는 각 모듈이 서로 얽히는 것을 최소화하기 위해 의존 관계를 약화시키고, 최종적으로 모듈의 결합도는 낮추고 응집도는 높이려는 시도를 하게 된다.
따라서 SRP는 모듈의 응집도를 높여 변경 원인을 축소함으로써 최종적으로 전체 시스템의 오류 가능성을 줄이려 하는 원칙이라고 이해할 수 있다.
SRP을 위반한 사례들을 통해 SRP의 중요성을 다시 한 번 알아보자.
SRP 위반 징후 (1) : 우발적 중복
위는 급여 애플리케이션 Employee 클래스로, 해당 클래스는 총 세 개의 메서드를 갖는다. 그리고 SRP를 위반했다. 왜일까? 바로 해당 클래스가 총 세 명의 액터를 가지기 때문이다 (CFO, COO, CTO). 예를 들어보자.
- CFO팀은 업무 시간을 계산하는 방식에 변경이 생겨, regularHours라는 메서드에서 계산 로직을 수정한다.
- 하지만 COO팀에는 이러한 변경 사항이 필요하지 않기에, regularHours의 메서드가 변하면 안된다.
- CFO팀은 regularHours를 바꾸고, 이로 인한 변경사항이 calculatePay에 제대로 적용된 것을 확인한다.
- COO팀은 이러한 변경을 알지 못하므로, reportHours가 이전과 다른 결과값을 갖는 것을 인지하지 못한다.
결론적으로 COO팀이 작성한 report들은 모두 엉망이 되었고, 이로 인해 수백만 달러의 잘못된 예산이 지출되었다.
위와 같은 문제는 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문에 발생한다. Employee 클래스의 개발자는 regularHours가 CFO와 COO에서 중복되기에 코드 중복을 없애려는 의도로 알고리즘을 공유하게 설계했지만, 이는 결국 끔찍한 결과를 낳았다. 이 사례에서 명심해야 하는 것은 무작정 중복을 없애는 것이 좋은 것만은 아니라는 것이다.
액터에 따라 특정 모듈이 서로 다른 변경 가능성을 갖는다는 점을 인지하고, 확장성을 고려하여 의도적으로 중복을 허용하는 것이 중요하겠다. 나는 개인적으로 DRY 원칙을 아주 좋아하는 편인데, 위와 같은 예시를 보고 중복을 마냥 없애는 것이 좋은 것은 아니라는 것을 알게 되었다. 서로 다른 액터가 의존하는 코드는 서로 분리하자! ⚠️
SRP 위반 징후 (2) : 병합
우리의 소스 파일에 아주 많은 메서드가 있다고 가정하자. 메서드가 많을 수록 병합이 발생하기가 쉬워지는데, 특히 이들 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성은 더욱 높아진다.
이게 뭔소리냐! 병합이 발생하는게 뭐냐! 잘 이해가 가지 않으니 예시를 살펴보자.
- DBA가 속한 CTO팀에서 Employee 테이블 스키마를 조금 수정하기로 결정한다.
- 동시에 인사 담당자가 속한 COO팀에서 reportHours() 메서드의 보고서 포맷을 변경하기로 결정한다.
- 이 과정에서 각각의 개발자는 Employee를 checkout 받아 각각 변경사항을 작업하고, 완료된 코드를 리모트 저장소에 올린다.
- 그리고 변경사항은 충돌하여 결과적으로 코드를 병합하게 된다. 이 과정에서 병합으로 인한 위험이 발생한다.
즉 많은 사람들이 (서로 다른 액터가) 서로 다른 목적으로 동일한 소스 파일 (단일 모듈)을 변경하는 경우에는 충돌과 병합이 뒤따르게 된다. 그래서 우리는 SRP을 이용해 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 해결 방법을 시도할 수 있다.
이제 여기서 오늘 포스팅의 주제인 Facade 패턴이 등장한다!
개발자는 데이터와 메소드를 분리해 위와 같은 문제를 해결하려고 한다. 하지만 위 과정에서 모든 클래스를 인스턴스화하고 추적해야 한다는 단점이 발생한다.
따라서 위와 같이 EmployeeFacade를 생성하고, 각 클래스의 객체를 생성한 후 요청을 특정 객체로 위임하는 시도를 할 수 있다. 반대로 데이터와 중요한 업무 규칙을 가까이 배치하고자 한다면 퍼사드를 아래와 같이 활용할 수도 있다.
중요한 업무 규칙은 Employee 클래스에 그대로 유지하되, Employee 클래스 자체를 나머지 메서드에 대한 퍼사드로 활용하는 것이다.
Spring Boot에 Facade 패턴을 어떻게 써먹을 수 있을까?
위 과정을 거쳐서, 우리는 Facade가 각 모듈간 의존성을 낮추고 응집도를 높이는 수단으로 사용될 수 있음을 배웠다. Spring 프레임워크는 여러 클라이언트에게서 아래와 같이 요청을 받을 수 있다.
위 사진을 보면 클라이언트는 하위 시스템과 너무 직접적으로 연관이 되어 있기에, 클라이언트가 하위 시스템 내부를 인식하게 된다. 이런 상황에서 데이터 저장소를 NoSQL 등 다른 DBMS로 바꾸거나, payment gateway를 다른 것으로 바꾸는 상황이 발생한다고 가정해보자.
이런 서비스 계층과 외부 리소스의 변경사항은 클라이언트 계층으로 그대로 전파가 되고, 따라서 하나만 변경해도 많은 것들을 따라서 줄줄이 수정해야 하는 문제가 발생한다.
그래서 우리는 Facade를 중간 계층에 두어 클라이언트와 하위 시스템 사이의 의존성을 끊어낼 수 있다. 클라이언트는 퍼사드에게 요청을 보내고, 퍼사드는 요청을 적절한 하위 시스템 클래스에 위임하는 방식이다. 즉 비즈니스 로직이 담긴 서비스 계층에 파사드를 이용해 간접적으로 접근하게 만들어, 잦은 변경의 위험으로부터 멀어지는 것이라고 이해할 수 있다.
그럼 내가 혼란에 빠졌던 우리 팀의 백엔드 컨벤션으로 다시 돌아와보자.
위에서 복기했던 Facade와 SRP 개념을 대입해 생각해보면 아래와 같은 이유로 도입된 친구이지 않을까 싶다.
- DTO는 레이어간 데이터 전송에만 쓰여야한다. 서비스 레이어에선 도메인 객체를 직접적으로 다루기에 DTO를 낑겨 넣으면 책임이 혼동될 수 있고, 데이터와 비즈니스 로직의 결합도가 높아진다.
- 즉 데이터 조회 및 가공 과정에서 DTO가 낀다면, 도메인 지식이 DTO에 직접적으로 연관되기에 요구사항이 달라지면 관련 DTO를 모두 수정해야 하는 문제가 생길 수 있는 것이다.
- 특히 도메인 지식은 자주 변하는 요소이기에, DTO는 더욱 변경에 취약해진다.
- 따라서 도메인 계층과 DTO 사이에 Facade를 두어 이러한 의존성을 줄이고 여러 액터가 서로에게 영향을 받지 않게 한다.
잘 추론한 건지는 모르겠다ㅋㅋ 한번 팀원분께 요런 의도가 맞는지 여쭤봐야겠다 ^____^
마무리
오늘은 팀의 개발 규칙을 보고 생긴 궁금증을 이론과 사례를 통해 직접 해소해봤다. 다만 한가지 의문이 생겼다.
근데 그럼 서비스 계층에서 무조건 엔티티를 뱉어야하지 않나? 🤔
OSIV를 비활성화해두면 프레젠테이션단에서 엔티티가 노출되어도 별 상관은 없다지만, 나는 response 등은 항상 DTO로 맵핑되어 리턴되어야 한다고 생각한다. 왜냐하면 요청에서 도메인에 속한 모든 필드를 원하지 않을 수 있는데, 굳이 엔티티로 오버헤드를 증가시킬 필요가 없기 때문이다. (select * 보다 select {특정 필드} 가 선호되는 것과 동일한 이유로)
또 User 데이터 등을 리턴한다고 하면, password 같이 외부로 노출되면 안되는 데이터를 가려주어야 하는 문제가 있다. 이를 해결하려면 프레젠테이션 계층에서 mapper 등을 이용해서 entity -> dto로 변환해주어야 하는데, 그럼 또 OCP 원칙을 어기는게 아닌가 싶기도 하고 해서 쩜 혼란스럽다 😇 (이전 프로젝트에선 controller에 mapper를 뒀는데, 그땐 controller dto와 service dto를 따로 둔 상태였다)
어쨌든 Facade가 마냥 좋은 건 아니고, 어찌보면 Facade가 만능 객체처럼 쓰일 수도 있으니 적당히 trade-off 맞춰서 주의해야겠다는 생각과 함께 오늘 포스팅 끝!
레퍼런스
- 헤드 퍼스트 디자인 패턴 : https://www.yes24.com/Product/Goods/108192370
헤드 퍼스트 디자인 패턴 - YES24
유지관리가 편리한 객체지향 소프트웨어 만들기!“『헤드 퍼스트 디자인 패턴(개정판)』 한 권이면 충분하다.이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈헤드 퍼스트〉 시리즈! 하나
www.yes24.com
- 패턴을 활용한 리팩터링 : https://product.kyobobook.co.kr/detail/S000001469867
패턴을 활용한 리팩터링 | 조슈아 케리에브스키 - 교보문고
패턴을 활용한 리팩터링 |
product.kyobobook.co.kr
퍼사드 패턴
/ 디자인 패턴들 / 구조 패턴 퍼사드 패턴 다음 이름으로도 불립니다: Facade 의도 퍼사드 패턴은 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터
refactoring.guru
클린 아키텍처 - YES24
살아있는 전설이 들려주는 실용적인 소프트웨어 아키텍처 원칙『클린 코드』와 『클린 코더』의 저자이자 전설적인 소프트웨어 장인인 로버트 C. 마틴은 이 책 『클린 아키텍처』에서 이러한
www.yes24.com
- Spring framework facade : https://springframework.guru/gang-of-four-design-patterns/facade-pattern/
Facade Pattern - Spring Framework Guru
The GoF Facade Pattern is a very common design pattern used to reduce coupling between classes, making your code cleaner and easier to maintain.
springframework.guru
- 요청과 응답으로 엔티티 대신 DTO를 사용하자 : https://tecoble.techcourse.co.kr/post/2020-08-31-dto-vs-entity/
요청과 응답으로 엔티티(Entity) 대신 DTO를 사용하자
…
tecoble.techcourse.co.kr
'Develop > Etc' 카테고리의 다른 글
[Cache/Spring] EhCache vs Cache2k vs Caffeine 캐시 비교하기 (0) | 2024.08.12 |
---|---|
[객체지향] 일급 컬렉션 (First Class Collection) (0) | 2023.11.13 |
[클린 코드] 클래스 응집도의 중요성, 그리고 완전 생성자와 값 객체 (0) | 2023.11.05 |