의존관계 주입 방법

의존관계 주입 방법은 대표적으로 4가지가 있다.

  1. 생성자 주입(Constructor Injection)
    • 생성자를 통해 의존 객체를 주입하는 방식으로 가장 권장하는 방법
    • 주입이 한 번만 일어나며, 주입받는 객체를 final로 선언해 불변성을 보장 가능
    • 생성자가 하나 뿐이면 @Autowired 생략 가능
  2. 수정자 주입(Setter Injection)
    • Setter 메소드를 통해 의존 객체를 주입하는 방식
    • 선택적이고 변경 가능한 의존관계에 적합
    • @Autowired(required=false)로 필수 여부 지정 가능
  3. 필드 주입(Field Injection)
    • 필드에 직접 @Autowired를 붙여 의존 객체를 주입하는 방식
    • 코드는 간결하지만, DI 컨테이너 없이 테스트가 불가하고 불변성이 보장되지 않아 미권장
  4. 일반 메소드 주입(General Method Injection)
    • 임의의 메소드에 @Autowired를 붙여 의존 객체를 주입하는 방식
    • 한 번에 여러 의존 객체를 주입할 수 있지만, 모호하므로 특별한 경우에만 사용

이렇게 의존관계를 주입하면서도, 주입할 빈이 없는 경우에도 동작해야할 때가 있다.

  • @Autowired(required=false) : 주입할 대상이 없으면 수정자 메소드 호출하지 않음
  • @Nullable : 주입할 대상이 없으면 null이 됨
  • Optional<> : 주입할 대상이 없으면 Optional.empty가 됨
    static class TestBean {
        //호출 X
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }

 

테스트 클래스를 작성해서 실행하면 noBean2와 3은 출력되지만, 1은 호출도 하지 않기 때문에 출력이 없다.

위에서 생성자 주입을 가장 권장한다고 했는데, 권장하는 이유는 여러가지가 있다.

  • 불변성 보장 : final 선언 및 생성자 1회 호출로 불변 객체 설계 가능
  • 의존성 누락/실수 방지 : 컴파일 시 의존성 누락 바로 확인 가능
  • 순환 참조 방지 : 애플리케이션 구동 시점에 순환 참조 오류 감지
  • 테스트 용이성 : DI 컨테이너 없이도 테스트 가능
  • 코드 명확성, 유지보수성 향상 : 의존성 명확, 롬복 등과 결합 가능

롬복(Lombok)

롬복은(Lombok)은 자바에서 반복적으로 생성해야 하는 코드(getter, setter, 생성자 등)를 자동으로 생성해주는 오픈소스 라이브러리이다. 어노테이션 기반으로 동작하며, 개발자가 클래스에 특정 Lombok 어노테이션을 붙이면 컴파일 과정에서 해당 메소드들이 자동으로 생성된다.

 

롬복을 사용하기 위해서는 우선 IntelliJ 설정에서 플러그인을 설치하고 추가 설정을 해준다.

 

다음으로 gradle 설정에 의존성을 추가해주어야한다.

//추가
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

//dependencies 내에 추가
dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

 

그리고 기존 코드에 롬복을 적용한다.

@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 어노테이션이 있다.

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

 

@RequiredArgsConstructor 어노테이션을 클래스에 추가하고, 생성자를 제거한다. 추가한 어노테이션의 이름처럼 final 필드들의 생성자를 자동으로 생성해준다.

 

코드에는 보이지 않지만 생성된걸 확인할 수 있다.

조회된 빈이 2개 이상일때 주입 방법

@Autowired는 타입으로 빈을 조회한다. 그래서 전에 배웠던 내용처럼 조회된 빈이 2개 이상이면 NoUniqueBeanDefinitionException이 발생한다. 하위 타입(구현체)를 지정해서 해결할 수 있으나 DIP 위반이므로 적절하지 않다.

Autowired 필드명 매칭

@Autowired
private DiscountPolicy discountPolicy

 

기존 코드가 이렇다면,

@Autowired
private DiscountPolicy rateDiscountPolicy

 

같이 필드명을 빈 이름으로 변경하면 fixDiscountPolicy와의 충돌 없이 rateDiscountPolicy가 주입된다. 필드명 매칭은 먼저 타입으로 매칭을 시도하고, 그 결과 빈이 2개 이상일 때 추가로 동작하는 기능으로 (1) 타입 매칭 → (2) 필드명, 파라미터명으로 빈 이름 매칭과 같이 동작한다.

@Qualifier 사용

@Qualifer 어노테이션은 추가 구분자를 붙여주는 방법으로, 빈 이름을 변경하는 것은 아니다.

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

 

빈에 @Qualifer 어노테이션을 붙인다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
}

 

그리고 생성자에서도 @Qualifier를 명시함으로 mainDiscountPolicy라는 Qualifier 이름을 가진 빈을 찾는다. 만약 없을 경우에는 빈 이름으로 검색해서 추가로 찾기는 하지만, @Qualifier만 찾는 용도로 쓰는 것이 명확하다.

@Primary 사용

@Primary 어노테이션은 우선 순위를 주는 방법으로, @Autowired 시 빈이 2개 이상이면 @Primary 어노테이션이 붙어있는 쪽이 우선이다.

 

@Primary는 기본으로 사용할 빈을 지정하고, @Qualifier는 특정 빈을 지정하기 때문에 둘 다 있을 때는 @Qualifier가 우선이다.

@Qualifier 대용 어노테이션 만들기

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

 

앞선 @Qualifier("mainDiscountPolicy")만 사용하면 컴파일 시 타입 체크가 안 되고, 무엇보다 조금 길게 느껴질 수 있다. 그래서 이런식으로 @MainDiscountPolicy 어노테이션을 만들면 @Qualifier("mainDiscountPolicy")를 다는 것과 동일한 역할을 한다.

둘 다 쓰기

AllBeanTest.java
package spring.basic.autowired;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AutoAppConfig;
import spring.basic.discount.DiscountPolicy;
import spring.basic.member.Grade;
import spring.basic.member.Member;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);


    }

    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

 

DiscountService는 Map으로 모든 DiscountPolicy를 주입받고, discount 메소드는 discountCode에 따라 매칭되는 빈을 사용하는 식으로 동작한다. Map에는 키(String)에 빈 이름이, 값으로 DiscountPolicy 타입으로 조회한 모든 빈이 들어간다. List는 빈만 들어간다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 10  (0) 2025.05.31
Spring 스터디2 - 9  (0) 2025.05.13
Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 5  (1) 2025.04.19

+ Recent posts