booknu PS & Algorithms, Tech Blog

토비의 스프링 vol.1 1장 정리 (오브젝트와 의존관계)


이 문서는 토비의 스프링 3.1 세트 책에 소개되는 개념들을 정리한 문서입니다.

스프링이 추구하는 것

  • 단순함 : 자바의 잃어버린 객체지향 언어적 장점을 다시 살릴 수 있도록 도와주는 도구 (POJO 프로그래밍)
  • 유연성 : 뛰어난 확장성으로 다른 많은 프레임워크 등과 쉽게 결합하여 사용할 수 있도록 할 것

“항상 프레임워크 기반의 접근 방법을 사용하라” 라는 것은 OOP의 장점을 최대한 살린 것이 스프링이고, 이것을 기반으로한 접근 방법을 사용하면 스프링이 추구하던 아키텍처, 설계의 근간이 흔들릴 일 없이 확장되어 결국은 확장된 코드마저도 스프링과 같이 확장성이 뛰어나고 유지보수가 쉬운 코드가 되기 때문

DAO 리팩토링

DAO

Data Access Object : DB를 사용해 데이터를 조회, 조작하는 기능을 전달하는 객체

JavaBean

원래는 비주얼 툴에서 조작 가능한 컴포넌트, 그러나 현재에는 다음 2가지 관례를 따라 만들어진 객체를 지칭

  1. Default Constructor을 갖고 있음 (툴/프레임워크에서 Reflection을 통해 오브젝트를 생성하기 때문)
  2. Property: Setter, Getter을 이용해 수정/조회할 수 있음

Naive한 구현의 경우

DAO에서 다음과 같은 일들을 한 메소드 안에서 전부 시키면 어떤 문제점이 발생할까?

  1. DB Connection을 가져오고
  2. SQL을 담을 Statement를 만들고 정보를 바인딩시켜 실행하고
  3. 작업이 끝나면 사용한 리소스를 해제한다.

만약 DB 계정의 id/pw를 바꾸는 등 사소한 수정 사항이 생기는 경우에도 전체 코드의 여러 곳을 산발적으로 수정해야 하는 문제가 생긴다. 이를 해결하기 위해 관심사의 분리를 해야 한다.

관심사의 분리 (Sepration of Concerns)

관심이 같은 것끼리는 하나의 객체 혹은 친한 객체로 모이게 하고, 관심이 다른 것끼리는 가능한 떨어뜨려 영향을 끼치지 않게 만드는 것.

이 경우 Connection을 얻어오는 부분을 다른 메소드로 빼는 방식으로 리팩토링을 하면 위에서 말한 문제는 해결된다.

메소드 추출 (Extract Method)

공통 기능을 그것을 담당하는 메소드로 중복된 코드를 뽑아내는 것

DB Connection의 분리

위와 같이 메소드 추출을 하면, 어느 정도의 문제는 해결된다. 하지만 사용하는 DB 자체를 바꿀 경우에는 어떻게 할 것인가?

물론 getConnection() 메소드를 조금만 수정하면 되는 일이지만, UserDao 자체는 클래스 바이너리 파일로 사용하고 싶으면 소스 자체를 고칠 수는 없는 일이다.

이 경우 getConnection()을 abstract method로 수정하고, 그 구현부를 상속을 받아 구현하는 식으로 리팩토링 할 수 있다.

템플릿 메소드 패턴 (Template Method Pattern)

슈퍼클래스에서 기능의 일부를 abstract method 혹은 overriding 가능한 protected method로 만든 뒤 서브클래스에서 필요에 따라 메소드를 구현하도록 하는 패턴

  • abstract method : 서브 클래스에서 반드시 구현해야 하는 기능
  • hook method : 서브 클래스에서 overriding을 통해 확장을 할 수 있는 기능

이 때 DaogetConnection() 메소드는 어떤 Connection 클래스를 생성할지 결정하는 방법이라고도 볼 수 있고, 이를 팩토리 메소드 패턴이라고 한다.

팩토리 메소드 패턴 (Factory Method Pattern)

템플릿 메소드 패턴과 원리는 같으나, 구체적인 오브젝트 생성 방법에 대해 결정하게 하는 것. 주로 Interface 타입 오브젝트를 반환하므로 서브 클래스는 다양한 방법으로 오브젝트를 생성하는 메소드를 작성 할 수 있다. 이 때 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드를 팩토리 메소드라고 한다.

자바에서는 어떤 오브젝트를 생성하는 메소드를 일반적으로 팩토리 메소드라고 하는데, 이것을 팩토리 메소드 패턴의 팩토리 메소드와 헷갈리면 안 된다.

  1. 데이터 액세스 로직을 어떻게 만들 것인가
  2. DB 연결을 어떻게 할 것인가

두 가지 관심을 상하위 클래스로 분리시키는데는 성공을 했지만, 여전히 문제가 남아있다.

클래스의 분리

Dao 객체 자체에 팩토리 메소드를 만들어버리면 다른 목적으로 Dao를 상속시키지 못한다는 단점이 있다. 또한 상속을 통한 상하위 클래스 관계는 긴밀하게 결합된다 것도 문제이다. 분명 Connection만을 가지고 오고 싶었을 뿐인데, 불필요한 결합이 생겨버리는 것이다. 마지막으로 이 Dao 객체 뿐 아니라 다른 Dao 객체도 getConnection()이 필요한데, 이 때문에 또 중복되는 코드가 생긴다.

이를 해결하는 간단한 방법이 있다. ConnectionMaker와 같이 아예 DB Connection을 얻어오는 전용 객체를 따로 만드는 것이다. 이렇게 하면 다중 상속 문제와 코드 재사용 문제가 동시에 해결된다.

단, ConnectionMaker를 구현할 때 getConnection() 메소드에 직접 구현을 때려박는다면 Dao 객체에 직접 구현했을 때와 마찬가지의 문제점이 생긴다. 따라서 당연히 ConnectionMaker는 인터페이스로 선언돼야하고, 직접적인 구현부는 그것의 서브클래스에 맡겨야한다. 즉, Dao에는 인터페이스로서 ConnectionMaker가 추상화되어 변수로 선언되어 있어 그 구현부가 바뀌어도 신경 쓸 일이 없어진다.

관계설정 책임의 분리

하지만, 선언은 인터페이스인 ConnectionMaker로 하더라도, Dao 안에서 인터페이스를 구현한 객체를 직접 생성한다면 여전히 문제는 존재한다! 즉, Dao 안에 new MyConnectionMaker() 라는 문장이 있는 한 여전히 구현부에 따라 Dao도 바뀌어야 할 것이다.

여태까지 Connection에 대한 관심을 다른 클래스로 모두 뺀 것처럼 보이지만, 여전히 Dao 안에는 Connection에 대한 관심이 존재하기 때문에 이런 현상이 발생한다. new MyConnectionMaker() 자체는 짧기만, 그 자체에 MyConnectionMaker을 사용하여 연결한다는 관심이 함축되어 있는 것이다. 이것은 즉, DaoConnectionMaker로는 MyConnectionMaker을 쓴다는 관계 설정에 대한 관심이다.

이런 관계 설정은 실제로 Dao를 사용하는 Client Object에서 하도록 분리하는 것이 가장 좋다. connectionMaker = new MyConnectionMaker(); 이라는 코드는 MyConnectionMaker 객체의 레퍼런스를 connectionMaker이라는 변수에 넣어 사용하게 함으로써 이 두 개의 객체가 “사용”이라는 관계를 맺도록 하는 것이다!

요약하자면, 객체의 Dynamic Type을 Dao를 실제로 사용하는 코드(클라이언트)에서 정하여 객체를 생성하고, 그것을 Dao에게 파라미터 등의 수단으로 전달하여 관계를 연결해주면 된다. (다형성 = Polymorphism)

클래스 사이에 만들어지는 관계 : 클래스 코드에 다른 클래스 이름이 나타나기 때문에 맺어지는 관계

객체 사이에 만들어지는 관계 : Runtime에서 객체의 Static Type과 Dynamic Type 사이에 맺어지는 관계

이로써 Dao 객체는 본래의 관심사인 데이터 액세스를 위한 SQL 생성, 실행에 집중할 수 있게 됐고 Connection을 설정하는 부분은 ConnectionMaker에, ConnectionMaker와 관계 설정을 하는 부분은 클라이언트에 떠넘길 수 있게 됐다.

요약

요약하자면 다음과 같은 과정을 통해 리팩토링을 하였다.

  1. Naive하게 모든 구현을 Dao 메소드에 때려박는다.
  2. 공통적으로 등장하는 코드를 메소드 추출을 통해 리팩토링한다.
  3. Dao 자체에 팩토리 메소드 패턴을 적용하여 추상화를 통한 확장성을 높인다.
  4. Dao의 다른 목적을 위한 상속 문제 해결을 위해 팩토리 메소드를 다른 새로운 클래스에게 구현하도록 한다.
  5. Dao의 Static Type에 Dynamic Type을 관계시키는 책임을 클라이언트에게 전가한다.

원칙과 패턴

개방 폐쇄 원칙

OCP(Open-Closed Principle)

클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

그 예로, 인터페이스를 사용하면 확장을 위해서는 개방되어 있지만, 인터페이스를 이용하는 클래스는 확장으로 인한 변화는 불필요하게 일어나지 않도록 닫혀있다.

객체지향 설계 원칙 (SOLID)

  • SRP(Single Responsibility Principle) : 단일 책임 원칙
  • OCP(Open Closed Principle) : 개방 폐쇄 원칙
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존관계 역전 원칙

높은 응집도와 낮은 결합도

높은 응집도

변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것. 이렇게 되면 바뀌지 않는 부분들에 대해 신경 쓸 필요가 없다.

즉, 하나의 모듈이 하나의 관심사에만 집중되어 있다는 것

낮은 결합도

관심사가 다른 모듈끼리는 느슨한 연결을 유지하는 것. 모듈끼리의 독립성을 높인다는 것이고, 하나의 모듈에 대해 신경 쓸 때는 다른 모듈에 대해 최소한의 정보만 알아도 된다는 것이다.

이를 통해 변화에 대응하기 쉬워지고, 구성이 깔끔해진다.

즉, 결합도란 하나의 모듈의 변화에 대해 관계를 맺는 다른 모듈에게 변화를 요구하는 정도

전략 패턴

자신의 기능(맥락 = Context)에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스화 하여 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 패턴.

알고리즘

독립적인 책임으로 분리가 가능한 기능

그 예로, Dao에서 getConnection() 알고리즘을 통째로 분리시켜 ConnectionMaker라는 인터페이스로 정의하고, 이를 구현한 클래스에 따라 전략을 바꿔가면서 사용할 수 있게 분리하였다.

제어의 역전 (IoC)

Ioc(Inversion of Control)

지금까지 리팩토리한 Dao도 괜찮지만, 아직 개선 사항이 남았다. Dao를 사용하는 클라이언트에서는 본래 관심사가 아닌 일을 두 가지나 하고 있다.

  1. ConnectionMaker 객체의 생성
  2. 생성된 객체와 Dao의 객체를 연결 (관계 주입)

따라서 이 두 가지 기능 역시 따로 분리되어야 한다.

팩토리

위의 두 기능을 DaoFactory 라는 객체를 새로 만들어서 맡기도록 하자. userDao() 라는 함수에서 이런 기능을 맡게 될 것이며, 결과적으로 생성된 Dao를 반환하게 될 것이다.

팩토리

객체를 생성하여 반환하는 클래스. 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 분리하기 위해서 도입 된 개념.

클라이언트는 팩토리 객체를 통해 Dao를 생성할 것이며, 두 가지 기능이 분리되었다.

이로써 DaoConnectionMaker은 각각 핵심적인 데이터로직, 기술 로직을 담당하고 있고, DaoFactory는 객체들을 구성하고 관계를 정의하는 책임을 담당하게 되었다.

즉, 전자는 실질적인 로직을 담당하는 컴포넌트, 후자는 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할이라고 볼 수 있다.

오브젝트 팩토리의 활용

DaoFactory 에서 여러 Dao를 생성한다면 어떻게 될까?

각 팩토리 메소드마다 ConnectionMaker을 생성하는 코드가 중복되서 나타난다. 코드 중복이 발생하면 이 코드를 수정할 때 의미없이 반복적으로 같은 코드를 수정해야 하기 때문에 이 부분 역시 DaoFactory 안의 새로운 메소드로 빼내는 것이 좋다.

제어관계 역전

일반적인 프로그램에서는?

main() 에서 시작해 여기에서 사용할 객체를 결정하여 생성하고, 해당 객체를 사용하는 것이 재귀적으로 반복된다.

제어의 역전이란?

  1. 객체가 사용할 객체를 스스로 선택하거나 생성하지 않는다.
  2. 객체 자신도 어떻게 만들어지고 어디서 사용되는지 알 수 없다.

이것이 가능한 이유는, 모든 제어의 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다.

예시

  • 서블릿의 경우 서블릿의 코드를 직접 실행할 수 있는 수단은 없지만, 그 제어 권한을 가진 컨테이너가 서블릿 객체를 만들고 사용하게 된다.

  • getConnection() 의 경우도 제어권을 상위 템플릿 메소드에 넘기고 그 자신은 필요할 때 호출되어 사용되는 구조를 가진다.

  • 프레임워크도 작성한 애플리케이션 코드가 상위 개념인 프레임워크에 의해 사용되는 구조이다.

    라이브러리 vs 프레임워크

    라이브러리 : 애플리케이션 코드가 직접 흐름을 제어하고, 라이브러리는 단순히 사용될 뿐이다.

    프레임워크 : 애플리케이션 코드는 프레임워크에 의해 사용되기만 할 뿐, 흐름의 제어는 프레임워크가 한다.

이런 관점에서, DaoFactory 역시 하나의 IoC 프레임워크라고 볼 수도 있다.

스프링의 IoC

빈 (Bean)

Spring Container가 제어권을 가지고 직접 생성하고 관계를 부여하는 대상이 되는 객체

SpringBean은 JavaBean과 비슷하게 객체 단위의 애플리케이션 컴포넌트이다. 동시에 Spring Container가 생성/관계설정/사용 등을 제어해주는 IoC 개념이 적용된 객체이기도 하다.

빈 팩토리 (Bean Factory)

Bean의 생성/관계설정 등의 제어를 담당하는 IoC 객체

빈을 등록/생성/조회/반환/관리 하는 기능을 담당하며, 보통은 빈 팩토리를 확장한 애플리케이션 컨텍스트를 사용한다.

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

Bean Factory를 상속하여 확장한 것으로 IoC 방식을 따라 만들어진 일종의 Bean Factory. Bean Factory와 동일함.

단, 코드 내에 제어에 관한 정보가 들어있는 것이 아니라 별도의 설정 정보를 담은 무언가를 가져와 활용하는 IoC 엔진.

즉, 스프링이 제공하는 각종 부가 서비스를 추가로 제공하는 것이다.

빈 팩토리 vs 애플리케이션 컨텍스트

기본적으로 둘은 동일한 개념을 가리키는 말이지만, 중점을 두는 부분이 다르다.

빈 팩토리 : 빈을 제어하는 IoC 기본 기능에 초점이 맞춰진 것.

애플리케이션 컨텍스트 : 애플리케이션 전반에 걸쳐 모든 구성요소의 제어를 담당하는 IoC 엔진이라는 의미가 부각된 것.

설정 정보 (Configuration, Metadata)

애플리케이션 컨텍스트가 IoC를 적용하기 위해 사용하는 메타정보로, 컨테이너의 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만 보통 IoC 컨테이너에 의해 관리되는 객체를 생성하고 구성할 때 사용된다.

컨테이너 / IoC 컨테이너

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

IoC 컨테이너의 경우 주로 빈 팩토리 관점에서 이야기할 때 쓰이며, 그냥 컨테이너/스프링 컨테이너라고 할 때는 애플리케이션 컨텍스트를 추상적으로 가리킬 때 사용한다.

애플리케이션 컨텍스트가 이름이 길기 때문에 스프링 컨테이너라고 부르기도 하며, 스프링이라고 부르기도 한다. 이 때는 “스프링에 빈을 등록하고” 라는 식으로 말하기도 한다.

스프링 프레임워크

IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 기능을 통틀어 말할 때 사용한다.

애플리케이션 컨텍스트 예제

설정 정보 생성

  • @Configuration : 이 클래스는 애플리케이션 컨텍스트가 사용할 설정 정보라는 것을 알림
  • @Bean : 오브젝트 생성을 담당하는 IoC용 메소드라는 표시
@Configuration
public class DaoFactory {
	@Bean
	public UserDao userDao() {
		UserDao dao = new UserDao(connectionMaker());
		return dao;
	}

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

어노테이션은 자바 코드인 것 같지만, 사실은 XML 같은 스프링 전용 설정 정보이다.

애플리케이션 컨텍스트 생성

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

AnnotationConfigApplicationContextApplicationContext를 구현한 클래스 중 하나로, @Configuration이 붙은 자바 코드를 설정 정보로 사용하기 위해 쓰인다.

getBean()ApplicationContext가 관리하는 오브젝트를 요청하는 메소드이다.

getBean()의 반환 타입은 Object이므로 형변환이 필요하지만, Java5 이상부터는 제네릭 메소드 방식을 통해 리턴 타입을 제공 받는 식으로 더 깔끔하게 구현할 수 있다.

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

왜 오브젝트 팩토리를 사용하지 않는가?

직접 구현한 오브젝트 팩토리는 제한적으로 객체 생성/관계설정을 하지만, 애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해 관리할 모든 객체에 대한 생성/관계 설정을 한다는 차이가 있다.

또한 애플리케이션 컨텍스트는 객체를 직접 생성하고 할당하는 코드가 없는 대신에 그것에 대한 정보를 별도의 설정 정보를 통해 얻는다. (떄로는 외부의 오브젝트 팩토리에 그 작업을 위임하고 결과를 사용하기도 한다.)

애플리케이션 컨텍스트 = 오브젝트 팩토리 = IoC 컨테이너 = 스프링 컨테이너 = 빈 팩토리

정확히는 ApplicationContextBeanFactory 인터페이스를 상속한다.

동작 방식

  1. @Configuration 등이 붙은 클래스를 설정 정보로 등록해두고
  2. @Bean이 붙은 메소드들의 이름을 가져와 이름과 메소드를 매핑해둔 Bean List를 생성해둔다.
  3. getBean()이 호출되면 Bean List에서 해당되는 이름의 메소드를 찾아 호출하고 그 결과를 클라이언트에게 반환한다.

장점

  • 클라이언트가 필요할 때마다 팩토리 클래스를 구현할 필요가 없다.

    직접 코드를 작성하는 대신, 어노테이션, XML 등의 간단한 방법으로 IoC 설정을 할 수 있다.

  • 종합 IoC 서비스를 제공한다.

    생성/관계 설정 뿐 아니라 생성 방식/시점/전략 등을 설정할 수 있고 자동생성/후처리 등 다양한 기능을 제공한다. 또한 빈이 사용할 수 있는 기반 기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 지원해주기도 한다.

  • 빈을 검색하는 다양한 방법을 제공한다.

    메소드 이름 뿐만이 아니라 타입, 특수 어노테이션 등으로 설정된 빈도 찾을 수 있다.

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

직접 구현한 DaoFactory에서 객체를 여러번 생성하는 것과 ApplicationContext를 통해서 객체를 여러번 생성하는 것에는 무슨 차이가 있을까?

당연하게도 DaoFactory를 사용하는 경우는 그 안쪽의 구현으로 인해 새로운 객체가 계속 생성되어 나온다. 하지만, ApplicationContext를 사용하는 경우는 놀랍게도 같은 객체가 반환된다!

동일성 (Identity) : 객체의 주소가 같은지, 즉 물리적으로 같은 객체인지?

동등성 (Equality) : 객체의 주소는 다르지만 동등의 판단 기준에 따라 값이 같은지?

싱글턴

위와 같은 현상이 발생하는 이유는, ApplicationContext가 싱글턴을 저장하고 관리하는 싱글턴 레지스트리이기 때문이다.

별다른 설정을 하지 않으면, 내부에서 생성하는 빈 객체를 모두 싱글턴으로 만든다.

디자인 패턴에서의 싱글턴과 개념은 비슷하지만, 구현 방식은 완전히 다르다.

이와 같이 처리되는 이유는 스프링이 서버 환경을 고려하여 설계되었기 때문이다. 서버에서는 TPS라는 용어가 있을 정도로 초당 수많은 요청이 들어오는데, 이 요청을 처리할 때마다 객체를 생성하는 것은 굉장한 낭비일 수 있다. 그래서 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 일찍부터 사용해왔고, 자바에서는 서블릿이 기본이고, 이 역시 싱글턴으로 존재한다.

싱글턴 패턴 (Singleton Pattern)

프로그램에서 객체가 단 하나만 존재하도록 강제하는 패턴. 주로 여러 애플리케이션에서 공유하는 경우에 사용된다.

단, 싱글턴 패턴은 여러 문제점을 발생시킬 수 있기 때문에 굉장히 조심해서 사용해야 한다.

싱글턴 레지스트리

일반적인 싱글턴 패턴을 Dao 객체에 적용했을 때 다음과 같은 문제점이 발생한다.

  • private 생성자 때문에 DaoFactory에서 Dao를 생성하여 반환할 수 없다.
  • private 생성자 때문에 상속을 시킬 수 없다.
  • 생성자를 통해 객체에 동적으로 정보를 주입할 수 없기 때문에 테스트하기 힘들다.
  • 전역 변수와 같은 문제점을 발생시킬 수 있다. 특히 OOP에서는 더욱 문제가 된다.

서버 환경에서 싱글턴을 쓰고는 싶은데 싱글턴 패턴과 private 생성자를 이용한 비정상적인 방법은 쓰기 힘들기 때문에 스프링에서는 싱글턴 레지스트리를 지원한다.

싱글턴 레지스트리 (Singleton Registry)

스프링이 직접 싱글턴 객체를 만들고 관리하는 기능을 제공하는 것.

이것은 평범한 public 생성자를 가진 자바 클래스를 싱글턴으로 활용할 수 있게 만들어준다. 그렇게 할 수 있는 이유가 클래스의 제어권을 IoC 방식의 컨테이너에게 넘기면 해당 컨테이너가 객체 생성에 대한 모든 권한을 가지고 있기 때문에 객체가 싱글턴으로 존재할 수 있게 관리를 할 수 있기 때문이다.

즉, IoC 컨테이너의 제어 역할을 통해서 빈을 싱글톤으로 만드는 것이다.

싱글턴과 오브젝트 상태

서버 환경에서는 여러 요청이 멀티스레딩으로 처리되므로 싱글턴 객체의 상태를 다룰 때 주의해야한다. 서비스 형태의 오브젝트로 사용되는 경우 내부에 변하는 상태를 갖지 않는 Stateless로 만들어져야 한다. 이 때 요청에 따라 달라지는 값에 대한 정보는 파라미터, 지역 변수, 반환 값 등을 이용하면 된다.

스프링 빈의 스코프

  • Default Scope : 싱글턴

    강제로 제거하지 않으면 스프링 컨테이너가 존재하는 동안 계속 유지됨

  • Prototype Scope : 빈을 요청할 때마다 새로 생성

  • Request Scope : HTTP 요청이 생길 때마다 생성

  • Session Scope : 웹의 세션과 스코프가 유사

  • …..

의존관계 주입 (DI)

IoC라는 용어는 매우 느슨하게 정의되어 폭 넓게 사용된다. 따라서 스프링이 제공하는 IoC 방식의 중점을 짚어주기 위해서 의존관계 주입이라는 용어가 등장했다.

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

오브젝트 레퍼런스를 외부로부터 주입 받고, 이를 통해 여타 오브젝트와 동적으로 의존관계가 만들어지는 것.

런타임 의존관계 설정

의존관계란 방향성이 존재하고, $A\rightarrow B$라는 의존관계가 있다면 $B$가 변하면 $A$도 영향을 받는다는 것을 뜻한다.

DaoConnectionMaker 또한 이러한 관계이다.

이 때 ConnectionMaker가 인터페이스가 아닌 일반 클래스라면 강하게 의존하고 있다는 뜻이 되고, 코드가 조금이라도 변한다면 Dao에서도 신경을 써야할 것이다.

그러나 아래 그림과 같이 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와는 관계가 느슨해지며 변화를 덜 받는 상태가 된다.

하지만 이와 같이 UML에서 말하는 의존관계는 설계 모델의 관점에서 보는 것이고, 이를 통해 드러나지 않는 의존관계가 존재한다.

런타임 시에 만들어지는 의존관계이다.

예를 들어 Dao에서는 코드 작성 시점에 어떤 ConnectionMaker와 관계를 맺을 지 알 수 없다. 그것은 클라이언트 측의 선택에 따라서 달라지기 때문이다.

의존관계 주입이란 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체(일반적으로 클라이언트) 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

의존 오브젝트 (Dependent Object)

런타임 시에 의존관계를 맺는 대상

즉, 의존관계 주입은 다음 세 가지 조건을 충족하는 작업이다.

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

2번의 좋은 예시로 Dao에서 직접 MyConnectionMaker를 생성하던 것을 제 3자인 클라이언트나 팩토리, 애플리케이션 컨텍스트에게 맡긴 것을 생각하면 된다. 이러한 제 3자들은 DI 컨테이너라고 부를 수 있다.

DI 컨테이너

두 객체 사이의 런타임 의존관계를 설정해주는 의존 관계 주입 작업을 주도하는 존재.

오브젝트 생성/초기화/제공 등의 작업을 수행함.

이처럼 DI는 자신이 사용할 객체에 대한 선택, 생성 제어권을 외부로 넘기고, 수동적으로 주입 받은 객체만을 사용한다는 점에서 IoC의 개념과 잘 들어맞는다.

의존관계 검색과 주입

의존관계 검색 (Dependency Lookup = DL)

객체 스스로 검색하여 의존관계를 맺는 것.

의존관계를 맺을 대상 객체를 결정하고 생성하는 작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

예를 들어 다음과 같은 코드는 의존관계 검색에 해당된다. 적어도 의존관계를 가져올 때는 스스로 요청하는 것이다.

public UserDao() {
  DaoFactory df = new DaoFactory();
	this.connectionMaker = df.connectionMaker();
}

DaoFactory 대신 ApplicationContext가 쓰였다면 미리 정해놓은 이름을 전달해서 해당되는 오브젝트를 찾게 될 것이다.

public UserDao() {
  AnnotationConfigApplicationContext context =
    new AnnotationConfigApplicationContext(DaoFactory.class);
  this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

보통의 경우 일반적인 의존관계 주입을 사용하는 것이 단순하고 깔끔하다. 의존관계 검색의 경우 객체 코드 안에 팩토리 클래스나 Spring API가 나타나는데, 이는 해야할 일에 집중해야하는 객체에게 있어 바람직하지 않은 구현 방식이다.

그러나 언젠가는 한 번 의존관계 검색 방식이 등장해야하기는 한다. 스태틱 메소드인 main()에서는 DI를 통해 객체를 주입 받을 방법이 없기 때문이다.

서버에는 main()메소드는 없지만, 이와 비슷하게 요청을 받을 때마다 비슷한 역할을 하는 서블릿에서 한 번쯤 DL 해야하나, 서블릿은 스프링이 미리 만들어 제공하기 때문에 직접 구현할 필요는 없다.

또 한 가지 특징으로는 DL에서는 검색을 하는 주체 객체는 꼭 스프링 빈일 필요가 없다는 것이다. DI를 받기 위해 스프링 빈이 되어야 했지만, 이제는 아무 관계도 주입받지 않고 스스로 관계를 맺기 때문이다.

반면 DI에서는 주입 받는 객체가 반드시 스프링 빈 객체여야한다.

의존관계 주입의 응용

기능 구현의 교환

일반적으로 개발을 할 때 개발용 DB와 운영용 DB가 분리되어 사용된다. 그런데 DI를 하지 않는다면 배포할 때 DB를 사용하는 코드 모든 부분을 고려해야하는 의미없는 수고가 필요하고, 배포 후 개발용 코드로 돌릴 때도 마찬가지이다.

하지만 DI를 이용한다면 ConnectionMaker을 상속한 클래스만 하나 따로 만들어주고 DI를 하는 부분에서 사용하는 Dynamic Type만 살짝 바꿔주면 해결이 된다.

부가기능 추가

DB 연결 횟수를 카운팅하는 기능을 추가하고 싶을 때는 어떻게 하면 될까? 모든 DAO의 getConnection() 메소드에 카운팅 기능을 추가하려면 엄청난 노력이 필요할 것이다.

그러나 DI를 이용하면 아주 간단해진다. DaoConnectionMaker 사이에 CountingConnectionMaker을 하나 더 추가하면 된다. 물론 이 때, CountingConnectionMakergetConnection 부분을 직접 구현하게 하면 안 된다. 이 또한 ConnectionMaker을 주입 받도록 만들어 CountingConnectionMaker가 본업인 연결 횟수를 카운팅하는데만 집중할 수 있도록 만들어줘야 한다.

public class CountingConnectionMaker implements ConnectionMaker {
  int cnt = 0;
  private ConnectionMaker realCM;
  public CountingConnectionMaker(ConnectionMaker realCM) {
    this.realCM = realCM; // CM을 DI받게 만들어 카운팅에만 집중한다!
  }
  public Connection getConnection() throws ClassNotFoundException, SQLException {
    ++this.cnt; // 여기에만 집중
    return realCM.getConnection(); // 실제 연결을 만드는건 주입 받은 객체가
  }
}

결과적으로 다음과 같은 의존관계가 생성된다.

Dao를 생성하는 부분도 DI를 주입하는 부분만 바꾸면 된다.

@Configuration
public class CountingDaoFactory {
  @Bean
  public UserDao userDao() {
    return new UserDao(connectionMaker()); // DAO 설정 부분은 바뀌지 않았다
  }
  @Bean
  public ConnectionMaker connectionMaker() {
    return new CountingConnectionMaker(realConnectionMaker()); // DI 받는 부분만 바뀌었다
  }
  @Bean
  public ConnectionMaker realConnectionMaker() {
    return new MyConnectionMaker();
  }
}

의존관계 주입을 하는 방법들

  • 생성자

  • Setter 메소드 (수정자)

  • 일반 메소드

    Setter 메소드의 경우 이름이 set으로 시작해야하고 한 개의 파라미터 밖에 가지지 못한다는 제약이 있다.

    반면 일반 메소드를 사용하면 여러개의 파라미터를 한꺼번에 받을 수 있고 Overloading을 통해 다양하게 파라미터를 받아올 수 있다는 장점이 있다.

XML을 이용한 설정

XML을 통해서도 DI를 구성할 수 있다. 어노테이션을 사용하는 경우 DI 정보를 수정하는 경우 재빌드를 해야하지만, XML을 이용하면 그럴 필요가 없다.

XML 설정

<beans>
	<bean id="myConnectionMaker" class="<pakage>.MyConnectionMaker" />
  <bean id="yourConnectionMaker" class="<pakage>.YourConnectionMaker" />
  <bean id="productionConnectionMaker" class="<pakage>.ProductionConnectionMaker" />
  <bean id="userDao" class="<pakage>.UserDao">
  	<property name="connectionMaker" ref="myConnectionMaker" />
  </bean>
</beans>
  • <beans> : @Configuration 역할 / XML의 루트 엘리먼트 / 안에 여러 개의 <bean>을 정의할 수 있다.
  • <bean> : @Bean 역할
  Java Code XML
빈 설정파일 @Configuration <beans>
빈 이름 @Bean methodName() <bean id="methodName"
빈 클래스 return new BeanClass() class="package.BeanClass">
Setter DI dao.setValue(value) <property name="value" ref="value" />

DTD, 스키마

XML 문서는 미리 정해진 구조에 따라 작성됐는지 검사할 수 있는데, 이러한 정의 방법에는 DTD와 스키마가 있다.

DTD (Document Type Definition)

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
	"http://www.springframework.org/dtd/spring-beans-2.0.dtd">

하지만 스프링에서 특별한 목적을 위해 별도의 태그를 사용할 수 있는 방법을 제공하는데, 이런 대그들은 별개의 스키마 파일에 정의되어 있고 독립적인 네임스페이스를 사용해야 한다. 따라서 이런 태그들을 사용하려면 DTD 대신 네임스페이스 기능이 있는 스키마를 사용해야 하고, 이런 이유로 대부분의 경우 스키마를 쓰는 것이 바람직하다.

스키마 (Schema)

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"></beans>

<beans>태그를 기본 네임스페이스로 하는 스키마 선언이다.

XML을 이용한 애플리케이션 컨텍스트

DaoFactory 대신 XML 설정정보를 활용하려면 GenericXmlApplicationContext를 사용한다.

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
  <bean id="connectionMaker" class="package.MyConnectionMaker" />
  <bean id="userDao" class="springbook.user.dao.UserDao">
  	<property name="connectionMaker" ref="connectionMaker" />
  </bean>
</beans>

GenericXmlApplicationContext

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

ClassPathXmlApplicationContext

ApplicationContext context = new ClassPathXmlApplicationContext("daoContext.xml", UserDao.class)

경로 정보 기준을 UserDao로 설정할 수 있어 좀 더 편하게 xml 파일 위치를 적을 수 있다.

DataSource 인터페이스로 변환

Java에는 우리가 직접 구현한 ConnectionMaker과 같이 DB 커넥션을 가져오는 기능을 추상화 한 DataSource 인터페이스가 있다.

그러나 DataSource에는 getConnection() 외에도 많은 메소드가 있기 때문에 직접 구현하기에는 부담이 있는데, 다행히도 이미 구현되어 있는 클래스가 있으니 그걸 쓰면 된다.

@Setter
public class Dao {
  private DataSource dataSource;
  public void add(User user) throws SQLException {
    Connection con = dataSource.getConnection();
    // ...
  }
}

DataSource를 DI 받았을 때는 위와 같이 ConnectionMaker을 사용하듯이 쓰면 된다.

@Bean
public DataSource dataSource() {
  SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
  dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
  dataSource.setUrl("jdbc:mysql://localhost/spring?serverTimezone=UTC");
  dataSource.setUsername("user name");
  dataSource.setPassword("password");
  return dataSource;
}
@Bean
public Dao dao() {
  Dao dao = new Dao();
  dao.setDataSource(dataSource());
  return dao
}

DaoFactory의 경우 테스트 환경에서 간단하게 사용할 수 있는 SimpleDriverDataSource를 사용해서 구현하였다.

<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />

새로 생긴 dataSource() 빈도 XML에 추가해줘야한다.

프로퍼티 값 주입

위와 같이 했을 때 XML에서 dataSource()에서 사용한 SimpleDriverDataSource의 Setter로 넣어준 접속 정보는 들어있지 않다는 문제점이 생긴다.

UserDao와 같이 다른 빈에 의존하는 경우 <property> 태그와 ref 속성으로 의존할 빈 이름을 넣어주면 되지만, SimpleDriverDataSource의 경우 Setter가 일반 클래스나 스트링 값이 파라미터로 들어오는데 어떻게 해야할까?

다행히도 Setter에는 빈 뿐만 아니라 객체, 스트링 등의 값도 넣어줄 수 있다. 즉, 다른 빈 객체의 레퍼런스가 아니어도 Setter로 데이터를 줄 수 있다는 소리이다. 하지만 이 때는 DI처럼 Dynamic Type을 자유자재로 바꾸려는 목적이 아닌 단순한 값을 주입하려는 목적이다. 이것도 일종의 DI라고 볼 수 있는데, Dynamic하게 정보가 바뀌지 않지만, 객체의 특성을 외부에서 변경할 수 있기 때문이다.

<property name="driverClass" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/spring?serverTimezone=UTC" />
<property name="username" value="user name" />
<property name="password" value="password" />

이와 같이 하드코딩 되어있던 설정 정보를 XML을 통해 주입해줄 수 있게 됐다.

즉, DataFactory를 사용하는 것이 아닌 XML을 사용해서 DI를 할 수 있는 것이다.

이 때 스프링이 프로퍼티 값을 Setter 메소드의 파라미터 타입으로 자동 형변환 해주기 때문에 "com.mysql.cj.jdbc.Driver"와 같이 객체를 String으로 줘도 되는 것이다.

실제 내부에서는 다음과 같이 코드가 변환된다.

Class driverClass = Class.forName("com.mysql.cj.jdbc.Driver");
dataSource.setDriverClass(driverClass);

이 떄 형변환은 Class, URL, File, Charset, List, Map, Set, Properties 등등으로도 가능하다.


Comments

Content