@Autowired를 이용한 의존관계 주입 방법

2022. 7. 29. 14:43JAVA/Spring

목차

  • 스프링 빈 의존관계 주입 방법
  • 의존관계 주입 옵션 처리
  • 의존관계 주입 방법중 생성자 주입을 권장하는 이유
  • Lombok @RequiredArgsConstructor를 이용한 의존관계 주입 코드 단축하기

 

1. 스프링 빈 의존관계 주입 방법

스프링 컨테이너 등록된 스프링 빈간에 의존관계를 주입하기 위한 방법으로는 4가지 방법이 있습니다.

  • 생성자 주입
  • setter 주입
  • 필드 주입
  • 일반 메소드 주입

 

1.1 생성자 주입

생성자 주입 방법은 클래스 안에 생성자를 이용하여 의존 관계를 주입받는 방법입니다. 생성자 주입 방법의 특징은 다음과 같습니다.

  • 생성자 호출시점에 1번만 호출됩니다.
  • 불변, 필수 의존관계에 사용됩니다.
    • 불변적인 의존관계가 되면 의존관계가 한번 초기화되면 다른 객체로 변경되지 않음을 의미합니다.
    • 필수적인 의존관계가 되면 스프링 빈의 의존관계 초기화를 무조건 해야함을 의미합니다.

 

생성자 주입 예시

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discardPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discardPolicy) {
        this.memberRepository = memberRepository;
        this.discardPolicy = discardPolicy;
    }
    // ...
}

OrderServiceImpl 객체는 MemberRepository와 DiscountPolicy 객체에 의존하고 있습니다. 생성자를 이용하여 주입하기 위해서는 의존하는 객체를 대상으로 매개변수 생성자를 정의합니다. 위와 같이 정의하면 스프링 컨테이너가 생성자를 이용하여 의존관계를 주입해줍니다.

 

참고 : 생성자가 1개만 있는 경우 @Autowired를 생략해도 자동 주입됩니다. 단, 스프링 빈에만 해당됩니다.

 

1.2 setter 주입

setter 주입 방법은 필드 멤버에 대한 설정자 메소드(setter)를 정의하여 의존 관계를 주입받는 방법입니다. setter 주입 방법의 특징은 다음과 같습니다.

  • 선택적, 변경 가능성이 있는 의존관계에서 사용될 수 있습니다.
  • 자바빈 프로퍼티 규약의 설정자 메소드 방식을 사용하는 방법입니다.
    • 자바빈 프로퍼티 규약은 필드의 값을 직접 변경하지 않고 getter/setter 메소드를 이용하여 값을 읽거나 수정해야하는 규칙을 의미합니다. 
    • 예를 들어 필드 멤버의 이름이 memberRepository라면 getter 메소드의 이름은 getMemberRepository, setter 메소드의 이름은 setMemberRepository가 됩니다.

 

setter 주입 예시

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discardPolicy;

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

    @Autowired
    public void setDiscardPolicy(DiscountPolicy discardPolicy) {
        this.discardPolicy = discardPolicy;
    }
    //...
}

@Autowired의 기본 동작은 주입할 대상(MemberRepository, DiscountPolicy)이 없으면 오류가 발생합니다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정해야 합니다.

 

1.3 필드 주입

필드 주입 방법은 의존 객체에 바로 주입하는 방법입니다. 필드 주입 방법의 특징은 다음과 같습니다.

  • 코드가 간결해지지만 외부에서 변경이 불가능해져서 테스트하기가 힘들어집니다.
    • 생성자가 없기 때문에 테스트시 외부에서 원하는 객체를 주입할 수 없습니다.
  • DI(Dependency Injection) 프레임워크가 없다면 아무것도 할 수 없습니다.
    • 생성자가 없이 주입하기 때문에 DI 프레임워크가 없다면 의존 객체에 주입할 방법이 없어집니다.
  • 스프링 설정을 목적으로 하는 @Configuration이 적용된 클래스에서만 특별한 용도로 사용됩니다.

 

 

필드 주입 예시

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discardPolicy;
    //...
}

 

필드 주입 방법을 비권장하는 이유

  • 순수한 자바 테스트 코드에는 @Autowired가 동작하지 않습니다.
  • @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 사용이 가능합니다.

 

1.4 일반 메소드 주입

일반 메소드 주입 방법은 말 그대로 일반 메소드를 통하여 의존관계를 주입할 수 있습니다. 일반 메소드 주입 방법의 특징은 다음과 같습니다.

  • 한번에 여러 필드를 주입받을 수 있습니다.

 

일반 메소드 주입 예시

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discardPolicy;

    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discardPolicy) {
        this.memberRepository = memberRepository;
        this.discardPolicy = discardPolicy;
    }
    // ...
}

위와 같이 init 메소드의 매개변수로 1개 이상의 의존 객체를 받아서 주입받을 수 있습니다.

 

2. 의존관계 주입 옵션 처리

@Autowired을 이용한 자동 주입 실행시 스프링 컨테이너에 자동 주입하고자 하는 스프링 빈이 존재하지 않는 경우 에러가 발생합니다. 이러한 경우 따로 옵션을 설정하여 스프링 빈이 없음에도 진행은 하도록 할 수 있습니다. 자동 주입 대상을 옵션으로 처리하는 방법에는 다음과 같은 방법이 있습니다.

  • @Autowired(required = false) 옵션 설정을 통한 처리
  • @Nullable을 이용한 처리
  • Optional을 이용한 처리

 

의존관계 주입 옵션 처리 예시

public class AutowiredTest {

    @Test
    public void AutowiredOption() {
        //given
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
            TestBean.class);
        //when

        //then
    }

    static class TestBean {

        // required=false 설정시 호출 안됨
        @Autowired(required = false)
        public void setNoBean1(Member member) {
            System.out.println("setNoBean1 = " + member);
        }

        // null 호출
        @Autowired
        public void setNoBean2(@Nullable Member member) {
            System.out.println("setNoBean2 = " + member);
        }

        // Optional.empty 호출
        @Autowired(required = false)
        public void setNoBean3(Optional<Member> member) {
            System.out.println("setNoBean3 = " + member);
        }
    }
}
  • @Autowired(required = false) : 주입할 스프링 빈이 없으면 호출 자체가 되지 않습니다.
  • @Nullable : 주입할 스프링 빈이 없으면 null을 전달합니다.
  • @Optional<Member> : 주입할 스프링 빈이 없으면 Optional.empty를 전달합니다.

 

3. 의존관계 주입 방법중 생성자 주입을 권장하는 이유

3.1 의존 관계 주입 방법중 생성자 주입을 권장하는 이유는 무엇인가?

스프링을 포함한 DI 프레임워크 대부분이 생성자 주입 방법을 권장합니다. 그 이유는 다음과 같습니다.

  • 대부분의 의존 관계 주입은 한번만 초기화하면 애플리케이션 종료 시점까지 의존관계가 변경될 일이 적습니다.
  • setter 주입 방법을 사용한다면, setXXX 메소드를 public으로 열어 두어야 하고 이는 외부에서 의존 객체를 변경할 가능성이 높아서 위험해집니다.
  • 생성자 주입을 사용하면 의존 객체를 무조건 초기화해야하기 때문에 누락하는 경우 컴파일 오류가 발생할 수 있습니다. 또한 IDE에서는 어떤 값을 필수로 주입해야 하는지도 알 수 있습니다.

 

 

3.2 생성자 주입에서 final 키워드를 사용하기

생성자 주입을 사용시 의존 객체에 final 키워드를 사용하면 생성자 주입시 누락되는 일이 없습니다.

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    // ...
}

예를 들어 위와 같이 memberRepository와 discountPolicy 의존 객체에 final 키워드를 붙였다고 가정합니다. 그리고 의존 객체에 대한 생성자 주입시 discountPolicy를 누락하고 구현시 컴파일 오류가 발생합니다. 또한 IDE에서도 discountPolicy가 누락되었다고 알려줍니다.

 

참고 : setter 주입, 필드 주입, 일반 메소드 주입 방식은 모두 생성자 이후에 호출되므로 필드에 final 키워드를 사용할 수 없습니다. 오직 생성자 주입 방법만 final 키워드를 사용할 수 있습니다.

 

4. Lombok @RequiredArgsConstructor를 이용한 의존관계 주입 코드 단축하기

대부분의 의존 객체는 불변인 상태가 많고 final 키워드를 사용하게 됩니다. 그리고 이러한 의존 객체를 주입하기 위해서 생성자 주입 방법을 많이 사용합니다. 불편한 점은 생성자를 만들게 되고 어떤 의존 객체가 추가되었을 경우 다시 생성자를 만들어야 하는 점이 불편한 점이 있습니다. 

 

예를 들어 다음과 같이 OrderServiceImpl 객체는 memberRepository와 discountPolicy 의존 객체에게 의존을 하고 생성자를 통해서 자동 주입을 수행합니다.

@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;
    }
    // ...
}

 

위와 같이 구현을 하다가 어떤 추가적인 의존 객체를 필드 멤버에 추가하였습니다.

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    private final Something something;

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

위와 같은 상태에서 기존 생성자를 제거하고 다시 생성자를 만들어야 하는 불편함이 존재합니다.

 

이러한 불편한 점을 해결하기 위해서 Lombok 라이브러리의 @RequiredArgsConstructor를 사용하면 생성자를 따로 만들지 않고 자동 주입을 수행할 수 있습니다.

 

4.1 @RequiredArgsConstructor를 이용한 의존관계 주입 코드 단축하기

Lombok 라이브러리의 @RequiredArgsConstructor 애노테이션을 사용하면 생성자를 직접 만들지 않고 컴파일시 자동으로 만들어줍니다.

 

 

기존 생성자 주입 코드

@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;
    }
    //...
}

 

Lombok 라이브러리를 이용한 개선된 생성자 주입 코드

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    //...
}
  • @RequiredArgsConstructor : 필드 멤버 중 final 키워드가 붙은 필드 멤버를 대상으로 매개변수 생성자를 생성합니다.

 

4.2 Lombok 라이브러리 적용 방법

1. build.gradle에 Lombok 라이브러리 및 환경 추가

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

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'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

2. IDE에서 Enable annotaion processing 체크

인텔리제이 기준 파일 -> 설정 -> 빌드,실행,배포 -> 컴파일러 -> 어노테이션 프로세서 -> 어노테이션 처리 활성화 체크박스 클릭

 

 

References

스프링 핵심원리 - 기본편