본문 바로가기
BackEnd/Spring

Spring - 오브젝트와 의존관계(스프링의 IoC와 DI)

by 규난 2023. 3. 21.
728x90

2023.03.19 - [Spring] - Spring - 오브젝트와 의존관계(관심사의 분리)

 

Spring - 오브젝트와 의존관계(관심사의 분리)

Spring 이란? 스프링은 자라를 기반으로 한 기술입니다. 스프링의 철학은 자바 엔터프라이즈 기술의 혼란 속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로부터 객체지향 프로

rbsks.tistory.com

2023.03.19 - [Spring] - Spring - 오브젝트와 의존관계(상속을 통한 확장과 인터페이스의 도입)

 

Spring - 오브젝트와 의존관계(상속을 통한 확장과 인터페이스의 도입)

2023.03.19 - [Spring] - Spring - 오브젝트와 의존관계(관심사의 분리) Spring - 오브젝트와 의존관계(관심사의 분리) Spring 이란? 스프링은 자라를 기반으로 한 기술입니다. 스프링의 철학은 자바 엔터프라

rbsks.tistory.com

 

IoC - 제어의 역전

앞의 두 포스트에서 관심사를 분리하고 더 나아가 상속을 통한 확장과 상속의 단점을 해결하기 위해 인터페이스 도입과 UserDao를 사용하는 클라이언트인 main() 메소드에서 관계 설정 책임까지 분리하여 UserDao가 ConnectionMaker 구현 클래스로부터 완벽하게 독립할 수 있었습니다.

 

하지만 main() 메소드도 UserDao의 기능이 잘 동작하는지 테스트하려고 만든 것인데 UserDao와 ConnectionMaker 구현 클래스를 생성해 오브젝트간의 관계를 설정해주는 책임까지 떠맡고 있습니다. 이렇게 성격이 다른 책임이나 관심사는 분리해버리는 것이 지금까지 해왔던 주요한 작업이니 이것도 분리하도록 하겠습니다.

 

분리될 기능은 UserDao와 ConnectionMaker 구현 클래스의 오브젝트를 만드는 것과, 만들어진 두 개의 오브젝가 연결돼서 사용될 수 있도록 관계를 맺어주는 것입니다. 이렇게 분리시킬 기능을 담당할 클래스의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것인데, 이런 일을 하는 오브젝트를 흔히 팩토리(factory)라고 부릅니다.

이는 디자인 패턴에서 말하는 특별한 문제를 해결하기 위해 사용되는 추상 팩토리 패턴이나 팩토리 메소드 패턴과는 다르니 혼당하지 않도록 주의해야 합니다. 이 팩토리는 단지 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하려는 목적으로 사용하는 것입니다.

 

Object Factory 코드

// DaoFactory
package one;

public class DaoFactory {

    public UserDao userDao() {
        UserDao userDao = new UserDao(getConnectionMaker());
        return userDao;
    }
    
    private ConnectionMaker getConnectionMaker() {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        return connectionMaker;
    }
}

// UserDao client main method
package one;

import java.sql.SQLException;

public class Main {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
//        UserDao userDao = new UserDao(new DConnectionMaker());
        UserDao userDao = new DaoFactory().userDao();

 		// test code ...
}

팩토리 역할을 맡을 DaoFactory 클래스를 만들고 UserDao를 사용하는 클라이언트인 main() 메소드에서 DaoFactory에 요청해서 미리 만들어진 UserDao 오브젝트 가져와 사용하게 만듭니다.

 

클라이언트인 main() 메소드는 이제 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경 쓰지 않고 DaoFactory로 부터 UserDao 오브젝트를 받아서 자신의 관심사인 테스트를 위해 활용하기만 하면 됩니다.

 

이제 제어의 역전이라는 개념에 대해 알아봅시다. 제어의 역전이라는 건, 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있습니다. 일반적인 프로그램의 흐름은 main() 메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고, 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 호출하고, 그 오브젝트 메소드 안에서 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복됩니다. 이런 프로그램 구조에서는 각 오브젝트는 프로그램의 흐름을 결정하거나 사용할 오브젝트를 구성하는 작업에 능동적으로 참여합니다.

 

제어의 역전이란 이런 제어의 흐름의 개념을 거꾸로 뒤집는 것입니다. 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않고 당연히 생성하지도 않습니다. 또 자신도 어떻게 만들어지고 어디서 사용되는지 알 수 없습니다. 모든 제어의 권한을 자신이 아닌 다른 대상에게 위임하기 때문입니다. 

프레임워크 vs 라이브러리

프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용됩니다. 보통 프레임워크 위에 개발한 클래스를 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식인 반면에 라이브러리는 라이브러리를 사용하는 애플리케이션 코드가 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용합니다. 

 

위에서 만든 UserDao와 DaoFactory에도 제어의 역전이 적용되어 있습니다. 원래 ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 제어권은 UserDao에게 있었는데 지금은 DaoFactory에 있습니다. 자신이 어떤 ConnectionMaker의 구현 클래스를 만들고 사용할지를 결정할 권한을 DaoFactroty에게 넘기고 UserDao 자기 자신도 DaoFactory에 의해 만들어지니 UserDao는 이제 수동적인 존재가 되었습니다. 이렇게 UserDao가 사용할 오브젝트인 ConnectionMaker 오브젝트와 자기 자신까지 생성하는 책임까지 모두 DaoFactory가 맡고 있습니다. 관심을 분리하고 책임을 나누고 유연하게 확장이 가능한 구조로 만들기 위한 지금까지의 과정이 바로 IoC를 적용하는 작업이라고 볼 수 있습니다. 

 

제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계 설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요합니다. 스프링은 IoC를 모든 기능의 기초가 되는 기반기술로 삼고 있으면 IoC를 극한까지 적용하고 있는 프레임워크입니다. 

스프링의 IoC

스프링에서 스프링 컨테이너가 오브젝트의 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 빈(Bean)이라고 부릅니다. 이렇게 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 부릅니다.

보통 빈 팩토리보다는 이를 좀 더 확장한 애플리케이션 컨텍스트(Application Context)를 주로 사용하고 애플리케이션 컨텍슨트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 생각하시면 됩니다.

 

그럼 DaoFactory를 애플리케이션 컨텍스트의 설정정보로 활용 될 수 있게 코드를 변경해보도록 하겠습니다.

먼저 스프링이 어플리케이션 컨텍스트 또는 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration 어노테이션과 오브젝트 생성을 담당하는 IoC용 메소드라는 표시인 @Bean 어노테이션을 붙여줍니다.

 

변경한 DaoFactory 코드

package one;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
@Configuration
public class DaoFactory {

    // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(getConnectionMaker());
        return userDao;
    }

    @Bean
    private ConnectionMaker getConnectionMaker() {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        return connectionMaker;
    }
}

 

애플리케이션 컨텍스트를 적용하도록 수정한 main() 메소드

@Configuration처럼 어노테이션이 붙은 자바 코드를 설정정보로 사용하려면 ApplicationContext의 구현 클래스인

AnnotationConfigApplicationContext를 이용해 생성자 파라미터로 DaoFactory 클래스를 넣어 사용하면됩니다.

이제 가져온 context의 getBean() 메소드를 이용하여 UserDao Bean을 가져올 수 있습니다.

getBean의 파라미터인 "userDao"는 애플리케이션 컨텍스트에 등록된 빈의 이름입니다.

package one;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.sql.SQLException;

public class Main {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
//        UserDao userDao = new UserDao(new DConnectionMaker());
//        UserDao userDao = new DaoFactory().userDao();

        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao userDao = context.getBean("userDao", UserDao.class);

        // test code ...
    }
}

코드를 보면 기존 오브젝트 팩토리인 DaoFactory보다 써야 될 코드도 더 있고 테스트 결과를 보면 동일한 것 같지만 중요한 차이점이 있습니다. 오브젝트 팩토리로 만들었던 DaoFactory의 userDao() 메소드를 두 번 호출해서 가져온 UserDao 오브젝트와 어플리케이션 컨텍스트에 DaoFactory를 등록하고 getBean() 메소드를 두 번 호출해서 가져온 UserDao의 오브젝트를 비교해봅시다.

 

package one;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.sql.SQLException;

public class Main {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        UserDao userDao1 = new DaoFactory().userDao();
        UserDao userDao2 = new DaoFactory().userDao();
        System.out.println(userDao1);
        System.out.println(userDao2);

        // 실행 결과
        // one.UserDao@1554909b
        // one.UserDao@6bf256fa
        
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao userDao3 = context.getBean("userDao", UserDao.class);
        UserDao userDao4 = context.getBean("userDao", UserDao.class);
        System.out.println(userDao3);
        System.out.println(userDao4);
        
        // 실행 결과
        // one.UserDao@593aaf41
        // one.UserDao@593aaf41
    }
}

위 코드블럭을 보시면 오브젝트 팩토리로 가져온 UserDao의 오브젝트는 동일한 오브젝트가 아니고 어플리케이션 컨텍스트에서 가져온 UserDao의 오브젝트는 동일한 오브젝트임을 확인할 수 있습니다.

 

어플리케이션 컨텍스트는 별다른 설정을 하지 않으면 어플리케이션 컨텍스트가 관리하는 오브젝트인 빈을 모두 싱글톤 저장하고 관리합니다(싱글톤 레지스트리). 그래서 여러번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 반환합니다. 

 

그렇다면 왜 스프링은 빈을 싱글톤으로 만들어서 관리를 할까요? 이는 스프링은 주로 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문에 그렇습니다. 이러한 서버환경은 서버 하나당 최대로 초당 수십에서 수백, 수천 번씩 브라우저나 외부 시스템으로부터 요청을 받아 처리하기 위해 데이터 엑세스 로직, 서비스 로직, 비즈니스 로직, 프레젠테이션 로직 등의 다양한 기능을 담당하는 오브젝트들이 참여하는 계층형 구조로 이루어진 경우가 대부분입니다. 그런데 매번 클라이언트 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용하게 된다면 아무리 자바의 오브젝트 생성과 가비지 컬렉션의 성능이 좋아졌다 하더라고 많은 부하로 인해 서버가 다운되는 일이 발생할 수 있게 됩니다. 이런 문제점으로 인해 싱글톤으로 빈을 관리하게 되었습니다.

 

자바에서 직접 싱글톤 패턴의 구현 방식은 여러 가지 단점이 존재하기 때문에(private 생성자, static field, static method 등) 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공합니다. 이 기능이 바로 싱글톤 레지스트리입니다. 싱글톤 레지스트리의 장점은 static method와 private 생성자를 사용하지 않고 평벙한 자바 클래스(POJO)를 싱글톤으로 활용하게 해준다는 점입니다.

따라서 public 생성자를 가질 수 있게 되며 테스트 환경에서도 자유롭게 오브젝트를 만들 수 있고 테스트를 위한 목 오브젝트로 대처하는 것도 DaoFactory에서 UserDao에 ConnectionMaker의 오브젝트를 사용하도록 관계를 설정해주듯이, 생성자 파라미터를 이용해서 사용할 오브젝트를 넣어줘면 되기때문에 간단하게 대처가 가능합니다. 이렇게 싱글톤 레지스트리 덕분에 디자인 패턴인 싱글톤 패턴과 달리 스프링이 지지하는 객체지향적인 설계 방식과 원칙 그리고 디자인 패턴(싱글톤 패턴은 제외)이 아무런 제약 없이 적용이 가능한 것입니다.

 

이제부터 스프링 IoC 컨테이너 기능의 대표적인 동작 원리인 DI(의존관계 주입)에 대해서 알아보도록 하겠습니다.

DI(Dependency Injection) - 의존관계 주입

의존한다는 건 의존대상이 변하면 의존하는 쪽에 영향을 미친다는 뜻입니다. 반대로는 의존대상은 의존하는 쪽이 변해도 의존대상에 영향을 미치지 않는다는 뜻이죠.

 

의존관계 주입이란 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말합니다. 의존관계 주입은 다음과 같은 세 가지 조건을 충족하는 작업을 말합니다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않아야합니다. 그러기 위해서는 인터페이스에만 의존하고 있어야 합니다.
  • 런타임 시점의 의존관계는 컨테이터나 팩토리 같은 제3의 존재가 결정합니다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 주입해줌으로써 만들어집니다.

의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것입니다.

스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지는 제 3의 존재라고 볼 수 있습니다.

 

밑의 사진을 보시면 UserDao는 ConnectionMaker 인터페이스에 의존하고 있습니다. ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 직접 받게 되겠죠. 하지만 ConnectionMaker 인터페이스를 구현한 클래스가 다른 것으로 바뀌거나 그 내부에서 사용하는 메소드에 변화가 생겨도 UserDao에는 영향을 주지 않습니다. 이렇게 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 됩니다. 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워지고 결합도가 낮다고 설명할 수 있습니다.

 

그리고 IoC 방식을 써서 UserDao로부터 런타임 의존관계를 드러내는 코드를 제거하고 제3의 존재(DaoFactory)에 의존관계 결정 권한을 위임했고, DaoFactory는 런타임 시점에 UserDao가 사용할 ConnectionMaker 타입의 오브젝트를 결정하고 이를 생성 후 UserDao의 생성자 파라미터에(파라미터는 꼭 인터페이스 타입의 파라미터이어야 합니다. 그래야 다이나믹하게 구현 클래스를 결정해서 DI를 받을 수 있습니다.) 주입해줌으로써 UserDao와 ConnectionMaker의 오브젝트와 런타임 의존관계를 맺게 해줍니다. 따라서 의존관계 주입의 세 가지 조건을 모두 충족한다 볼 수 있고, 이미 DaoFactory가 만들어진 시점에 DI를 적용한 셈입니다.

 

DaoFactory는 오브젝트 사이의 런타임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재이며, 동시에 IoC 방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너입니다. 따라서 DaoFactory는 IoC/DI 컨테이너라고 말할 수 있습니다.

 

1장에서 크게 관심사의 분리, 상속을 통한 확장과 인터페이스의 도입 그리고 마지막으로 IoC/DI에 대해서 공부를 하였는데

디자인 패턴을 선행 학습을 하고 봐서 그런지 이해도 잘 됐고 아주 재미있게 학습을 했습니다. 항상 스프링을 사용하면서 스프링의 컨테이너 동작 원리와 구조 그리고 어떤 디자인 패턴들이 적용 되었는지 궁금하였는데 이번 학습을 통해서 어느정도 궁금증을 해결 할 수 있어서 유익한 시간이었던거 같습니다. 다음 학습 목차는 제가 항상 궁금해 하는데 테스트인데요 이번 1장 처럼 저의 궁금증이 많이 풀리는 학습이 되었으면 좋겠습니다. 

출처 - 토비의 스프링 3. 1 Vol 1 스프링의 이해와 원리

728x90