티스토리 뷰

Spring

스프링 의존성 주입 방식 종류와 차이

기내식은수박바 2021. 8. 8. 15:44
반응형

의존성 주입 (Dependency Injection) 의 종류

1. 생성자 주입 방식 (Constructor Injection)

  • 생성자를 통해 의존성을 주입하는 방식이다.
  • 총 3가지 주입 방식 중 가장 권장되는 주입 방식이다. (아래에서 자세히 설명)
public class ExampleClass {
	
    // final을 사용할 수 있다.
    private final Example example;
    
    @Autowired
    public ExampleClass(Example example) {
    	this.example = example;
    }
}

 

2. 필드 주입 방식 (Field Injection)

  • @Autowired 어노테이션을 통해 의존성을 주입하는 방식이다.
  • 사용법이 매우 간단하다.
public class ExampleClass {
	
    @Autowired
    private Example example;
}

 

3. 수정자 주입 방식 (Setter Injection)

  • 수정자 (setXX) 를 통해 의존성을 주입하는 방식이다.
public class ExampleClass {
	
    private Example example;
    
    // 함수 이름이 setXX일 필요는 없지만 일관성을 위해 setXX을 사용하는 것이 좋다.
    @Autowired
    public void setExample(Example example) {
    	this.example = example;
    }
}

 

생성자 주입 방식이 권장되는 이유

의존성을 주입하는 방법은 3가지인데, 왜 생성자 주입 방식이 권장되는 것일까?

1. 순환 참조 방지

  • 예를 들어, 아래와 같이 두 개의 클래스와 메인 클래스가 있다고 해보자.
@Component
public class Example1 {
	
    @Autowired
    private Example2 example2;
    
    public void playMusic() {
    	example2.playGame();
    }
}

@Component
public class Example2 {
	
    @Autowired
    private Example1 example1;
    
    public void playGame() {
    	example1.playMusic();
    }
}

@SpringBootApplication
public class TestApplication implements CommandLineRunner {

	@Autowired
	private Example1 example1;

	@Autowired
	private Example2 example2;

	public static void main(String[] args) {
		SpringApplication.run(TestApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		example1.playMusic();
		example2.playGame();
	}
}
  • 위 코드를 실행하면 과연 어떤 결과가 나올까?

.....

.....

.....

.....

.....

.....

.....

.....

.....

  • 위 이미지를 통해 런타임 에러가 발생한 것을 알 수 있으며, 이는 애플리케이션이 정상적으로 구동되었지만 프로그램이 수행할 수 없는 동작을 수행하려고 했다는 것이다.
  • 그렇다면 왜 이러한 결과가 나온 것인지 필드 주입 방식을 생성자 주입 방식으로 수정하여 실행해보면 아래와 같은 결과를 얻을 수 있다.
***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   testApplication (field private com.example.test.Example1 com.example.test.TestApplication.example1)
┌─────┐
|  example1 defined in file [.../com/example/test/Example1.class]
↑     ↓
|  example2 defined in file [.../com/example/test/Example2.class]
└─────┘

 

 

필드 주입 방식과 생성자 주입 방식의 결과가 왜 다른 것일까?

  • 이는 의존성을 주입하는 시점이 (생성자) - (필드, 수정자) 방식이 다르기 때문이다.
  • 필드, 수정자 주입 방식의 경우, 빈 객체를 먼저 생성한 뒤에 의존성을 주입 하기 때문에 빈 객체를 생성하는 시점에는 순환 참조가 발생하는지 알 수 있는 방법이 없다. (생성 -> 주입)
  • 생성자 주입 방식의 경우, 빈 객체를 생성하는 시점에 생성자의 파라미터 빈 객체를 찾아 먼저 주입한 뒤에 주입받은 빈 객체를 이용하여 생성한다. (주입 -> 생성)
    • 이 때문에 런타임 시점이 아닌 애플리케이션 구동 시점에 순환 참조를 발견할 수 있다.

 

2. final 키워드 사용으로 불변성 (Immutable) 을 통한 오류 방지, Lombok 활용

  • 필드, 수정자 주입 방식은 예기치 못한 상황으로 빈 객체가 변경될 수 있어 이로 인한 오류 (ex. NPE) 가 발생할 수 있다.
  • 생성자 주입 방식의 경우, final을 사용할 수 있고 이를 통해 객체 생성 시점에 필수적으로 빈 객체 초기화를 수행해야 하기 때문에 null이 아님을 보장할 수 있고, 초기화된 객체는 변경될 수 없다.
  • 또한, 생성자 주입 방식은 Lombok의 어노테이션 (@RequiredArgsConstructor) 을 활용하여 간단히 작성할 수 있다.
@Component
public class Example1 {

    private final Example2 example2;

    public Example1(Example2 example2) {
        this.example2 = example2;
    }

    public void playMusic() {
        example2.playGame();
    }
}

// Lombok 어노테이션을 통한 의존성 주입
@Component
@RequiredArgsConstructor
public class Example2 {

    private final Example1 example1;

    public void playGame() {
        example1.playMusic();
    }
}

 

3. 테스트 코드를 작성하기 쉽다.

  • 필드 주입 방식으로 코드를 작성한 경우, 순수한 자바 코드로 단위 테스트를 작성하는 것은 불가능하다.
  • 예를 들어, 아래와 같은 코드가 있다고 해보자.
@Service
public class ExampleService {

    @Autowired
    private ExampleRepository exampleRepository;

    public void save(int seq) {
    	exampleRepository.save(seq);
    }
}

public class ExampleTest {
	
    @Test
    public void test() {
    	ExampleService exampleService = new ExampleService();
        exampleService.save(1);
    }
}

 

 

ExampleTest 코드를 실행하면 어떻게 될까?

  • Spring과 같은 DI (Dependency Injection) 프레임워크 위에서 동작하지 않기 때문에 의존성 주입이 되지 않을 것이다.
  • 그렇다면 exampleService = null일 것이고, exampleService.add(1)은 NPE를 발생시킬 것이다.
  • 수정자 주입 방식을 사용하는 경우, SOLID (객체 지향 설계 원칙) 의 OCP (개방-폐쇄 원칙, Open-Closed Principle) 을 위반하게 된다.

 

  • 생성자 주입 방식을 이용하면 아래와 같이 테스트 코드를 작성할 수 있다.
    • 이는 컴파일 시점에 빈 객체를 주입받아 테스트 코드를 작성할 수 있고, 빈 객체를 주입하지 못한 경우 컴파일 시점에서 오류를 발견할 수 있다는 것이다.

 

@Service
public class ExampleService {

    private final ExampleRepository exampleRepository;
    
    public ExampleService (ExampleRepository exampleRepository) {
    	this.exampleRepository = exampleRepository;
    }

    public void save(int seq) {
    	exampleRepository.save(seq);
    }
}

public class ExampleTest {
	
    @Test
    public void test() {
    	ExampleRepository exampleRepository = new ExampleRepository();
    	ExampleService exampleService = new ExampleService(exampleRepository);
        exampleService.save(1);
    }
}
반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함