김영한의 스프링 핵심 원리(기본편) - Bean 주입 방식과 Bean 중복 조회 문제

2025. 2. 2. 12:47·김영한의 스프링 핵심 원리(기본편)

Question

  • 수정자 주입의 단점은?
  • 수정자 주입을 사용할만한 케이스는?
  • 필드 주입의 단점은?
  • 생성자 주입을 사용해야 하는 이유는?
  • @RequiredArgsConstructor는 어떻게 자동 생성자들을 만들어 주는가?
  • @Autowired로 빈이 2개 이상 조회 되는 경우 무슨 일이 발생하는가?
  • @Qualifer에 대해 설명하시오
  • @Primary에 대해 설명하시오
  • 자동 주입보다 수동 주입 빈을 사용하면 좋은 케이스를 설명하시오

 

 

생성자 주입

@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;
    }
}
  • 생성자 호출 시점에 딱 1번만 호출되는 것을 보장
  • 불변, 필수 의존관계에 사용
  • 만약 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 되며, 위의 예제도 생서자가 하나기 때문에 사실상 @Autowired가 없어도 자동주입됨.

 

수정자 주입(Setter 주입)

@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

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

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
  • 선택, 변경 가능성이 있는 의존관계에서 사용
  • 만약 @Autowired를 했는데 안에 주입할 대상이 없으면 오류가 발생하며, @Autowired(required = false)로 지정하면 주입할 대상이 없어도 오류가 발생하지 않음.
  • 자바에서는 필드의 값을 직접 변경하지 않고 setXxx, getXxx 라는 메서드를 통해 값을 읽거나 수정하는 규칙을 만들었는데, 이를 자바빈 프로퍼티 규약이라 부름.

 

필드 주입

@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private DiscountPolicy discountPolicy;
}
  • 필드에 바로 주입하는 방법으로, 코드가 간결해 가장 편한 방법처럼 보임
  • 외부에서 값을 변경할 수 있는 방법이 없어서 테스트 하기 힘들다는 치명적인 단점이 존재
  • DI 프레임 워크가 없으면 아무것도 할 수가 없음.
  • 따라서, 사용해서는 안되는 방식
  • @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 사용 가능

 

일반 메서드 주입

@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
  • 생성자 세터가 아닌 일반 메서드를 통해 주입받는 방법
  • 한번에 여러 필드를 주입 받을 수 있음
  • 일반적으로 잘 사용하지 않음.

 

생성자 주입을 해야하는 이유

  • 과거에는 수정자 주입, 필드 주입도 많이 사용했지만 최근에는 생성자 주입만 권장함
  • 불변 보장
    • 대부분 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없으며, 변하면 안되는 경우가 많음
    • 수정자 주입은 setter 메서드를 public으로 열어둬야 하기 때문에 누군사 실수로 변경할수도 있음.
    • 생성자 주입은 객체를 생성할 때 딱 한 번만 호출되므로 이후 호출되지 않아 불변의 속성을 가짐
  • 누락 방지
    • 수정자 방식을 사용할 경우 자바 코드를 단위 테스트 하는 경우 다음과 같은 코드는 에러가 나지 않음.
      OrderServiceImpl orderService = new OrderServiceImpl()
    • 그 이유는 생성자가 없기 때문에 parameter로 넣어주지 않아도 된다는 것을 의미하고, 이 경우 실수로 주입을 해주지 않아서 런타임시 널 포인트 에러가 날 수 있음.
    • 하지만, 생성자 주입시에는 new OrderServiceImpl() 에서 주입할 데이터가 누락되 컴파일 에러가 나서 추후 오류를 방지할 수 있음.
  • final 키워드 사용 가능
    • 아래 코드에서 실수로 생성자에 discountPolicy를 설정해주는 부분이 누락되었는데, final로 선언되어있기 때문에 컴파일 오류를 통해 우리는 미리 문제를 찾을 수 있음.
@Component
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
	
    @Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
 		this.memberRepository = memberRepository;
	}
 //...
}
  • 정리
    • 생성자 주입은 프레임워크에 의존하지 않고 순수 자바 언어의 특징을 잘 살릴 수 있음.
    • 기본으로 생성자 주입을 사용하고 필수 값이 아닌 경우 수정자 주입 옵션을 부여하여, 생성자 + 수정자 조합을 사용할 수는 있음.
    • 필드 주입은 사용하지 마라.

 

롬복과 @RequiredArgsConstructor

//기존 코드
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;
	}
}

//롬복을 적용한 코드
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
}
  • @RequiredArgsConstructor는 final이 붙은 필드를 모아서 자동으로 생성자를 만들어 줌
  • 위 코드에서 기존 코드와 롬복을 적용한 코드는 정확하게 같은 코드지만 후자가 훨씬 간결함
  • 최근에는 생성자를 하나만 두고 @Autowired를 생략하는 방법을 주로 사용하며, 롬복의 @RequiredArgsConstructor도 함께 사용함

 

롬복 라이브러리 적용 방법

plugins {
    id 'org.springframework.boot' version '2.3.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

// lombok 설정 추가 시작
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
// lombok 설정 추가 끝

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'

    // lombok 라이브러리 추가 시작
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    // lombok 라이브러리 추가 끝

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}
  • build. gralde에 위의 설정 추가
  • Preferences(윈도우 File Settings) plugin lombok 검색 설치 실행 (재시작)
  • Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)
  • 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인

 

@Autowired에서 빈이 2개 이상 조회되는 경우

@Autowired
private DiscountPolicy discountPolicy
  • 만약 위와 같이 DiscountPolicy 빈을 자동 주입받고자 했는데, FixDiscountPolicy, RateDiscountPolicy 모두 스프링 빈으로 등록할 경우에는 NoUniqueBeanDefinitionException 에러가 발생함
  • 이 때, 하위 타입으로 바로 지정을 할 수도 있지만 이는 DIP를 위배하고 유연성이 떨어짐
  • 하지만 만약 필드 명을 빈 이름으로 변경하면, @Autowired가 타입 매칭 이후 결과가 2개 이상일 경이 필드명, 파라미터 명으로 빈 이름 매칭을 시도함.
  • 예를 들어서 위의 코드를 private DiscountPolicy rateDiscountPolicy 라고 선언하면, 스프링이 FixDiscountPolicy가 아닌 RateDiscountPolicy를 우선으로 매칭함.

 

@Qualifier 사용

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}


@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}
  • @Autowired로 여러 빈이 조회되면 @Qualifier를 통해 이름을 지정해 매칭시켜줄 수 있음.
  • @Qualifer는 빈 이름을 변경하는 것은 아니고 추가적인 구분자를 붙여주는 방식 
  • @Qualifier로 매칭되는게 없는 경우 mainDiscountPolicy 라는 이름의 스프링 빈을 추가로 찾음.
  • 하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는게 명확함

 

@Primary 사용

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
  • @Autowired로 동시에 여러 빈이 매칭되면 @Primary가 우선권을 가짐
  • @Qualifier는 @Component에도 @Qualifier를 붙여줘야하고 @Autowired에도 붙여줘야하지만 @Primary는 @Bean에만 붙여주면 됨
  • 따라서, @Primary를 일반적으로 사용을 해주고 예를 들어 서브 데이터베이스를 명시적으로 한 번씩 사용이 필요할 경우에 @Qualifer를 지정해서 사용하는 방식을 채택 가능
  • @Primary보다는 @Qualifier가 더 상세하기 동작하므로 @Qualifier가 @primary보다는 우선권이 높음.

 

조회된 여러 빈이 모두 필요할 경우 List, Map을 사용

@Autowired
private final Map<String, DiscountPolicy> policyMap;

@Autowired
private final List<DiscountPolicy> policies;


DiscountPolicy discountPolicy = policyMap.get(discountCode);
  • 위와 같이 Map이나 List를 사용하여 여러 빈을 조회해서 꺼내 사용하는 방법도 있음.

 

실무에서의 빈 주입 방법

  • 점점 자동 주입을 선호하는 추세
  • 컴포넌트 스캔을 기본으로 사용하고, @Controller, @Service, @Repository 처럼 계층에 맞추어 자동 스캔 활용
  • 스프링 부트에서는 여러 설정을 기본적으로 자동 주입으로 지원
  • 구성하는 영역과 실제 동작하는 영역을 명확하게 나누는 것이 이상적이지만, 개발자 입장에서 @Component만 넣어주면 되는 일을 @Configuration 설정 정보에 @Bean을 하나하나 추가하고 주입할 대상을 적어주는 일은 너무 번거로운 일
  • 또한, 관리할 @Bean이 많아지면 부담이 많아짐
  • 결정적으로, 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있음.

 

수동 빈 등록을 사용하면 좋은 케이스

  • 업무 로직 빈은 자동으로 기술 로직빈은 수동으로
    • 업무 로직 빈
      • 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 레포지토리 등
      • 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
      • 숫자가 매우 많고, 한번 개발할 때 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 존재
      • 자동 주입 기능을 적극적으로 사용하면 좋은데, 그 이유는 어느정도 유사한 패턴이 존재하기 때문에 어디서 문제가 발생했는지 파악이 어렵지 않음
    • 기술 지원 빈
      • 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용됨
      • 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술
      • 업무 로직에 비해 그 수가 매우 적고 애플리케이션 전반에 영향을 미침
      • 광범위하게 영향을 미치기 때문에 기술 지원 빈의 문제를 찾기가 어려울 수 있음
      • 따라서, 가급적 수동 빈 등록을 해서 명확하게 드러내주는 게 좋음

'김영한의 스프링 핵심 원리(기본편)' 카테고리의 다른 글

김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 스코프와 Provider 그리고 Proxy  (3) 2025.02.02
김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 라이프사이클과 생명주기 콜백(@PostConstruct, @PreDestory)  (2) 2025.02.02
김영한의 스프링 핵심 원리(기본편) - @ComponentScan과 @Autowired  (2) 2025.01.31
김영한의 스프링 핵심 원리(기본편) - 스프링의 싱글톤 패턴과 @Configuration  (1) 2025.01.31
김영한의 스프링 핵심 원리(기본편) - BeanFactory와 ApplicationContext  (1) 2025.01.31
'김영한의 스프링 핵심 원리(기본편)' 카테고리의 다른 글
  • 김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 스코프와 Provider 그리고 Proxy
  • 김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 라이프사이클과 생명주기 콜백(@PostConstruct, @PreDestory)
  • 김영한의 스프링 핵심 원리(기본편) - @ComponentScan과 @Autowired
  • 김영한의 스프링 핵심 원리(기본편) - 스프링의 싱글톤 패턴과 @Configuration
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      양방향 맵핑
      JPQL
      조회 성능 최적화
      @args
      락
      Thread
      Target
      typequery
      @discriminatorcolumn
      WAS
      페치 조인
      @within
      버퍼
      단방향 맵핑
      빈 후처리기
      jpq
      @discriminatorvalue
      김영한
      requset scope
      고급
      cglib
      프록시
      hibernate5module
      reentarantlock
      gesingleresult
      자바
      log trace
      프록시 팩토리
      jdk 동적 프록시
      스레드
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 스프링 핵심 원리(기본편) - Bean 주입 방식과 Bean 중복 조회 문제
    상단으로

    티스토리툴바