토비의 스프링 정리 프로젝트 #1.7 의존관계 주입(DI)

Jake Seo·2021년 7월 9일
1

토비의 스프링

목록 보기
8/29

제어의 역전(IoC)과 의존관계 주입

객체지향적인 설계, 디자인 패턴, 컨테이너에서 동작하는 서버 기술을 사용하면 자연스럽게 IoC를 적용하거나 그 원리로 동작하는 기술을 사용하게 된다.

IoC라는 단어가 매우 느슨하게 정의되어 폭넓게 사용되는 용어이기 때문에 스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다.

서블릿 컨테이너처럼 서버에서 동작하는 서비스 컨테이너라는 건지 템플릿 메소드 패턴을 이용해 만들어진 프레임워크인지 아니면 기타 다른 IoC 특징을 지닌 기술을 가진 프레임워크인지 알기 어렵다.

그래서 나온 새로운 용어가 의존관계 주입(Dependency Injection)이다. 스프링이 제공해주는 기능에 대한 의도가 더 명확히 드러난다.

DI(Dependency Injection)는 오브젝트 오브젝트 레퍼런스를 외부로부터 제공받고 이를 통해 여타 오브젝트와 다이나믹하게 의존관계가 만들어지는 것이 핵심이다. 용어는 동작방식(매커니즘)보다는 의도를 가지고 이름을 짓는 것이 좋다.

런타임 의존관계 설정

의존관계란?

UML모델에서는 위와 같이 점선과 화살표로 의존관계가 표현된다. 위 UML 모델이 의미하는 것은 다음과 같다.

  • AB 두 클래스가 의존 관계를 가지고 있다.
  • AB에 의존하고 있다.

의존한다는 것은 이를테면 AB를 사용하고, B를 변경하면 A에 영향이 미친다는 것이다. 사용의 관계에 있는 경우에 AB는 의존성이 있다.

또, 의존한다는 것은 방향성이 있는데 A에는 변경이 있어도 B에 영향을 미치지 않지만, B에 변경이 있다면 A에 영향을 미친다.

UserDao의 의존관계

UserDaoConnectionMaker라는 인터페이스를 느슨하게 의존하고 있다. 느슨하게 의존하는 이유는 인터페이스에 의존하기 때문에 구현 클래스와 관계를 덜 받기 때문이다. 이 상태는 결합도가 낮다고 할 수 있다.

UML에서 말하는 의존관계는 이렇게 설계 모델의 관점에서 이야기하는 것이다. 그런데 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 외에, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 이를 런타임 의존관계, 오브젝트 의존관계라고 하는데 모델링 시점의 의존관계와는 성격이 다르다.

런타임 시에 의존관계를 맺는 대상, 즉 실제 사용 대상인 오브젝트를 의존 오브젝트(Dependency Object)라고 한다.

의존관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라잉너트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

의존관계 주입이란 다음 세가지 조건을 충족하는 작업을 말한다.

  • 클래스 모델이나 코드에서는 런타임 시점의 의존관계가 드러나지 않아야 한다. 즉, 인터페이스에만 의존해야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리(이전의 DaoFactory)와 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

스프링에서 말하는 제3의 존재애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등으로 볼 수 있다.

UserDao의 의존관계 주입

public class UserDao {
    ConnectionMaker connectionMaker;

    // DI (Dependency Injection) 를 이용한 방법
    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
    
    ...
@Configuration
// `@Configuration`은 `애플리케이션 컨텍스트` 혹은 `빈 팩토리`가 사용할 설정 정보라는 표시이다.
// `@Component`와는 다르게 의존 정보를 설정할 수 있는 곳이다.
// `@Component`에서 아래와 같이 내부에서 직접 생성하는 메소드를 사용하면,
// `Method annotated with @Bean called directly. use dependency injection` 이라는 에러 문구가 뜬다.
// `@Configuration`에서는 내부에서 직접 생성하는 메소드를 사용해도 빈 의존관계로 취급된다.
public class DaoFactory {
    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다.
    public UserDao userDao() {
        return new UserDao(simpleConnectionMaker());
    }

    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다.
    public ConnectionMaker simpleConnectionMaker() {
        return new NConnectionMaker();
    }
}
public class UserDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDao userDao = applicationContext.getBean("userDao", UserDao.class);

        User user = new User();
        user.setId("12");
        user.setName("제이크12");
        user.setPassword("jakejake");

        userDao.add(user);

        System.out.println(user.getId() + " register succeeded");

        User user2 = userDao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());

        System.out.println(user2.getId() + " query succeeded");
    }
}

현재 코드는 의존관계 주입의 조건을 충족한 상태이다.

밑줄친 :클래스명의 의미는 런타임에 존재하는 오브젝트라는 뜻이다.

의존관계 검색과 주입

IoC 방법에는 의존관계 검색이라는 방법도 있다. 외부로부터 주입을 받는 것이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색(Dependency Lookup)이라고 불린다.

의존관계 검색은 런타임시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 IoC에 맡기지만, 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

public class UserDao {
    ConnectionMaker connectionMaker;

    // DL (Dependency Lookup) 를 이용한 방법
    public UserDao() {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(DaoFactory.class);

        this.connectionMaker = applicationContext.getBean(ConnectionMaker.class);
    }
@Configuration
// `@Configuration`은 `애플리케이션 컨텍스트` 혹은 `빈 팩토리`가 사용할 설정 정보라는 표시이다.
// `@Component`와는 다르게 의존 정보를 설정할 수 있는 곳이다.
// `@Component`에서 아래와 같이 내부에서 직접 생성하는 메소드를 사용하면,
// `Method annotated with @Bean called directly. use dependency injection` 이라는 에러 문구가 뜬다.
// `@Configuration`에서는 내부에서 직접 생성하는 메소드를 사용해도 빈 의존관계로 취급된다.
public class DaoFactory {
    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다.
    public UserDao userDao() {
        return new UserDao(simpleConnectionMaker());
    }

    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다.
    public ConnectionMaker simpleConnectionMaker() {
        return new NConnectionMaker();
    }
}
public class UserDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao userDao = new UserDao();

        User user = new User();
        user.setId("15");
        user.setName("제이크15");
        user.setPassword("jakejake");

        userDao.add(user);

        System.out.println(user.getId() + " register succeeded");

        User user2 = userDao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());

        System.out.println(user2.getId() + " query succeeded");
    }
}

달라진 건 클라이언트였던 UserDaoTest에서 의존관계를 주입해주지 않고, UserDao에서 직접 애플리케이션 컨텍스트를 검색해 의존할 오브젝트를 찾았다는 점이다.

의존관계 검색 vs 의존관계 주입

UserDao의 코드에 스프링 API가 나타나는 것은 책임에 정확히 맞지 않고 어색하므로, 대부분은 의존관계 주입 방식을 사용하는 편이 좋다.

그러나 의존관계 검색이 필요한 경우가 있다. UserDaoTest와 같은 클라이언트에서는 스프링 IoC와 DI를 컨테이너를 적용했다고 하더라도, 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. 스태틱 메소드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다.

의존관계 검색과 의존관계 주입에서의 가장 큰 차이는 의존관계 주입에서는 주입받는 오브젝트 자신도 스프링 빈이어야 한다. 반면에 의존관계 검색에서는 검색하는 오브젝트 자신이 스프링 빈일 필요가 없다는 점이다.

의존관계 주입의 응용

의존관계 주입의 장점은 결국 객체지향 설계와 프로그래밍 원칙을 따랐을 때 얻을 수 있는 장점이 그대로 적용된다. 스프링이 제공하는 기능의 99%가 DI의 혜택을 이용하고 있다.

기능 구현의 교환

만일, 운영DB와 개발DB를 번갈아가며 써야하는 상황이라면, 이전 초난감 DAO와는 다르게 ConnectionMaker 인터페이스를 상속하는 클래스, 이를테면 TestConnectionMaker를 구현한 뒤에

@Bean
public ConnectionMaker connectionMaker() {
    return new TestConnectionMaker();
}

위와 같이 connectionMaker 빈이 반환하는 객체만 TestConnectionMaker로 바꿔주면 된다. 그 외에 어떤 기존 코드도 수정할 필요 없다. 다시 운영DB로 연결하고 싶다면 반환하는 부분만 ProductionConnectionMaker 등으로 다시 바꿔주면 된다.

부가기능 추가

Connection이 성립되었을 때, 총 연결된 횟수를 출력하고 싶다고 가정해보자. 이전에 작성했던 소스코드를 이용하여 간단하게 작성할 수 있다.

public class LoggingConnectionMaker implements ConnectionMaker{
    int counter = 0;
    private final ConnectionMaker realConnectionMaker;

    public LoggingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    @Override
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        counter++;
        System.out.println("커넥션 성립, 커넥션 연결 횟수: " + counter);
        return realConnectionMaker.makeNewConnection();
    }
}

ConnectionMaker인터페이스를 상속하여 makeNewConnection()counter를 증가시키고, 커넥션 연결 횟수를 출력하는 부분을 추가했다. 그리고 마지막에 반환은 의존성 주입으로 가져올 realConnectionMaker를 반환한다.

@Configuration
public class LoggingDaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new LoggingConnectionMaker(realConnectionMaker());
    }

    @Bean
    public ConnectionMaker realConnectionMaker() {
        return new DConnectionMaker();
    }
}

의존관계의 설정정보(청사진)를 구성한다. realConnectionMakerDConnectionMaker가 될 것이다.

public class LoggingDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(LoggingDaoFactory.class);

        UserDao userDao = new UserDao(applicationContext.getBean("connectionMaker", ConnectionMaker.class));

        User user = new User();
        user.setId("213");
        user.setName("제이크212");
        user.setPassword("123123");

        userDao.add(user);
        System.out.println(user.getId() + " register succeeded");

        User user2 = userDao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());
        System.out.println(user2.getId() + " query succeeded");
    }
}

간단히 테스트 코드를 작성해보았다.

정상적으로 잘 출력된다.

여기서 다시 한번 상기해야 할 점은 우리는 의존 관계 설정 코드만을 건드려서 부가기능을 추가했다는 것이다. 이전에 작성했던 코드들은 전혀 건드리지 않았다.

LoggingConnectionMaker를 적용한 후의 의존관계는 위와 같다.

다시 이전으로 복구하고 싶다면, 클라이언트에서 애플리케이션 컨텍스트 생성에 쓰이는 클래스만 new AnnotationConfigApplicationContext(DaoFactory.class)와 같이 DaoFactory로 바꿔주면 된다.

메소드를 통한 의존관계 주입

의존관계 주입 시에 반드시 생성자를 이용해야 하는 것은 아니다.

수정자 메소드를 통한 의존관계 주입

  • set... 등의 수정자를 이용하여 DI 가능하다.
  • 한 번에 한 개의 파라미터만 가질 수 있다.
  • 부가적으로 입력 값에 대한 검증이나 그 밖의 작업도 수행 가능하다.
  • 스프링은 전통적으로 수정자 메소드를 가장 많이 사용해왔다.
  • XML을 사용하는 경우 자바빈 규약을 따르는 수정자 메소드가 가장 사용하기 편하다.
    • setterIDE에서 자동으로 생성해주는 규약을 따르는 것이 좋다.

일반 메소드를 통한 의존관계 주입

  • 수정자 메소드처럼 set...으로 시작해야 한다.
  • 한번에 여러 개의 파라미터를 받을 수 있다.
profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글