AtraFelis's Develop Diary

[Spring] 컴포넌트 스캔 (스프링 핵심원리 - 기본편 Section7) 본문

Programming/Spring

[Spring] 컴포넌트 스캔 (스프링 핵심원리 - 기본편 Section7)

AtraFelis 2025. 1. 17. 01:56

스프링 핵심 원리 - 기본편 강의를 수강하며 작성한 글입니다.
*Section 7

이제까지는 설정 정보, 그러니까 @Configuration을 이용하여 스프링 빈을 등록해주었다. 하지만 이렇게 일일히 스프링 빈을 등록하는 것도 굉장히 귀찮은 일이다. 프로젝트의 규모가 커지면(언제나 세계 규모의 프로젝트를 진행한다고 상상을 하며 공부를 한다) 빈으로 등록해주어야 할 클래스들도 엄청나게 늘어날 것이다.

이런 귀찮은 일을 해소하기 위해 만들어진 것이, 컴포넌트 스캔이다.

컴포넌트 스캔을 이용하여 의존관계를 자동 주입하는 방법은 간단하다.

구성 파일로 사용하고자 하는 클래스에 @ComponenetScan을, 스프링 빈으로 등록하고자 하는 클래스에는 @Component를 붙이면 된다.

@ComponentScan
public class AppConfig {}
@Component  
public class MemoryMemberRepository implements MemberRepository {  

    private static Map<Long, Member> store = new HashMap<>();  

    @Override  
    public void save(Member member) { ... }  

    @Override  
    public Member findById(Long memberId) { ... }  
}

이렇게 하면, 자동으로 스프링 빈에 MemoryMemberRepository가 등록이 된다. 이름은 수동으로 등록할 때 와 동일한 규칙을 적용하여, 따로 지정을 하지 않을 경우 첫 문자만 소문자로 바꾸어 memoryMemberRepository로 스프링 빈에 등록된다.

즉, @Component가 붙은 클래스가 스캔의 대상이 되어 스프링 컨테이너에 등록이되는 것이다.

그리고 중요한 애노테이션이 하나 더 있는데, 바로 @AutoWired이다.

이렇게 OrderServiceImpl 같은 경우에는 다른 두 가지의 클래스 memberRepositorydiscountPolicy에 의존적이다.

@Component  
public class OrderServiceImpl implements OrderService {  

    private final MemberRepository memberRepository;  
    private final DiscountPolicy discountPolicy;  

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {  
        this.memberRepository = memberRepository;  
        this.discountPolicy = discountPolicy;  
    }

    /* 생략 */
}

그렇기에 기존의 AppConfig에서 직접적으로 의존관계를 명시해주었다. 하지만 @ComponentScan을 사용할 때는 이런 설정 정보 자체가 사라지므로, 클래스 내부에서 직접적으로 명시를 해주어야한다.

그 역할을 하는 것이 바로 @AutoWired이다.

@Component  
public class OrderServiceImpl implements OrderService {  

    private final MemberRepository memberRepository;  
    private final DiscountPolicy discountPolicy;  

    @AutoWired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {  
        this.memberRepository = memberRepository;  
        this.discountPolicy = discountPolicy;  
    }

    /* 생략 */
}

이렇게 생성자 위에 @Autowired를 붙여주면, 스프링에서 의존관계를 자동으로 주입해준다.

💡 Tip  의존 관계 주입 방식

의존 관계를 주입하는 방법에는 세 가지가 존재한다.

1. 생성자 주입

@Component  
public class MemberServiceImpl implements MemberService {  
    private MemberRepository memberRepository;  

    @Autowired  
    public MemberServiceImpl(MemberRepository memberRepository) {  
        this.memberRepository = memberRepository;  
    }
}


2. setter 주입

@Component  
public class MemberServiceImpl implements MemberService {  
    private MemberRepository memberRepository;  

    @Autowired  
    public void setMemberRepository(MemberRepository memberRepository) {  
        this.memberRepository = memberRepository;  
    }
}


3. 필드 주입

@Component
public class MemberServiceImpl implements MemberService {
	@AutoWired
    private MemberRepository memberRepository;
}


하지만 의존관계를 실행 중 동적으로 변경할 경우는 거의 없으므로 생성자 주입을 권장한다. 이것에 대한 더 자세한 설명은 바로 다음 Section에 나온다.

 

컴포넌트 스캔의 동작 과정

1. @ComponentScan 

@Component가 붙은 모든 클래스를 스캔하여, 스프링 컨테이너에 등록한다. 이때 스캔의 대상은 따로 지정하지 않았다면, @ComponentScan이 붙은 설정 파일과 동일한 패키지와 그 하위 패키지가 된다.

위에서 언급했지만, 스프링 빈의 이름은 지정하지 않았을 시, 클래스 명에서 첫 문자만 소문자로 바꾸어 스프링 컨테이너에 등록한다.

💡 Tip  이름 부여하는 법

이름을 따로 지정하고 싶을 경우, @Component("AtraFelis")와 같이 지정해줄 수 있다. 하지만 이전 포스팅에서도 말했듯, 타당한 이유 없이 자의적으로 변경하는 것은 개발에 혼란을 불러올 수 있으므로 권장하지 않는다.

2. @AutoWired 

스프링 빈에 등록을 했으므로, 이 스프링 빈 객체를 이용해 의존관계를 주입해준다. @AutoWired가 붙어 있는 생성자(혹은 필드, setter 메소드)를 찾아 스프링 컨테이너가 자동으로 주입해준다.

 

컴포넌트 스캔의 대상

@Component가 붙은 클래스가 컴포넌트 스캔의 대상이라고 하였다. 하지만, 이것이 이외에도 컴포넌트 스캔이 되는 애노테이션이 더 존재한다.

// Service

@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Component  
public @interface Service { ... }
// Controller

@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Component  
public @interface Controller{ ... }
// Repository

@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Component  
public @interface Repository{ ... }
// Configuration

@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Component  
public @interface Configuration{ ... }

위 애노테이션들을 보면 전부 @Component를 포함하고 있는 것을 알 수 있다. 즉, 이 애노테이션을 붙이면 자동으로 @Component가 붙은 것과 동일하게 컴포넌트 스캔의 대상이 되는 것이다.

또, 여기서 한 가지 중요한 것은 설정 파일을 의미하는 @Configuration도 컴포넌트 스캔의 대상이라는 것이다. 이전의 기억을 떠올려보면, AppConfig또한 스프링 빈에 등록되었던 것을 알 수 있을 것이다.

그 이유가 바로 @Configuration에 포함된 @Component 때문이다.

어? 하지만 기존에는 @Configuration을 스캔할 클래스가 없었지 않나요?

날카로운 의문이다. 이 의문에 대한 답은 바로 @SpringBootApplication에 존재한다.

 

@SpringBootApplication

스프링 부트를 이용해 스프링 프로젝트를 처음 생성하면, 이런 애노테이션이 붙은 클래스가 기본적으로 생성되어 있다.

이 애노테이션을 직접 확인해보면,

@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Inherited  
@SpringBootConfiguration  
@EnableAutoConfiguration  
@ComponentScan( ... )  
public @interface SpringBootApplication { ... }

@ComponentScan이 붙어 있는 것을 알 수 있다.

즉, 스프링 부트를 사용한다면 따로 설정 파일 클래스를 만들 필요 없이, 스프링 빈으로 등록하고 싶은 클래스에만 @Comopnent, @AutoWired를 붙이면 되는 것이다.

[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술]

 

[지금 무료]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확

www.inflearn.com

이 강의를 들으셨던 분은 생각해보면 설정 파일을 따로 만들기 전에 @Service, @Repository 등의 애노테이션를 이용해 자동으로 스프링 빈에 등록했던 기억이 있을 것이다.

@ComponentScan으로 설정 파일을 구성하지 않았음에도, 그것이 되었던 이유가 바로 @SpringBootApplication 때문이다.

또 위에서 던졌던 질문인 "@Configuration을 스캔하는 클래스가 없었지 않나요?"에 대한 답도 이것으로 되었을 것이다.

 

중복 등록

자동 빈 등록 vs 자동 빈 등록

자동으로 등록 시에는 이미 같은 이름으로 등록된 스프링 빈이 존재하면, ConflictingBeanDefinitionException 예외가 발생한다.

수동으로 등록한 빈들끼리는 이름이 중복되었을 경우에는 설정에 따라 하나의 빈을 덮어버려 이 예외가 발생하지 않을 수 있다.

 

자동 빈 등록 vs 수동 빈 등록

이 경우에는 수동 빈이 자동 빈을 오버라이딩하는 형태로 우선권을 가진다.

Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing

이런 로그가 나타난다.

하지만, 스프링 부트에서는 이 경우 오류가 발생하도록 기본값으로 설정이 되어있다. @SpringBootApplication이 붙은 스프링 부트 클래스를 실행해보면, 실행되지 않고 에러가 나타나는 것을 확인할 수 있다.

 


References

스프링 핵심 원리 - 기본편