대학생 인증 코드가 유효한지 체크하는 controller를 작성하던 중에 여러 서비스를 주입받아서 로직을 구현하고 있는 코드를 작성하게 되었다. 현재 나의 로직은 다음과 같았다.
@PostMapping("/verify-code")
public ResponseEntity<SuccessResponseDto> verifyCode(@Valid @RequestBody RequestCodeCheckDto dto) {
String username = securityUtils.getCurrentUsername();
// 인증 코드 검증
VerifiedCodeCheckDto verifiedCodeCheckDto = VerifiedCodeCheckDto.of(username, dto);
universityEmailVerificationServiceImpl.verifyCode(verifiedCodeCheckDto);
// 프로필 이메일 업데이트
profileService.updateProfileEmail(username, dto.universityEmail());
// 멤버 정보 업데이트
SetUniversityDto setUniversityDto = SetUniversityDto.builder()
.username(username)
.university(dto.university())
.build();
memberService.setUniversity(setUniversityDto);
return ResponseEntity.ok(new SuccessResponseDto("verify code success"));
}
물론 이 코드 자체로 기능적인 문제가 있는 것은 아니라서 수정할 필요는 없었다. 그러나 컨트롤러단에서 여러 서비스를 직접 호출하는 복잡한 로직을 담고 있다는 생각이 들어서, 이 과정을 포함하는 Service를 따로 작성하는 좋겠다는 생각으로 어떻게 코드를 작성하는게 좋을지 찾아보았다.
일단 컨트롤러 단에서 발생한 문제점을 정리하자.
해당 문제점의 해결책 중에서 나의 눈에 들어온 해결책은 바로 Facade 패턴이었다.

퍼사드 패턴(Facade Pattern)은 구조 패턴(Structural Pattern)의 한 종류로써, 복잡한 서브 클래스들의 공통적인 기능을 정의하는 상위 수준의 인터페이스를 제공하는 패턴이다.
여러 서브시스템(SubSystem)들의 기능을 하나의 Facade Object로 정의하고, 클라이언트(Client)가 Facade Object를 사용하는 형태이다.
퍼사드 패턴을 통해 SubSystem들 간의 종속성을 줄여줄 수 있으며, Client는 여러 서브 클래스들을 호출할 필요 없이 편리하게 사용할 수 있다.
호텔을 예약하는 서비스가 있다고 가정하자. 호텔 예약에는 3가지 과정이 필요하다.
facade 패턴이 없다면 다음과 같이 코드를 작성해야 할 것이다.
bookRoom(String roomType, String customerName);;
processPayment(String customerName, double amount);
sendBookingConfirmation(String customerName);
별도의 서비스를 클라이언트가 직접 호출해야 되는 것이다. 그리고 방 예약 → 결제 처리 → 예약 확인 순서가 중요 해서 만약 결제가 실패한 후에 방이 예약되거나, 예약 확인이 잘못된 순서로 전송되면 문제가 발생할 수 있다.
public void bookHotel(String roomType, String customerName, double amount) {
// 방 예약
roomBookingService.bookRoom(roomType, customerName);
// 결제 처리
paymentService.processPayment(customerName, amount);
// 예약 확인 알림
notificationService.sendBookingConfirmation(customerName);
}
다음과 같이 bookHotel이라는 하나의 Facade 패턴을 적용한 메서드를 만들면,
public class Client {
public static void main(String[] args) {
HotelBookingFacade hotelBookingFacade = new HotelBookingFacade();
// 호텔 예약 요청
hotelBookingFacade.bookHotel("Deluxe", "John Doe", 200.00);
}
}
클라이언트 쪽에서는 hotelBookingFacade.bookHotel 로 간편하게 사용할 수 있다. 복잡한 로직을 사용할 필요도 없다.
// facade 패턴 적용
@Service
@RequiredArgsConstructor
public class VerificationAndUpdateFacadeService {
private final UniversityEmailVerificationServiceImpl universityEmailVerificationServiceImpl;
private final ProfileService profileService;
private final MemberService memberService;
public void verifyAndUpdateProfileAndMember(String username, RequestCodeCheckDto dto) {
// 인증 코드 검증
VerifiedCodeCheckDto verifiedCodeCheckDto = VerifiedCodeCheckDto.of(username, dto);
universityEmailVerificationServiceImpl.verifyCode(verifiedCodeCheckDto);
// 프로필 이메일 업데이트
profileService.updateProfileEmail(username, dto.universityEmail());
// 멤버 정보 업데이트
SetUniversityDto setUniversityDto = SetUniversityDto.builder()
.username(username)
.university(dto.university())
.build();
memberService.setUniversity(setUniversityDto);
}
}
// 컨트롤러 코드
@PostMapping("/verify-code")
public ResponseEntity<SuccessResponseDto> verifyCode(@Valid @RequestBody RequestCodeCheckDto dto) {
String username = securityUtils.getCurrentUsername();
verificationAndUpdateFacadeService.verifyAndUpdateProfileAndMember(username, dto);
return ResponseEntity.ok(new SuccessResponseDto("verify code success"));
}
컨트롤러 코드가 아주 간결해진 것이 보이는가? 덕분에 서비스 단의 복잡한 코드를 컨트롤러 단에서 몰라도 되게 구현하였다.
다만 주의사항도 존재한다.
단일 Facade 클래스의 비대화 가능성
Facade 패턴을 적용하면, 서브시스템의 여러 기능을 하나의 Facade 클래스에 모두 포함시킬 수 있는데, 이로 인해 Facade 클래스가 지나치게 커질 가능성이 있다. 이는 단일 책임 원칙(SRP)을 위반할 수 있으며, 클래스가 복잡해지고 유지보수가 어려워진다.
SRP를 준수하면서 Facade 클래스를 여러 개로 나누어 각 Facade가 하나의 책임만을 갖도록 설계하는 것이 좋다. 예를 들어, BookingFacade, PaymentFacade 등으로 분리할 수 있는 것처럼 말이다.
테스트의 어려움
추가적인 추상화 레이어
Facade 패턴을 사용하면 컨트롤러 단에서 간편하게 코드를 작성할 수 있다!
하지만 그만큼 TradeoOff가 존재하므로 신중하게 적용하자.