의존관계란 무엇일까요??
두 개의 클래스 또는 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여해야합니다.
즉, 누가 누구에게 의존하는 관계에 있다는 식이어야 합니다.
ex) A 클래스 -> B 클래스 라는 의존관계가 있다고 생각해봅시다.
여기서는 B가 변하면 A에게 영향을 미친다는 뜻입니다. B의 기능이 추가되거나 변경된다면 그 영향이 A로 전달된다는 뜻입니다.
대표적으로 A가 B를 사용하는 경우, A에서 B에 정의된 메소드를 호출해서 사용하는 경우를 사용데 대한 의존관계
라고 합니다.
service
클래스의 의존관계
UserRepositoryV5.java
public interface UserRepositoryV5 {
Long save(User user);
Optional<User> findById(Long userId);
void remove(Long userId);
}
UserRepositoryV5Impl.java
@RequiredArgsConstructor
public class UserRepositoryV5Impl implements UserRepositoryV5 {
private final EntityManager em;
@Override
public Long save(User user) {
em.persist(user);
return user.getId();
}
@Override
public Optional<User> findById(Long userId) {
return Optional.ofNullable(em.find(User.class,userId));
}
@Override
public void remove(Long userId) {
User findUser = findById(userId).orElse(null);
if(findUser==null)
throw new IllegalArgumentException();
em.remove(findUser);
}
}
UserServiceV5.java
@Transactional
public class UserServiceV5 {
private final UserRepositoryV5 userRepositoryV5;
public UserServiceV5(UserRepositoryV5 userRepositoryV5) {
this.userRepositoryV5 = userRepositoryV5;
}
public Long join(User user){
return userRepositoryV5.save(user);
}
public User findOne(Long userId){
Optional<User> findUser=userRepositoryV5.findById(userId);
return findUser.orElseThrow(()->{
throw new RuntimeException();
}
);
}
}
그럼 초기에 만들었던 service
클래스와 repository
인터페이스간의 예를 봅시다.
service
클래스는 repository
를 의존하고 있는 형태입니다.
따라서 repository
인터페이스가 변하게된다면, service
가 직접적인 영향을 받게 됩니다.
하지만, 인터페이스를 구현하고 있는 repositoryImpl
클래스가 변하게 된다면, 받는 영향은 없습니다.
이렇게, 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 됩니다.
service
클래스는 repository
인터페이스에게만 의존한다고 볼 수 도 있는 형태입니다.
service
는 구현체의 존재도 알지 못하는 상황입니다.
그런데, 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있습니다.
이를 런타임 의존관계
라고 부릅니다. 즉, 설계 시점의 의존관계가 실체화 되는 것이라고 보시면 됩니다.
프로그램이 시작되고 service
오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 말합니다.
런타임 시점 의존관계
service
와 repositoryImpl
) 도와주는 제 3의 존재AppConfig.java
@Configuration
public class SpringAppConfigV1 {
@Bean
@Primary
public LocalEntityManagerFactoryBean getEmf(){
LocalEntityManagerFactoryBean emf=new LocalEntityManagerFactoryBean();
emf.setPersistenceUnitName("hello");
return emf;
}
@Bean
@Primary
public EntityManager getEm(){
return getEmf().getObject().createEntityManager();
}
@Bean
public UserRepositoryV5 userRepository(){
return new UserRepositoryV5Impl(getEm());
}
@Bean
public UserServiceV5 userService(){
return new UserServiceV5(userRepository());
}
}
위의 코드는 런타임 시점에 service
가 사용할 repository
타입의 오브젝트를 결정하고 이를 생성한 후에 service
의 파라미터로 주입해서
구현체와의 런타임 의존관계를 맺게 해줍니다.
또한, IoC방식으로 오브젝트의 생성과 초기화, 제공등의 작업등도 수행하고 있으므로 IoC/DI 컨테이너라고 많이 부릅니다.
DI컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레퍼런스를 전달받는 과정이 마치 멕소드를 통해 DI 컨테이너가 service
에 주입해주는 것과 같다고 해서
이를 ㅇ의존관계 주입이라고 부릅니다.
의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색이라고 불립니다.
의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾습니다. 어떤 구현체를 사용할지는 결정하지 않습니다.
의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한
주입 대신 스스로 컨테이너에게 요청하는 방법을 사용합니다.
UserServiceV6.java
@Transactional
public class UserServiceV6 {
private final UserRepositoryV5 userRepositoryV5;
public UserServiceV6() {
AnnotationConfigApplicationContext ac=new AnnotationConfigApplicationContext(SpringAppConfigV1.class);
this.userRepositoryV5 = ac.getBean("userRepository",UserRepositoryV5.class);
}
public Long join(User user){
return userRepositoryV5.save(user);
}
public User findOne(Long userId){
Optional<User> findUser=userRepositoryV5.findById(userId);
return findUser.orElseThrow(()->{
throw new RuntimeException();
}
);
}
}
테스트
@SpringBootTest
@Transactional
public class UserServiceV6Test {
UserServiceV6 userService=new UserServiceV6();
@Test
@DisplayName("의존관계 검색 테스트")
void 의존관계_검색(){
User user=createUser("hong","1234");
Long saveId = userService.join(user);
User findUser = userService.findOne(saveId);
Assertions.assertThat(findUser.getName()).isEqualTo(user.getName());
}
private User createUser(String name, String password){
return User.createUser()
.name(name)
.password(password)
.build();
}
}
동작 확인
코드를 보시면 외부로 부터 의존관계를 주입 받는 것이 아닌 스스로 IoC 컨테이너에게 요청을 합니다.
의존관계 검색과 주입의 차이점
의존관계 검색 방법은 적어도 한번이상은 쓰입니다
애플리케이션이 시작되는 시점에서 스태틱 메소드인 main()은 DI를 이용하여 오브젝트를 주입받을 방법이 없기때문입니다.
서버에서도 의존관계 검색 방법이 사용됩니다. 사용자의 요청을 받을 때마다 DispatcherServlet 에서 스프링 컨테이너에 담긴 오브젝트를 사용하기 위해서 입니다.