Home 의존성 주입 (DI, Dependency Injection) 톺아보기
Post
Cancel

의존성 주입 (DI, Dependency Injection) 톺아보기

의존성 주입에 대해 설명하기에 앞서, ‘의존’ 이라는 단어에 대한 이해부터 하고 들어가자!

의존이란 무엇인가!

A 클래스에서 B 클래스를 사용하고 있을 때, A 클래스가 B 클래스에 의존한다고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
	private B b = new B();
	
	public void performAction() {
        b.doSomething();
    }
}

public class B {
	public void doSomething() {
        System.out.println("B is doing something.");
    }
}

의존성 주입이란?

다시 의존성 주입으로 돌아와서, 의존성 주입이란 무엇일까?

위처럼 A 클래스에서 B 클래스를 사용할 때 직접 객체를 생성해서 사용하는 것이 아니라, 외부에서 클래스를 주입받아 사용하는 것을 말한다.

클래스를 주입해주는 외부란 어디일까?

그 ‘외부’에서는 객체를 주입하기 위해 애플리케이션 실행 시점에 필요한 객체들을 생성해야 하고, 의존성이 있는 객체 사이의 관계를 파악해 특정 객체를 다른 객체로 주입해주어야 한다.

Spring에서는 이러한 객체들을 Bean이라 부르고, Spring Container에서 이 Bean을 생성하고 관리하는 역할을 한다. 즉, ‘외부’는 스프링 컨테이너라고 이해하면 된다!

아니 다 알겠고, 그럼 의존성 주입이 뭐가 좋은데?! 🤔

의존성 주입으로 코드 결합도를 낮춰 변경에 유연한 코드를 짤 수 있다!

예시를 들어 설명해보겠다. ‘의존’ 개념을 설명할 때 들었던 예시를 다시 가져와보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class A {
	private B b;
	
	public A() {
		this.b = new B();
	}
	
	public void performAction() {
        b.doSomething();
    }
}

public class B {
	public B() {}
	
	public void doSomething() {
        System.out.println("B is doing something.");
    }
}

아래와 같이 B 클래스에서 ‘name’이라는 값을 받아 사용하도록 B 클래스가 수정된다면 어떻게 될까?

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
public class A {
	private B b;
	
	public A() {
		this.b = new B("Kate");  // 수정
	}
	
	public void performAction() {
        b.doSomething();
    }
}

public class B {
	// 추가
	private String name;
	
	// 수정
	public B(String name) {
		this.name = name;
	}
	
	public void doSomething() {
        System.out.println("Hi, " + name + "! B is doing something." );
    }
}

B 클래스가 변경되어 A 클래스의 코드도 수정해야 하는 상황이 되었다.

만약 B 클래스를 A 클래스 뿐만 아니라 다른 여러 클래스에서도 사용하고 있었다면? 해당 클래스의 코드도 모두 수정해주어야 한다.

의존성 주입을 사용하면, 하나의 수정으로 인해 여러 클래스를 수정해야 하는 상황을 방지할 수 있다. 다음 예시를 살펴보자.

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
public class A {
	private B b;
	
	// 생성자를 통한 B 클래스 주입
	public A(B b) {
		this.b = b;
	}
	
	public void performAction() {
        b.doSomething();
    }
}

public class B {
	// 추가
	private String name;
	
	// 수정
	public B(String name) {
		this.name = name;
	}
	
	public void doSomething() {
        System.out.println("Hi, " + name + "! B is doing something." );
    }
}

생성자를 통해 B 클래스를 주입받아 사용하도록 수정했다.

A 클래스에서는 B 클래스의 구현과는 상관 없이 외부에서 해당 객체를 주입받아 사용하기만 하면 된다. B 클래스에서 어떤 코드가 수정되더라도 A 클래스(혹은 B 클래스를 사용하는 또 다른 클래스들)에서는 알 필요가 없다!

의존성 주입 방법

자, 그러면 의존성을 주입하는 방법에 대해 알아보자.

1. 생성자 주입

  • 생성자를 통해 주입받는 방식이다. 스프링에서 권장하는 방식이다.
  • 생성자 주입은 생성자 호출 시점에 딱 한 번 호출된다. 그래서 주입 받은 객체가 변하지 않거나, 객체 주입이 반드시 필요한 경우에 사용할 수 있다. (의존 관계의 변경이 필요한 상황은 거의 없기 때문에 생성자 주입을 권장하는 것)
  • 의존성 주입이 많아질 경우, 생성자의 매개변수 수가 증가해 가독성이 떨어질 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    
    @Controller
    public class Controller {
      private Service service;
    	
      public Controller(Service service) {
          this.service = service;
      }
    }
    

2. Setter 주입

  • 객체 생성 후, Setter를 이용해 의존성을 주입받는 방식이다.
  • 객체가 생성된 후 의존성을 주입하는 방식이기 때문에, 해당 객체의 불변성이 보장되지 않는다.
  • 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다. (하지만 변경될 가능성은 극히 드물다)
    1
    2
    3
    4
    5
    6
    7
    8
    
    @Controller
    public class Controller {
      private Service service;
    	
      public void setService(Service service) {
          this.service = service;
      }
    }
    

3. 필드 주입

  • @Autowired 어노테이션을 이용해 필드에 직접 주입 받는 방식이다.
  • 코드가 간결하고, 주입 로직이 간단하다.
  • 객체가 완전히 초기화되기 전에 사용될 수 있어 위험하다.
  • 외부에서 접근이 불가능하다는 단점 때문에, 테스트 코드 작성 시 사용하기 어렵다. (그래서 과거에 비해 권장하지 않는 방식이다)

+ IntelliJ에서 필드 주입 방식을 사용하면 ‘Field injection is not recommended’라는 경고 문구가 발생한다.

1
2
3
4
5
@Controller
public class Controller {
	@Autowired
	private Service service;
}

생성자 주입 방식은 순환 참조 에러를 막을 수 있다!

순환 참조 에러란, 아래 예시와 같이 A 클래스가 B 클래스를 의존하고, 다시 B 클래스가 A 클래스에 의존하는 상황일 때, 두 클래스가 생성될 때 무한루프에 빠질 수 있는 것을 말한다.

1
2
3
4
5
6
7
8
9
public class A {
	@Autowired
  private B b;
}

public class B {
	@Autowired
  private A a;
}

A 클래스를 생성할 때 B 클래스를 주입받으려고 하고, 이를 위해 B 클래스가 생성될 때 다시 A 클래스를 주입받으려고 하는 상황이 반복되며 무한 루프에 빠지고, 결국 StackOverflow 에러가 발생할 것이다.

@Autowired를 이용하는 필드 주입 방식은 모든 객체의 생성이 완료된 후에 의존 관계에 따라 의존성이 주입된다. 그렇기 때문에, 애플리케이션 구동 시점에 에러가 발생하지 않아 문제를 발견하지 못하고 서버가 운영될 수 있고, 해당 부분을 호출할 때가 되서야 문제를 발견하게 되는 것이다.

이러한 문제는 생성자 주입을 이용하면 해결할 수 있다!

생성자 주입은 객체가 생성될 때(생성자가 호출되므로) 의존 관계에 따라 의존성이 주입된다. 그래서 애플리케이션을 구동하려고 할 때 에러가 발생할 것이고, 사전에 해결할 수 있는 것이다.

참고
SpringBoot 2.6부터는 기본적으로 순환 참조가 허용되지 않도록 변경되었다. 필드 주입을 사용하더라도 순환 참조 에러가 발생한다면 애플리케이션 로딩 시점에 에러가 발생하게 된다!

참고

This post is licensed under CC BY 4.0 by the author.

Cloud SQL Proxy로 비공개 DB 접속하기

final과 의존성 주입