제어의 역전(IOC)

오브젝트 팩토리

현재까지 우리는 DAO를 깜끔한 구조로 리팩토링 하였습니다.

하지만 UserDaoTest는 그냥 넘겼습니다. 현재는 UserDaoTest에서 Connection을 할 때 어떤 클래스를 사용하는지 정의를 하면서 UserDao가 담당하던 기능을 분리하였습니다.

그러나 원래 UserDaoTest를 만든 목적은 UserDao의 기능이 잘 동작하는지를 테스트하기 위해서 만든 것입니다. 그렇긱 때문에 이러한 관심사가 겹치는 것을 분리해보도록 하겠습니다.


팩토리 (Config)

분리시킬 기능을 당담하는 클래스를 만듭니다.

이 클래스는 객체의 생성방법을 결정하고 그렇게 만들어진 Object를 돌려주는 역할을 합니다.

이런것을 우리는 팩토리라고 부릅니다.

이제 DaoFactory라는 클랙스를 생성하고 UserDaoConnectionMaker 관련 생성작업을 옮기도록 합시다.

public class DaoFactory{
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new AConnectionMaker(); // A회사것을 쓰겠다.
        UserDao userDao = new UserDao(connectionMaker);
        return UserDao;
    }
}

이제 DaoFactory 클래스의 userDao() 라는 메서드를 호출한다면 A회사의 AConnectionMaker를 사용해 DB 커넥션을 가져오도록 설계된 UserDao를 반환하게 됩니다.

이제 그러면 UserDaoTest 에서 객체를 설정해주면서 생성해줄 필요가없기 때문에 다음과 같이 고칩니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{
                UserDao dao = new DaoFactory().userDao();
        }
}

설계도로서의 팩토리

결론적으로 이렇게 분리를하게 된다면 DaoFactory 라는 클래스에서 Client가 요청을 한다면

알아서 내가 어떤 것을 Connection으로 사용할지 생성을하고 UserDao에서 그 클래스를 사용하도록 해주게 된다.

이제 회사에 UserDao를 납품할 때 DaoFactory도 함께 납품을 해서 클래스 변경이 일어나면 DaoFactory의 코드만 수정하면 됩니다.


오브젝트 팩토리의 활용

만약, DaoFactory 에서 UserDao 라는 Dao 클래스가 아닌 다른 클래스를 넣는다면 어떻게 될까? 이런식으로 DAO가 늘어나게된다면 Parameter로 넘길 때 ConnectionMaker 의 인스턴스를 생성해서 넘기는 코드가 중복되게 됩니다.

public class DaoFactory{
    public UserDao userDao(){
            return new UserDao(new AConnectionMaker());
    }

    public AccountDao AccountDao(){
            return new AccountDao(new AConnectionMaker());
    }

    public MessageDao messageDao(){
            return new MessageDao(new AConnectionMaker());
    }
}

이런식으로 Parameter에 사용한 ConnectionMaker 를 생성해주는 코드가 중복이 됩니다.

이를 해결하는 제일 좋은 방법은 또 부리하는 것 입니다.

ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메소드로 뽑아내는 것이 좋은 결정입니다.

public class DaoFactory{
    public UserDao userDao(){
            return new UserDao(connectionMaker());
    }

    public AccountDao AccountDao(){
            return new AccountDao(connectionMaker());
    }

    public MessageDao messageDao(){
            return new MessageDao(connectionMaker());
    }

    public ConnectionMaker connectionMaker(){
        return new AconnectionMaker();
    }
}

제어권의 이전을 통한 제어관계 역전

제어의 역전(IOC)가 우리가 배우고자 하는 목표입니다.

제어의 역전은 간단하게 프로그램의 제어 흐름 구조가 뒤바뀌는 것을 말합니다.

보통으로는 main() 메서드에서 다음과 같은 흐름을 가집니다.

  1. 시작되는 지점에서 다음에 사용할 오브젝트를 결정한다.
  2. 결정한 오브젝트를 생성한다.
  3. 만들어진 오브젝트에 있는 메서드를 호출한다.
  4. 그 오브젝트 메서드 안에서 다음에 사용할 것을 결정한다.
  5. 이 과정을 반복한다.

그래서 우리가 초기에 만든 UserDao의 흐름을 살펴보면 다음과 같습니다.

  1. UserDao 클래스의 오브젝트를 직접 생성합니다.
  2. 만들어진 오브젝트의 메서드를 사용합니다.
  3. UserDao는 자신이 사용할 ConnectionMaker의 구현클래스를 자기가 결정합니다.
  4. 그 오브젝트를 필요한 시점에 생성합니다.
  5. 각 메서드가 이를 사용합니다.

우리는 이러한 제어 흐름의 개념을 거꾸로 뒤집어서 사용할 것 입니다.

이를 사용하면 이제 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않습니다.

왜냐하면 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문입니다.

현재 우리는 DaoFactory를 만들어서 제어의 역전(IOC)를 도입하였습니다.

원래는 ConnectionMaker의 구현클래스의 결정은 UserDao에서 하였지만 현재는 이런 구현클래스의 결정에 대해서 DaoFactory에 넘겼습니다.

넘김으로써 UserDao는 능동적이 아닌 DaoFactory가 주는 것을 사용하는 수동적인 존재가 되었습니다.

우리는 현재 IOC프레임워크를 사용하지 않고 DaoFactory를 이용해서 IoC를 구현한 셈입니다.

물론 DaoFactory 같이 Ioc를 만들어도 되지만 프레임워크의 도움을 받는 편이 유리하기 때문에 프레임워크를 사용할 예정입니다.


스프링의 IoC

오브젝트 팩토리를 이용한 스프링 IoC

애플리케이션 컨텍스트와 설정정보

우리는 DaoFacotory를 스프링에서 사용이 가능하도록 변신시킬 것 입니다.

일단 들어가기전에 빈(Bean) 이라는 중요한 개념이 존재합니다.

  • (Bean)
    • 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 말합니다.
    • 자바 빈 또는 엔터프라이즈 자바빈에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말합니다.
    • 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말입니다.

또한, 우리는 이러한 빈(Bean)의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 합니다.

하지만 이러한 빈 팩토리보다 Application context를 자주 사용하게 됩니다. 이것은 IoC 방식을 따라 만들어진 일종의 빈 팩토리 입니다.


DaoFactory를 사용하는 애플리케이션 컨텍스트

DaoFactory를 스프링의 빈 팩토리가 사용할 수 있는 본격적인 설정정보로 만들어봅시다.

우리는 @Configuration 이라는 애노테이션을 사용해서 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식 시킵니다.

그리고 @Bean 이라는 애노테이션을 오브젝트를 만들어주는 메서드를 붙여줍니다.

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

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

    @Bean
    public ConnectionMakerr connectionMaker(){
        return new AConnectionMaker();
    }
}

이제 설정을 완료했으니 DaoFactory 를 설정정보로 사용하는 Application Context를 만들어 볼 것입니다.

@Configuration 이 붙은 자바 코드를 설정정보로 사용하기 위해서는 AnnocationConfigApplicationContext 를 이용하면 됩니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{

                ApplicationContext context = 
                    new AnnotationCongifApplicationContext(DaoFactory.class);

                UserDao dao = context.getBean("userDao", UserDao.class);
        }
}

우리는 ApplicationContextgetBean() 이라는 메서드를 통해서 UserDao의 오브젝트를 가져올 수 있습니다.

이런식으로 @Bean 이라는 애노테이션을 붙인 매서드가 바로 빈의 이름이 됩니다.

그래서 이름에 따라서 getBean("메서드이름", 사용클래스.class)로 가져올 수 있습니다.


애플리케이션 컨텍스트의 동작방식

기존의 오브젝트 팩토리를 이용한 방식과 스프링 애플리케이션 컨텍스트를 사용한 방식을 비교해봅시다.

스프링에서는 ApplicationContext라는 인터페이스를 통해서 애플리케이션 컨텍스트를 구현합니다.

이러한 ApplicationContextBeanFactory 인터페이스를 상속합니다. 그래서 다음과 같이 불립니다.

  • IoC 컨테이너
  • 스프링 컨테이너
  • 빈 팩토리
  • 스프링

이전에 구현한 DaoFactoryuserDao 를 비롯한 DAO를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 합니다.

하지만 ApplicationContext를 사용하면 DaoFacotory와 달리 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 존재하지 않습니다. 이러한 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻습니다.

그래서 다음과 같은 순서로 작동하게 됩니다.

  1. Client가 UserDao 를 요청합니다.
  2. ApplicationContext 에서 getBean() 메서드를 통해서 생성을 요청합니다.
  3. @Configuration 이 붙은 DaoFactory 에서 @Bean 으로 등록된 UserDao() 메서드를 호출해서 객체를 생성합니다.
  4. 생성한 객체를 Client에게 넘겨줍니다.

이렇게 구현을한다면 다음과 같은 장점이 생깁니다.

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
    • 애플리케이션 컨텍스트를 사용한다면 오브젝트 팩토리가 아무리 많아져도 이를 알아야하거나 직접 사용할 필요가 없습니다. 또한, XML을 사용해서 컨텍스트가 사용할 IoC 설정정보도 만들 수 있습니다.
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줍니다.
    • 애플리케이션 컨텍스트가 단지 오브젝트 생성과 다른 오브젝와의 관계설정을 해준다고 생각할 수도 있다.
    • 오브젝트가 만들어지는 방식 오브젝트에 대한 후처리 등등 다양한 기능을 제공하기도 합니다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공합니다.
    • 애플리케이션의 getBean() 메서드를 보면 빈의 이름을 이용해서 빈을 찾아 줄 수 있습니다. 또한, 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수도 있습니다.

스프링 IoC의 용어 정리

빈(Bean)

빈은 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻입니다.

이를 관리 오브젝트(Managed object) 라고도 부릅니다. 하지만 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 아닌 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고합니다.


빈 팩토리(Bean Factory)

빈 팩토리는 스프링의 IoC를 담당하는 핵심 컨테이너를 가리킵니다

빈 팩토리는 다음과 같은 기능을 합니다.

  • 빈을 등록한다.
  • 빈을 생성한다.
  • 빈을 조회후 반환한다.

보통은 빈 팩토리를 바로 사용하지 않고 상속받은 Application Context를 이용합니다.


애플리케이션 컨텍스트(Application Context)

애플리케이션 컨텍스트는 빈 팩토리를 확장한 IoC컨테이너 입니다.

빈 팩토리와 기본적인 기능은 비슷하지만 추가적인 기능들이 확장되어 있습니다.


설정정보/설정 메타정보 (Configuration metadata)

스프링의 설정정보는 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해서 사용하는 메타정보를 말합니다.

IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용됩니다.

이를 Blueprints(청사진)이라고도 합니다.


컨테이너 또는 IoC 컨테이너

애플리케이션 컨텍스트나 빈 팩토리를 말합니다.

이는 애플리케이션 컨텍스트보다는 추상적인 표현입니다.


스프링 프레임워크

스프링 프레임워크는 IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 떄 주로 사용합니다. 주로, 스프링이라고 주여서 말하기도 합니다.


싱글톤 레지스트리와 오브젝트 스코프

우리는 오브젝트 팩토리를 만든것과 애플리케이션 컨텍스트를 사용한것 2가지를 만들었습니다.

이 둘은 중요한 차이점이 존재합니다.

만약, DaoFactoryuserDao() 메서드를 2번 호출하면 2개의 오브젝트는 같을까? 라는것 입니다.

다음과 같은 코드로 오브젝트를 확인해봅시다.

// 오브젝트 팩토리 방식
DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

System.out.println(dao1);
System.out.println(dao2);

확인해보면 출력값이 서로 다른것을 확인할 수 있습니다.

이를 통해서 userDao() 를 매번 호출하면 계속해서 새로운 오브젝트가 생성된다는 것을 알 수 있습니다.

하지만 만약, ApplicationContext 를 사용한 것에서는 어떤지 확인해봅시다.

// ApplicationContext 사용
ApplicationContext = context = new AnnotationConfigApplication(DaoFactory.class)

UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);

System.out.println(dao3);
System.out.println(dao4);

만약 ApplicationContext 를 실행하면 출력값이 서로 같은 것을 볼 수 있습니다.

결론적으로 여러개의 오브젝트를 생성하려고해도 매번 동일한 오브젝트를 돌려준다는 것 입니다.


싱글톤 레지스트리로서의 애플리케이션 컨텍스트

애플리케이션 컨텍스트는 IoC 컨테이너로 역할을 하지만 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(Singleton Registry)입니다.

스프링에서는 별다른 설정을 하지 않는 이상 내부에서 생성하는 빈 오브텍트를 싱글톤으로 만듭니다.


서버 애플리케이션과 싱글톤

만약에 우리가 사용하는 UserDao 가 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트가 새로만들어진다고 가정한다면

우리는 계속해서 요청에 따라서 수만개의 오브젝트를 생성하고 지우고를 반복해야합니다.

이러면 서버에 많은 부하가 걸리게 됩니다. 그래서 우리는 싱글톤을 사용하게 됩니다.

싱글톤 패턴은 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것 입니다.

이러한 싱글톤 패턴은 장점만 있을것 같지만 안티패턴으로 피해야하는 패턴으로 불리기도 합니다.


싱글톤 패턴의 한계

싱글톤을 구현하기 위해서는 다음과 같은 방식으로 구현을 합니다.

  • 클래스 밖에서는 오브젝트를 생성하지 못하게 생성자를 private로 만듭니다.
  • 생성도니 싱글톤 오브젝트를 저장하기 위해서 자기와 같은 타입의 static 필드를 정의합니다.
  • 스태틱 팩토리의 메서드인 getInstance()를 만들고 메서드가 오출되는 시점에서 한번의 오브젝트가 만들어지게 합니다.
  • 한번의 오브젝트가 만들어지고 난 뒤는 getInstance()를 통해서 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨줍니다.

이렇게 만든 UserDao의 예제를 살펴봅시다.

public class UserDao{
    private static UserDao INSTANCE;

    public static synchronized UserDao getInstacne(){
        if(INSTANCE == null) INSTANCE = new UserDao(???);
        return INSTANCE;
    }

    private UserDao(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }
}

이런식으로 만들어진 싱글톤 패턴은 코드가 상당히 지저분해지고 private로 바귄 생성자는 외부에서 호출을 할 수 없기 때문에 DaoFactory 에서 UserDao 를 생성하며 ConnectionMaker 오브젝트를 넣어지는것이 더이상 안된다.

이러한 싱글톤 패턴구현 방식에는 다음과 같은 문제가 존재합니다.

  • private 생성자를 가져 상속할 수 없다.
    • private 를 사용해서 생성자를 제한한다면 객체지향의 장점인 상속과 이를 이용한 다형성을 사용할 수없다. static과 메서드를 사용하는 것도 객체지향의 장점을 사용할 수 없게 한다.
  • 싱글톤은 테스트하기가 힘들다
    • 싱글톤은은 만들어지는 방식이 제한적이여서 테스트에서 사용될 때 목 오브젝트 등으로 대체하기가 힘들어서 테스트하기가 힘듭니다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    • 서버에서 클래스 로더를 어떻게 구성하고 있냐에 따라서 싱글톤 클래스여도 하나이상의 오브젝트가 만들어질 수 있습니다.
  • 싱글톤 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
    • 싱글톤은 static 메서드를 이용해서 애플리케이션 어디서든지 사용될 수 있습니다. 이러한 방식은 객체지향 프로그래밍에서 권장되지 않는 프로그램 모델입니다.

싱글톤 레지스트리

자바의 기본적인 싱글톤 패턴 구현 방식은 여러가지 단점이 존재합니다.

그래서 스프링은 싱글톤 레지스트리(Singleton Registry)를 사용해서 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공합니다.

이러한 싱글톤 레지스트리는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너라고도 볼 수 있습니다.

기존에 사용했던 privatestatic을 사용해서 구현한 클래스가 아닌 기본 클래스에서 싱글톤으로 관리를 해줘서 단점을 보완합니다.

그래서 스프링이 지지하는 객체지향적인 설계 방식과 디자인 패턴등을 적용하는데 아무런 제약이 사라집니다.

이제 주의할점들을 살펴봅시다.


싱글톤 오브젝트와 상태

싱글톤은 멀티스레드 환경에서 여러 스레드가 동시에 접근해서 사용할 수 있습니다.

그렇기 때문에 더욱 상태 관리에 주의를 기울여야합니다.

싱글톤은 우선 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되면 상태정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야합니다.

이런것들을 파라미터나 로컬 변수, 리턴 값등을 이용해서 상태정보를 다룹니다.

//인스턴스 변수를 사용하도록한 UserDao
public class UserDao{
    private ConnetcionMaker connectionMaker;
    private Connection c;
    private User user;

    public User get(String id) throws ClassNotFoundException, SQLException{
            this.c = connectionMaker.makeConnection();

            /...
            this.user = new User();
            this.user.setId(rs.getString("id"));
            this.user.setName(rs.getString("name"));
            this.user.setPassword(rs.getString("password"));

            /...
            return this.user;
    }
}

이 코드는 기존가 다른점으로는 원래는 ConnectionUser는 로컬 변수로 각 메서드에서 선언해서 사용했는데 이를 클래스의 인스턴스 필드로 선언했다는 것 입니다.

이런식으로 로컬이 아닌 인스턴스 변수로 정의해서 사용한다면 심각한 문제가 발생합니다.

A가 접근을 할때 B도 동시에 접근을한다면 A의 정보를 추가해야하는데 B의 정보를 잘 못 추가할 수도 있기 때문입니다.

하지만 현재 기존코드에서도 connectionMaker는 인스턴스 변수를 사용을 했습니다.

하지만, 이것은 문제를 일은키지 않습니다. 왜냐하면 읽기전용 정보이기 때문입니다.

또한, 이 변수는 @Bean을 붙여서 싱글톤으로 관리되어 있습니다.

이런식으로 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도일 경우는 인스턴스 변수를 사용해도 무관합니다.

또한, 읽기전용 속성을 가진 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋습니다.

그렇지만 단순한 읽기전용이라면 static final 이나 final 을 사용하는 것이 더욱 효율적 입니다.


스프링 빈의 스코프

스프링이 관리하는 오브젝트인, 빈이 생성되고, 적용되는 범위는 10장에서 배우게 됩니다.

보통은 스프링 빈의 기본 스코프는 싱글톤입니다.

이러한 싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서 강제로 제거하지 않는 이상 스프링 컨테이너가 존재하는 동안 계속 유지됩니다.

+ Recent posts