[SpringBoot] [3] 7. 의존관계 자동 주입 (1)

1️⃣ 다양한 의존관계 주입 방법

  • 생성자를 통해서 의존관계 주입
  • 수정자 주입(setter를 통한 주입)
  • 필드에 바로 주입
  • 일반 (아무) 메소드에다가 주입

생성자 주입

: 생성자를 통해 의존관계 주입 (지금껏 사용했던 방법)

⭐️ 특징

  • 생성자 호출 시점에 딱 한 번 호출
  • ⭐️불변, 필수 의존관계에 사용
    (딱 한 번 호출될 때 바로 그때 값이 세팅되고 그 다음부터 값을 세팅할 수 없도록 막을 수 있음 ➡️ 불변

    절대절대 setter, getter을 둬선 안된다. 있다면 꼬옥 누군가 그 값을 바꾸게 되어있다 ,,^^ 개발은 혼자하는 것이 아니기 때문 ,,^^)

즉, 로미오와 줄리엣👩🏼‍❤️‍👨🏼 공연에 비유하자면 공연 도중 배우를 바꾸고 싶지 않은 것. 공연 전 배우를 모두 정해놓고 공연을 시작 !!

(웬만해서는 생성자에는 모두 값을 채워주어야 함 !! ➡️ 필수)

📌 OrderServiceImpl.java

@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를 생략해도 자동 주입. (물론 스프링 빈에만 해당)

수정자 주입(setter 주입)

: setter (필드의 값을 변경하는 수정자 메소드)를 통해 의존관계를 주입하는 방법

( 참고: 스프링 컨테이너의 사이클 1. 스프링 빈 등록 2. 연관관계 자동 주입(@Autowired) → (순서는 생성자, 수정자(setter)) )

⭐️ 특징

  • 선택, 변경의 가능성이 있는 의존관계에 사용
  • java bean property 규약의 수정자 메소드 방식(setter)을 사용하는 방법

📌 (참고): 자바빈 프로퍼티는 필드의 값을 직접 변경하지 않고, setXxx, getXxx라는 메소드를 통해 값을 읽고 변경하도록 하는 규칙 ➡️ 자바빈 프로퍼티 규약

class Data {
	private int day;
    
    public void setDay(int day) {
    	this.day = day;
    }
    public int getDay() {
    	return day;
    }
}

필드 주입


👽: 그냥 쓰지 마 !! (권장하지 않는 방법 ^^)
(어차피 setter를 또 만들어야됨. 그럼 이걸 왜 써?)

필드 주입 방법은 이름 그대~로 필드에 바~로 주입하는 방법이다. (아래와 같이)

@Autowired private MemberRepository memberRepository;

⭐️ 특징

  • "😺: 우와 코드가 간결하네? 왠지 쓰고싶어 !!"
    쓰 지 마 라. 외부에서 변경이 불가능해 테스트하기 힘들다는 치명적인 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다. (@Autowired는 순수한 자바 코드로만 이루어진 테스트에서 동작하지 않는다. @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능.)
  • 다른 말 필요 없다. 그냥 쓰지마라

단, 사용해도 되는 곳이 있긴하다.🤷🏻

  • 애플리케이션의 실제 코드와 관계 없는 테스트 코드
  • 스프링 설정(config)을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

일반 메소드 주입

⭐️ 특징

  • 한 번에 여러 필드를 주입 받을 수 있다.
  • 일반적으로 잘 사용 X

📌 (참고): 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작.
스프링 빈이 아닌 (ex.Member) 클래스에서 @Autowired코드를 적용하면 아무런 기능이 없다.


2️⃣ 옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 경우는 있다.

이때 @Autowired(required 옵션 default값이 true이기 때문에)만 사용한다면 자동 주입 대상이 없을 때 오류가 발생한다.

📌 해결방법

  • @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메소드 자체가 호출 안 됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력 (값이 있을 수도 있고 null일 수도 있는 상태를 감싼 것)

✔️ test/hello.core에 autowired 패키지 생성 후 AutowiredTest.java

package hello.core.autowired;

import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;

import java.util.Optional;

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        // 이렇게 TestBean을 넣으면 스프링 빈으로 등록 됨.
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }

    static class TestBean {

        // 그냥 @Autowired로만 했을 때 기본값이 true이기 때문에 에러 가 남.
        // Member가 스프링 빈으로 등록되는 것이 아니기 때문
        @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 setNoBean2(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

(Member는 스프링 빈이 아님)

📌 (참고): @Nullable, Optional은 스프링 전반에 걸쳐 지원됨. 예를 들어 생성자 자동 주입에서 특정 필드에만 사용해도 됨.


3️⃣ 생성자 주입을 선택해라!

😺: 생성자 주입이 좋다~ 좋다~ 하는데 왜 생성자 주입이 좋다는 걸까?

불변

대부분의 의존관계 주입은 한 번 발생하면 애플리케이션 종료 시점까지 의존관계가 변경될 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된 다 !! ➡️ 불변

수정자 주입을 사용하면 어차피 setXxx 메소드를 public으로 열어두어야 하고
이렇게 열어두면 누군가 실수로 변경해버릴 수도 있다.
(변경하면 안 되는 메소드를 이렇게 열어두는 것은 좋은 설계 방법이 아니다.)

생성자 주입은 객체를 생성할 때 딱 한 번만 호출되므로 이후 호출되는 일이 없어 불변하게 설계가 가능하다.

누락

프레임워크 없이 순수 자바 코드만으로 단위 테스트를 하는 경우는 다분하다.

수정자 의존관계의 경우 @Autowired가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만 프레임워크 없이 순수 자바 코드 단위 테스트에서는 실행은 가능하다.

하지만 NPE(Null Point Exception)이 발생하는데 이는 의존관계 주입이 누락 되었기 떄문이다.

✔️ 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생하며 IDE 에서 어떤 값을 필수로 주입해야 하는지 바로 알려준다.

final 키워드

생성자 주입을 사용하면 필드에 final키워드를 사용할 수 있다. 이는 값이 꼬옥 설정되어야 하는데 혹시라도 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

  java: variable discountPolicy might not have been initialized

👽: 컴파일 오류는 세상에서 가장 빠르고 좋은 오류입니다 !!

📌 생성자 주입을 제외한 나머지 주입 방식은 모두 생성자 이후 호출되므로 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드 사용 가능

즉,

  • 생성자 주입을 사용하는 이유는 프레임 워크에 의존하지 않고 순수 자바 언어의 특징을 잘 살리는 방법이기 때문이다.
  • 기본적으로 생성자 주입을 사용하고 필수 값이 아닌 경우에만 수정자 주입 방식을 옵션으로 부여하자 !
    ( 생성자 주입과 수정자 주입은 동시 사용이 가능 )
  • 그냥 항상 생성자주입을 사용한다고 생각 !!
    가~끔 옵션이 필요할 때만 수정자주입.
    필드주입은 ...
    그냥 쓰지 말아라.

4️⃣ 롬복과 최신 트랜드

위에서 계속, 몇번이나 필드 주입은 쓰지 말라고 했다. 근데 너무 편하지 않은가? 간단하고,,

개발 시 불변이 대부분이라 생성자에 final 키워드를 많이 사용한다. 그런데 생성자도 만들고, 주입 받은 값을 대입하는 코드도 만들고,, 이런걸 생각하면 자꾸 필드 주입에 손이 간다. 🦐

이러다 필드 주입을 사용해버리기 전에 코드를 최적화하는 방법을 알아보자.

✔️ 우선 우리가 쓰던 원래 코드의 일부 OrderServiceImpl.java

@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를 일단 생략
(한 줄 줄였다 ^ ^)

그리고 롬복 라이브러리를 적용하자.

롬복 라이브러리 적용하기

✔️ build.gradle에 내용 추가

plugins {
	id 'org.springframework.boot' version '2.5.3'
	id 'io.spring.dependency-management' version '1.0.11.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'
	testImplementation ('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}

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

	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	// lombok 라이브러리 추가 끝
}

test {
	useJUnitPlatform()
}

➡️ 그 다음 코끼리 꼬옥 눌러주기 (아니면 아래처럼)

⬆️ (mac기준) preferences에서 plugins에서 lombok 설치
⬆️ 노란색 화살표 꼭꼭 체크 !!

그리고 이제 아무 클래스나 생성해서 setter, getter를 확인해보자.
✔️ HelloLombok.java

package hello.core;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
// 이렇게 해두면 setter, getter 자동으로 만들어줌
// (원래는 우리가 setter, getter 코드를 생성했었음 -> 코드를 확 줄여줌)
public class HelloLombok {

    private String name;
    private int age;

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

        String name = helloLombok.getName();
        System.out.println("name = " + name);
    }
}

➡️ 오~~ 이전까지는 setter, getter 코드를 일부러 생성했는데 그런 번거로움 없이도 setter, getter를 사용할 수 있게 됐다 !!

롬복 적용

이제 롬복 라이브러리를 적용 시켰으니 다시 코드로 돌아가 최적화 시켜보자.

✔️ OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        // 할인 정책은 오직 discountPolicy에게 맡기고 있기 때문에 설계가 잘 된 케이스
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

➡️ 롬복 라이브러리가 제공하는 @RequiredArgsConstructor기능을 사용하면 final이 붙은 필드를 모아 생성자를 자동 생성해준다. (보이진 않음)

실제로 클래스 안 코드는

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

이렇게만 남은 것 !! 기능은 동일하지만 롬복이 자바의 애노테이션 프로세서 기능을 이용해 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class를 열어보면 코드를 확인할 수 있다.

📌 최근에는 많이들 생성자를 딱 하나만 두고 @Autowired생략한다.

또한 Lombok 라이브러리@RequiredArgsConstructor를 사용해 기능은 다 제공받되, 코드는 깔끔하게 사용한다.



내용이 길어 2로 이어쓰겠습니다.

좋은 웹페이지 즐겨찾기