스프링 빈 주입 방법

홍지범·2023년 4월 25일
0

현재 진행중인 타임딜 프로젝트에서 DI를 위해 당연하듯이 @Autowired 혹은 @RequiredArgsConstructor를 사용했습니다.

무분별하게 사용하다보니 A 클래스에서는

@RequiredArgsConstructor
public class A {
	private final Q q;
}

또 B 클래스에서는

public class b {
	@Autowired
    private W w;
]

이렇듯 정확한 기준 없이 bean을 주입하고 있었는데,

코드 리뷰를 통해 bean 주입 방식에 대해 다시 알아보기로 했고, 블로깅을 통해 아래와 같이 남겨보려 합니다.

  • bean 주입이란?
  • bean 주입 방식의 종류
  • 각 장단점

먼저 용어를 간단하게 해보겠습니다.

  • IOC(Inversion Of Control) : 프로그램 제어의 흐름을 개발자가 아닌 외부에 의해(설정 파일, 스프링 등) 조절되는 방법.
  • DI(Dependency Injection) : IOC를 구현한 방법.
    개발자는 정적 의존 관계로(인터페이스) 개발하지만 애플리케이션 런타임 때 외부에서 동적으로 구현 객체를 생성해(인터페이스 구현체) 인터페이스의 주입하여 한 곳에서 관리를 하거나(AppConfig) 자동으로(스프링) 설정할 수 있다.
  • 스프링 빈 : 스프링에서 IOC를 위한 컨테이너인 스프링 컨테이너에서 관리하는 자바 객체.
    스프링 빈으로 등록 된다는 것은 웹 환경에서 요청마다 새로운 객체를 생성하는 것이 아닌 빈으로 등록된 객체의 인스턴스 1개를 공유해서 사용하기 때문에(싱글톤 패턴) 자원 낭비를 막을 수 있다.

간단하게 용어를 이해했으니 본격적으로 bean 주입 방법에 대해 알아볼까요?

bean 주입이란?

bean 주입을 이해하기 위해 아래의 단계로 리팩토링 해보겠습니다.

  1. 직접 인스턴스를 생성하는 방법
  2. 외부에서 주입하는 방법
  3. 스프링 컨테이너를 이용하는 방법

으로 리펙토링 해보겠습니다.

1. 직접 인터페이스를 생성

  • A구현체
  • B인터페이스
  • B구현체
  • C인터페이스
  • C구현체
    준비물들은 아래와 같은 의존관계를 가지고 있습니다.
    A -> B -> C
    실제 코드로 구현한다면 아래와 같습니다.
public class A {
	private final B인터페이스 b = new B구현체();
}

public class B구현체 {
	private final C인터페이스 c = new C구현체();
}

유연한 설계를 위해 다형성을 적용했으니 인터페이스의 구현체를 입맛에 맛게 갈아끼울 수 있어 보입니다.

🤔무엇이 문제일까요?

기존의 C구현체가 아닌 새C구현체.java 를 만들었다고 가정 해보겠습니다.

public class B구현체 {
	//private final C인터페이스 c = new C구현체();
	private final C인터페이스 c = newC구현체();
}

새로운 구현체 적용을 위해 B구현체 클래스의 코드를 직접 수정 했습니다.
이 방법은 두 가지 문제가 있습니다.

  • 추상화된 인터페이스에 의존하는 것과 동시에 구현체에도 함께 의존하고 있어 DIP를 위배
  • 만약 클래스가 100개, 1000개 라면? 수정사항이 발생하면 변경된 클래스를 사용하는 클래스의 모든 부분을 변경하는 수고로움이 발생 합니다.

이런 번거로움을 피하기 위해 외부에서 설정하는 방법으로 리팩토링 해보겠습니다.

2. 외부에서 주입하는 방법
김영한님이 강의에서 언급하신 관심사(책임)의 분리를 적용해보겠습니다.
연극을 비유로 쉽게 이해해볼까요?

  • 배역 : 인터페이스
  • 배우 : 구현체
  • 설정파일 : 공연 기획자
    실제 연극은 어떤 배역을 연기하는 실제 배우가 있습니다. 그리고 이 배우는 공연 기획자가 캐스팅 하죠.
    일반적으로 공연 기획자가 어떤 배역을 연기하지 않고, 어떤 배우가 될 수도 없습니다.
    각 역할에 맞는 책임이 있는 것이죠.
    이런 책임을 코드로 나눠보겠습니다.
public class A {
	private final B인터페이스 b;
    public A(B b) {
    	this.b = b;
    }
}

public class B구현체 {
	private final C인터페이스 c;
    public B(C c) {
    	this.c = c;
    }
}

public class AppConfig {
	public C인터페이스 c인터페이스호출() {
    	return new C구현체();
    }

    public B인터페이스 b인터페이스호출() {
    	return new B구현체(c인터페이스호출());
    }

    public A구현체 a구현체호출() {
    	return new A구현체(b인터페이스호출());
    }
}

이제 각 클래스는 구현체에 의존하지 않고 추상화된 인터페이스에만 의존합니다.
그리고 구현체를 외부(AppConfig)에서 생성자를 통해 생성하고 주입됩니다.
이전 1.에서 발생한 새C구현체.java가 만들어 지더라도 사용 부분에서 변경할 필요 없이 외부(AppConfig) 한 곳만 변경하면 사용 부분에서는 새로운 구현체를 사용할 수 있습니다.
위에서 말한 책임이 분리된 것이죠.

하지만 여전히 남은 문제는?
추상화도 잘 했고, 책임을 분리도 했는데 어떤 문제가 남았을까요?
실제 애플리케이션에서 외부 주입한 객체를 사용해보겠습니다.

public class Main {
	public static void main(String[] args) {
    	AppConfig appConfig = new AppConfig();
        A a1 = appConfig.a구현체호출();
       ...
        A a2 = appConfig.a구현체호출();

        boolean reuslt = a1.equals(a2);
        //false
    }
}

가정된 상황이지만 appConfig를 통해 a 인스턴스를 두 번 호출 했습니다.
appConfig를 다시 한 번 볼까요?

public class AppConfig {
	public C인터페이스 c인터페이스호출() {
    	return new C구현체();
    }

    public B인터페이스 b인터페이스호출() {
    	return new B구현체(c인터페이스호출());
    }

    public A구현체 a구현체호출() {
    	return new A구현체(b인터페이스호출());
    }
}

이 메서드는 A 구현체를 매 번 새로 생성하고 있습니다.
더불어 A 를 생성할 때 연쇄적으로 b, c도 함께 새로 생성하고 있죠.
만약 웹 애플리케이션이라 100번의 요청이 발생한다면?
300개의 인스턴스가 생성됩니다.
한 요청을 처리하기 위한 공통된 로직이 요청마다 생성된다면 이는 과도한 리소스 낭비를 야기합니다.
이 문제를 스프링 컨테이너를 통해 해결해보겠습니다.


3. 스프링 컨테이너를 이용하는 방법
잠깐 다시 용어로 돌아가 스프링 빈을 읽어볼까요?
스프링 빈으로 등록된 자바 객체는 스프링 컨테이너에 의해 싱글톤 패턴으로 관리 됩니다.
싱글톤 패턴으로 관리된다면 객체의 인스턴스는 1개만 생성되어 공유됩니다. 2.처럼 불필요한 객체를 생성하지 않습니다.
이제 스프링으로 리팩토링해 이 문제를 해결해보겠습니다.

@Configuration
public class AppConfig {
	@Bean
	public C인터페이스 c인터페이스호출() {
    	return new C구현체();
    }

    @Bean
    public B인터페이스 b인터페이스호출() {
    	return new B구현체(c인터페이스호출());
    }

    @Bean
    public A구현체 a구현체호출() {
    	return new A구현체(b인터페이스호출());
    }
}

크게 달라진 것 없이 @Configuration, @Bean만 추가했는데 어떻게 달라진걸까요?


먼저 각 어노테이션이 무슨 역할을 하는지 보겠습니다.
@Bean에 대해 알아야 할 것

  • 스프링 빈으로 수동 등록할 때 사용.
  • 빈 등록 시 @Configuration과 함께 사용함으로서 싱글톤을 보장받을 수 있다.
  • 반대로 @Configuration을 붙이지 않은 @Bean은 스프링 컨테이너에 등록 되지만 CGLib로 생성하지 않아 싱글톤을 보장받을 수 없다.

@Configuration에 대해 알아야 할 것

  • CGLib(바이트코드를 조작해 동적으로 클래스 생성) 기술을 이용해 @Bean이 붙은 클래스의 싱글톤을 보장해준다.
  • 내부에 @Component를 가지고 있다.
  • 수동 등록(AnnotationConfigApplicationContext에 등록)할 수 있다.
  • 자동 등록(@ComponentScan 경로에 @Component 위치) 할 수 있다.

이제 불필요한 인스턴스가 생성되지 않는지 확인해 볼까요?
public class Main {
	public static void main(String[] args) {
		ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
		A a1 = (A)ac.getBean("a구현체호출");
		A a2 = (A)ac.getBean("a구현체호출");

        boolean reuslt = a1.equals(a2);
        //true
    }
}

이제 스프링 컨테이너에 빈으로 등록된 자바 객체는 새롭게 인스턴스를 생성하며 리소스를 낭비하지 않게 되었습니다.

지금까지 빈 주입이란 무엇인지, 어떻게 달라지는지 알아봤습니다.

그럼 다음으로,

bean 주입 방식의 종류

bean 주입 방식의 종류를 이해하기 위해 이전 코드의 수동 주입 방식에서 자동 주입으로 리펙토링 해보겠습니다.

@Component
public class A {
	//불변 객체
	private final B인터페이스 b;
    //생성자 주입
    @Autowired
	public A(B인터페이스 b) {
    	this b = b;
    }
}

@Component
public class B구현체 implements B인터페이스{
	private final C인터페이스 c;
    @Autowired
    public B(C인터페이스) {
    	this c = c;
    }
}

@Component
public class C구현체 implements C인터페이스{
}

이번에도 각 어노테이션의 역할을 알아보겠습니다.
@Component 에 대해 알아야 할 것

  • @Component 가 붙은 클래스를 @SpringBootApplication이 ComponentScan의 대상으로 인식해 스프링 빈으로 자동 등록 해준다.
  • @Contriller, @Service, @Repository, @Configuration 등 도 모두 내부에 @Component를 가지고 있다.

@Autowired 에 대해 알아야 할 것

  • 생성자에 @Autowired를 붙이면 스프링 빈에서 해당 클래스를 찾아 파라미터로 주입한다.
  • ApplicationContext.getBean(어쩌구class.class); 과 같다.

자 이제 bean 주입 방식을 수동에서 자동으로 바꾸어보았으니 대략적인 개념은 이해 되셨을 겁니다.
지금까지의 내용은 아래를 이해하기 위한 빌드업 이었습니다.

이 외에 다른 의존관계 주입 방식이 있는데요.
그 중 권장되는 방법과 권장되지 않는 방법이 있는데요. 왜 그런지 알아볼까요?

여러 bean 주입 방법

생성자 주입 방법

생성자 주입 방법은 생성자 호출 시점에 딱 한 번 호출 되는 것을 의미합니다.
스프링 빈으로 등록해두었다면 싱글톤을 보장하기 때문에 의존관계도 한 번만 주입받을 수 있습니다.
기본적인 생성자 주입 방식은 위에 있으니 lombok을 활용해 한 번 더 리펙토링 해보겠습니다.

@Component
//final이 붙은 필드를 모아 생성자를 자동으로 만들어 줌
@RequiredArgsConstructor
public class A {
	private final B인터페이스 b;
    private final C인터페이스 c;
    //생성자가 한 개만 있다면 @Autowired를 생략해도 자동 주입
}

수정자 주입 방법

수정자 주입 방법은 주입할 필드를 두고 set 메서드를 만들어 주입받는 방법 입니다.
자바빈 프로퍼티 규약으로 필드를 직접 변경하는 것이 아닌 get, set 등의 메서드로 변경하는 방식 입니다.

@Component
public class A {
	private final B인터페이스 b;

    @Autowired
    public void setB(B인터페이스 b) {
    	this b = b;
    }
}

필드 주입 방식

도입부에 필드 주입 방식 입니다.
이 방식은 오직 DI 프레임워크(스프링)에 의존하고 있기 때문에 자바 코드로 하는 테스트에서는 작동하지 않는 위험이 있습니다.
또한 이전에 AppConfig 처럼 외부에서 bean 등록을 조절할 때 할 수 없는 방식 입니다.

@Component
public class A {
	@Autowired
	private final B인터페이스 b;
}

일반 메서드 주입 방법

일반 메서드를 정의해 주입하는 방법 입니다.
생성자 주입 방법이 있기 때문에 잘 사용하지 않습니다.
set 방식과 메서드 이름만 다르고 방식은 같습니다.

@Component
public class A {
	private final B인터페이스 b;

    @Autowired
    public void init(B인터페이스 b) {
    	this b = b;
    }
}

결론

생성자 주입 방식을 사용 합시다.

  • final 키워드를 활용해 컴파일 타임에 생성자 생성에 대한 오류를 체크해준다.
  • 프레임워크에 의존하지 않는 순수 자바 코드다.
  • 대부분의 의존 관계는 한 번 주입하면 애플리케이션 종료 시점 까지는 달라지지 않는다.
  • 생성자 주입 방식을 메인으로 선택하고 가끔 옵션이 필요하면 수정자 주입을 선택하자.
  • 필드 주입은 쓰지 말자.

참고
_- 김영한의 스프링 핵심 원리 기본편

  • 토비의 스프링_
profile
왜? 다음 어떻게?

0개의 댓글

관련 채용 정보