
기존에 내가 자주 썼던 디렉토리 구조는
Controller → Service (interface) → ServiceImpl → DAO(VO) → Mapper(MyBatis)
이었고, 이번 프로젝트에서 Facade 패턴과 Usecase를 처음 접하면서 두가지 모두를 적용해보고 싶었다.
하지만 역할을 명확하게 이해하지 못한채로 나온 결과물은 Facade를 Interface처럼 쓰고 Usecase를 구현체로 사용해버리고 말았다.
둘다 비즈니스 로직을 다루고,서로 유사한 구조를 가지고 있기 때문에 헷갈렸던것 같다.
그래서 이 글을 통해 나와 같은 실수를 범하는 것을 막고자 Facade, Use Case, Service의 역할을 명확히 구분하고, 올바르게 적용하는 방법을 정리하고자 한다.
전통적인 3계층 아키텍처에서 비즈니스 로직 담당
Service는 레이어드아키텍처의 3계층 아키텍처에서 비즈니스 로직을 처리하는 계층
@Service
public class ReservationService {
private final ConcertRepository concertRepository;
private final SeatRepository seatRepository;
private final AccountRepository accountRepository;
private final ReservationRepository reservationRepository;
public ReservationService(ConcertRepository concertRepository,
SeatRepository seatRepository,
AccountRepository accountRepository,
ReservationRepository reservationRepository) {
this.concertRepository = concertRepository;
this.seatRepository = seatRepository;
this.accountRepository = accountRepository;
this.reservationRepository = reservationRepository;
}
public Reservation reserve(Long userId, Long concertId, Long seatId) {
Concert concert = concertRepository.findById(concertId);
Seat seat = seatRepository.findById(seatId);
Account account = accountRepository.findByUserId(userId);
if (!seat.isAvailable()) {
throw new RuntimeException("Seat is not available");
}
if (account.getBalance() < concert.getPrice()) {
throw new RuntimeException("Insufficient balance");
}
seat.reserve();
account.withdraw(concert.getPrice());
Reservation reservation = new Reservation(userId, concert, seat);
return reservationRepository.save(reservation);
}
}
ReservationService가 좌석 확인, 결제, 예약 처리까지 모든 기능을 직접 수행함 → SRP 위반 가능성하나의 비즈니스 로직 단위를 담당
특정한 비즈니스 흐름(유스케이스)를 수행하는 단위
public interface ReservationUseCase {
Reservation execute(Long userId, Long concertId, Long seatId);
}
@Service
public class ReservationService implements ReservationUseCase {
private final CheckSeatAvailabilityUseCase checkSeatAvailabilityUseCase;
private final ProcessPaymentUseCase processPaymentUseCase;
private final ProcessReservationUseCase processReservationUseCase;
public ReservationService(CheckSeatAvailabilityUseCase checkSeatAvailabilityUseCase,
ProcessPaymentUseCase processPaymentUseCase,
ProcessReservationUseCase processReservationUseCase) {
this.checkSeatAvailabilityUseCase = checkSeatAvailabilityUseCase;
this.processPaymentUseCase = processPaymentUseCase;
this.processReservationUseCase = processReservationUseCase;
}
@Override
public Reservation execute(Long userId, Long concertId, Long seatId) {
checkSeatAvailabilityUseCase.execute(seatId); // ✅ 좌석 확인
processPaymentUseCase.execute(userId, concertId); // ✅ 결제 처리
return processReservationUseCase.execute(userId, concertId, seatId); // ✅ 예약 처리
}
}
여러 개의 Use Case를 묶어주는 역할
Facade는 여러 개의 Use Case를 조합하여 단순한 인터페이스를 제공하는 패턴
@Service
public class ReservationFacade {
private final ReservationUseCase reservationUseCase;
private final SendNotificationUseCase sendNotificationUseCase;
public ReservationFacade(ReservationUseCase reservationUseCase, SendNotificationUseCase sendNotificationUseCase) {
this.reservationUseCase = reservationUseCase;
this.sendNotificationUseCase = sendNotificationUseCase;
}
public Reservation reserve(Long userId, Long concertId, Long seatId) {
Reservation reservation = reservationUseCase.execute(userId, concertId, seatId); // ✅ 예약 Use Case 호출
sendNotificationUseCase.execute(userId, "Your reservation is confirmed!"); // ✅ 예약 성공 알림 전송
return reservation;
}
}
Facade는 여러 개의 비즈니스 흐름(Use Case)을 조합하여 하나의 API를 제공하는 역할을 하고,
Service와 Use Case는 같은 레이어드 아키텍처에서 나왔지만, Use Case는 SRP(단일 책임 원칙, Single Responsibility Principle)를 더욱 엄격하게 지키는 패턴이다.
결국 "조합한다"라는 개념에서는 Facade와 Use Case가 같지만, 역할은 다르다
Use Case는 "예약(Reservation)"이라는 특정 비즈니스 로직을 조합하여 실행하는 역할을 하고
Facade는 "콘서트 예약 전체 과정"과 같은 더 상위 개념에서 여러 개의 Use Case를 조합하는 역할을 한다.
비즈니스 복잡도에 따라 선택하자.
콘서트 예매 서비스를 예로 들면,
즉, Facade는 필요할 때만 쓰면 된다.
Controller → Service (interface) → ServiceImpl → DAO(VO) → Mapper(MyBatis)
전형적인 3계층 아키텍처에 가깝고, 유지보수성 향상을 위해 일부 4계층(클린 아키텍처) 개념이 섞여있다.