자바 백엔드 입문 18편. @Component·@Service·@Repository·@Controller로 Bean을 등록하고 @Autowired로 의존성을 주입하는 핵심 어노테이션 4종 + 3종을 음식 배달 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 18편이에요. 17편에서 ApplicationContext가 Bean 보관소라고 풀었다면, 이번 18편은 그 컨테이너에 "Bean을 어떻게 등록하고, 어떻게 의존성을 주입받는가" 의 실전 어노테이션을 깊이 들여다봅니다. Spring 코드의 절반이 이 어노테이션들이에요.
@Component·@Autowired가 헷갈리는 이유
처음 Spring 코드를 보면 @Component·@Service·@Repository·@Controller 가 거의 같은 일을 하는데 이름만 다르고, @Autowired·@Resource·@Inject 가 또 비슷한데 미묘하게 다른 게 헷갈려요.
이 글에서는 음식 배달 시스템 비유로 풀어 가요. @Component = "식당이 배달 플랫폼에 등록", @Autowired = "고객이 음식을 시키면 배달 플랫폼이 알아서 가까운 식당을 골라줌". 끝까지 따라오시면 7개 어노테이션이 한 그림으로 정리돼요.
Bean 등록 어노테이션 4종 — 이름만 다르고 동작은 같음
@Component 가 가장 기본. "이 클래스로 Bean 만들어 컨테이너에 등록해 줘" 메모예요. 그 변형이 세 개 더 있어요.
@Component // 기본 — 일반 컴포넌트
@Service // 비즈니스 로직 레이어
@Repository // 데이터 접근 레이어 (DB)
@Controller // 웹 컨트롤러 (HTML 렌더)
@RestController // 웹 컨트롤러 (JSON 응답)
핵심 사실 — 네 어노테이션 모두 내부 동작은 100% 동일해요. Spring이 시작 시점에 클래스 패스를 스캔해서 이 어노테이션이 박힌 클래스들을 모두 Bean으로 등록. 이름이 다른 이유는 단 한 가지 — 이 클래스의 역할을 코드 읽는 사람이 한눈에 알게 해주기 위해서.
| 어노테이션 | 표시하는 역할 |
|---|---|
@Component |
일반 컴포넌트 (어디에 속하는지 애매할 때) |
@Service |
비즈니스 로직 (주문 처리·결제·회원 가입) |
@Repository |
데이터베이스 접근 (CRUD·쿼리) |
@Controller |
웹 요청 처리 (HTML 렌더) |
@RestController |
웹 요청 처리 (JSON 응답) |
@Repository 에는 추가 보너스가 하나 있어요 — DB 예외를 Spring 표준 예외로 자동 변환해줘요. JDBC·JPA·MyBatis가 던지는 서로 다른 예외를 Spring이 통일된 DataAccessException 으로 바꿔주는 거예요. 그래서 데이터 접근 클래스에는 무조건 @Repository 권장.
컴포넌트 스캔 — Spring이 클래스를 찾는 방법
이 어노테이션이 박힌 클래스를 Spring이 어떻게 발견하느냐? 컴포넌트 스캔(Component Scanning) 이라는 자동 탐색 메커니즘 덕분이에요.
@SpringBootApplication
public class MyShopApplication { ... }
@SpringBootApplication 안에 들어있는 @ComponentScan 이 "이 클래스가 속한 패키지부터 시작해 모든 하위 패키지를 검사해라" 라는 명령이에요. 그래서 메인 클래스가 com.example.myshop 패키지에 있으면, com.example.myshop 과 그 하위 패키지의 모든 클래스를 검사해서 @Component·@Service 등이 박힌 클래스를 찾아 Bean으로 등록해요.
여기서 시험 함정 자주 나와요. "메인 클래스 패키지보다 위 또는 옆 패키지에 박힌 클래스는 어떻게 되나?" — 답은 스캔 안 됨. 입문자가 가장 자주 막히는 함정이에요. 메인 클래스를 com.example.myshop.MyShopApplication 으로 만들었다면, com.other.something.SomeService 는 절대 Bean으로 등록 안 됩니다. 항상 메인 클래스를 루트 패키지에 두고, 모든 하위 코드를 그 아래에 둬요.
@Autowired — 의존성 자동 주입
Bean을 등록했으니 이제 끼워 넣을 차례. @Autowired 가 "이 자리에 적절한 Bean을 찾아 끼워 넣어 줘" 메모예요.
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
@Autowired // 생성자 주입 — 생성자가 한 개면 생략 OK
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
Spring이 시작 시점에 "OrderService 객체를 만들려면 PaymentGateway 가 필요하구나" 를 파악하고, 컨테이너에서 PaymentGateway 구현체를 찾아 생성자 인자로 넘겨줘요. 우리가 new 할 일이 없어요.
핵심 룰 — Spring 4.3 이후 생성자가 한 개면 @Autowired 생략 가능. 위 코드도 @Autowired 빼도 동작해요. 그래서 현대 Spring 코드에서는 @Autowired 가 거의 안 보이고, 생성자 주입이 묵시적으로 동작해요. Lombok @RequiredArgsConstructor 와 조합하면 더 짧아져요.
@Service
@RequiredArgsConstructor // final 필드들의 생성자 자동 생성
public class OrderService {
private final PaymentGateway paymentGateway; // 자동으로 생성자 주입됨
}
이게 2026년 Spring Boot 표준 패턴이에요. 3줄로 모든 의존성 주입 끝.
@Autowired 3가지 위치 — 생성자·세터·필드
@Autowired 는 세 위치에 박을 수 있어요.
// 1. 생성자 — 권장 (불변성·테스트 용이성)
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
// 2. 세터 — 옵션 의존성에 적합
@Service
public class OrderService {
private PaymentGateway paymentGateway;
@Autowired
public void setPaymentGateway(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
// 3. 필드 — 편하지만 비권장
@Service
public class OrderService {
@Autowired
private PaymentGateway paymentGateway;
}
Spring 공식 권장은 생성자 주입. 15편에서 다룬 이유 그대로 — final 가능 / 누락 발견 빠름 / 테스트 쉬움.
@Primary·@Qualifier — 같은 타입 여러 개일 때
같은 인터페이스를 구현한 Bean이 두 개 이상이면 Spring이 어느 걸 끼울지 모호해져요. 예를 들어:
@Service public class KakaoPayGateway implements PaymentGateway { ... }
@Service public class TossPayGateway implements PaymentGateway { ... }
OrderService 가 PaymentGateway 를 주입받으려는데 — 카카오와 토스 둘 중 어느 걸 끼울까? Spring이 답을 못 찾으면 NoUniqueBeanDefinitionException 던져요. 두 가지 해결법:
@Primary — 기본값 지정
@Service
@Primary // ← 이게 기본값
public class KakaoPayGateway implements PaymentGateway { ... }
@Service
public class TossPayGateway implements PaymentGateway { ... }
@Primary 박힌 Bean이 자동 선택돼요. "별도 지정 없으면 카카오" 라는 선언.
@Qualifier — 사용처에서 명시 지정
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(@Qualifier("tossPayGateway") PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
@Qualifier 로 "토스 쓰겠다" 를 명시적으로 박는 방식. Bean 이름(기본은 클래스명 첫 글자 소문자)으로 지정해요.
@Qualifier 가 @Primary 보다 우선이에요. "전역적으로는 카카오가 기본이지만, 이 자리에서만큼은 토스" 라는 시나리오가 가능합니다.
@Resource·@Inject — @Autowired와 닮은 둘
자바 표준 또는 다른 표준에서 비슷한 어노테이션이 있어요. 입문 단계엔 "이런 게 있다" 만 알아둬요.
@Resource(자바 EE 표준,javax.annotation.Resource) — 이름 기반 매칭이 기본. 타입 매칭은 부가@Inject(JSR-330 표준,javax.inject.Inject) —@Autowired와 거의 동일. 자바 표준이라 Spring 외에도 통용
실무는 거의 100% Spring의 @Autowired 또는 생성자 주입(어노테이션 X) 사용. 다른 두 개는 "옛 코드에서 봤을 때 당황 안 하면 OK".
음식 배달 시스템 비유 — 전체 그림 한 번에
지금까지 다룬 7개 어노테이션을 음식 배달로 다시 풀어볼게요.
@Component/@Service/@Repository/@Controller= "식당이 배달 플랫폼에 등록" (@Service는 "우리 식당은 양식점",@Repository는 "중식당" 같은 카테고리 표시)- 컴포넌트 스캔 = "배달 플랫폼이 지정 구역의 모든 식당을 자동 등록"
@Autowired= "고객이 음식 주문 → 플랫폼이 자동으로 가까운 식당 골라 배달"@Primary= "이 동네 기본 식당은 OO" 선언@Qualifier("토스")= "오늘은 토스 식당에서 시키겠다" 명시 지정
이 비유를 머리에 박아두면 Spring 코드에서 어노테이션 만났을 때 자연스럽게 해석돼요.
한 줄 정리 — @Component/@Service/@Repository/@Controller = Bean 등록 메모. @Autowired = 의존성 자동 주입 메모. @Primary·@Qualifier = 같은 타입 여러 개일 때 분기.
시험 직전 한 번 더 — 어노테이션 입문자가 매번 헷갈리는 것
@Component= Bean 등록 기본 어노테이션@Service·@Repository·@Controller·@RestController=@Component의 "역할별 별칭". 내부 동작 동일- 이름 다른 이유 = 코드 읽는 사람이 "이 클래스의 역할" 한눈에 파악
@Repository는 보너스 — DB 예외를 Spring 표준DataAccessException으로 자동 변환- 컴포넌트 스캔 = 메인 클래스 패키지부터 시작해 하위 모든 패키지 자동 탐색
- 메인 클래스 패키지보다 "위/옆" 패키지의 클래스는 스캔 X (매번 막히는 함정)
- 항상 메인 클래스를 루트 패키지에 두고 하위 코드 배치
@Autowired= 의존성 자동 주입 메모- Spring 4.3+ 생성자가 1개면
@Autowired생략 가능 - 2026년 표준 =
@Service+ Lombok@RequiredArgsConstructor+final필드들 @Autowired3가지 위치 = 생성자·세터·필드- 생성자 주입이 공식 권장 —
final가능 + 누락 발견 빠름 + 테스트 쉬움 - 필드 주입(
@Autowired private) 비권장 —final불가 + 테스트 어려움 - 같은 타입 Bean 여러 개 →
NoUniqueBeanDefinitionException발생 - 해결 1 =
@Primary— "이게 기본값" 선언 - 해결 2 =
@Qualifier("이름")— 사용처에서 명시 지정 @Qualifier가@Primary보다 우선@Resource(자바 EE 표준) — 이름 기반 매칭, Spring 외 코드에서 가끔 봄@Inject(JSR-330 표준) —@Autowired와 거의 동일- 실무 거의 100% =
@Service+ 생성자 주입(어노테이션 X) + Lombok
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 13편 — application.yml Spring Profiles
- 14편 — 첫 Bean 만들기 Hello Spring
- 15편 — 의존성 주입이 왜 필요한가
- 16편 — Bean이란 일반 객체와의 차이
- 17편 — ApplicationContext 컨테이너 본체
다음 글: