Facade, Use Case, Service 패턴, 도대체 뭐가 다를까?

김원중·2025년 3월 9일
post-thumbnail

기존에 내가 자주 썼던 디렉토리 구조는

ControllerService (interface)ServiceImplDAO(VO)Mapper(MyBatis)

이었고, 이번 프로젝트에서 Facade 패턴과 Usecase를 처음 접하면서 두가지 모두를 적용해보고 싶었다.
하지만 역할을 명확하게 이해하지 못한채로 나온 결과물은 Facade를 Interface처럼 쓰고 Usecase를 구현체로 사용해버리고 말았다.
둘다 비즈니스 로직을 다루고,서로 유사한 구조를 가지고 있기 때문에 헷갈렸던것 같다.
그래서 이 글을 통해 나와 같은 실수를 범하는 것을 막고자 Facade, Use Case, Service의 역할을 명확히 구분하고, 올바르게 적용하는 방법을 정리하고자 한다.


✅ Facade, Use Case, Service의 개념과 역할 정리

1️⃣ Service (서비스)

전통적인 3계층 아키텍처에서 비즈니스 로직 담당

✅개념

Service는 레이어드아키텍처의 3계층 아키텍처에서 비즈니스 로직을 처리하는 계층

  • Controller -> Service -> Repository 구조에서 비즈니스 로직을 캡슐화하는 역할
  • 보통 여러 기능을 한 클래스에 묶어 제공하는 경우가 많다.

✅ 역할

  • Controller에서 받은 요청을 처리하고, Repository를 통해 데이터를 저장 및 조회
  • 비즈니스 로직을 수행(Ex:주문 생성, 결제 처리, 할인 적용...)
  • 여러 개의 기능을 하나의 서비스 클래스에서 처리할 수 있음(Fat Service가 될 위험 존재)

⚠️ 주의할 점

  • 서비스 클래스가 너무 커지면 유지보수가 어려움 (Fat Service)
  • 단일 책임 원칙(SRP)을 위반할 가능성이 높음
  • 비즈니스 로직과 데이터 액세스 로직이 뒤섞일 위험이 있음

예제


@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 위반 가능성
  • 유지보수하기 어려워지고, 코드가 커질 가능성이 있음

2️⃣ Use Case

하나의 비즈니스 로직 단위를 담당

✅개념

특정한 비즈니스 흐름(유스케이스)를 수행하는 단위

  • 하나의 기능만 담당(Ex: 주문 생성, 주문 취소, 예약 기능 ...)
  • UseCase는 비즈니스 규칙을 구현하고, 다른 계층에 의존하지 않도록 설계
  • 클린 아키텍처에서 서비스 대신 사용하는 개념

✅역할

  • 하나의 UseCase는 하나의 비즈니스 기능만 처리해야 함
  • Service처럼 여러 기능을 한 곳에서 처리하지 않음.
  • 테스트하기 쉬움(단일 기능이므로 Mocking이 간단)

⚠️주의사항

  • UseCase를 너무 세분화하면 파일이 너무 많아질 수 있음
  • 모든 기능을 UseCase로 만들 필요는 없음(CRUD는 그냥 Service로 관리해도 됨)

예제

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로 분리하여 SRP를 철저히 준수함.
  • `유지보수가 용이하며, 개별 기능을 독립적으로 테스트할 수 있음.

3️⃣ Facade

여러 개의 Use Case를 묶어주는 역할

✅ 개념

Facade는 여러 개의 Use Case를 조합하여 단순한 인터페이스를 제공하는 패턴

  • 비즈니스 로직을 직접 수행하지 않고, 여러 Use Case를 조합해서 실행
  • Controller에서 여러 개의 Use Case를 직접 호출하는 복잡성을 줄여줌

✅ 역할

  • 여러 개의 UseCase를 호출하여 하나의 단순한 API 제공
  • Controller에서 여러 UseCase를 호출하는 대신, Facade에서 한 번에 처리
  • 비즈니스 로직을 수행하지 않고, UseCase 실행을 조합하는 역할만 수행

⚠️ 주의사항

  • 불필요한 Facade를 만들면 오히려 코드가 복잡해짐
  • 단순한 기능은 그냥 UseCase로 호출하는 것이 더 효율적일 수 있음

예제

@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;
    }
}
  • Controller가 여러 UseCase를 직접 호출하지 않도록 Facade가 중간에서 조정
  • 비즈니스 로직을 수행하지 않고, UseCase의 실행 순서를 조정하는 역할만 수행
  • 코드 가독성 향상, 유지보수성 향상

결론

Facade는 여러 개의 비즈니스 흐름(Use Case)을 조합하여 하나의 API를 제공하는 역할을 하고,
Service와 Use Case는 같은 레이어드 아키텍처에서 나왔지만, Use Case는 SRP(단일 책임 원칙, Single Responsibility Principle)를 더욱 엄격하게 지키는 패턴이다.

결국 "조합한다"라는 개념에서는 Facade와 Use Case가 같지만, 역할은 다르다

Use Case는 "예약(Reservation)"이라는 특정 비즈니스 로직을 조합하여 실행하는 역할을 하고
Facade는 "콘서트 예약 전체 과정"과 같은 더 상위 개념에서 여러 개의 Use Case를 조합하는 역할을 한다.

어떻게 사용하는게 좋을까?

비즈니스 복잡도에 따라 선택하자.

  • Use Case만으로 충분한 경우 → Facade 없이 바로 사용
  • 여러 개의 Use Case가 함께 실행되어야 하는 경우 → Facade 도입

콘서트 예매 서비스를 예로 들면,

☑️ UseCase만 사용하는 경우

  • 예약 프로세스가 단순하고, 한 번의 Use Case 호출로 모든 로직이 해결되는 경우
  • Controller에서 단 하나의 Use Case만 호출하면 되는 경우
  • 예약 이후 추가적인 프로세스(알림 전송, 포인트 적립 등)가 필요하지 않은 경우

☑️ Facade만 사용하는 경우

  • 예약 외에도 여러 개의 Use Case를 함께 실행해야 하는 경우
  • 예약 후 알림 전송, 포인트 적립, 데이터 로깅 등의 추가적인 작업이 필요할 경우
  • Controller에서 여러 개의 Use Case를 직접 호출하면 코드가 복잡해질 경우

즉, Facade는 필요할 때만 쓰면 된다.


🤔궁금한점

  1. 그럼 기존에 자주 썼던 구조는 뭘까?
ControllerService (interface)ServiceImplDAO(VO)Mapper(MyBatis)

전형적인 3계층 아키텍처에 가깝고, 유지보수성 향상을 위해 일부 4계층(클린 아키텍처) 개념이 섞여있다.

profile
while(life) { keep_growing() };

0개의 댓글