Spring 스테레오타입 애너테이션 차이: Component, Controller, Service, Repository를 언제 써야 할까
빠른 답
@Service,@Controller,@Repository는 모두@Component기반이지만 계층 의미와 일부 런타임 동작이 다르다.- Spring 6 이상, 현재 Spring Framework 7 기준에서는
@RequestMapping만 붙은@Component가 MVC 핸들러로 등록되지 않는다. @Repository는 데이터 접근 예외를 Spring의DataAccessException계층으로 변환하는 흐름과 연결된다.- 계층별 애너테이션은 요청 처리, 트랜잭션, AOP, 테스트 범위를 읽는 기준이 된다.
목차
한눈에 비교
네 애너테이션의 차이는 “Bean이 되느냐”만 보면 작아 보인다. 하지만 서버 애플리케이션은 요청을 받고, 값을 바인딩하고, 검증하고, 서비스를 호출하고, 데이터 접근 중 발생한 예외를 응답으로 바꾸는 흐름을 가진다. 이 흐름에서 각 클래스가 어느 책임을 맡는지 드러내는 표식이 Spring 스테레오타입 애너테이션이다.
시간 흐름으로 이해하기
이 순서에서 @Controller는 MVC 초기화 시점에 중요하다. @Repository는 데이터 접근 계층에서 발생한 예외가 서비스와 웹 계층으로 전달되는 시점에 의미가 커진다. @Service는 Spring이 특별한 요청 매핑 동작을 붙이는 애너테이션은 아니지만, 트랜잭션 경계와 비즈니스 흐름을 읽는 기준으로 자주 쓰인다.
왜 모두 Bean인데 다르게 쓰는가
@Component는 가장 일반적인 스테레오타입이다. 특정 계층이라고 부르기 어려운 변환기, 이름 생성기, 외부 요청 서명기, 공통 어댑터 같은 객체에 잘 맞는다. 계층 의미보다 “Spring이 관리하는 객체”라는 점이 중요할 때 사용한다.
@Controller는 Spring MVC의 웹 경계에 둔다. 이 계층에서는 경로 변수, 쿼리 파라미터, 요청 본문, 검증 결과, HTTP 상태 코드, 응답 직렬화 방식이 드러난다. JSON API에서는 보통 @RestController를 쓰며, 이는 @Controller와 @ResponseBody를 합친 편의 애너테이션이다.
@Service는 애플리케이션 유스케이스를 표현한다. 회원 가입, 주문 생성, 결제 승인처럼 여러 정책과 저장소 호출을 하나의 흐름으로 묶는 위치다. 트랜잭션 경계를 서비스 메서드에 두는 코드가 많은 이유도 이 계층이 요청 하나의 비즈니스 단위를 대표하는 경우가 많기 때문이다.
@Repository는 데이터 접근 계층을 나타낸다. JDBC, JPA, MyBatis, jOOQ처럼 저장소 기술과 가까운 코드가 놓인다. 직접 작성한 DAO나 저장소 구현체라면 @Repository가 계층 의미뿐 아니라 예외 변환에도 영향을 줄 수 있다.
현재 버전 기준으로 달라진 부분
2026년 4월 기준 Spring의 현재 라인은 Spring Framework 7.x와 Spring Boot 4.x다. Spring Boot 4는 Spring Framework 7.x를 사용하며 Jakarta EE 11 기반으로 이동했다. Spring Boot 3과 Spring Framework 6에서 이미 javax.*에서 jakarta.*로 옮겨온 흐름이 Boot 4에서 더 이어진다.
컨트롤러 탐지 방식도 오래된 설명과 현재 설명을 구분해야 한다. Spring Framework 5.3의 RequestMappingHandlerMapping 문서에는 타입 레벨 @Controller 또는 @RequestMapping이 핸들러 조건으로 언급되어 있었다. 반면 Spring Framework 6과 7의 현재 문서에서는 타입 레벨 @Controller가 필요하다고 설명한다. 그래서 예전 예제처럼 @Component와 @RequestMapping만 조합한 클래스는 Spring Boot 3 이상, Spring Boot 4 환경에서 요청 매핑이 등록되지 않을 수 있다.
Spring Boot 4로 올릴 때는 의존성 이름도 함께 살펴볼 필요가 있다. 예를 들어 기존 spring-boot-starter-web은 Boot 4에서 더 구체적인 spring-boot-starter-webmvc 방향으로 정리되고, 테스트 스타터도 기술별로 나뉘었다. 이 글의 주제는 애너테이션이지만, 웹 계층이 동작하지 않을 때는 애너테이션뿐 아니라 스타터와 자동 구성도 함께 봐야 한다.
피해야 할 예전 형태는 다음과 같다.
@Component
@RequestMapping("/orders")
public class OrderEndpoint {
@GetMapping("/{id}")
public OrderResponse find(@PathVariable Long id) {
return new OrderResponse(id, "READY");
}
}
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public OrderResponse find(@PathVariable Long id) {
return orderService.find(id);
}
}
첫 번째 클래스는 Bean으로 등록될 수는 있어도 현재 Spring MVC의 핸들러 탐지 조건에는 맞지 않는다. JSON API라면 두 번째처럼 @RestController를 사용해 웹 요청 처리와 응답 본문 직렬화 의미를 함께 드러내는 편이 명확하다.
요청부터 응답까지의 코드 구조
아래 예시는 컨트롤러, 서비스, 저장소의 책임을 나눈 최소 구조다. 컨트롤러는 HTTP 입출력과 검증을 맡고, 서비스는 유스케이스 흐름을 잡고, 저장소는 데이터 접근만 담당한다.
@RestController
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping
public MemberResponse create(@Valid @RequestBody CreateMemberRequest request) {
Long id = memberService.create(request.email(), request.name());
return new MemberResponse(id);
}
}
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public Long create(String email, String name) {
return memberRepository.save(email, name);
}
}
@Repository
public class MemberRepository {
private final JdbcTemplate jdbcTemplate;
public MemberRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Long save(String email, String name) {
jdbcTemplate.update(
"insert into members(email, name) values (?, ?)",
email,
name
);
return jdbcTemplate.queryForObject(
"select id from members where email = ?",
Long.class,
email
);
}
}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(DuplicateKeyException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse duplicateKey(DuplicateKeyException ex) {
return new ErrorResponse("DUPLICATED_RESOURCE", "이미 존재하는 값입니다.");
}
}
@Valid는 요청 본문이 객체로 바인딩된 뒤 검증을 수행하게 한다. Spring Boot 3 이상에서는 보통 jakarta.validation.Valid를 사용한다. @RequestBody와 반환값은 HttpMessageConverter를 거쳐 JSON 같은 본문으로 역직렬화·직렬화된다.
중복 이메일처럼 데이터베이스 제약 조건이 깨지면 저장소 계층에서 예외가 발생한다. @Repository와 예외 변환 구성이 적용된 경우, 벤더별 SQLException이나 ORM 예외를 웹 계층에서 직접 다루기보다 Spring의 DuplicateKeyException, DataIntegrityViolationException 같은 DataAccessException 계층으로 처리할 수 있다.
설정과 디버깅 단서
요청 매핑이 잡히지 않는 문제는 “Bean 등록 실패”와 “MVC 핸들러 등록 실패”를 나눠서 보는 편이 좋다. Bean이 없으면 컴포넌트 스캔 범위나 의존성 주입 오류를 봐야 하고, Bean은 있는데 404가 난다면 요청 매핑 등록 여부를 확인해야 한다.
Actuator의 mappings 엔드포인트와 요청 매핑 TRACE 로그를 켜면 어떤 컨트롤러가 실제로 등록됐는지 확인하기 쉽다. 운영 환경에서는 Actuator 노출 범위를 인증과 네트워크 정책에 맞게 제한해야 한다.
management:
endpoints:
web:
exposure:
include: health,mappings
logging:
level:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor: DEBUG
다음처럼 mappings 엔드포인트를 조회하면 등록된 핸들러와 URL 조건을 확인할 수 있다.
$ curl -s http://localhost:8080/actuator/mappings | grep -A 5 OrderController
"handler": "com.example.order.OrderController#find(Long)",
"predicate": "{GET [/orders/{id}]}",
"details": {
"handlerMethod": {
"className": "com.example.order.OrderController"
}
}
@Component와 @RequestMapping만 붙인 클래스가 현재 Spring MVC에서 핸들러로 등록되지 않으면 애플리케이션은 정상 시작해도 요청은 404가 될 수 있다. 로그는 대략 다음 흐름으로 남는다.
2026-04-13T10:15:21.123+09:00 TRACE 12345 --- [main]
o.s.w.s.m.m.a.RequestMappingHandlerMapping :
Mapped "{GET [/actuator/health]}" onto ...
2026-04-13T10:15:25.451+09:00 WARN 12345 --- [nio-8080-exec-1]
o.s.web.servlet.PageNotFound :
No mapping for GET /orders/1
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"timestamp": "2026-04-13T01:15:25.452+00:00",
"status": 404,
"error": "Not Found",
"path": "/orders/1"
}
이 경우에는 OrderEndpoint가 Bean인지 확인하는 것만으로는 충분하지 않다. /actuator/mappings에 해당 URL이 있는지, 클래스에 @Controller 또는 @RestController가 있는지, 웹 MVC 스타터와 자동 구성이 로딩됐는지를 함께 확인해야 한다.
데이터 접근 쪽에서는 예외 타입을 본다. 제약 조건 위반이 발생했는데 서비스나 컨트롤러까지 벤더별 예외가 그대로 올라온다면 직접 만든 DAO에 @Repository가 빠졌거나, 예외 변환 후처리 구성이 기대와 다르게 적용됐을 수 있다. Spring Data JPA 인터페이스 저장소처럼 프레임워크가 저장소 Bean을 만들어주는 경우에는 직접 @Repository를 붙이지 않는 코드도 많지만, 수동 DAO에서는 이 차이가 드러날 수 있다.
계층별로 잘못 섞이기 쉬운 지점
@Controller 안에 비즈니스 규칙과 데이터 접근 코드가 길게 들어가면 HTTP 요청 처리와 유스케이스가 강하게 엮인다. 이 구조는 같은 기능을 배치, 메시지 컨슈머, 내부 API에서 재사용하기 어렵게 만들고 테스트 범위도 커진다.
@Service가 HttpServletRequest나 세션 객체를 직접 읽기 시작하면 웹 계층과 서비스 계층의 경계가 흐려진다. 서비스는 가능한 한 HTTP 세부사항보다 애플리케이션 입력값과 도메인 규칙을 다루는 쪽에 두는 편이 테스트와 재사용에 유리하다.
@Repository 대신 @Component만 붙인 직접 DAO는 처음에는 동작해 보일 수 있다. 그러나 예외 변환, AOP 포인트컷, 아키텍처 검사에서 저장소 계층으로 분류되지 않아 나중에 원인을 찾기 어려운 차이를 만들 수 있다.
@Service는 @Repository처럼 특정 후처리 하나로만 설명되지는 않는다. 대신 트랜잭션, 권한 검사, 로깅, 관측성, 아키텍처 규칙을 적용할 때 비즈니스 계층을 가리키는 안정적인 표식으로 쓰인다. 그래서 “Spring이 특별히 해주는 일이 적다”는 이유만으로 모두 @Component로 통일하면 코드가 커질수록 읽을 단서가 줄어든다.
공식 문서에서 확인할 지점
- Spring Framework 컴포넌트 스캔과 스테레오타입 애너테이션: https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html
- Spring Framework 현재
RequestMappingHandlerMapping: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.html - Spring Framework 5.3
RequestMappingHandlerMapping: https://docs.spring.io/spring-framework/docs/5.3.11/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.html PersistenceExceptionTranslationPostProcessorAPI: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.html@RestControllerAPI: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestController.html- Spring Boot Actuator mappings endpoint: https://docs.spring.io/spring-boot/api/rest/actuator/mappings.html
- Spring Boot 4.0 마이그레이션 가이드: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
원문 참고
https://www.maeil-mail.kr/question/72
댓글
댓글 쓰기