Service 레이어로 분리 (Controller ↔ Service, DI, 예외처리)

EthAnalog·2025년 9월 11일

Spring Boot

목록 보기
16/16
post-thumbnail

✅ 어떤 기능을 만드는 것인가?

  • Controller: 요청/응답·뷰 반환만 담당
  • Service: 비즈니스 로직(검증, 계산, 트랜잭션 경계 등) 담당
  • Repository(JPA): DB 입·출력 담당
    → 기존 @Controller에 있던 DB 저장 코드@Service로 이동하고, 컨트롤러는 서비스를 호출만 하도록 분리

👉 왜 이걸 배워야 하지?

  • 단일 책임: 한 함수·클래스 = 한 역할 → 변경 쉬움
  • 재사용/테스트 용이: 비즈니스 로직을 독립적으로 테스트 가능
  • DI/IoC 장점: 객체 생성·수명 Spring에게 위임(성능·결합도↓)
  • 예외 처리 표준화: 서비스에서 예외를 던지고, 컨트롤러/글로벌 핸들러에서 일괄 처리

📚 개념 정리

개념설명
ControllerURL·HTTP Method 매핑, 뷰/상태코드 반환
Service비즈니스 규칙·검증·도메인 작업 조합
RepositoryJPA 인터페이스로 DB 입·출력
DI(의존성 주입)필요 객체를 외부(Spring)가 주입
IoC Container/BeanSpring이 new해서 보관/관리하는 객체
애너테이션@Service, @Controller, @Repository, @RequiredArgsConstructor, @Autowired
예외처리@ExceptionHandler, @ControllerAdvice, RuntimeException

⚙️ 구현 흐름 및 코드

1) Service 클래스 만들기

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    public void saveItem(String title, Integer price) {
        // (예) 비즈니스 검증
        if (title == null || title.isBlank() || title.length() > 200) {
            throw new IllegalArgumentException("상품명 형식이 올바르지 않습니다.");
        }
        if (price == null || price < 0) {
            throw new IllegalArgumentException("가격은 0 이상이어야 합니다.");
        }

        Item item = new Item();
        item.setTitle(title);
        item.setPrice(price);
        itemRepository.save(item);
    }
}

2) Controller에서 Service 주입 후 호출

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;

    @PostMapping("/add")
    String writePost(String title, Integer price) {
        itemService.saveItem(title, price);   // 비즈니스는 서비스가 처리
        return "redirect:/list";              // 컨트롤러는 흐름 제어/뷰 반환
    }
}

3) (선택) 글로벌 예외 처리

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> badRequest(IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> unknown(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body("서버 오류가 발생했습니다.");
    }
}

💡 이런 곳에 활용할 수 있어요

  • 상품 등록/수정/삭제 로직 공통화
  • 결제/재고/주문 상태 전이 같은 규칙 많은 도메인
  • REST API/페이지 반환 둘 다 재사용해야 할 때

✍️ 개인 정리 및 회고

  • 컨트롤러에서 비즈니스 코드 빼니 가독성·테스트성이 확 올라감
  • DI로 new 남발 제거 → 수명·순환참조 고민 줄어듦
  • 예외는 서비스에서 던지고, 핸들러에서 모아 처리가 깔끔

🔑 오늘 배운 핵심 3줄 요약

  1. 웹(컨트롤러)비즈니스(서비스)DB(리포지토리) 레이어 분리
  2. 서비스에서 검증/규칙 수행 후 필요 시 예외 던지기
  3. DI/IoC로 객체 생성·주입은 스프링에게 맡겨 결합도↓, 유지보수↑

0개의 댓글