스프링의 핵심 원리 IoC (Inversion of Control)
- IoC : 제어의 역전
- 객체 생성과 의존 관계 설정을 개발자가 직접 관리하는 대신 스프링 컨테이너가 담당하도록 하는 것
기존 방식:
1
2
3
4
class Service {
private Dao dao = new DaoImpl(); // 개발자가 직접 생성
// ...
}
스프링 IoC 방식: 스프링 IoC를 사용하면 Service 클래스 내에서 Dao 객체를 직접 생성하지 않고, 스프링 컨테이너에 의존성을 주입받는다.
1
2
3
4
5
6
7
8
9
@Service
public class Service {
private final Dao dao;
@Autowired
public Service(Dao dao) {
this.dao = dao;
}
}
의존성 주입을 받는 이유
- 낮은 결합도:
- 객체 간의 의존 관계를 명확히 분리하여 코드의 유연성을 높인다.
- 특정 구현체에 의존하지 않고 인터페이스를 통해 의존하므로, 코드 변경 시 영향 범위를 최소화할 수 있다.
- 테스트 용이성:
- 의존 객체를 모킹 객체로 쉽게 교체하여 단위 테스트를 수행할 수 있다.
- 재사용성:
- 한 번 정의된 빈(Bean)을 다양한 곳에서 재사용할 수 있다.
- 확장성:
- 새로운 기능을 추가하거나 기존 기능을 변경할 때 유연하게 대처할 수 있다.
- 스프링 생태계 활용:
- 스프링이 제공하는 다양한 기능(AOP, 트랜잭션 관리 등)을 활용할 수 있다.
의존성 주입 방식
- 생성자 주입:
- 객체 생성 시 생성자를 통해 의존성을 주입하는 방식
- 필수적인 의존성을 명확하게 표현하고, 불변성을 보장한다.
- Setter 주입:
- 객체 생성 후 setter 메서드를 통해 의존성을 주입하는 방식
- 생성자 주입보다 유연하지만, 의존성이 필수적인지 명확하지 않을 수 있다.
- 필드 주입:
- 필드에
@Autowired
어노테이션을 사용하여 의존성을 주입하는 방식 - 가장 간편하지만, 테스트하기 어렵고 의존성이 명확하지 않을 수 있다.
- 필드에
1. 생성자 주입
- 생성자 파라미터를 통해 의존성을 주입: 객체 생성 시 필수적인 의존성을 명확하게 지정한다.
- 장점:
- 의존성이 필수적인지 명확하게 드러낸다.
- 불변성을 보장하여 객체 상태를 안정적으로 유지할 수 있다.
- 테스트하기 쉽다.
- 단점: 코드가 다소 길어질 수 있다.
1
2
3
4
5
6
7
8
9
@Component
public class MyService {
private final UserRepository userRepository;
@Autowired
public MyService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
2. @Autowired 어노테이션
- 필드 또는 메소드에 직접 적용: 의존해야 할 객체의 타입을 명시하고, 스프링 컨테이너가 자동으로 해당 객체를 찾아 주입한다.
- 장점: 간결하고 편리하게 의존성을 주입할 수 있다.
- 단점:
- 필드 주입의 경우, 의존성이 필수적인지 명확하지 않을 수 있다.
- 테스트 시 의존성을 조작하기 어려울 수 있다.
1
2
3
4
5
@Component
public class MyService {
@Autowired
private UserRepository userRepository;
}
@Autowired vs 생성자 주입 비교
특징 | @Autowired | 생성자 주입 |
---|---|---|
적용 위치 | 필드, 메소드 | 생성자 |
장점 | 간결, 편리 | 의존성 명확, 불변성, 테스트 용이 |
단점 | 의존성 필수 여부 불명확, 테스트 어려움 | 코드 길어짐 |
권장 | 선택적 의존성, 간단한 경우 | 필수 의존성, 불변성 보장, 테스트 중심 개발 |
왜 생성자 주입을 더 선호하는가?
명확한 의존성:
생성자를 통해 의존성을 주입하면 어떤 객체가 필요한지 명확하게 드러나므로 코드의 가독성이 높아진다.
1
2
3
4
5
6
7
8
9
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id);
}
}
- UserService는 UserRepository에 의존하고 있지만, UserRepository가 언제, 어떻게 주입되는지 명확하지 않다.
- 의존성 주입 시점에 문제가 발생할 경우 오류를 찾기 힘들다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id);
}
}
생성자 주입을 사용하면 UserService가 생성될 때 반드시 UserRepository가 필요하다는 것을 명확하게 보여준다. 의존성 관리가 보다 쉽다.
불변성:
생성자를 통해 주입된 의존성은 변경될 수 없으므로 객체 상태를 안정적으로 유지할 수 있다. 객체 생성 후 의존성이 변경될 가능성이 줄어들기 때문이다.
테스트 용이성:
의존성을 모킹하기 쉽기 때문에 단위 테스트를 효과적으로 수행할 수 있다.
-
@Autowired 필드 주입을 받을 때: UserService를 테스트하기 위해 UserRepository를 모킹하려면, 스프링 테스트 컨텍스트를 설정하고
@MockBean
어노테이션을 사용해야 한다. 이는 테스트 코드를 복잡하게 만들고, 의도하지 않은 사이드 이펙트가 발생할 가능성을 높인다. -
생성자 주입을 할 때: UserService를 생성할 때 모킹한 UserRepository를 전달하여 쉽게 테스트할 수 있다.
1
2
3
4
5
6
7
8
@Test
public void getUserById_success() {
UserRepository userRepository = mock(UserRepository.class);
when(userRepository.findById(1L)).thenReturn(Optional.of(new User()));
UserService userService = new UserService(userRepository);
User user = userService.getUserById(1L);
}
SonarLint가 @Autowired
를 지양하는 이유
Autowired를 사용했을 때, SonarLint에서 경고가 나왔다. 왜 지양하는지 이유를 알고 싶었다.
알고보니 스프링 공식 문서에서도 생성자 주입을 권장하고 있었다.
스프링에서 의존성 주입은 IoC라는 핵심 원리에 기반하여 객체 간의 결합도를 낮추고 유연한 시스템을 구축하는 데 필수적이다. 특히, 생성자 주입 방식은 의존성을 명확하게 표현하고 테스트하기 쉽다는 장점이 있어 권장된다.
생성자 주입이 일반적으로 더 선호되는 방식이지만, @Autowired도 간단한 경우나 선택적 의존성일 때 유용하게 사용될 수 있다. 프로젝트의 특성과 개발 팀의 규약에 따라 적절한 방식을 선택하는 것이 좋다.
핵심은 의존성을 명확하게 관리하고, 코드의 가독성과 유지보수성을 높이는 것이다.
요약:
- 스프링 IoC는 객체 생성과 의존 관계 설정을 자동화한다.
- 의존성 주입은 코드의 유연성, 테스트 용이성, 재사용성을 높인다.
- 생성자 주입이 가장 권장되는 방식이다.