[Spring] 07. @Component와 Component Scan

[Spring] 07. @Component와 Component Scan

이전 글에서는 Spring Container와 ApplicationContext에 대해 살펴봤습니다. Spring Container는 Bean을 생성하고, 보관하고, 필요한 곳에 주입해주는 공간입니다. 그리고 ApplicationContext는 Spring Container 역할을 수행하는 대표적인 인터페이스입니다.

이번 글에서는 Spring이 Bean으로 등록할 클래스를 어떻게 찾는지 알아보겠습니다. 핵심 개념은 @ComponentComponent Scan입니다.

Component Scan은 Spring이 특정 패키지 안을 살펴보면서 Bean으로 등록할 클래스를 찾는 과정입니다.


1. Component란?

Component는 Spring이 관리할 수 있는 구성 요소를 의미합니다. 즉, 애플리케이션 안에서 하나의 역할을 담당하는 클래스라고 볼 수 있습니다.

Spring에서는 클래스 위에 @Component를 붙이면 해당 클래스를 Bean 등록 대상으로 인식할 수 있습니다.

@Component
public class MailSender {

    public void send() {
        System.out.println("메일 발송");
    }
}

위 클래스는 단순한 Java 클래스가 아니라, Spring이 찾아서 Bean으로 등록할 수 있는 대상이 됩니다.

다만 @Component를 붙였다고 무조건 Bean으로 등록되는 것은 아닙니다. Spring이 이 클래스를 찾을 수 있어야 합니다. 이때 필요한 과정이 Component Scan입니다.


2. Component Scan이란?

Component Scan은 Spring이 지정된 패키지 범위를 탐색하면서 Bean으로 등록할 클래스를 찾는 과정입니다.

Component Scan
 → 패키지 탐색
 → @Component 계열 어노테이션 확인
 → Bean 등록
 → Spring Container에서 관리

Spring은 실행될 때 특정 패키지를 기준으로 클래스를 살펴봅니다. 그리고 @Component, @Controller, @Service, @Repository 같은 어노테이션이 붙은 클래스를 찾아 Bean으로 등록합니다.

즉, Component Scan은 Bean 등록 대상을 자동으로 찾아주는 기능입니다.


3. @Component 기본 예시

다음은 @Component를 사용한 간단한 예시입니다.

@Component
public class FileStorage {

    public void saveFile() {
        System.out.println("파일 저장 처리");
    }
}

이 클래스가 Component Scan 범위 안에 있다면 Spring은 FileStorage 객체를 생성하고 Bean으로 등록합니다.

Spring Container
 └─ fileStorage Bean

Bean 이름은 기본적으로 클래스 이름의 첫 글자를 소문자로 바꾼 형태가 됩니다. FileStorage 클래스라면 보통 fileStorage라는 이름으로 등록됩니다.


4. @Component 계열 어노테이션

Spring에서는 @Component 외에도 역할별로 나누어진 어노테이션을 자주 사용합니다. 대표적으로 @Controller, @Service, @Repository가 있습니다.

@Component 일반적인 Spring 관리 객체에 사용합니다.
@Controller 사용자의 요청을 처리하는 클래스에 사용합니다.
@Service 기능의 흐름과 규칙을 처리하는 클래스에 사용합니다.
@Repository DB 접근을 담당하는 클래스에 사용합니다.

이 어노테이션들은 모두 Spring이 Bean 등록 대상으로 인식할 수 있습니다. 그래서 Component Scan 과정에서 발견되면 Spring Container에 Bean으로 등록됩니다.


5. 왜 @Component만 쓰지 않을까?

모든 클래스에 @Component만 붙여도 Bean 등록은 가능합니다. 하지만 그렇게 작성하면 각 클래스가 어떤 역할을 하는지 한눈에 보기 어렵습니다.

@Component
public class BoardController {
}

@Component
public class BoardService {
}

@Component
public class BoardRepository {
}

위 코드도 Bean 등록은 가능하지만, 역할 구분이 명확하지 않습니다.

그래서 보통은 역할에 맞는 어노테이션을 사용합니다.

@Controller
public class BoardController {
}

@Service
public class BoardService {
}

@Repository
public class BoardRepository {
}

이렇게 작성하면 코드만 봐도 어떤 클래스가 요청 처리, 기능 처리, DB 접근을 담당하는지 알 수 있습니다.


6. Controller, Service, Repository 예시

게시판 기능을 기준으로 보면 다음과 같이 계층을 나눌 수 있습니다.

@Controller
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }
}
@Service
public class BoardService {

    private final BoardRepository boardRepository;

    public BoardService(BoardRepository boardRepository) {
        this.boardRepository = boardRepository;
    }
}
@Repository
public class BoardRepository {

    public void save() {
        System.out.println("게시글 저장");
    }
}

Spring은 Component Scan을 통해 위 클래스들을 Bean으로 등록합니다. 그리고 생성자를 확인해 필요한 Bean을 자동으로 연결합니다.

BoardController Bean
 → BoardService Bean 주입

BoardService Bean
 → BoardRepository Bean 주입

이 흐름이 Spring에서 자주 사용하는 계층 구조입니다.


7. Spring Boot에서 Component Scan 범위

Spring Boot에서는 보통 메인 실행 클래스에 @SpringBootApplication이 붙어 있습니다.

@SpringBootApplication
public class BoardApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardApplication.class, args);
    }
}

@SpringBootApplication 안에는 Component Scan 기능이 포함되어 있습니다. 그래서 메인 클래스가 위치한 패키지와 그 하위 패키지를 기준으로 Bean 등록 대상을 찾습니다.

com.example.board
 ├─ BoardApplication
 ├─ controller
 │   └─ BoardController
 ├─ service
 │   └─ BoardService
 └─ repository
     └─ BoardRepository

위 구조에서는 BoardApplication이 있는 패키지 아래에 controller, service, repository가 있으므로 Component Scan 대상이 됩니다.

그래서 Spring Boot 프로젝트에서는 메인 실행 클래스를 가장 상위 패키지에 두는 것이 좋습니다. 그래야 하위 패키지의 Controller, Service, Repository를 자연스럽게 찾을 수 있습니다.


8. Component Scan 범위 밖에 있으면?

어노테이션을 제대로 붙였는데도 Bean 등록이 되지 않는 경우가 있습니다. 대표적인 원인은 클래스가 Component Scan 범위 밖에 있는 경우입니다.

com.example.board
 └─ BoardApplication

com.other.service
 └─ BoardService

위 구조에서 BoardApplication이 com.example.board 패키지에 있다면, 기본적으로 com.example.board와 그 하위 패키지를 스캔합니다.

하지만 com.other.service는 그 하위 패키지가 아닙니다. 그래서 @Service가 붙어 있어도 Bean으로 등록되지 않을 수 있습니다.

이 문제를 줄이려면 프로젝트 패키지 구조를 처음부터 정리하는 것이 중요합니다.

좋은 구조

com.example.board
 ├─ BoardApplication
 ├─ controller
 ├─ service
 ├─ repository
 ├─ dto
 └─ vo

메인 실행 클래스 아래로 주요 패키지를 모으면 Component Scan 문제를 줄일 수 있습니다.


9. MyBatis Mapper는 어떻게 등록할까?

Spring 프로젝트에서 MyBatis를 사용한다면 Mapper 인터페이스가 등장합니다. Mapper는 DB 쿼리를 실행하는 역할을 합니다.

@Mapper
public interface BoardMapper {

    void insertBoard(BoardDto boardDto);
}

MyBatis Mapper는 보통 @Mapper를 붙이거나, 설정 클래스 또는 실행 클래스에 @MapperScan을 사용해서 등록합니다.

@SpringBootApplication
@MapperScan("com.example.board.mapper")
public class BoardApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardApplication.class, args);
    }
}

즉, 일반적인 Service나 Controller는 Component Scan으로 찾고, MyBatis Mapper는 @Mapper 또는 @MapperScan으로 등록하는 경우가 많습니다.

처음에는 이 차이 때문에 헷갈릴 수 있습니다. Controller와 Service는 @Controller, @Service를 사용하고, Mapper 인터페이스는 MyBatis 방식에 맞게 등록한다고 이해하면 됩니다.


10. Component Scan과 DI의 연결

Component Scan은 Bean 등록 대상을 찾는 과정입니다. DI는 등록된 Bean을 필요한 곳에 넣어주는 과정입니다.

둘은 다음 흐름으로 연결됩니다.

1. Component Scan으로 Bean 후보 찾기
2. Spring Container에 Bean 등록
3. 생성자나 필드를 확인해 필요한 의존성 파악
4. 필요한 Bean을 찾아 주입
5. 애플리케이션 실행

예를 들어 BoardController가 BoardService를 필요로 한다면, 먼저 BoardService가 Bean으로 등록되어 있어야 합니다. 그리고 Spring은 BoardController를 만들 때 BoardService Bean을 찾아 넣어줍니다.

@Service 발견
 → boardService Bean 등록
 → BoardController 생성 시 boardService 주입

따라서 DI가 동작하려면 주입할 객체가 먼저 Bean으로 등록되어 있어야 합니다. 이때 Bean 등록을 도와주는 대표적인 과정이 Component Scan입니다.


11. 자주 하는 실수

Component Scan을 공부할 때 자주 하는 실수는 클래스에 어노테이션을 붙이지 않는 것입니다.

public class BoardService {
}

이 상태에서는 Spring이 BoardService를 Bean 등록 대상으로 인식하기 어렵습니다. Service 계층이라면 다음처럼 @Service를 붙이는 것이 좋습니다.

@Service
public class BoardService {
}

또 다른 실수는 패키지 위치가 잘못된 경우입니다. 어노테이션이 있어도 Component Scan 범위 밖에 있으면 Bean으로 등록되지 않을 수 있습니다.

어노테이션 있음
+ Component Scan 범위 안에 있음
= Bean 등록 가능

마지막으로 DTO나 VO에 무조건 @Component를 붙이는 것도 피하는 것이 좋습니다. DTO와 VO는 보통 데이터를 담는 객체이며, Spring Container가 계속 관리해야 하는 객체가 아닙니다.


정리

@Component는 Spring이 Bean으로 등록할 수 있는 클래스를 표시하는 어노테이션입니다. @Controller, @Service, @Repository@Component 계열 어노테이션이며, 역할을 더 명확하게 구분하기 위해 사용합니다.

Component Scan은 Spring이 특정 패키지 범위를 탐색하면서 Bean 등록 대상을 찾는 과정입니다. Spring Boot에서는 메인 실행 클래스가 있는 패키지와 그 하위 패키지를 기본적으로 스캔합니다.

Component Scan으로 Bean이 등록되고 나면, Spring Container는 DI를 통해 필요한 Bean을 필요한 곳에 주입합니다. 즉, Component Scan은 Bean 등록의 시작점이고, DI는 등록된 Bean을 연결하는 과정입니다.


연습해보기

아래 구조에서 어떤 클래스가 Component Scan 대상이 될 수 있는지 생각해보세요.

com.example.board
 ├─ BoardApplication
 ├─ controller
 │   └─ BoardController
 ├─ service
 │   └─ BoardService
 ├─ mapper
 │   └─ BoardMapper
 └─ dto
     └─ BoardDto

BoardController에는 @Controller, BoardService에는 @Service를 붙일 수 있습니다. BoardMapper는 MyBatis를 사용한다면 @Mapper 또는 @MapperScan으로 등록할 수 있습니다. BoardDto는 데이터를 담는 객체이므로 보통 Bean으로 등록하지 않습니다.

다음 글에서는 Spring에서 자주 사용하는 역할별 어노테이션인 @Controller, @Service, @Repository를 더 자세히 정리하겠습니다.