본문 바로가기
Spring

[Spring] 스프링 빈을 등록하는 방법 - 수동 빈 등록 @Bean, 자동 빈 등록 @Component

by jinjin98 2022. 8. 19.

 

먼저 빈(bean) 이란건 무엇일까?

스프링 프레임워크의 특징은 IoC(Inversion of Control) 입니다.

개발자가 갖고 있는 프로그램에 대한 제어권이 스프링에게 넘어간 것을 의미합니다.

이 때 스프링이 찾고 직접 관리하고 있는 객체를 빈 이라고 합니다.

스프링이 관리하는 빈은 개발자가 등록을 해줘야 하고 빈을 등록하는 방법은 수동 빈 등록과 자동 빈 등록이 있습니다.

xml파일을 통해 빈을 등록하는 방법도 있지만 여기선 앞에서 언급한 2가지 방법만 정리해보겠습니다.

 

 수동 빈 등록

 

@Configuration
public class Config {
    
    @Bean(name = "MemberServiceImpl")
    public MemberService memberService(){
        return new MemberServiceImpl( memberRepository() );
    }
    
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

 

먼저 스프링 설정 클래스를 생성하고 클래스 위에 @Configuration을 붙여줍니다. 

 @Configuration 를 붙여주는건 Config 라는 클래스에 설정을 구성해주고

스프링에 등록되는 빈이 싱글톤이 되도록 보장해줍니다. 또한 Config 도 빈으로 등록됩니다.

싱글톤은 요청을 할 때마다 객체를 새로 생성하는게 아닌 딱 하나의 객체만 만들어 공유하도록 하는걸 말합니다.

클래스 안에는 빈으로 등록할 메서드를 만들어주고 메서드 위에 @Bean 을 붙여주면

나중에 프로그램이 실행될 떄 스프링에 빈으로 등록됩니다.

빈으로 등록될 때 빈의 이름은 메서드 이름로 저장이 되지만 다른 이름으로 지정하고 싶다면

위 코드의 memberService() 메서드처럼 등록될 이름을 직접 적어줄 수도 있습니다.

 

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);

        MemberServiceImpl memberService = ac.getBean("MemberServiceImpl", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService ->memberRepository = " + memberRepository1);
        System.out.println("orderService ->memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }

}

 

빈이 제대로 등록되어있는지 확인해보겠습니다. 먼저 빈을 찾고 관리하는 스프링 컨테이너 객체를 만들어야 합니다.

스프링 컨테이너는 ApplicationContext 클래스로 생성하고

AnnotationConfigApplicationContext 객체를 주입해줍니다.

AnnotationConfigApplicationContext 는  ApplicationContext 의 구현체이고 우리는 @인 어노테이션으로 

빈을 등록했으니 이 구현체를 사용하며 구현체 파라미터에는 아까 생성한 설정 클래스 Config를 넣어줍니다.

스프링 컨테이너인 ApplicationContext 클래스 객체에서 getBean() 메서드로 등록된 빈을 조회하는데

조회할 때는 빈 이름과 타입 클래스를 파라미터로 넣어줘야 합니다.

빈 메서드의 반환타입이 중복되는게 없다면 빈 이름은 넣지 않고 반환 클래스타입만 넣어줘도 무방하지만

반환 클래스타입으로만 조회한다면 해당 클래스와 해당 클래스의 하위 타입(구현체 클래스 타입) 까지 

모두 가져오기 때문에 이럴 경우 getBean() 메서드를 사용하면 에러가 납니다.

 

memberService() 메서드와 orderService() 메서드는 둘 다 반환 객체 파라미터안에 memberRepository() 메서드를

담고 있어 memberRepository() 메서드를 통해 받은 MemoryRepository 객체를 가져올 수 있습니다,

MemberServiceImpl OrderServiceImpl 두 클래스에서 정의한 getMemoryRepository 로 MemoryRepository

객체를 가져오고 memoryRepository 이름으로 등록된 빈으로 MemoryRepository 객체를 가져와 출력해보면

 

 

3개의 객체 모두 같은 해시코드를 출력해 동일한 객체인 것을 확인할 수 있습니다.

위에서 설명한거 처럼 @Configuration 를 설정 클래스 Config에 붙여줘 등록된 빈 객체가 싱글톤으로 

관리되어 하나의 객체만 생성해 공유되어 사용하므로 이런 결과가 나오게 됩니다.

 

̱ 자동 빈 등록

 

package hello.core.order;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        basePackages = "hello.core.member",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoConfig {

}

 

자동 빈 등록은 설정 클래스에 @ComponentScan 를 붙이고

빈으로 등록하고 싶은 객체의 해당 클래스에 @Component 를 붙이면 됩니다.

그러면 @ComponentScan 이 @Component 가 붙어있는 클래스를 모두 찾아 빈으로 등록해줍니다.

이 때 등록되는 빈의 이름은 클래스 이름에서 앞글자만 소문자로 바꿔서 저장됩니다.

ex)  MemberServicceImpl  -> memberServicceImpl 

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

@Component 뿐만 아닙니다. @Configuration 과 웹 어플리케이션을 만들 때 사용되는 어노테이션인

@Service, @Repository, @Controller 도 @ComponentScan 의 스캔 대상입니다.

이 어노테이션 모두 @Component 가 붙어있기 때문입니다,

 

 

@ComponentScan 의  스캔 범위는 설정 클래스가 있는 패키지와 그 하위 패키지이고 

basePackage 로 따로 탐색 범위를 지정 할 수도 있습니다.

또한 excludeFilter 로 스캔 대상에서 제외할 클래스도 설정 할 수 있습니다.

위 코드는 @Configuration 어노테이션이 붙어있는 클래스는 스캔하지 않는다는 의미입니다.

 

참고로 스프링 부트를 사용하게 되면 따로 설정 클래스를 만들어주지 않아도 됩니다.

스프링 부트로 프로젝트를 만들면 프로젝트 시작 루트 위치에 프로젝트를 실행할 수 있는 클래스가 기본으로 생성되어

있는데 이 클래스에 @SpringBootApplication 가 붙어있고

이 어노테이션안에 @ComponentScan이 있기 때문입니다.

 

package hello.core.member;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

}

 

@ComponentScan 탐색 범위인 hello.core.member 패키지안에 있는 MemberServicceImpl 클래스에

@Component 를 붙여 현재 클래스가 빈에 등록되게 했습니다.

여기서 의문이 듭니다.

수동 빈 등록에서는 반환되는 객체 파라미터 안에 @Bean으로 등록된  memberRepository() 메서드를

호출해 MemberRepository 객체를 넣어줬는데 자동 빈 등록에서는 어떻게 넣어주는걸까?

 

@Bean
public MemberService memberService(){

    return new MemberServiceImpl( memberRepository() );
}

 

자동 빈 등록은 수정 빈 등록처럼 설정 클래스에서 설정 정보와 의존 관계를 직접 작성해주지 못합니다.

(의존은 그 코드를 알고 있고 해당 코드가 변경되면 영향을 받는 관계가 말합니다.

ex) MemberRepository 메서드 이름이 바뀐다면  MemberServicceImpl 의 메서드를 사용하는

 MemberServicceImpl 클래스 안의 코드도 변경해야합니다.)

그렇기 때문에 의존 관계 주입을 현재 MemberServicceImpl 클래스 안에서 해줘야 합니다.

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

이 생성자 코드를 작성하면 

new MemberServiceImpl( new MemoryMemberRepository ) 처럼 MemberServiceImpl 이 의존하는

MemberRepository 객체를 직접 주입해줘야 합니다.

이 때 생성자에 @Autowired 를 붙여주면 스프링 컨테이너가 등록된 빈중에서 클래스 타입이

MemberRepository 인걸 자동으로 찾아서 이 생성자 파라미터에 주입해줍니다. 수동 빈 등록 테스트 코드에서

getBean(MemberRepository.class) 와 비슷힙니다.

그래서 MemberRepository 구현체인 MemoryMemberRepository 클래스에도 @Component 를 붙여줘 빈으로

등록시켜 주고 MemberServiceImpl 생성자에 @Autowired 를 붙여주면 자동 의존 주입 됩니다.

이렇게 의존 객체를 직접 생산하는게 아닌 의존 객체를 컨테이너에서 대신 주입해주는 것을 스프링에서는

DI(Depengency Injection) 이라고 부릅니다.

 

package hello.core.member;

import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;


@Component
public class MemoryMemberRepository implements MemberRepository{

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

    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(long memberId) {
        return store.get(memberId);
    }
}

 

@Autowired는 파라미터 타입을 보고 해당 타입에 맞는 객체를 주입해준다고 했는데

getBean(MemberRepository.class) 와 같이 명시해준 타입과 그 하위 타입으로 등록된 빈을 모두 찾아오기 때문에

원하는 빈 객체를 주입하고 싶다면 어떤 빈 객체를 주입할지 설정을 해야 합니다.

만약 MemoryMemberRepository 이외에 MemberRepository 의 다른 구현체가 있고

MemoryMemberRepository 빈 객체를 주입 받고 싶다면 MemberServiceImpl 의 생성자의 파라미터 이름을

MemoryMemberRepository 빈 이름과 같은 memoryMemberRepository 로 작성해주는 방법도 있고

MemoryMemberRepository 클래스에 @Qualifier("값")붙이고 MemberServiceImpl 의

생성자의 파라미터 타입 앞에도 똑같이 @Qualifier("값")을 붙여 주입해줄 빈 객체를 식별시키는 방법도 있습니다.

 

@Component
@Qualifier("memoryMemberRepository")
public class MemoryMemberRepository implements MemberRepository{
@Autowired
public MemberServiceImpl(@Qualifier("memoryMemberRepository") MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

 

참고로 만약 생성자가 하나라면 @Autowired 를 생략이 가능합니다.

현재 설명한건 생성자 의존 관계 주입인데요. 이 뿐만 아니라 다른 방법으로도 의존 관계 주입이 가능합니다.

 

수정자 주입

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

 

필드 주입

@Autowired
private MemberRepository memberRepository

 

일반 메서드 주입

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

 

 

이렇게 여러가지 방법이 있지만 생성자 주입 방법이 가장 좋은 방법입니다.

의존관계는 어플리케이션 종료 전까지 변하면 안되는데 생성자 주입은 객체를 생성할 때

딱 1번만 호출되므로 불변하게 설계할 수 있습니다.

 

public class AutoConfigTest {

    @Test
    void basicScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);

        MemberRepository memberRepository = memberService.getMemberRepository();

        MemberRepository memberRepository2 = ac.getBean(MemberRepository.class);

       	System.out.println("memberRepository = " + memberRepository);
        System.out.println("memberRepository2 = " + memberRepository2);
    }
}

 

수동 빈 테스트처럼 이번에도 스프링 컨테이너 생성자 파라미터로

자동 빈 설정 클래스를 넣어주고 빈을 꺼내와봤습니다.

MemberServiceImple 클래스 타입으로 조회한 빈 객체로 주입받은 MemberRepository 객체를 출력해보고

MemberRepository 클래스 타입으로 조회한 빈 객체를 출력해봤습니다.

 

 

수동 빈 등록과 똑같이 싱글톤을 보장해 같은 객체임을 알 수 있습니다.

 

댓글