SPRING - 오브젝트와 의존관계 (7)

🚀 의존관계 주입(DI)

🔧 런타임 의존관계 설정

의존관계란 무엇일까요??

두 개의 클래스 또는 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여해야합니다.
즉, 누가 누구에게 의존하는 관계에 있다는 식이어야 합니다.

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 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 말합니다.

런타임 시점 의존관계

  • 컨테이너나 팩토리 같은 제3의 존재가 결정합니다.
    • 설계 시점에서는 알지 못했던 두 오브젝트의 관계를 맺도록 (servicerepositoryImpl) 도와주는 제 3의 존재
    • DI에서 말하는 제3의 존재는 바로 관계설정 책임을 가진 코드를 분리해서 만들어지는 오브젝트
      • 스프링 애플리케이션 컨텍스트, IOC컨테이너
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어집니다.

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 컨테이너에게 요청을 합니다.

의존관계 검색과 주입의 차이점

  • 검색 방식
    • 자신이 스프링의 빈일 필요가 없습니다.
    • 그냥 직접 new 로 생성해도 됩니다.
  • 주입 방식
    • 자신도 반드시 빈이어야 합니다.
      • 컨테이너가 오브젝트를 주입하기 위해서는 생성과 초기화 권한을 갖고 있어야하기 때문입니다.

의존관계 검색 방법은 적어도 한번이상은 쓰입니다

애플리케이션이 시작되는 시점에서 스태틱 메소드인 main()은 DI를 이용하여 오브젝트를 주입받을 방법이 없기때문입니다.

서버에서도 의존관계 검색 방법이 사용됩니다. 사용자의 요청을 받을 때마다 DispatcherServlet 에서 스프링 컨테이너에 담긴 오브젝트를 사용하기 위해서 입니다.

👋 이상으로 포스팅을 마치겠습니다. 감사합니다!

좋은 웹페이지 즐겨찾기