NestJS 공부할 때부터 접하던 개념인데 이번에 Spring을 공부하면서 제대로 짚고 넘어가야겠단 생각이 들었다.
Dependency Injection
만약 A 객체가 B 객체의 변화에 영향을 받는다면 A는 B를 의존한다고 한다.
예를 들면 아래와 같다.
public class UserService {
private final UserRepository users = new MemoryUserRepository();
...
}
위의 UserService는 MemoryUserRepository를 의존한다고 할 수 있다.
MemoryUserRepository가 변한다면, 결국 UserService도 변화하게 되는 것이다.
소프트웨어 엔지니어링에서 의존성 주입이란 하나의 객체가 다른 객체의 의존성을 제공하는 기술을 말한다.
만약 위 상황에서 Memory를 기반으로 작성한 Repository에서 DB를 사용한 Repository로 변경하고자 한다면
Service 코드 내부에서 직접 변경해주어야한다.
코드를 보자면 아래와 같다.
public class UserService {
// private final UserRepository users = new MemoryUserRepository();
private final UserRepository users = new UserRepository();
...
}
실제 DB와 상호작용하는 로직을 구현한 새로운 객체를 연결하기 위해 직접 Service의 코드를 수정해준 모습이다.
그러나 위와 같은 모습은 의존 관계일 뿐 의존성 주입을 해주었다고 하지 않는다.
의존성 주입이란,
추상에만 의존하고 구체에는 의존하지 않는다는 것.
간단히 말해 의존 관계를 객체 내부에서가 아닌 외부에서 결정하는 것으로,
사용할 객체에 대한 reference를 외부에서 제공해준다는 것이다.
그래서 런타임 단계에서 외부에서 주입해주어 의존성을 주입해준다고 하는 것이다.
의존성 주입을 도입함으로써
- 변경에 민감하지 않으며
- 설정(구성) 파일에서 지정만 해준다면 코드의 변경이 필요없어 재사용성이 증가하고,
- 코드의 종속성이 이전보다 뚜렷하게 나타나 가독성이 증가한다.
와 같은 장점을 가지게 된다.
체감하기 위해 코드와 함께 다시 살펴보도록 하자.
지금 이 포스트에서 가장 먼저 등장한 코드를 다시 살펴보면
public class UserService {
private final UserRepository users = new MemoryUserRepository();
...
}
DB 설정이 되기 전 Memory로 DB를 대신하는 로직이 구현되어있는 Repository와 의존관계를 맺어준 모습이었다.
후에 DB를 연결해주기 위해
public class UserService {
// private final UserRepository users = new MemoryUserRepository();
private final UserRepository users = new UserRepository();
...
}
위처럼 새로 구현 후 직접 코드를 수정해주었었는데,
이 방식에선 의존관계에 변화에 민감하여 의존성이 매우매우 높은 코드인 것을 느낄 수 있을 것이다.
그러나,
public class UserService {
/**
* DI 가 가능하도록 코드를 변경
*/
private final UserRepository users;
public UserService(UserRepository users) {
this.users = users;
}
...
}
이렇게 코드를 변경하게 되면 UserService 내에서 의존 관계의 객체를 직접 정해주는 것이 아닌
외부에서 결정해주게 된다.
Service가 호출되는 그 시점에 의존관계가 설정되며
이 경우엔 객체 자체가 아니라 런타임에서 프레임 워크에 의해 주입되므로
제어권을 외부. 즉 프레임 워크가 가지게 되는 것이다.
IOC(Inversion of Control)의 개념이 바로 이것이다.
Config file
@Configuration
public class SpringConfig {
@Bean
public UserService userService() {
return new UserService(userRepository);
}
@Bean
public UserRepository userRepository() {
// return new MemoryUserRepository();
// 간단하게 설정 파일만 변경(다른 어떤 코드도 손대지 않음)
return new UserRepository();
}
}
코드를 직접 수정할 필요없이 설정 파일에서 관계만 설정해주면 추가적인 코드 수정없이
의존관계가 업데이트되며 덕분에 의존성이 줄어든다. (의존 대상의 변화에 덜 취약해진다.)
의존관계 주입 방법의 종류
본문에서는 생성자를 통해 의존관계를 주입해주었다.
Spring에서 의존관계를 주입하는 방법은 크게 3가지로 나눌 수 있는데,
이는 다음과 같다.
1. 생성자 주입
2. 메서드 주입
3. 필드 주입
생성자 주입
위에서 사용한 방법이다.
생성자 호출 시점(즉, 객체가 생성될 때)에 단 1번만 호출되어 불변성이 보증되는 방법이다.
가장 보편적으로 사용된다.
메서드 주입
클래스 내의 정의한 메서드를 통해 의존관계를 주입받는 방법이다.
예시는 아래와 같다.
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
@Autowired
public void injection(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
위와 같이 injection이라는 메서드를 정의하고,
@Autowired 어노테이션을 추가하여 Spring에게 의존 관계를 주입해달라고 명시하였다.
참고로, setter 메서드를 통해서
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
위와 같이 의존 관계를 주입받을 수 있다.
메서드 또는 setter를 통해 주입받게 되면 필드의 값을 변경할 수 있어
의존 관계를 변경할 가능성이 있을 때 유용하게 쓰인다.
필드 주입
마지막으로 필드에 바로 주입받는 방법이다.
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
...
}
위의 예시에서 알 수 있듯이 매우 코드가 간단하고 간편한데,
외부에서 변경이 불가능하여 테스트가 매우 힘들며, 프레임워크 없이는 아무것도 할 수 없는 방식이라
추천하지 않는다.
예시 코드들에서 공통적으로 등장한 @Autowired 어노테이션은 당연하게도 Spring Bean에서만 동작한다.
+ 마무리
대부분의 의존관계는 한번 주입하면 프로그램 종료 시까지 변경하는 경우가 드물며,
생성자 주입이 아닌 다른 방법들은 실수로 의존 관계에 영향을 미칠 수 있으며
사용 시 불변성을 보장할 수 없어 생성자 주입을 통해 의존 관계를 주입하는 것을 추천한다.
결론적으로
Dependency Injection을 사용함으로써
구성 요소의 불필요한 종속성을 줄일 수 있으며
쉽게 재사용이 가능한 코드를 작성하게 되고,
테스트에 용이한 코드를 작성할 수 있게 된다.
이해하고 나니 내가 왜 MashUp 면접에서 떨어졌는지 스스로 깨닫게 되었다...
참고자료
'Dev' 카테고리의 다른 글
Intellij 단축키 정복하기 (0) | 2022.05.22 |
---|---|
Singleton Pattern(싱글톤 패턴) (2) | 2022.05.10 |
Github Copilot 설정하기 (2) | 2022.04.25 |
address already in use 에러 해결하기 ( awk와 함께 ) (0) | 2022.04.02 |
TypeORM - 버전 0.3 && ORM이란? (0) | 2022.03.28 |