퍼사드 (Facade) 패턴
- 복잡한 서브 시스템 의존성을 최소화하는 디자인 패턴
문제
의존성이 많을수록 해당 코드를 변경하기가 어려운데, 코드가 변경되어야 하는 이유도 많아지며, 테스트하기도 어려워집니다.
레이어드 아키텍처로 어느 정도 의존성을 해소할 수 있었으나, 플랫폼이 커질수록 서비스 레이어(Service Layer)가 의존하는 리포지토리(Repository)와 외부 라이브러리들이 많아질 수밖에 없는 구조였습니다.
특히 JPA를 사용하는 환경에서는 도메인 간 상호작용이 많아질수록 Repository 의존성이 과다하게 증가하여, Mock을 사용하는 단위 테스트는 검증이 제대로 되지 않을 뿐만 아니라 목을 하나하나 지정해주기도 어려웠습니다.
이러한 문제를 해결하기 위해 퍼사드 패턴(Facade Pattern)을 적용한 사례를 비슷한 예시로 정리해 보았습니다.
퍼사드 패턴이란?
퍼사드 패턴은 복잡한 서브시스템을 간단한 인터페이스로 제공하여 클라이언트와의 결합도를 낮추는 구조적 디자인 패턴입니다.
여러 클래스나 인터페이스가 복잡하게 얽혀 있는 시스템에서 단순한 인터페이스를 제공함으로써 사용자는 서브시스템의 복잡한 내부 구현을 몰라도 쉽게 기능을 사용할 수 있습니다.
REST API도 일종의 퍼사드 패턴입니다.
@RequestMapping("/api/members")
public class MemberAPI {
private final MemberService memberService;
@GetMapping
public ResponseEntity<Page<MemberListDTO>> getMembers(PageDTO dto) {
return ResponseEntity.ok(memberService.getMembers(dto));
}
}
클라이언트는 복잡한 내부 구조를 몰라도 Service의 호출만으로 쉽게 기능을 사용합니다.
레이어드 아키텍처의 문제점
Service Layer가 직접 Repository와 상호작용하며, JPA를 통해 데이터를 처리하고 있었습니다.
도메인 간 상호작용이 많아질수록 Service Layer의 코드가 복잡해지고, Repository 의존성도 증가했습니다. (findById, save, saveAll 등 사용을 위해 엔티티별 Repository를 의존하게 됨)
이러한 구조에서는 단위 테스트 시 많은 Repository를 Mock으로 구현해야 했으며, 이는 매우 번거롭고 비효율적이었습니다.
많게는 하나의 Service에서 20개 이상의 Repository를 의존하고, 하나의 메서드에서 서로 다른 Repository를 많게는 5개 이상 사용되고 있는 부분들도 보았습니다.
기존 구조의 예시 코드
의존성이 많은 기존 구조의 간단한 예시 코드입니다. 원래는 cascade 설정을 통해 연관관계 엔티티를 한번에 저장할 수 있지만, 쉬운 예시를 위해 아래와 같이 작성했습니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final MemberFileRepository memberfileRepository;
private final AttachFileRepository attachFileRepository;
private final AddressRepository addressRepository;
private final LockerRepository lockerRepository;
private final TeamRepository teamRepository;
@Transactional
public void createMember(MemberSaveFormDto form) {
Member member = memberRepository.save(form.toMemberEntity());
Team team = teamRepository.save(Team.builder()
.name(form.getTeamName())
.build());
Locker locker = lockerRepository.save(Locker.builder()
.number(1L)
.build());
Address address = addressRepository.save(form.toAddressEntity());
member.changeTeam(team);
member.changeLocker(locker);
member.changeAddress(address);
AttachFile attachFile = attachFileRepository.save(uploadFile(form.getProfile(), member.getName()));
MemberFile memberFile = memberfileRepository.save(MemberFile.builder()
.member(member)
.attachFile(attachFile)
.position(PROFILE)
.build());
}
private AttachFile uploadFile(MultipartFile file, String memberName) {
// 파일 업로드 로직
return new AttachFile();
}
}
이러한 구조는 아이러니 하게도 레이어드 아키텍처의 원칙을 지키기 위해 Service에서 다른 Service를 의존하지 않도록 개발한 결과입니다. 즉, Service가 다른 Service를 의존하지 않으므로 모듈화할 수 있는 부분도 각각 따로 개발된 것입니다.
의존성이 많은 클래스는 코드가 복잡해져서 읽기 어려워지고, 하나의 의존성이 변경될 때마다 영향을 받을 가능성이 큽니다. 이는 유지보수를 복잡하고 어렵게 만듭니다.
해결 방안
관련 글들을 살펴보면, 순환 참조를 피해 Service가 다른 Service를 단방향으로 참조하여 공통 코드를 모듈화하고 의존성을 일부 해결하는 것이 나쁜 방법은 아니라는 의견도 많습니다.
레이어드 아키텍처에서 플랫폼이 커지면 Service Layer가 비대해지는 것은 피할 수 없는 문제라는 의견도 많이 존재합니다.
그럼에도 불구하고, Service가 다른 Service를 참조하게 하면 협업 환경에서 순환 참조 에러 등의 예기치 못한 문제가 발생할 가능성이 큽니다. 따라서 이러한 문제를 해결하기 위해 퍼사드 패턴을 적용하는 것이 적절하다고 판단했습니다.
적용할 퍼사드 패턴 구조
퍼사드 패턴 적용
퍼사드 패턴을 적용하여 리포지토리와의 직접적인 상호작용을 서비스 레이어에서 분리하였습니다. 퍼사드 클래스를 여러 개로 분리하여 의존성을 관리하고, 서비스 레이어에서 이를 사용하는 구조로 개선하였습니다.
회원과 관련된 의존성을 퍼사드로 분리합니다.
// MemberManagementFacade
@Component
@RequiredArgsConstructor
public class MemberManagementFacade {
private final MemberRepository memberRepository;
private final TeamRepository teamRepository;
private final LockerRepository lockerRepository;
private final AddressRepository addressRepository;
@Transactional
public Member manageMember(MemberSaveFormDto form) {
Member member = memberRepository.save(form.toMemberEntity());
Team team = teamRepository.save(Team.builder()
.name(form.getTeamName())
.build());
Locker locker = lockerRepository.save(Locker.builder()
.number(1L)
.build());
Address address = addressRepository.save(form.toAddressEntity());
member.changeTeam(team);
member.changeLocker(locker);
member.changeAddress(address);
return member;
}
}
파일과 관련된 의존성을 퍼사드로 분리합니다.
// FileManagementFacade
@Component
@RequiredArgsConstructor
public class FileManagementFacade {
private final MemberFileRepository memberfileRepository;
private final AttachFileRepository attachFileRepository;
@Transactional
public MemberFile manageFile(Member member, MultipartFile profile) {
AttachFile attachFile = attachFileRepository.save(uploadFile(profile, member.getName()));
return memberfileRepository.save(MemberFile.builder()
.member(member)
.attachFile(attachFile)
.position(PROFILE)
.build());
}
private AttachFile uploadFile(MultipartFile file, String memberName) {
// 파일 업로드 로직
return new AttachFile();
}
}
서비스 레이어에서 퍼사드 사용
서비스 레이어에서는 퍼사드 클래스를 통해 리포지토리와 상호작용하도록 변경합니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberManagementFacade memberManagementFacade;
private final FileManagementFacade fileManagementFacade;
@Transactional
public void createMember(MemberSaveFormDto form) {
Member member = memberManagementFacade.manageMember(form);
MemberFile memberFile = fileManagementFacade.manageFile(member, form.getProfile());
}
}
퍼사드 패턴을 통한 Repository와의 상호작용 분리
퍼사드 패턴을 적용하여 Repository와의 직접적인 상호작용을 Service Layer에서 분리하였습니다. 이를 통해 Service Layer의 코드가 간결해지고, 의존성도 감소하였습니다.
그러나 퍼사드로 분리하면, 이전 Service처럼 퍼사드가 거대해지는 문제를 피하기 어렵습니다. 그렇지만 퍼사드 클래스를 적절히 분리하고 책임을 분산시켜 복잡성을 줄이는 방법을 통해 문제를 완화할 수 있습니다.
이렇게 만들어진 퍼사드는 다른 Service Layer에서 의존하여 사용하더라도 의존 관계를 해치지 않기 때문에 순환 참조 문제를 피할 수 있습니다.
결론적으로 퍼사드가 거대해지는 문제를 완전히 피하기는 어렵지만, 이를 적절히 관리하여 코드의 유지보수성과 가독성을 높일 수 있습니다.
퍼사드 패턴 적용 후의 장점
- 코드 간결화: Service Layer의 코드가 간결해지고 가독성이 향상되었습니다.
- 의존성 감소: Service Layer의 Repository 의존성이 퍼사드 클래스로 집중되어 관리가 용이해졌습니다.
- 테스트 용이성: 퍼사드 클래스를 Mock으로 대체하여 단위 테스트를 보다 쉽게 수행할 수 있게 되었습니다.
퍼사드 패턴 적용 후의 단점
- 퍼사드 클래스의 복잡성 증가: 퍼사드 클래스는 여러 리포지토리를 감싸기 때문에 클래스가 복잡해질 수 있습니다. 이를 적절하게 관리하지 않으면 퍼사드 클래스가 또 다른 복잡성을 유발할 수 있습니다.
결론
퍼사드로 분리하면, 이전 Service처럼 퍼사드가 거대해지는 문제를 피하기 어렵습니다. 그러나 퍼사드 클래스를 적절히 분리하고 책임을 분산시켜 복잡성을 줄이는 방법을 통해 문제를 완화할 수 있습니다.
이렇게 만들어진 퍼사드는 다른 Service Layer에서 의존하여 사용하더라도 의존 관계를 해치지 않기 때문에 순환 참조 문제를 피할 수 있고, 중복 코드를 줄일 수 있습니다.
결론적으로 퍼사드가 거대해지는 문제를 완전히 피하기는 어렵지만, 이를 적절히 관리하여 코드의 유지보수성과 가독성을 높일 수 있습니다.
퍼사드 패턴을 적용함으로써 의존성 문제를 효과적으로 해결할 수 있었습니다. 코드의 복잡성을 줄이고, 테스트의 용이성을 높여 보다 유지보수가 용이한 시스템을 구축할 수 있었습니다.