✅ 어떤 기능을 만드는 것인가?
- Controller: 요청/응답·뷰 반환만 담당
- Service: 비즈니스 로직(검증, 계산, 트랜잭션 경계 등) 담당
- Repository(JPA): DB 입·출력 담당
→ 기존 @Controller에 있던 DB 저장 코드를 @Service로 이동하고, 컨트롤러는 서비스를 호출만 하도록 분리
👉 왜 이걸 배워야 하지?
- 단일 책임: 한 함수·클래스 = 한 역할 → 변경 쉬움
- 재사용/테스트 용이: 비즈니스 로직을 독립적으로 테스트 가능
- DI/IoC 장점: 객체 생성·수명 Spring에게 위임(성능·결합도↓)
- 예외 처리 표준화: 서비스에서 예외를 던지고, 컨트롤러/글로벌 핸들러에서 일괄 처리
📚 개념 정리
| 개념 | 설명 |
|---|
| Controller | URL·HTTP Method 매핑, 뷰/상태코드 반환 |
| Service | 비즈니스 규칙·검증·도메인 작업 조합 |
| Repository | JPA 인터페이스로 DB 입·출력 |
| DI(의존성 주입) | 필요 객체를 외부(Spring)가 주입 |
| IoC Container/Bean | Spring이 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줄 요약
- 웹(컨트롤러) ↔ 비즈니스(서비스) ↔ DB(리포지토리) 레이어 분리
- 서비스에서 검증/규칙 수행 후 필요 시 예외 던지기
- DI/IoC로 객체 생성·주입은 스프링에게 맡겨 결합도↓, 유지보수↑