- DAO(Data Access Object) : DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 말한다.
- 자바 빈(JavaBean) : 두 가지 관례에 따라 만들어진 오브젝트를 가리킨다. 간단히 빈이라고 부르기도 한다.
○ 디폴트 생성자 : 자바 빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다.
○ 프로퍼티 : 자바 빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 수정자 메서드(setter)와 접근자 메서드(getter)를 이용해 수정 또는 조회할 수 있다.
DAO에서 JDBC를 이용하는 작업의 순서는 다음과 같다.
1. DB 연결을 위한 Connection을 가져온다.
2. SQL을 담을 Statement( 또는 Prestatement)를 만든다.
3. 만들어진 Statement를 실행한다.
3-1. 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트(예를 들어 VO)에 옮겨준다.
4. 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
5. JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메서드에 throws를 선언해서 예외가 발생하면 메서드 밖으로 던지게 한다.
public void add(UserVO user) throws ClassNotFoundException, SQLException{
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:XE", "","");
PreparedStatement pstmt = c.prepareStatement("insert into users(id, name,password) values(?,?,?)");
pstmt.setString(1, user.getId());
pstmt.setString(2, user.getName());
pstmt.setString(3, user.getPassword());
pstmt.executeUpdate();
pstmt.close();
c.close();
}
public UserVO get(String id) throws ClassNotFoundException, SQLException{
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:XE", "","");
PreparedStatement pstmt = c.prepareStatement("select * from users where id = ?");
pstmt.setString(1, id);
ResultSet rs = pstmt.executeQuery();
rs.next();
UserVO user = new UserVO();
user.setId(rs.getString("id"));
user.setName(rs.getNString("name"));
user.setPassword(rs.getNString("password"));
rs.close();
pstmt.close();
c.close();
return user;
}
DAO를 위 코드 블록처럼 작성을 했을 때, 실제 프로젝트에서 보는 사람을 당황하게 만들만한 코드이다. 그렇다면 이 코드를 어떻게 개선할 수 있을지 또는 개선함으로써 얻는 이득이 무엇인지 알아보고자 한다.
■ DAO의 분리
- 관심사의 분리
: 모든 것을 한데 모으면 처음에는 쉽고 편하지만 언젠가는 뭉쳐 있는 관심사를 적절하게 분리하는 작업을 해야 하는 날이 온다. 관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 효과적으로 집중할 수 있게 만들어준다.
- 커넥션 만들기의 추출
: DAO를 구현한 코드를 살펴보면 메서드 하나에서 적어도 3가지의 관심사항을 발견할 수 있다.
1. DB와 연결을 위한 커넥션을 어떻게 가져올까?
2. DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것.
3. 작업이 끝나면 사용한 리소스를 닫아줘서 시스템으로 돌려주는 것.
가장 먼저 할 일은 커넥션을 가져오는 중복된 코드를 분리하는 것이다.
이렇게 분리해준 것만으로 각 메서드에서 Connection c = getConnection() 만으로도 커넥션을 가져올 수 있다.
DAO 클래스에 메서드가 많아질수록 커넥션을 분리한 것이 빛을 보게 될 것이다. 이러한 작업을 리팩토링이라고 하며
중복된 코드를 뽑아내는 것을 리팩토링에서는 메서드 추출 기법이라고 부른다.
리팩토링이란?
-> 기존의 코드를 외부의 동작 방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다.
코드의 설계가 개선되어 가독성이 올라가고 변화에 효율적으로 대응할 수 있다.
- 상속을 통한 확장
: 메서드의 구현 코드는 제거하고 getConnection()을 추상 메서드로 만들어놓는다. 추상 메서드라서 코드는 없지만 메서드 자체는 존재한다. 따라서 add(), get() 메서드에서 getConnection()을 호출하는 코드는 유지할 수 있다.
추상 클래스를 상속을 하면 부모 클래스의 수정 없이 자식 클래스에서 원하는 대로 구현이 가능하다. 단순히 변경이 용이하다는 수준을 넘어서 손쉽게 확장된다고 말할 수 있다. 하지만, 상속을 사용했다는 단점이 발생한다. 자바는 다중 상속을 허용하지 않기 때문에 후에 다른 목적으로 상속을 적용하기가 힘들다. 또한 다른 DAO클래스를 만들면 그 클래스에 적용할 수 없다는 점도 단점으로 나타난다.
디자인 패턴이란?
-> 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션을 말한다. 패턴에서 가장 중요한 것은 각 패턴의 핵심이 담긴 목적 또는 의도다. 패턴을 적용할 상황, 해결해야 할 문제, 솔루션의 구조와 각 요소의 역할과 함께 핵심 의도가 무엇인지를 기억해둬야 한다.
- 클래스의 분리
: 이번에는 커넥션을 가져오는 메서드를 아예 다른 클래스로 분리를 해서 인스턴스를 생성해서 저장해 두고 메서드에서 사용하게 한다. 하지만 이 방법도 문제는 존재하는데 DAO 클래스만 공급하고 상속을 통해 커넥션 기능을 확장해서 사용하게 했던 게 불가능해졌다. 현재 DAO는 특정 클래스에 종속되어 있기 때문에 DAO 클래스의 수정 없이 커넥션 생성 기능을 변경할 방법이 없다. 납품 후에 고객이 커넥션을 가져오는 방법을 자유롭게 확장하기가 힘들어졌다. 이래서는 상속을 이용한 방법보다 못한 상황이 돼버린다.
- 인터페이스의 도입
: 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다.
추상화란 어떤 것들의 공통적인 성격을 뽑아내어 따로 분리하는 것인데 자바에서 추상화를 제공하는 가장 유용한 도구는 인터페이스다. 이 인터페이스를 사용하는 DAO는 인터페이스 타입의 오브젝트라면 어떤 클래스로 만들어졌는지 상관없이 메서드만 호출하면 Connection 타입의 오브젝트를 만들어서 돌려줄 것이라고 기대할 수 있다.
그럼에도 초기에 한 번 어떤 클래스의 오브젝트를 사용할 것인지 결정하는 생성자의 코드는 제거되지 않아서 결국 원점으로 돌아왔다.
- 관계 설정 책임의 분리
: 위 그림의 생성자에 cm = new DConnectionMaker(); 에서 DConnectionMaker()를 없애지 않으면 DAO는 독립적으로 확장이 가능한 클래스라고 볼 수 없다. 여기서 객체지향의 특징인 '다형성'을 주목해보자. 생성자 함수의 파라미터를 추가하고 아래와 같이 바꾸었다.
이렇게 변경함으로써 앞에서 사용했던 상속을 통한 확장 방법보다 더 깔끔하고 유연한 방법으로 UserDAO와 ConnectionMaker 클래스들을 분리하고, 서로 영향을 주지 않으면서도 필요에 따라 자유롭게 확장할 수 있는 구조가 됐다.
- 오브젝트 팩토리
위에서 문제가 많은 DAO를 깔끔한 구조로 리팩토링하는 작업을 수행했다.
그 과정에서 메인 클래스에서 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능을 맡게 되었다.
UserDAO가 독립할 수 있도록 메인 클래스에서 수고를 담당하게 되었다.
근데 메인 클래스는 UserDAO가 잘 작동하는지 확인하기 위해 만든 것인데 다른 책임까지 맡고 있으니 문제가 생겼다.
그러니 이것도 아래처럼 분리해주도록 하자!
1. UserDAO와 ConnectionMaker 구현 클래스의 오브젝트를 만드는 것.
2. 그렇게 만든 두 개의 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺는 것.
분리를 담당할 클래스를 하나 만들겠다. 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것인데, 이런 일을 하는 오브젝트를 흔히 팩토리라고 부른다.
팩토리 역할을 맡을 클래스를 DaoFactory라고 생성하고 메인 클래스에서 UserDAO, ConnectionMaker 관련 생성 작업을 팩토리로 옮긴다. 그리고 메인 클래스에서는 팩토리에서 만든 UserDAO 오브젝트를 가져와 사용하게 한다.
- 오브젝트 팩토리의 활용
DaoFactory에 UserDAO가 아닌 다른 DAO의 생성 기능을 넣으면 어떻게 될까??
메서드를 보면 DConnectionMaker라는 인스턴스를 만드는 부분이 반복돼서 나타나는 것을 볼 수 있다. 이렇게 오브젝트 생성 코드가 중복되는 것은 좋지 않은 현상이다. 이 문제를 해결하기 위해 분리해내자! 이전에 UserDAO에서 커넥션을 가져오는 메서드를 분리한 것처럼 하면 된다.
- 제어권의 이전을 통한 제어 관계 역전
: 일반적으로 프로그램의 흐름이라고 하면 main() 메서드 -> 사용할 오브젝트 결정 -> 결정한 오브젝트 생성 -> 만들어진 오브젝트에 있는 메서드 호출 -> 그 오브젝트 메서드 안에서 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복된다. 모든 작업을 사용하는 쪽에서 제어하는 구조이다.
제어의 역전이란 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다. 우리가 만든 UserDAO와 DaoFactory에도 제어의 역전이 적용되어 있다. 원래 ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 제어권은 UserDAO에 있었지만 현재는 DaoFactory에게 있다. 그러므로 UserDAO는 수동적인 존재가 됐다. 자연스럽게 관심을 분리하고 책임을 나누고 유연하게 확장 가능한 구조로 만들기 위해 오브젝트 팩토리를 도입했던 과정이 바로 IoC를 적용하는 작업이라고 볼 수 있다. 이제부터 본격적으로 스프링의 IoC에 대해 살펴보자.
- 스프링의 IoC
스프링에서 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(Bean)이라고 부른다. 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계 설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리킨다.
이제 DaoFactory를 스프링에서 사용이 가능하도록 변신시켜보자.
@Configuration : 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식.
@Bean: 오브젝트 생성을 담당하는 IoC용 메서드라는 표시.
설정 정보 : 애플리케이션 컨텍스트가 IoC를 적용하기 위해 사용하는 메타정보로, 컨테이너의 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만 보통 IoC 컨테이너에 의해 관리되는 객체를 생성하고 구성할 때 사용된다.
빈 : 스프링이 IoC방식으로 관리하는 오브젝트이며 관리되는 오브젝트라고 부르기도 한다.
스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 다 빈은 아니다. 그중에서 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.
빈 팩토리 : 스프링의 IoC를 담당하는 핵심 컨테이너. 빈을 등록, 생성, 조회, 반환 등 부가적인 빈을 관리하는 기능을 담당.
보통은 빈 팩토리를 확장한 애플리케이션 컨텍스트를 이용한다.
애플리케이션 컨텍스트 : 빈 팩토리를 확장한 IoC 컨테이너. 빈을 등록하고 관리하는 기본적인 기능은 빈 팩토리와 동일하다. 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 스프링에서는 애플리케이션 컨텍스트라는 용어를 더 많이 사용한다.
설정 정보/설정 메타정보 : 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다.
영어로 'configuration'이라고 하는데, 이는 구성정보 내지는 형상 정보라는 의미이다.
실제로 스프링의 설정 정보는 컨테이너가 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용된다.
컨테이너 또는 IoC 컨테이너 : IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 한다.
스프링 프레임워크 : IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다. 스프링이라고 줄여서 말하기도 한다.
DaoFactory를 설정 정보로 사용하는 애플리케이션 컨텍스트를 만들어보자. DaoFactory처럼 @Configuration이 붙은 자바 코드를 설정 정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다.
getBean() : ApplicationContext가 관리하는 오브젝트를 요청하는 메서드.
기본적으로 Object 타입으로 리턴하게 되어있어서 매번 리턴되는 오브젝트에 다시 캐스팅해줘야 하는 부담이 있지만, Java 5 이상의 제네릭 메서드를 사용해 getBean()의 두 번째 파라미터에 리턴 타입을 주면 캐스팅을 하지 않아도 된다.
UserDAOTest를 위와 같이 수정해준다. 파라미터인 "userDAO"는 ApplicationContext에 등록된 빈의 이름이다.
DaoFactory에서 @Bean이라는 어노테이션을 userDAO라는 이름의 메서드에 붙였는데, 이 메서드의 이름이 바로 빈의 이름이 된다.
만약 메서드의 이름이 myPreciousUserDao()라고 했다면 getBean("myPreciousUserDao", UserDAO.class)로 가져올 수 있다.
※ UserDAO를 가져오는 메서드는 하나인데 굳이 이름을 사용할까??
-> UserDAO를 생성하는 방식이나 구성을 다르게 가져가는 메서드를 추가할 수 있기 때문이다.
-> 그때는 specialUserDAO()라는 메서드를 만들고 getBean("specialUserDAO", UserDAO.class)로 가져온다.
- 애플리케이션 컨텍스트의 동작 방식
오브젝트 팩토리를 이용한 방식 VS 스프링의 애플리케이션 컨텍스트를 사용한 방식
오브젝트 팩토리는 오브젝트를 생성/관계를 맺는 제한적인 역할을 하는 데 반해 애플리케이션 컨텍스트는 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성/관계 설정을 담당한다.
애플리케이션 컨텍스트 = IoC 컨테이너 = 스프링 컨테이너 = 빈 팩토리
애플리케이션 컨텍스트는 빈 팩토리 인터페이스를 상속했으므로 일종의 빈 팩토리이다.
애플리케이션 컨텍스트는 오브젝트 팩토리와 달리 직접 오브젝트를 생성하고 관계를 맺는 코드가 없고, 그런 정보들을 별도의 설정 정보를 통해 얻는다. 때로는 외부의 오브젝트 팩토리에 그 작업을 위임하고 결과를 가져다가 사용하기도 한다.
※ 애플리케이션 컨텍스트의 장점
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
자바 코드를 작성하는 대신에 단순한 방법을 사용해 IoC 설정 정보를 만들 수 있다.
- 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고 부가적으로 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정 방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다.
- 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.
getBean()은 빈의 이름을 이용해 빈을 찾아준다. 타입, 어노테이션 등으로 설정된 빈도 찾을 수 있다.
- 싱글톤 레지스트리와 오브젝트 스코프
DaoFactory와 @Configuration 어노테이션을 추가한 애플리케이션 컨텍스트는 테스트 결과만 보면 동일한 것 같지만 차이점이 있다. 그렇다면 어떤 차이점이 있는지 보도록 하자.
먼저 알아보고 싶은 것은 'DaoFactory의 userDAO()를 여러 번 호출하면 동일한 오브젝트가 돌아오는가?'이다.
매번 userDAO()를 호출할 때마다 new 연산자에 의해 새로운 오브젝트가 만들어지게 되어있다. 정말 그런지 눈으로 확인해보자.
출력 결과에서 각기 다른 값을 가진 오브젝트가 생겼다는 사실을 확인할 수 있었다. 이것을 통해 userDAO()를 매번 호출할 때마다 다른 오브젝트가 생성된다는 것을 증명했다. 이번에는 애플리케이션 컨텍스트에 DaoFactory를 설정 정보로 등록하고 getBean()으로 userDAO라는 이름으로 등록된 오브젝트를 가져와보자.
결과를 보면 DaoFactory에서 오브젝트를 가져왔을 때랑 다르다는 것을 알 수 있다. getBean()을 두 번 호출해서 오브젝트를 가져오면 동일하다는 것을 알 수 있다. 스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다는 것이다. 왜 그럴까?
애플리케이션 컨텍스트는 우리가 만든 오브젝트 팩토리와 비슷한 방식으로 동작하는 IoC 컨테이너다. 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기도 하다. 스프링은 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다.
스프링은 다양한 기능을 담당하는 오브젝트들이 참여하는 계층형 구조로 이뤄진 경우가 대부분이다. 그런데 매번 요청이 올 때마다 오브젝트를 새로 만든다면..... 아무리 자바의 오브젝트 생성과 가비지 컬렉션의 성능이 좋아졌다고 해도 서버가 감당하기 힘들 것이다.
그래서 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다. 이렇게 애플리케이션 안에서 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것이 싱글톤 패턴의 원리다.
- 싱글톤 패턴의 한계
- private 생성자를 갖고 있기 때문에 상속할 수 없다.
- 싱글톤은 테스트가 힘들다.
- 서버 환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
- 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
- 싱글톤 레지스트리
스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 그것이 바로 싱글톤 레지스트리이다.
스프링 컨테이너는 싱글톤을 생성하고 관리하고 공급하는 싱글톤 관리 컨테이너이기도 하다. 싱글톤 레지스트리의 장점은 평범한 자바 클래스를 싱글톤으로 활용하게 해 준다는 점이다. 평범한 자바 클래스라도 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있다. 오브젝트 생성에 관란 권한은 IoC 기능을 제공하는 애플리케이션 컨텍스트에게 있기 때문이다.
가장 중요한 것은 싱글톤 패턴과 달리 스프링이 지지하는 객체지향적인 설계 방식과 원칙, 디자인 패턴 등을 적용하는데 아무런 제약이 없다는 것이다. (*스프링은 IoC 컨테이너일 뿐만 아니라, 고전적인 싱글톤 패턴을 대신해서 싱글톤을 만들고 관리해주는 싱글톤 레지스트리라는 점을 기억해두자*)
- 싱글톤과 오브젝트의 상태
싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다.
기본적으로 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태 정보를 내부에 갖고 있지 않은 무상태 stateless 방식으로 만들어져야 한다. 따라서 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태 유지stateful 방식으로 만들지 않는다.
상태가 없는 방식으로 클래스를 만드는 경우에 각 요청에 대한 정보나 DB, 서버의 리소스로부터 생성한 정보는 어떻게 다뤄야 할까?
이때는 파라미터와 로컬 변수, 리턴 값 등을 이용하면 된다.
- 스프링 빈의 스코프
스프링이 관리하는 오브젝트가 생성되고 존재하고 적용되는 범위에 대해 알아보자. 스프링에서는 이것을 빈의 스코프 scope라고 한다. 스프링 빈의 기본 스코프는 싱글톤이다. 싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서, 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다. 경우에 따라 싱글톤 외의 스코프를 가질 수 있다.
- 프로토 타입 : 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다.
- 요청 스코프 : 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성된다.
- 세션 스코프 : 웹의 세션과 유사하다.
스프링에서 만들어지는 빈의 스코프는 싱글톤 외에도 다양한 스코프를 사용할 수 있다. 자세한 것은 10장에서 알아보자.
- 의존관계 주입(DI)
IoC라는 용어는 IoC가 매우 느슨하게 정의돼서 폭넓게 사용되는 용어라는 것이다. 때문에 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다. 그래서 스프링이 제공하는 IoC 방식의 핵심을 짚어주는 의존관계 주입Dependency Injection이라는 좀 더 명확한 이름을 사용하기 시작했다.
스프링을 여타 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 새로운 용어를 사용할 때 나타난다.
초기에는 IoC 컨테이너라고 불리던 스피링이 지금은 DI 컨테이너라고 더 많이 불린다.
의존관계 주입 (Dependency Injection)
-> DI가 일어하는 방법에 초점을 맞춘 것이다. 엄밀히 말해서 오브젝트는 다른 오브젝트에 주입할 수 있는 것이 아니다. 오브젝트의 레퍼런스가 전달될 뿐이다. DI는 오브젝트 레퍼런스를 외부로부터 제공받고 이를 통해 여타 오브젝트와 동적으로 의존관계가 만들어지는 것이 핵심이다.
- 런타임 의존관계 설정
먼저 의존관계란 무엇인지 생각해보자. 두 개의 클래스 또는 모듈이 의존관계가 있다고 말할 때는 항상 방향성을 부여해줘야 한다. 즉, 누가 누구에게 의존하는 관계에 있다는 식이어야 한다.
위 그림은 A가 B에 의존하고 있음을 나타낸다. 그렇다면 의존하고 있다는 것은 무슨 의미일까? B가 변하면 그것이 A에 영향을 미친다는 뜻이다. 예를 들어 A에서 B에 정의된 메서드를 호출해서 사용하는 경우다. 이럴 땐 '사용에 대한 의존관계'가 있다고 말할 수 있다. 만약 B에 새로운 메서드가 추가되거나 기존 메서드의 형식이 바뀌면 A도 그에 따라 수정되거나 추가돼야 할 것이다. 이렇게 사용의 관계에 있는 경우 A와 B는 의존관계가 있다고 말할 수 있다. 반대로 말하면 B는 A에 의존하지 않는다. 즉 B는 A에 영향을 받지 않는다는 뜻이다.
DAO가 ConnectionMaker에만 의존하고 있는 형태이다. 따라서 ConnectionMaker 인터페이스가 변하면 그 영향을 DAO가 직접 받게 된다. 인터페이스를 구현한 클래스가 바뀌어도 DAO에는 영향을 주지 않는다.
이렇게 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는다. 그러면 결합도가 낮다고 설명할 수 있다.
하지만 UML에서 말하는 의존관계는 설계 시점의 관계를 보는 것이다. 따라서 드러나지 않는 의존관계 즉, 런타임 의존 관계는 성격이 분명히 다르다.
DAO의 코드를 작성하는 시점에는 어떤 ConnectionMaker의 구현 클래스와 관계를 맺을지 알 수 없다. 그것은 클라이언트 측의 선택에 따라 달라지기 때문이다.
그래서 의존관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체 (클라이언트라고 부르는 ) 오브젝트를 런타임 시에 연결해주는 작업을 말한다.
의존관계 주입 시 세 가지 조건
1. 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서 인터페이스에만 의존해야 한다.
2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.
의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다.
DI에서 말하는 제3의 존재는 앞에서 만든 DaoFactory와 같은 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너처럼 외부에서 관계를 맺어주는 책임을 가진 존재들을 DI 컨테이너라고 부른다.
- 의존관계 검색과 주입
의존관계를 맺는 방법이 의존관계 주입만 있는 것이 아니라 의존관계 검색dependency lookup라고 불리는 것도 있다.
의존관계 검색 (dependency lookup)
자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다. 다만 자신이 이용할 오브젝트를 결정하지는 않는다. 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트 생성 작업은 외부 컨테이너에게 맡기지만,
이를 가져올 때는 스스로 컨테이너에게 요청하는 방법을 사용한다.
UserDAO의 생성자를 이렇게 변경해도 UserDAO는 여전히 어떤 ConnectionMaker 오브젝트를 사용할지 알지 못한다.
하지만 적용 방법은 외부로부터 주입이 아니라 IoC 컨테이너인 DaoFactory에게 요청하는 것이다.
스프링의 애플리케이션 컨텍스트는 getBean()가 의존관계 검색에 사용되는 것이다. 애플리케이션 컨텍스트를 사용해서 의존관계 검색 방식으로 ConnectionMaker 오브젝트를 가져올 수 있다.
의존관계 주입과 의존관계 검색 두 가지를 사용해봤지만 의존관계 주입이 더 깔끔한 코드를 보여준다. 그럼에도 의존관계 검색을 적어도 한 번은 사용해서 오브젝트를 가져와야 한다.
왜냐? 스태틱 메서드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다. 서버도 main()과 같은 기동 메서드는 없어 의존관계 검색 방식으로 오브젝트를 가져와야 한다. 다행히 이런 서블릿은 스프링이 미리 만들어서 제공한다.
의존관계 주입과 의존관계 검색의 차이
> 의존관계 검색은 검색하는 오브젝트 자신이 스프링의 빈일 필요가 없다. 반면에 의존관계 주입은 주입받는 객체가 반드시 스프링 빈 객체여야 한다. 의존관계 검색과 의존관계 주입은 적용 방법에 차이가 있다.
- 의존관계 주입의 응용
기능 구현의 교환
개발을 할 때 개발 시 로컬 DB와 운영 DB로 나눠서 사용한다. DI를 사용하지 않고 배포를 한다고 하면 개발용 클래스를 운영용 클래스로 이름을 바꿔야 한다. 이 과정이 한두 개면 그럴려니 하겠지만 DAO가 100개 넘어간다면 그때부터는 매우 힘든 작업이 시작된다. 마찬가지로 배포한 것을 다시 개발한다고 하면 그것 역시 매우 힘든 작업이다. 하지만 DI를 사용한다면 단 한 줄만 수정하면 아주 쉽게 전환이 가능하다.
부가기능 추가
DB 연결 횟수를 세는 기능을 추가한다고 생각해보자. 모든 DAO에 카운팅 기능을 추가하는 것은 힘든 작업이다.
하지만 DI를 사용하면 아주 간단한 방법으로 기능을 추가할 수 있다.
CountingConnectionMaker 클래스는 ConnectionMaker 인터페이스를 구현했지만 직접 DB 커넥션은 만들지 않았다. 대신 DAO가 makeConnection()을 호출할 때마다 카운터를 증가시킨다. 그래야 DB커넥션을 사용해서 정상적으로 동작할 수 있다.
생성자를 보면 CountingConnectionMaker도 DI를 받는 것을 알 수 있다. 아마 실제 DB 커넥션을 돌려주는 DConnectionMaker 클래스의 오브젝트일 것이다.
그러면 CountingConnectionMaker가 추가되면서 런타임 의존관계가 어떻게 바뀌는지 살펴보자.
UserDAO가 DI 받는 대상의 설정을 조정해서 CountingConnectionMaker 오브젝트로 바꿔치기하는 것이다.
이렇게 해두면 DB 커넥션을 가지고 올 때마다 CountingConnectionMaker의 makeConnection() 메서드가 실행되고 카운터는 하나씩 증가한다. 그렇다고 해서 DB 커넥션을 제공해주지 않으면 DAO가 작동하지 않을 테니 CountingConnectionMaker가 DConnectionMaker를 호출하도록 해야 한다. 그러면 아래 그림과 같은 런타임 의존관계가 형성된다.
- 메서드를 이용한 의존관계 주입
지금까지 UserDAO의 의존관계 주입을 위해 생성자를 사용했다. 그런데 의존관계 주입 시 반드시 생성자를 사용해야 하는 것은 아니다. 생성자가 아닌 일반 메서드를 사용할 수 있을 뿐 아니라, 생성자를 사용하는 방법보다 더 자주 사용된다.
수정자 메서드를 이용한 주입
수정자 setter 메서드는 외부에서 오브젝트 내부의 속성 값을 변경하려는 용도로 주로 사용된다. 핵심 기능은 파라미터로 전달된 값을 보통 내부의 인스턴스 변수에 저장하는 것이다. 수정자 메서드는 외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메서드에서 사용하게 하는 DI 방식에서 활용하기 적당하다.
일반 메서드를 이용한 주입
수정자 메서드의 제약이 싫다면 여러 개의 파라미터를 갖는 일반 메서드를 DI용으로 사용할 수 있다. 임의의 초기화 메서드를 이용하는 DI는 적절한 개수의 파라미터를 가진 여러 개의 초기화 메서드를 만들 수도 있기 때문에 한 번에 모든 필요한 파라미터를 다 받아야 하는 생성자보다 낫다. 즉, 여러 개의 파라미터를 받을 수 있고 overloading으로 다양하게 파라미터를 받을 수 있다는 장점이 있다.
- XML을 이용한 설정
스프링은 DaoFactory와 같은 자바 클래스를 이용하는 것 외에도, 다양한 방법을 통해 DI 의존관계 설정 정보를 만들 수 있다. 가장 대표적인 것이 XML이다. XML은 단순한 텍스트 파일이기 때문에 다루기 쉽다. 또 쉽게 이해할 수 있고 컴파일과 같은 별도의 빌드 작업이 없다는 것도 장점이다. 이제 DaoFactory 자바 코드에 담겨 있던 DI를 위한 오브젝트 의존관계 정보를 XML을 이용해 만들어보자.
XML설정
DI 정보가 담긴 XML 파일은 <beans>를 루트 엘리먼트로 사용한다. 이름에서 알 수 있듯이 <beans> 안에는 여러 개의 <bean>을 정의할 수 있다.
@Configuration을 <beans>, @Bean을 <bean>에 대응해서 생각하면 이해하기 쉬울 것이다.
빈의 이름 : @Bean 메서드 이름이 빈의 이름이다. 이 이름은 getBean()에서 사용된다.
빈의 클래스 : 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의한다.
빈의 의존 오브젝트 : 빈의 생성자나 수정자 메서드를 통해 의존 오브젝트를 넣어준다. 의존 오브젝트도 하나의 빈이므로 이름이 있을 것이고 그 이름에 해당하는 메소드를 호출해서 의존 오브젝트를 가져온다. 의존 오브젝트는 하나 이상일 수도 있다.
connectionMaker() 전환
DaoFactory의 @Bean 메서드에 담긴 정보를 1:1로 XML의 태그와 애트리뷰트로 전환해주기만 하면 된다. 단, <bean> 태그의 class 애트리뷰트에 지정하는 것은 자바 메서드에서 오브젝트를 만들 때 사용하는 클래스 이름이라는 점을 주의하자.
userDAO() 전환
userDAO() 에는 DI 정보의 세 가지 요소가 모두 들어있다. 여기서 관심은 수정자 메서드를 사용해 의존관계를 주입해주는 부분이다. 개발자가 수정자 메소드를 선호하는 이유는 XML로 의존관계 정보를 만들 때 편리하다는 점도 있다.
<property> 태그는 name과 ref라는 두 개의 애트리뷰트를 갖는다. name은 프로퍼티 이름이고 ref는 수정자 메소드를 통해 주입해줄 오브젝트의 빈 이름이다. 마지막으로 이 <property> 태그를 <bean> 태그 안에 넣어주면 된다.
XML의 의존관계 주입 정보
<property> 태그 애트리뷰트의 차이
name : DI에 사용할 수정자 메서드의 프로퍼티 이름.
ref : 주입할 오브젝트를 정의한 빈의 ID.
보통 두 개가 같은 경우가 많은데 이름은 주입할 오브젝트의 인터페이스를 따르는 경우가 많고, 빈 이름도 역시 인터페이스 이름을 사용하는 경우가 많기 때문이다. 빈의 이름을 바꾸는 경우 그 이름을 참조하는 다른 빈의 <property> ref 애트리뷰트의 값도 함께 변경해줘야 한다.
XML을 이용하는 애플리케이션 컨텍스트
애플리케이션 컨텍스트가 XML 설정 정보를 활용하도록 만들어보자. XML에서 빈의 의존관계 정보를 이용하는 IoC/DI 작업에는 GenericXmlApplicationContext를 사용한다. 생성자 파라미터로 XML 파일의 클래스 패스를 지정해주면 된다.
DataSource 인터페이스로 변환
DataSource 인터페이스를 사용하면 우리가 DB 커넥션을 하기 위해 만든 ConnectionMaker와 동일한 기능을 구현할 수 있다. 다만, DataSource는 getConnection()이라는 DB커넥션을 가져오는 기능 외에도 여러 개의 메서드를 가지고 있어 인터페이스를 직접 구현하기는 부담스럽다.
다음은 DataSource 구현 클래스가 필요하다. 스프링이 제공해주는 DataSource 구현 클래스 중에 테스트 환경에 간단히 사용할 수 있는 SimpleDriverDataSource를 사용해서 DI를 재구성할 것이다.
XML 설정 방식
applicationContext.xml에 앞에서 만든 connectionMaker를 지우고 dataSource라는 이름의 bean을 추가해준다.
근데 여기서 문제가 발생한다. dataSource() 메서드에서 SimpleDriverDataSource 오브젝트의 수정자로 넣어준 DB 접속 정보는 나타나 있지 않다는 것이다. UserDAO처럼 다른 빈에 의존하는 경우에는 <property> 태그와 ref 애트리뷰트로 빈 이름을 넣어주면 된다. 하지만 XML에서 어떻게 DB 연결 정보를 넣도록 설정을 만들 수 있을까?
프로퍼티 값의 주입
수정자 메서드에는 다른 빈이나 오브젝트뿐 아니라 스트링 같은 단순 값을 넣어줄 수 있다. 이때는 DI에서처럼 오브젝트의 구현 클래스를 다이내믹하게 바꿀 수 있게 해주는 것이 목적은 아니다. 대신 클래스 외부에서 DB 연결 정보와 같이 변경 가능한 정보를 설정해줄 수 있도록 만들기 위해서다.
텍스트나 단순 오브젝트 등을 수정자 메서드에 넣어주는 것을 스프링에서는 '값을 주입한다'라고 말한다. 성격은 다르지만 일종의 DI라고 볼 수 있는 것이 오브젝트의 특성을 외부에서 변경할 수 있기 때문이다.
ref 대신 value를 사용했을 뿐이지 기존의 <property> 태그를 사용했던 것과 내용과 방법은 동일하다. value 애트리뷰트에 들어가는 것은 실제 수정자 메서드의 파라미터로 전달되는 스트링 그 자체다.
'Spring > 토비의 스프링 정리' 카테고리의 다른 글
2.5 학습 테스트로 배우는 스프링 (0) | 2022.04.06 |
---|---|
2.4 스프링 테스트 적용 (0) | 2022.04.05 |
2.3 개발자를 위한 테스팅 프레임워크 JUnit (2) | 2022.04.04 |
2.2 UserDaoTest 개선 (0) | 2022.04.01 |
2-1 UserDaoTest 다시보기 (0) | 2022.03.31 |