[Spring] 13. @Configuration과 @Bean

[Spring] 13. @Configuration과 @Bean

이전 글에서는 Singleton Bean과 상태값 주의사항에 대해 살펴봤습니다. Spring Bean은 기본적으로 Singleton으로 관리되며, 하나의 Bean 객체를 여러 곳에서 공유합니다. 그래서 사용자별로 달라지는 값을 Service 필드에 저장하면 위험하다는 점을 정리했습니다.

이번 글에서는 Spring Bean을 직접 등록하는 방법인 @Configuration@Bean에 대해 알아보겠습니다. 지금까지는 @Controller, @Service, @Repository, @Component를 사용해서 Bean을 자동으로 등록했습니다.

하지만 모든 객체를 어노테이션으로 자동 등록할 수 있는 것은 아닙니다. 직접 만든 클래스가 아니거나, 객체 생성 과정에 설정이 필요한 경우에는 @Bean을 사용해 직접 Bean으로 등록할 수 있습니다.


1. @Configuration이란?

@Configuration은 Spring 설정 클래스라는 뜻입니다. 즉, 이 클래스 안에서 Bean 등록과 관련된 설정을 작성할 수 있습니다.

@Configuration
public class AppConfig {
}

위 클래스는 일반 Java 클래스가 아니라 Spring 설정 정보를 담는 클래스가 됩니다. Spring은 @Configuration이 붙은 클래스를 설정 클래스로 인식하고, 그 안에 있는 @Bean 메서드를 확인합니다.

@Configuration
 → Spring 설정 클래스

@Bean
 → Spring Bean으로 등록할 객체를 반환하는 메서드

쉽게 말하면 @Configuration은 Bean 등록 설정을 모아두는 곳입니다.


2. @Bean이란?

@Bean은 개발자가 직접 객체를 생성해서 Spring Container에 Bean으로 등록할 때 사용하는 어노테이션입니다.

@Configuration
public class AppConfig {

    @Bean
    public BoardService boardService() {
        return new BoardService();
    }
}

위 코드에서 boardService() 메서드가 반환하는 객체는 Spring Bean으로 등록됩니다. 즉, new BoardService()로 만든 객체를 Spring Container에 맡기는 것입니다.

boardService() 메서드 실행
 → BoardService 객체 생성
 → Spring Container에 Bean으로 등록
 → 필요한 곳에 주입 가능

@Bean은 클래스 위에 붙이는 어노테이션이 아니라, 메서드 위에 붙입니다. 그리고 그 메서드가 반환하는 객체가 Bean이 됩니다.


3. @Component와 @Bean의 차이

Spring에서 Bean을 등록하는 대표적인 방법은 두 가지입니다. 하나는 @Component 계열 어노테이션을 사용하는 자동 등록 방식이고, 다른 하나는 @Bean을 사용하는 직접 등록 방식입니다.

구분 @Component 계열 @Bean
사용 위치 클래스 위에 붙입니다. 메서드 위에 붙입니다.
등록 방식 Component Scan으로 자동 등록합니다. 설정 클래스에서 직접 등록합니다.
주 사용 대상 직접 만든 Controller, Service, Repository 외부 라이브러리 객체, 설정이 필요한 객체
예시 @Service, @Repository @Bean 메서드

직접 만든 Service 클래스라면 보통 @Service를 붙여 자동 등록합니다. 반대로 클래스에 직접 어노테이션을 붙일 수 없거나, 객체 생성 과정에 설정이 필요하다면 @Bean을 사용합니다.


4. 왜 @Bean이 필요할까?

@Bean이 필요한 이유는 Spring이 자동으로 찾을 수 없는 객체도 Bean으로 등록하기 위해서입니다.

예를 들어 직접 만든 클래스라면 클래스 위에 @Component@Service를 붙이면 됩니다.

@Service
public class BoardService {
}

하지만 외부 라이브러리에서 제공하는 클래스에는 우리가 직접 @Component를 붙일 수 없습니다. 또는 객체를 만들 때 특정 값이나 설정을 넣어야 할 수도 있습니다.

@Bean이 필요한 경우

1. 외부 라이브러리 객체를 Bean으로 등록할 때
2. 객체 생성 과정에 설정값이 필요할 때
3. 직접 생성 방식을 제어해야 할 때
4. 인터페이스의 구현체를 직접 선택해서 등록할 때
5. Component Scan 대상이 아닌 객체를 등록할 때

이런 상황에서는 설정 클래스에서 @Bean을 사용해 직접 Bean으로 등록할 수 있습니다.


5. @Bean 기본 예시

간단한 예시로 날짜 포맷을 담당하는 객체를 Bean으로 등록해보겠습니다.

@Configuration
public class DateConfig {

    @Bean
    public DateTimeFormatter dateTimeFormatter() {
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    }
}

위 코드에서 dateTimeFormatter() 메서드가 반환하는 DateTimeFormatter 객체는 Spring Bean으로 등록됩니다.

이제 다른 Bean에서 생성자 주입으로 사용할 수 있습니다.

@Service
public class BoardService {

    private final DateTimeFormatter dateTimeFormatter;

    public BoardService(DateTimeFormatter dateTimeFormatter) {
        this.dateTimeFormatter = dateTimeFormatter;
    }
}

Spring Container 안에 DateTimeFormatter Bean이 등록되어 있기 때문에, BoardService에서 주입받을 수 있습니다.


6. @Bean 이름

@Bean으로 등록한 Bean의 이름은 기본적으로 메서드 이름을 사용합니다.

@Bean
public DateTimeFormatter dateTimeFormatter() {
    return DateTimeFormatter.ofPattern("yyyy-MM-dd");
}

위 Bean의 이름은 dateTimeFormatter입니다.

메서드 이름: dateTimeFormatter
Bean 이름: dateTimeFormatter

Bean 이름을 직접 지정할 수도 있습니다.

@Bean("basicDateFormatter")
public DateTimeFormatter dateTimeFormatter() {
    return DateTimeFormatter.ofPattern("yyyy-MM-dd");
}

이 경우 Bean 이름은 basicDateFormatter가 됩니다.

이름을 직접 지정하면 @Qualifier와 함께 사용할 수 있습니다.

public BoardService(
        @Qualifier("basicDateFormatter") DateTimeFormatter dateTimeFormatter
) {
    this.dateTimeFormatter = dateTimeFormatter;
}

7. @Bean 메서드에서 의존성 주입받기

@Bean 메서드도 필요한 Bean을 파라미터로 주입받을 수 있습니다.

@Configuration
public class BoardConfig {

    @Bean
    public BoardService boardService(BoardMapper boardMapper) {
        return new BoardService(boardMapper);
    }
}

위 코드에서 boardService() 메서드는 BoardMapper를 파라미터로 받습니다. Spring은 Container 안에서 BoardMapper Bean을 찾아 이 메서드에 전달합니다.

BoardMapper Bean 찾기
 → boardService(BoardMapper boardMapper)에 전달
 → BoardService 객체 생성
 → BoardService Bean 등록

즉, @Bean 메서드 안에서도 Spring의 의존성 주입이 동작합니다.


8. 인터페이스 구현체를 직접 선택하기

@Bean은 인터페이스의 구현체를 직접 선택해서 등록할 때도 사용할 수 있습니다.

public interface PayService {

    void pay();
}
public class KakaoPayService implements PayService {

    @Override
    public void pay() {
        System.out.println("카카오페이 결제");
    }
}
public class NaverPayService implements PayService {

    @Override
    public void pay() {
        System.out.println("네이버페이 결제");
    }
}

이제 설정 클래스에서 어떤 구현체를 Bean으로 등록할지 직접 선택할 수 있습니다.

@Configuration
public class PayConfig {

    @Bean
    public PayService payService() {
        return new KakaoPayService();
    }
}

위 코드는 PayService 타입의 Bean으로 KakaoPayService 구현체를 등록합니다.

PayService 타입 Bean
 → 실제 객체는 KakaoPayService

나중에 구현체를 바꾸고 싶다면 설정 코드만 변경할 수 있습니다.

@Bean
public PayService payService() {
    return new NaverPayService();
}

이처럼 @Bean을 사용하면 어떤 객체를 Spring Bean으로 등록할지 개발자가 직접 제어할 수 있습니다.


9. @Configuration이 중요한 이유

@Bean을 사용할 때는 보통 @Configuration 클래스 안에 작성합니다. 그 이유는 Spring이 설정 클래스를 특별하게 관리하기 때문입니다.

예를 들어 다음 설정을 보겠습니다.

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemberRepository();
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderService(memberRepository());
    }
}

코드만 보면 memberService()orderService()에서 memberRepository()를 각각 호출하므로 MemberRepository 객체가 여러 번 만들어질 것처럼 보입니다.

하지만 @Configuration이 붙은 설정 클래스에서는 Spring이 Bean을 Singleton으로 관리할 수 있도록 처리합니다. 그래서 같은 Bean 메서드를 여러 번 호출해도 Spring Container에 등록된 동일한 Singleton Bean을 사용하게 됩니다.

@Configuration이 있는 경우
 → Spring이 설정 클래스를 관리
 → @Bean 메서드 호출 시 Singleton Bean 유지
 → 같은 Bean을 여러 곳에서 공유

처음에는 복잡하게 느껴질 수 있지만, 핵심은 간단합니다. @Bean 설정은 @Configuration 클래스 안에 작성하는 것이 안전하고 일반적입니다.


10. @Bean과 생성자 주입 연결

@Bean으로 등록한 객체도 다른 Bean과 똑같이 생성자 주입을 받을 수 있습니다.

@Configuration
public class AppConfig {

    @Bean
    public FileStorageService fileStorageService() {
        return new LocalFileStorageService();
    }
}

위 설정으로 FileStorageService Bean이 등록되었다면, 다른 Service에서 주입받을 수 있습니다.

@Service
public class BoardFileService {

    private final FileStorageService fileStorageService;

    public BoardFileService(FileStorageService fileStorageService) {
        this.fileStorageService = fileStorageService;
    }
}

BoardFileService 입장에서는 해당 Bean이 @Component로 등록되었는지, @Bean으로 등록되었는지 중요하지 않습니다. Spring Container 안에 Bean으로 등록되어 있으면 주입받을 수 있습니다.

Bean 등록 방식
 → @Component
 → @Service
 → @Repository
 → @Bean

등록된 후에는 모두 Spring Bean

11. @Bean을 쓰기 좋은 상황

@Bean은 아무 때나 쓰기보다 직접 등록이 필요한 객체에 사용하는 것이 좋습니다.

상황 설명
외부 라이브러리 객체 등록 클래스에 직접 @Component를 붙일 수 없을 때 사용합니다.
생성 과정에 설정값 필요 생성할 때 특정 값, 옵션, 전략을 넣어야 할 때 사용합니다.
구현체 직접 선택 인터페이스 타입에 어떤 구현체를 등록할지 직접 정할 때 사용합니다.
조건에 따른 객체 생성 환경에 따라 다른 객체를 등록해야 할 때 사용할 수 있습니다.

반대로 직접 만든 Service, Controller, Repository는 보통 @Service, @Controller, @Repository로 등록하는 것이 더 자연스럽습니다.


12. 자주 하는 실수

1) @Bean과 @Service를 동시에 사용해 중복 등록하는 경우

같은 클래스를 @Service로 자동 등록해놓고, 설정 클래스에서 다시 @Bean으로 등록하면 Bean이 중복될 수 있습니다.

@Service
public class BoardService {
}
@Configuration
public class AppConfig {

    @Bean
    public BoardService boardService() {
        return new BoardService();
    }
}

이런 구조는 피하는 것이 좋습니다. 자동 등록을 사용할지, 직접 등록을 사용할지 기준을 정해야 합니다.

직접 만든 Service
 → 보통 @Service 사용

외부 객체 또는 생성 제어가 필요한 객체
 → 보통 @Bean 사용

2) DTO를 @Bean으로 등록하는 경우

DTO는 보통 요청 데이터나 응답 데이터를 담는 객체입니다. 요청마다 값이 달라질 수 있으므로 Spring Container가 Singleton Bean으로 관리할 대상이 아닙니다.

@Bean
public BoardDto boardDto() {
    return new BoardDto();
}

이런 방식은 일반적으로 사용하지 않습니다. DTO는 요청이 들어올 때 필요한 값으로 생성해서 사용하는 객체입니다.

3) @Configuration 없이 복잡한 @Bean 설정을 작성하는 경우

@Bean 메서드는 @Component 클래스 안에서도 사용할 수 있지만, 설정 용도라면 @Configuration 클래스를 사용하는 것이 일반적입니다.

특히 @Bean 메서드끼리 서로 호출하는 구조라면 @Configuration을 사용하는 것이 안전합니다.

4) Bean 이름을 헷갈리는 경우

@Bean의 기본 이름은 메서드 이름입니다. 클래스 이름이 아니라 메서드 이름이라는 점을 기억해야 합니다.

@Bean
public PayService mainPayService() {
    return new KakaoPayService();
}

위 Bean의 이름은 mainPayService입니다. kakaoPayService가 아닙니다.


13. @Component 등록과 @Bean 등록 흐름 비교

두 방식의 흐름을 비교하면 다음과 같습니다.

@Component 계열 등록 흐름

1. 클래스에 @Controller, @Service, @Repository, @Component 작성
2. Component Scan이 해당 클래스를 찾음
3. Spring Container에 Bean으로 등록
4. 필요한 곳에 DI로 주입
@Bean 등록 흐름

1. @Configuration 클래스 작성
2. @Bean 메서드 작성
3. 메서드가 반환하는 객체를 Spring Container에 등록
4. 필요한 곳에 DI로 주입

결국 두 방식 모두 Spring Container에 Bean을 등록하는 방법입니다. 등록 방식만 다를 뿐, 등록된 이후에는 똑같이 Spring Bean으로 관리됩니다.


정리

@Configuration은 Spring 설정 클래스를 의미합니다. @Bean은 메서드가 반환하는 객체를 Spring Container에 Bean으로 등록할 때 사용합니다.

@Configuration Bean 등록 설정을 담는 클래스에 사용합니다.
@Bean 메서드가 반환하는 객체를 Bean으로 등록합니다.

직접 만든 Controller, Service, Repository는 보통 @Controller, @Service, @Repository로 등록합니다. 반면 외부 라이브러리 객체나 생성 과정에 설정이 필요한 객체는 @Bean으로 직접 등록하는 경우가 많습니다.

직접 만든 계층 클래스
 → @Controller, @Service, @Repository

직접 생성 방식을 제어해야 하는 객체
 → @Configuration + @Bean

등록 방식은 달라도 Spring Container에 등록된 후에는 모두 Bean입니다. 그리고 필요한 곳에서 생성자 주입으로 사용할 수 있습니다.


연습해보기

아래 상황에서 어떤 방식으로 Bean을 등록하는 것이 좋을지 생각해보세요.

상황 추천 방식
BoardController 등록 @Controller
BoardService 등록 @Service
BoardMapper 등록 @Mapper 또는 @MapperScan
날짜 포맷 객체 등록 @Configuration + @Bean
특정 구현체를 직접 골라 등록 @Configuration + @Bean
BoardDto 등록 보통 Bean으로 등록하지 않습니다.

다음 글에서는 Bean 생명주기에 대해 정리하겠습니다. Spring Bean이 생성되고, 의존성이 주입되고, 초기화되고, 종료되는 흐름을 살펴보겠습니다.