본문 바로가기

백엔드 잡학사전

[스프링 핵심] 의존관계 자동 주입 +α

의존관계 주입하는 다양한 방법이 있다

생성자 주입, setter 주입, 필드 주입, 일반 메서드 주입 - 거의 생성자 주입을쓰고, setter도 가끔 쓴다.

 

생성자 주입

참고로 생성자 주입은 지금껏 우리가 한 행위다.

생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. 불변, 필수 의존관계에 사용.

@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로 지정을 해줘야하지만, 하나만 있으면 생략 가능하다. 그러나 어지간하면 지정을 해주는 버릇을 가지자.

 

수정자 주입

getter와 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;
 }
}

 

* 자바빈 프로퍼티 - 자바의 규정(?) - 에 의해 필드의 값을 직접 변경하는 대신 set, get 메서드를 활용한다.

 

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

  • 불변: 한마디로, 대부분의 주입은 한번 일어나면 앱 종료할 때까지 의존관계를 변경할 일이 없다고 한다. 오히려 아예 변하면 안되는 경우가 대부분이다. 그렇기에 생성자 주입을 써서 아예 변화를 할 수 없게 하는 게 좋다.
  • 누락: 생성자 주입의 경우 생성자를 만들 때에 파라미터를 넘겨주는 방식으로 진행하기 때문에, 필요한 객체를 누락할 가능성이 없다.
  • final 키워드: JAVA에서 쓰는 final로 설정을 하면, 정의 시에 = 뭐시기 해주거나 아니면 생성자에서만 수정이 가능하다. 그래서 만약 누락이 되거나 변경이 되는 경우, compile 오류가 뜨면서 일침을 가해준다. 강사님은 compile 오류가 세상에서 가장 빠르고 좋은 오류라고 강조하신다.

 

옵션 처리

@Autowired 는 디폴트가 required=true로 되어 있어서 자동 주입할 대상이 없으면 오류가 발생한다.

그때는 required=false로 붙여주면 된다. 그렇게하면 만약 주입 대상 없으면 아예 안 부른다.

 

그게 아니면 null 혹은 Optional.empty 호출해도 된다.

//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);
}

 

그치만 실무에서 어떤 경우에 이걸 쓸지는 아직 잘 모르겠다.

 

롬복, 최신 트렌드

생성자 주입이 기본이 되어야 하지만, 생각보다 주입 시에 쓸 코드가 많다. 그래서 이걸 간소화하는 걸 또 만들어줬다. 그 이름이 바로 "롬복"이라는 라이브러리다. initializr 쓸 때 추가해줬으면 되는데, 우리는 안 해줬으니 build.gradle에 아래의 코드들을 통해 추가해줘야 한다.

추가로, Preferences -> Annotation Processors 검색 후 Enable annotation processing 체크되게 해야한다.

마지막으로 코끼리 눌러줘야 하고.

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()
}

 

예를 들어 HelloLombok이라는 클래스를 만들고, 내부에 name과 age라는 변수를 두면, 저렇게 위에 @Getter, @Setter 애노테이션만 추가해줘도, 알아서 생성자에 딸린 프로퍼티에 대한 getter와 setter를 만들어준다.

package hello.core.order;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class HelloLombok {
    private String name;
    private int age;

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("king");

        System.out.println(helloLombok.getName());
    }
}

 

예를 들어 @RequiredArgsContstructor 같은 애노테이션은, 기존의 생성자 만드는 코드를 대체해준다 그러니까,

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

// 기존의 코드가 아래처럼 변한다.

@Component
@RequiredArgsConstructor

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

 

구현체의 생성자 자체를 만들 필요가 없어지는 것이다. 프로퍼티들을 저장해두면, 그걸 필수로 파라미터로 포함하는 생성자를 자체적으로 만들어줘버린다,,,

 

킹-토와이어드보다도 더 깔끔한 갓-복이다. 실무에서는 이걸 쓴다고 한다. 진짜 어쩌다 생성자가 필요한 경우 킹-토와이어드를 쓰면 되고.

 

조회할 빈이 2개 이상인 경우

기본적으로 Autowired든, 그 기저의 getBean이든, 타입을 기준으로 빈 조회를 한다. 그러니까 같은 타입의 빈이 여러개인 경우 문제가 발생한다. 이때,

1 하위타입으로 지정해주면 문제는 해결되지만 그렇게 하면 구현체를 부르는 것이므로 DIP를 위반한다.

2 만약 수동으로 빈 만들어주면 문제가 해결되겠지만, 더 좋은 다른 방법들이 있다.

 

  • @Autowired 필드명

타입 매칭을 시도하고 나서 여러개가 있는 경우 추가로 매칭하는 기능이 Autowired에게는 존재한다!!

그러니까 예를 들어, 다음과 같다.

  • @Qualifier

추가 구분자. 추가적인 이름을 붙일 수 있다. 약간 subname 같은 느낌쓰.

  • @Primary

이게 편해서 자주 사용한다고 한다. 여러 빈이 매칭되는 경우 @Primary가 우선권을 가진다.

 

ex) 메인 DB는 9할, 보조 DB가 1할이면 메인을 쓴다고 봐도 되므로 @Primary 써버리면 된다.

 

애노테이션을 내가 만들어주기

 

이런 새로운 annotation을 만들어줄 수 있다. 그러면 기존의 @Qualifier("mainDiscountPolicy") 이걸 깔쌈하게

이렇게 된다.

 

특정한 빈 뿐 아니라 모든 빈이 다 필요할 때 - List, Map

와,,, 진짜 신기하다. 그러니까 Map에다가 불러주면, 저절로 key는 이름, 값은 해당 빈이 들어간다.

아, Spring은 자동으로 키값에 해당 빈의 이름, 값에 해당 빈을 넣어준다.

값에 빈이 들어간다는 게 핵심이다. 그러면 이제 키로 이름을 불러주면 해당 빈을 갖다가 쓸 수 있다는 것.

 

자동과 수동 빈 - 자동을 기본으로 쓰자, 그러나 수동도 필요하다