◇ PART · MVC

Service 계층

비즈니스 로직과 트랜잭션의 자리

학습 목표

  • Service 계층의 단 하나의 책임 을 안다
  • Controller 와 Mapper 사이에서 왜 필요 한지 이해
  • @Transactional 의 자리가 왜 Service 인지 안다
  • 「Fat Service, Thin Controller」 의 의미를 안다

⚠️ Service 없이 짠 코드

컨트롤러에 모든 걸 넣었을 때

@PostMapping("/order")
public String order(...) {
    // 재고 확인 (mapper 직접 호출)
    Item i = itemMapper.findById(itemId);
    if (i.getStock() < quantity) throw new ...;
    // 주문 저장
    orderMapper.insert(order);
    // 재고 감소
    itemMapper.decreaseStock(itemId, quantity);
    // 결제 처리
    paymentApi.charge(amount);
    // 알림 발송
    notificationService.send(...);

    return "redirect:/order/list";
}

한 메서드에 5 가지 책임. 테스트 불가능, 재사용 불가능, 트랜잭션 처리 모호.

🛠️ Service — 메인 셰프

비즈니스 로직 + 트랜잭션

Controller(종업원)에게 주문서를 받아 실제 요리(비즈니스 로직)를 책임지는 메인 셰프.

Controller "주문 받았어요" ↓ Service "재료 확인 → 조리 → 검수 → 트랜잭션 관리" ↓ Mapper "DB 에서 재료 가져오기" ↓ DB

① 비즈니스 로직 — 의존 부품


@Service
public class OrderService {

    @Autowired private ItemMapper itemMapper;
    @Autowired private OrderMapper orderMapper;
    @Autowired private PaymentApi paymentApi;

    public Long order(int itemId, int quantity, int userId) {
        // 다음 슬라이드에서 4 단계 처리
    }
}

한 주문을 처리하려면 세 부품이 필요합니다 — 상품 / 주문 / 결제.

① 비즈니스 로직 — 4 단계 처리


public Long order(int itemId, int quantity, int userId) {
    // ① 재고 확인 (비즈니스 규칙)
    Item item = itemMapper.findById(itemId);
    if (item.getStock() < quantity) {
        throw new OutOfStockException();
    }

    // ② 주문 저장
    Order order = new Order(itemId, quantity, userId);
    orderMapper.insert(order);

    // ③ 재고 감소
    itemMapper.decreaseStock(itemId, quantity);

    // ④ 결제
    paymentApi.charge(order.getAmount());

    return order.getId();
}

② Controller 는 깔끔해진다


@PostMapping("/order")
public String order(@RequestParam int itemId,
                     @RequestParam int quantity,
                     HttpSession session) {

    Member u = (Member) session.getAttribute("loginUser");
    Long orderId = service.order(itemId, quantity, u.getId());
    return "redirect:/order/" + orderId;
}

👉 컨트롤러는 「세션에서 사용자 가져옴 → 서비스 호출 → 응답」 만. 단 한 줄짜리 위임.

Service 의 5 가지 책임

  • 비즈니스 규칙 적용 — 재고 확인·할인 계산 등 도메인 규칙
  • 여러 Mapper 조합 — 한 동작에 여러 DB 작업
  • 트랜잭션 경계 — @Transactional
  • 외부 시스템 호출 — 결제 API, 메일 발송
  • 예외 변환 — 기술 예외 → 비즈니스 예외

@Transactional — 원자성


@Service
public class TransferService {

    @Transactional
    public void transfer(int from, int to, int amount) {
        accountMapper.withdraw(from, amount);   // 출금
        accountMapper.deposit(to, amount);      // 입금
    }
}
@Transactional 의 약속: - 출금 + 입금 둘 다 성공 → 커밋 - 둘 중 하나라도 실패 → 둘 다 롤백 - "돈이 사라지는 일은 없다" 보장

왜 @Transactional 은 Service 에 붙이나

두 곳 중에 — 왜 Service 인가?

  • Controller: 한 트랜잭션 단위로 묶기 어려움 (요청·응답 처리도 함께)
  • Mapper: 너무 작은 단위 — 여러 Mapper 호출을 한 트랜잭션으로 묶을 수 없음
  • Service: 「의미 있는 비즈니스 단위」 의 자연스러운 경계

Self-invocation 함정

⚠️ 같은 클래스 내부 호출은 트랜잭션 X

@Service
public class MemberService {
    public void register(Member m) {
        save(m);    // ⚠️ 여기서 호출하면
    }

    @Transactional
    public void save(Member m) {
        // ⚠️ 트랜잭션이 적용 안 됨!
    }
}

Spring AOP 의 프록시 메커니즘 때문. Self-invocation 우회 — 다른 빈에서 호출 또는 클래스 분리.

Service 인터페이스 vs 구현 클래스


// 인터페이스 (선택적)
public interface MemberService {
    Member find(int id);
    void register(Member m);
}

@Service
public class MemberServiceImpl implements MemberService {
    @Override
    public Member find(int id) { ... }
    @Override
    public void register(Member m) { ... }
}

👉 옛 컨벤션: 인터페이스 + Impl. 본 과정은 단순화 — 구현 클래스만.
인터페이스는 다른 구현이 필요할 때 (테스트 mock 등) 추가.

「Fat Service, Thin Controller」

계층분량이유
Controller얇게 (Thin)HTTP 처리만
Service두툼하게 (Fat)비즈니스 로직 모음
Mapper얇게SQL 만

👉 Service 가 두툼해야 — 도메인 변경 시 한 곳만 수정.

예외 처리 — 비즈니스 예외


public class OutOfStockException extends RuntimeException {
    public OutOfStockException(String msg) { super(msg); }
}

@Service
public class OrderService {
    public Long order(int itemId, int qty, int userId) {
        Item i = itemMapper.findById(itemId);
        if (i.getStock() < qty) {
            throw new OutOfStockException("재고 부족");
        }
        // ...
    }
}

// Controller 또는 @ControllerAdvice 에서
@ExceptionHandler(OutOfStockException.class)
public String handleOutOfStock(...) { ... }

실수 모음

실수증상
@Service 빠뜨림NoSuchBeanDefinitionException
Service 가 너무 얇음Controller 가 비대해짐
@Transactional 을 Mapper 에여러 Mapper 호출 시 트랜잭션 안 묶임
Self-invocation트랜잭션 적용 안 됨
Service 가 Controller 호출의존 방향 위배. 절대 X

🔄 Before / After

전 차시 끝

Service 가 무엇인지는 알지만 어디까지가 Service 의 일인지 흐릿.

이번 차시 끝

5 가지 책임을 외운다. @Transactional 의 자리가 Service 인 이유를 안다.

이번 차시의 데이터 흐름

Controller
@Service
+ @Transactional
여러 Mapper
DB
Service 가 비즈니스 단위로 「묶음」을 만듦

정리

오늘 들고 가는 것

  • Service = 비즈니스 로직 + 트랜잭션 자리
  • 5 가지 책임: 규칙·조합·트랜잭션·외부호출·예외변환
  • @Transactional 은 Service 에 (Controller·Mapper 가 아니라)
  • Self-invocation 함정 주의
  • 「Fat Service, Thin Controller」