스프링은 자바 엔터프라이즈 애플리케이션 개발에 사용되는 애플리케이션 프레임워크이다. 애플리케이션 프레임워크는 애플리케이션 개발을 빠르고 효율적으로 할 수 있도록 애플리케이션의 바탕이 되는 틀과 공통 프로그래밍 모델, 기술 API 등을 제공해준다.
- 프로그래밍 모델 : 애플리케이션을 구성하는 오브젝트가 생성되고 동작하는 방식에 대한 틀
스프링을 사용한다는 것은 바로 이 세 가지 요소를 적극적으로 활용해서 애플리케이션을 개발한다는 뜻
클래스는 스프링 컨테이너 위에서 오브젝트로 만들어져 동작하게 만들고, 코드는 스프링의 프로그래밍 모델을 따라서 작성하고, 엔터프라이즈 기술을 사용할 때는 스프링이 제공하는 기술 API와 서비스를 활용하도록 해주면 된다.
- DAO(Data Access Object)
DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트
자바빈(JavaBean) 규약
원래 비주얼 툴에서 조작 가능한 컴포넌트 즉, 비주얼 컴포넌트라고 불렸지만 이젠 다음 두 자기 관례를 따라 만들어진 오브젝트를 가리킨다. 간단히 빈이라고 하기도 한다.
- 디폴트 생성자 : 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야한다. 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
- 프로퍼티 : 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 set으로 시작하는 수정자 메서드(setter)와 get으로 시작하는 접근자 메서드(getter)를 이용해 수정 또는 조회할 수 있다.
package springbook.user.domain;
public class User {
String id;
String name;
String password;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
이제 User 오브젝트에 담긴 정보가 실제로 보관될 DB의 테이블을 만들어보자.
사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어보자.
사용자 정보 등록, 수정, 삭제와 같은 각종 조회 기능을 만들어야겠지만,
일단 사용자 생성(add), 아이디 가지고 사용자 정보를 읽어오는(get) 두 개의 메서드를 생성하자.
JDBC를 이용하는 작업의 일반적인 순서
- DB 연결을 위한 Connection을 가져온다.
- SQL을 담은 Statement(또는 PreparedStatement)를 만든다.
- 만들어진 Statement를 실행한다.
- 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트(여기서는 User)에 옮겨준다.
- 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
- JDBC API가 만들어내는 예외(Exception)을 잡아서 직접 처리하거나, 메서드에 throws를 선언해서 예외가 발생하면 메서드 밖으로 던지게 한다.
일단 예외는 모두 메서드 밖으로 던져버리는 편이 간단하다.
package com.springbook.user.dao;
...
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values (?, ?, ?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values (?, ?, ?)");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
}
만들어진 코드의 기능을 검증하고자 할 때 사용할 수 있는 가장 간단한 방법은 오브젝트 스스로 자신을 검증하도록 만들어주는 것이다. 모든 클래스에는 자신을 엔트리 포인트로 설정해 직접 실행 가능하게 해주는 static 메서드 main() 있다.
- 그전에 gradle에 mysql-connector-java 를 추가해주자.
public static void main(String[] args) throws ClassNotFoundException, SQLException{
UserDao dao = new UserDao();
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
SpringApplication.run(SpringbookApplication.class, args);
}
오브젝트에 대한 설계와 이를 구현한 코드는 끊임없이 변한다.
애플리케이션이 기반을 두고 있는 기술도 시간이 지남에 따라 변하고 운영하는 환경도 변화한다.
그래서 개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.
그러면 어떻게 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있었을까? 그것은 분리와 확장을 고려한 설계가 있었기 때문이다.
객체지향 설계와 프로그래밍이 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다. 객체지향 기술은 흔히 실세계를 최대한 가깝게 모델링해낼 수 있는데 의미가 있다. 하지만 그보다는 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는 데 더 큰 의미가 있다.
먼저, 분리에 대해 생각해보자.
모든 변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어난다.
문제는, 변화는 대체로 집중된 한 가지 관심에 대해 일어나지만 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다는 점이다.
변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한 가지 관심이 한 군데에 집중되게 하는 것이다. 즉, 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.
프로그래밍의 기초 개념 중에 관심사의 분리라는 게 있다. 이를 객체지향에 적용해보면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이라 생각할 수 있다.
UserDao
의 add()
메서드 하나에서만 적어도 세 가지 관심사항을 발견할 수 있다.
Statement
를 만들고 실행하는 것Statement
에 바인딩시키고, Statement
에 담긴 SQL을 DB를 통해 실행시키는 방법이다. 파라미터를 바인딩하는 것과 어떤 SQL을 사용할지를 다른 관심사로 분리할 수도 있지만, 우선은 이것도 하나로 묶어 생각하자.Statement
와 Connection
오브젝트를 닫아줘서 공유 리소스를 시스템에 돌려주는 것DB커넥션을 가져오는 코드는 다른 관심사와 섞여서 같은 add()
메서드에 담겨있다. 더 큰 문제는 add()
메서드에 있는 DB 커넥션을 가져오는 코드와 동일한 코드가 get()
메서드에도 중복되어 있다는 점이다.
가장 먼저 할 일은 커넥션을 가져오는 중복된 코드를 분리하는 것이다. 중복된 DB 연결 코드를 getConnection()
이라는 이름의 독립적인 메서드로 만들어둔다.
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
return c;
}
DB 연결과 관련된 부분에 변경이 일어났을 경우, 앞으로는 getConnection()
이 한 메서드의 코드만 수정하면 된다.
이렇게 관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다. 관심이 다른 코드가 있는 메서드에는 영향을 주지도 않을뿐더러, 관심 내용이 독립적으로 존재하므로 수정도 간단해졌다.
방금 한 작업은 UserDao의 기능에는 아무런 변화를 주지 않았다. 여러 메서드에 중복돼서 등장하는 특정 관심사항이 담긴 코드를 별도의 메서드로 분리해낸 것이다. 기능이 추가되거나 바뀐 것은 없지만 UserDao
는 이전보다 훨씬 깔끔해졌고 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드가 됐다. 이런 작업을 리팩토링이라고 한다. 또한 위에서 사용한 getConnection()
이라고 하는 공통의 기능을 담당하는 메서드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메서드 추출 기법이라고 부른다.
리팩토링
기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술이다. 리팩토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다.
현재 앞에서 만들어뒀던 main()
메서드 테스트는 두 번째부터는 무조건 예외가 발생한다. 테이블의 기본키인 id
값이 중복되기 때문이다. 따라서 main()
메서드 테스트를 다시 실행하기 전에 User
테이블의 사용자 정보를 모두 삭제해줘야 한다.
만약 UserDao
가 인기를 끌더니 N 사와 D 사에서 사용자 관리를 위해 이 UserDao
를 구매하겠다는 주문이 들어왔다고 상상해보자. 문제는 N 사와 D 사가 각기 다른 종류의 DB를 사용하고 있고, DB 커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다. 더욱 큰 문제는 UserDao
를 구매한 이후에도 DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다는 점이다.
게다가, UserDao
가 비밀기술이라 고객에게는 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶다. 과연 이런 경우에 UserDao
소스코드를 N 사와 D 사에 제공해주지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao
를 사용하게 할 수 있을까?
이럴 땐 기존 UserDao 코드를 한 단계 더 분리하면 된다. 일단 UserDao에서 메서드의 구현 코드를 제거하고 getConnection()을 추상 메서드로 만들어 놓는다. 이러면 add(), get() 메서드에서 getConnection()을 호출하는 코드는 그대로 유지할 수 있다. 이제 이 추상 클래스인 UserDao를 N 사와 D 사에 판매한다.
포탈사들은 UserDao 클래스를 상속해 NUserDao 같은 서브클래스를 만들고 그 클래스에서 getConnection() 메서드를 원하는 방식대로 구현할 수 있다. 이렇게 하면 UserDao의 소스코드를 제공하지 않고 getConnection() 메서드를 원하는 방식으로 확장한 후에 UserDao의 기능과 함께 사용할 수 있다.
기존에는 같은 클래스에 다른 메서드로 분리됐던 DB 커넥션 연결이라는 관심을 이번엔 상속을 통해 서브클래스로 분리해버리는 것이다.
public abstract class UserDao{
...
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
public class NUserDao extends UserDao{
public Connection getConnection() throws ClassNotFoundException, SQLException {
// N 사 DB Connection 생성코드
}
}
public class DUserDao extends UserDao{
public Connection getConnection() throws ClassNotFoundException, SQLException {
// N 사 DB Connection 생성코드
}
}
수정한 코드를 보면 DAO의 핵심 기능인 어떻게 데이터를 등록하고 가져올 것인가(SQL 작성, 파라미터 바인딩, 쿼리 실행, 검색정보 전달)라는 관심을 담당하는 UserDao와, DB 연결 방법은 어떻게 할 것인가라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 구분되고 있다.
클래스 계층구조를 통해 두 개의 관심이 독립적으로 분리되면서 변경 작업은 한층 용이해졌다. 새로운 DB 연결 방법을 적용해야 할 때는 UserDao의 코드의 수정 없이 상속을 통해 확장해주기만 하면 된다.
이렇게 슈퍼클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환)을 만들고, 그 기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤 서브클래스에서 이런 메서드를 필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메서드 패턴이라고 한다. 즉, UserDao는 어떤 기능을 사용한다는 데에, NUserDao는 어떤 식으로 기능을 제공하는 지에 관심이 있다.
또한, UserDao이 getConnection() 메서드는 Connection 타입 오브젝트를 생성한다는 기능을 정의해놓은 추상 메서드다. 그리고 UserDao의 서브클래스의 getConnection() 메서드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지 결정하는 방법이라고도 볼 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메서드 패턴이라고 부르기도 한다. 즉,UserDao는 Connection 인터페이스 타입의 오브젝트라는 것만, NUserDao는 어떤 방법으로 Connection 오브젝트를 만들어내는지에 관심이 있다. NUserDao와 DUserDao가 모두 같은 종류의 Connection 구현 클래스의 오브젝트를 리턴할 수도 있다. 그래도 오브젝트를 생성하는 방식이 다르다면, 이는 팩토리 메서드 패턴으로 이해할 수 있다.
템플릿 메서드 패턴
상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. 슈퍼 클래스에서는 미리 추상 메서드 또는 오버라이드 가능한 메서드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메서드를 만든다. 슈퍼클래스에서 디폴트 기능을 정의해두고나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메서드를 훅(hook)메서드라고 한다. 서브클래스에선 추상 메서드를 구현하거나, 훅 메서드를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.
팩토리 메서드 패턴
팩토리 메서드 패턴도 템플릿 메서드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 그래서 구조도 비슷하다. 슈퍼클래스 코드에서는 서브클래스에서 구현할 메서드를 호출해서 필요한 타입의 오브젝트를 가져와 사용한다. 서브 클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에선 알지 못한다. 서브클래스는 다양한 방법으로 오브젝트를 생성하는 베서드를 재정의할 수 있다. 이렇게 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메서드를 팩토리 메서드라고 하고, 이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법을 팩토리 메서드 패턴이라고 한다.
이렇게 템플릿 메서드 패턴 또는 팩토리 메서드 패턴으로 관심사항이 다른 코드를 분리해내고, 서로 독립적으로 변경 또는 확장할 수 있도록 만드는 것은 간단하면서도 매우 효과적인 방법이다.
하지만 이 방법은 상속을 사용했다는 단점이 있다.
지금까지 데이터 액세스 로직을 어떻게 만들 것인가와 DB 연결을 어떤 방법으로 할 것인가라는 두 개의 관심을 상하위 클래스로 분리시켰다. 이 두 개의 관심은 변화의 성격 즉, 변화의 이유와 시기, 주기 등이 다르다.
추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 바로 이렇게 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서다. 그러나 상속은 단점이 많다.
이번에는 아예 상속관계도 아닌 완전히 독립적인 클래스로 만들어보겠다. DB 커넥션과 관련된 부분을 서브클래스가 아닌 아예 별도의 클래스로 담고, 이를 UserDao가 이용하게 하면 된다.
SimpleConnectionMaker라는 새로운 클래스를 만들고 DB 생성 기능을 그 안에 넣는다. 그리고 UserDao는 new 키워드를 사용해 SimpleConnectionMaker 클래스의 오브젝트를 만들어두고, 이를 add(), get() 메서드에서 사용하면 된다.
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
return c;
}
}
public class UserDao {
private SimpleConnectionMaker scm = new SimpleConnectionMaker();
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = scm.makeNewConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = scm.makeNewConnection();
...
}
}
이번엔 N 사와 D 사에 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던 게 다시 불가능해졌다. 왜냐하면 UserDao의 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되어 있기 때문에 상속을 사용했을 때 처럼 UserDao 코드의 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없다.
이렇게 클래스를 분리한 경우에도 상속때와 마찬가지로 자유롭게 확장하려면 두 가지 문제를 해결해야 한다.
이런 문제의 근본적인 원인은 UserDao가 바뀔 수 있는 정보, 즉 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문이다. 어떤 클래스가 쓰일지, 그 클래스에서 커넥션을 가져오는 메서드는 이름이 뭔지까지 일일이 알고 있어야 한다.
이에 대한 가장 좋은 해결책은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다. 추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.
인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이지 구현 방법은 나타나 있지 않다.
UserDao가 인터페이스를 사용하게 된다면 인터페이스의 메서드를 통해 알 수 있는 기능에만 관심을 가지면 되지, 그 기능을 어떻게 구현했는지에는 관심을 둘 필요가 없다.
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
public class UserDao {
private ConnectionMaker cm;
public UserDao() {
this.cm = new DConnection();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = cm.makeConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = cm.makeConnection();
...
}
}
고객에게 납품할 때는 UserDao
클래스와 함께 ConnectionMaker
인터페이스도 전달한다. 납품받은 포털사의 개발자는 ConnectionMaker
인터페이스를 구현한 클래스를 만들고, 자신들의 DB 연결 기술을 이용해 DB 커넥션을 가져오도록 메서드를 작성해주면 된다.
하지만 UserDao
코드를 자세히 살펴보면 DConnection
이라는 클래스 이름이 보인다. UserDao
의 다른 모든 곳에서는 인터페이스를 이용하게 만들어서 DB 커넥션을 제공하는 클래스에 대한 구체적인 정보는 모두 제거 가능했지만, 초기에 한 번 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 제거되지 않고 남아 있다.
이 때문에 인터페이스를 이용한 분리에도 불구하고 여전히 UserDao
변경 없이는 DB 커넥션 기능의 확장이 자유롭지 못한데, 그 이유는 UserDao
안에 분리되지 않은, 또 다른 관심사항이 존재하고 있기 때문이다.
UserDao
에는 어떤 ConnectionMaker
구현 클래스를 사용할지를 결정하는 new DConnectionMaker()
라는 코드가 있다. 이 코드는 간단히 말해 UserDao
와 UserDao
가 사용할 ConnectionMaker
의 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다.
이 관심사를 담은 코드를 UserDao
에서 분리하지 않으면 UserDao
는 결코 독립적으로 확장가능한 클래스가 될 수 없다.
UserDao의 클라이언트 오브젝트가 바로 제3의 관심사항인 UserDao와 ConnectionMaker 구현 클래스의 관계를 결정해주는 기능을 분리해서 두기에 적절한 곳이다.
여기서의 클라이언트는
UserDao
를 사용하는 오브젝트이고UserDao
는 서비스를 제공하는 것이다.
UserDao
의 클라이언트에서 UserDao
를 사용하기 전에, 먼저 UserDao
오브젝트와 특정 클래스로부터 만들어진 ConnectionMaker
오브젝트 사이에 관계를 설정해줘보자.
클래스 사이를 설정해주는 것이 아닌 오브젝트 사이의 관계를 설정해주는 것이다. 클래스 사이에 관계가 만들어진다는 것은 한 클래스가 인터페이스 없이 다른 클래스를 직접 사용한다는 뜻이기 때문이다.
오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 만들어진다. 오브젝트 사이의 관계가 만들어지려면 (1) 직접 생성자를 호출해 직접 오브젝트를 만들거나 (2) 외부에서 만들어준 것을 가져오는 방법이 있다.
직접 오브젝트를 만들 필요는 없고, 외부에서 만든 오브젝트를 전달받으려면 메서드 파라미터나 생성자 파라미터를 이용하면 된다. 이때 파라미터의 타입을 전달받을 오브젝트의 인터페이스로 선언해뒀다고 한 경우, 해당 인터페이스 타입의 오브젝트라면 파라미터로 전달 가능하고, 파라미터로 제공받은 오브젝트는 인터페이스에 정의된 메서드만 이용한다면 그 오브젝트가 어떤 클래스로부터 만들어졌는지 신경 쓰지 않아도 된다.
또한, UserDao
의 모든 코드는 ConnectionMaker
인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안되게 해야 한다. 그래야만 UserDao의 수정 없이 DB 커넥션 구현 클래스를 변경할 수 있다.
물론 UserDao
오브젝트가 동작하려면 특정 클래스의 오브젝트와 관계를 맺어야 하긴 하지만, 클래스 사이에 관계가 만들어진 것이 아닌 단지 오브젝트 사이에 다이내믹한 관계가 만들어지는 것이다.
UserDao
오브젝트가 DConnectionManager
오브젝트를 사용하게 하려면 두 클래스의 오브젝트 사이에 런타임 사용관계 또는 링크, 또는 의존관계라고 불리는 관계를 맺어주면 된다. 이러한 런타임 오브젝트 관계를 갖는 구조로 만들어주는 게 바로 클라이언트의 책임이다.
클라이언트는 자기가
UserDao
를 사용해야 할 입장이기 때문에UserDao
의 세부 전략이라고도 볼 수 있는ConnectionMaker
의 구현 클래스를 선택하고, 선택한 클래스의 오브젝트를 생성해서UserDao
와 연결해줄 수 있다.
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
DConnectionMaker
를 생성하는 코드는UserDao
와 특정ConnectionMaker
구현 클래스의 오브젝트 간 관계를 맺는 책임을 담당하는 코드였는데, 그것을UserDao
의 클라이언트에게 넘겨버렸다.
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao dao = new UserDao(connectionMaker);
...
}
}
기존의
UserDao
클래스의main()
메서드가UserDao
의 클라이언트였는데 좀 더 깔끔하게 구분하기 위해UserDaoTest
라는 클래스를 생성해서 분리했다.
UserDaoTest
는 UserDao
와 ConnectionMaker
구현 클래스와의 런타임 오브젝트 의존관계를 설정하는 책임을 담당해야 한다. 그래서 특정 ConnectionMaker
구현 클래스의 오브젝트를 만들고, UserDao
생성자 파라미터에 넣어 두 개의 오브젝트를 연결해준다.
이제는 UserDao
의 변경 없이도 자유롭게 포털사들이 자신들을 위한 DB 접속 클래스를 만들어서 UserDao
가 사용하게 할 수 있다. 즉, UserDao
는 자신의 관심사이자 책임인 사용자 데이터 액세스 작업을 위해 SQL을 생성하고, 이를 실행하는 데만 집중할 수 있게 됐다.
UserDao
는 DB 연결 방법이라는 기능을 확장하는 데는 열려 있다. UserDao
에 전혀 영향주지 않고도 얼마든지 기능을 확장할 수 있게 되어 있는 동시에, UserDao
자신의 핵심 기능을 구현한 코드는 그런 변화에 영향받지 않고 유지할 수 있으므로 변경에는 닫혀 있다.ConnectionMaker
인터페이스를 이용해 DB 연결 기능을 독립시킨 경우, ConnectionMaker
구현 클래스를 새로 만들기만 하면 된다.ConnectionMaker
의 클래스를 결정하는 책임을 DAO의 클라이언트로 분리한 덕분에 사용할 ConnectionMaker
구현 클래스가 바뀌어도, DAO 클래스의 코드를 수정할 필요가 없게 됐다.정리하자면, UserDao
클래스는 그 자체로 자신의 책임에 대한 응집도가 높다. ConnectionMaker
또한 자신의 기능에 충실하도록 독립돼서 순수한 자신의 책임을 담당하는 데만 충실할 수 있다.
동시에 UserDao
와 ConnectionMaker
의 관계는 인터페이스를 통해 매우 느슨하게 연결되어 있다. UserDao
는 구체적인 ConnectionMaker
구현 클래스를 알 필요도 없고, 구현 방법이나 전략 등에 대해 신경 쓰지 않아도 된다. 꼭 필요한 관계만 ConnectionMaker
라는 인터페이스를 통해 낮은 결합도로 최소한으로 연결되어 있다.
UserDaoTest-UserDao-ConnectionMaker
구조는 전략 패턴에 해당한다.UserDao
는 전략 패턴의 컨텍스트에 해당한다. 컨텍스트는 자신의 기능을 수행하는 데 필요한 기능 중에서 변경 가능한, DB 연결 방식이라는 알고리즘을 ConnectionMaker
라는 인터페이스로 정의하고, 이를 구현한 클래스, 즉 전략을 바꿔가면서 사용할 수 있게 분리했다.UserDao
)를 사용하는 클라이언트(UserDaoTest)
는 컨텍스트가 사용할 전략(ConnectionMaker
를 구현한 클래스)을 컨텍스트의 생성자 등을 통해 제공해주는 게 일반적이다.원래 UserDaoTest
는 UserDao
의 기능이 잘 동작하는지 테스트하려고 만든 것이다. 그런데 지금은 또 다른 책임까지 떠맡고 있으니 성격이 다른 책임이나 관심사는 분리하자. 이렇게 분리될 기능은 (1) UserDao
와 ConnectionMaker
구현 클래스의 오브젝트를 만드는 것과, (2) 그렇게 만들어진 두 개의 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺어주는 것이다.
분리시킬 기능을 담당할 클래스의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것인데, 이런 일을 하는 오브젝트를 흔히 팩토리(factory)라고 부른다.
단지 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하려는 목적으로 사용하는 것이다.
public class DaoFactory {
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new DaoFactory().userDao();
...
}
}
UserDaoTest
는 이제 UserDao
가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경 쓰지 않고 팩토리로부터 UserDao
오브젝트를 받아다가, 자신의 관심사인 테스트를 위해 활용하기만 하면 된다.
분리된 오브젝트들의 역할과 관계를 분석해보자. UserDao
와 ConnectionMaker
는 각각 애플리케이션의 핵심적인 데이터 로직과 기술 로직을 담당하고 있고, DaoFactory
는 이런 애플리케이션의 오브젝트들을 구성하고 그 관계를 정의하는 책임을 맡고 있다. 전자가 실질적인 로직을 담당하는 컴포넌트라면, 후자는 애플리케이션을 구성하는 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할을 한다고 볼 수 있다.
설계도란 간단히 어떤 오브젝트가 어떤 오브젝트를 사용하는 지를 정의해놓은 코드라고 생각하면 된다.
이제 UserDao
를 공급할 때 UserDao
, ConnectionMaker
, DaoFactory
를 제공한다. 여기서 DaoFactory
는 소스를 제공한다. 새로운 ConnectionMaker
구현 클래스로 변경이 필요하면 DaoFactory
를 수정해서 변경된 클래스를 생성해 설정해주도록 코드를 수정해주면 된다. 여전히 우리의 핵심 기술이 담긴 UserDao는 변경이 필요 없으므로 안전하게 소스코드를 보존할 수 있다. 동시에 DB 연결 방식은 자유로운 확장이 가능하다.
DaoFactory
를 분리했을 때 얻을 수 있는 장점 중 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다는 데 가장 의미가 있다.
DaoFactory
에서 다른 DAO(AccountDao
, MessageDao
)의 생성 기능을 넣었다고 하자. 이 경우 UserDao
를 새성하는 userDao()
메서드를 복사해서 만들면 어떤 ConnectionMaker
구현 클래스를 사용할 지 결정하는 기능이 중복돼서 나타난다.
이 역시 ConnectionMaker
의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메서드로 뽑아내자. 처음 DAO코드에서 getConnection
메서드를 따로 분리해낸 것과 동일한 리팩토링 방법이다.
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 DConnectionMaker();
}
}
일반적으로 프로그램 흐름은 main() 메서드와 같은 프로그램 시작 지점에서 다음 사용할 오브젝트 결정 -> 결정한 오브젝트 생성 -> 만들어진 오브젝트에 있는 메서드 호출 -> 그 오브젝트 메서드 안에서 다음에 사용할 것을 결정하고 호출
하는 식의 작업이 반복된다. 이런 프로그램 구조에서 각 오브젝트는 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장한다. 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조다.
제어의 역전에서는 이런 제어 흐름의 개념을 거꾸로 뒤집어, 오브젝트가 자신이 사용할 오브젝트를 생성하지도, 자신이 사용할 오브젝트를 스스로 선택하지도 않는다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하여 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.
제어의 역전 개념이 적용된 예는 서블릿, 템플릿 메서드 패턴, 프레임워크 등이 있다.
그 중 프레임워크에 대해 설명하자면, 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다. 반면 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 보통 프레임워크 위에 개발한 클래스를 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식이다. 애플리케이션 코드가 프레임워크가 짜놓은 틀에서 수동적으로 동작해야 프레임워크라고 부를 수 있다.
우리가 만든 UserDao
와 DaoFactory
에도 제어의 역전이 적용되어 있다.
ConnectionMaker
의 구현 클래스를 결정하고 오브젝트를 만드는 권한이 DaoFactory
에 넘겨서 UserDao
는 수동적인 존재가 되었다.UserDao
자신도 팩토리에 의해 수동적으로 만들어지고 자신이 사용할 오브젝트도 DaoFactory
가 공급해주는 것을 수동적으로 사용해야 할 입장이 되었다.UserDaoTest
는 DaoFactory
가 만들고 초기화해서 자신에게 사용하도록 공급해주는 ConnectionMaker
를 사용할 수밖에 없다.UserDao
와 ConnectionMaker
의 구현체를 생성하는 책임도 DaoFactory
가 맡고 있다.이렇게 관심을 분리하고 책임을 나누고 유연하게 확장 가능한 구조로 만들기 위해 DaoFactory
를 도입했던 과정이 바로 IoC를 적용하는 작업이었다. 제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다. DaoFactory
는 오브젝트 수준의 가장 단순한 IoC 컨테이너 내지는 IoC 프레임워크라고 볼 수 있다.
스프링의 핵심을 담당하는 건 바로 빈 팩토리 또는 애플리케이션 컨텍스트라고 불리는 것이다. 이 두 가지는 우리가 만든 DaoFactory가 하는 일을 좀 더 일반화한 것이라고 설명할 수 있다.
빈 팩토리와 애플리케이션 컨텍스트라는 용어는 동일하다고 보면 된다.
앞으로 빈 팩토리라고 말할 때는 빈을 생성하고 관계를 설정하는 IoC의 기본 기능에 초점을 맞춘 것이고, 애플리케이션 컨텍스트라고 말할 때는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진이라는 의미가 좀 더 부각된다고 보면 된다.
애플리케이션 컨텍스트는 어떤 클래스의 오브젝트를 생성하고 어디에서 사용하도록 연결해줄 것인가 등에 관한 정보를 직접 담고 있진 않고, 대신 별도로 설정정보를 참고해서 빈(오브젝트)의 생성, 관계설정 등의 제어 작업을 총괄하는 범용적인 IoC 엔진 같은 것이라 볼 수 있다.
앞에서 DaoFactory 자체가 설정정보까지 담고 있는 IoC 엔진이다.
DaoFactory
를 스프링의 빈 팩토리가 사용할 수 있는 본격적인 설정정보로 만들어보자.
@Configuration
을 추가한다.@Bean
을 붙여준다.userDao()
: UserDao
타입 오브젝트를 생성하고 초기화해서 돌려주는 메서드connectionMaker()
: ConnectionMaker
타입의 오브젝트를 생성하는 메서드@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
}
이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어보자.
애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트다. ApplicationConetxt를 구현한 클래스 중 DaoFactory처럼 @Configuration이 붙은 자바 코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다.
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext ac = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = ac.getBean("userDao", UserDao.class);
}
}
getBean()
: ApplicationContext
가 관리하는 오브젝트를 요청하는 메서드ApplicationContext
에 등록된 빈의 이름을 같이 넘겨준다.userDao
라는 이름의 빈을 가져온다는 것은 DaoFactory
의 userDao() 메서드를 호출한 결과를 가져온다는 것이다.왜 이름을 사용할까?
UserDao
를 생성하는 방식이나 구성을 다르게 가져가는 메서드를 추가할 수 있기 때문이다.
ex)specialUserDao()
라는 메서드라면,getBean("specialUserDao", UserDao.class)
로 가져오면 된다.
기존에 오브젝트 팩토리를 이용했던 방식과 스프링의 애플리케이션 컨텍스트를 사용한 방식을 비교해보자.
오브젝트 팩토리에 대응되는 것이 스프링의 애플리케이션 컨텍스트다. 스프링에서는 이 애플리케이션 컨텍스트를 IoC 컨테이너라 하기도 하고, 간단히 스프링 컨테이너라고 부르기도 한다. 또는 빈 팩토리라고 부를 수도 있다. 애플리케이션 컨텍스트는 BeanFactory 인터페이스를 상속받은 ApplicationContext 인터페이스를 구현하므로 일종의 빈 팩토리이다.
애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다. 애플리케이션 컨텍스트는 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없고, 그런 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻는다. 때로는 외부의 오브젝트 팩토리에 그 작업을 위임하고 그 결과를 가져다가 사용하기도 한다.
@Configuration
이 붙은 클래스는 이 애플리케이션 컨텍스트가 활용하는 IoC 설정정보다. 내부적으로는 애플리케이션 컨텍스트가 @Bean
이 붙은 메서드를 호출해서 오브젝트를 가져온 것을 클라이언트가 getBean()
으로 요청할 때 전달해준다.
정확히는, 애플리케이션 컨텍스트는
@Configuration
이 붙은 클래스를 설정정보로 등록해두고,@Bean
이 붙은 메서드의 이름을 가져와 빈 목록을 만들어둔다. 클라이언트가 이 애플리케이션 컨텍스트의getBean()
메서드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메서드를 호출해서 오브젝트를 생성시킨 후 클라이언트에 돌려준다.
클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.
빈 또는 빈 오브젝트는 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 스프링이 사용하는 애플리케이션에서 만들어지는 모든 오브젝트 중 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.
스프링의 IoC를 담당하는 핵심 컨테이너를 가리킨다. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외에 부가적인 빈을 관리하는 기능을 담당한다. 보통은 빈 팩토리를 확장한 애플리케이션 컨텍스트를 이용한다. 빈 팩토리가 구현하고 있는 가장 기본적인 인터페이스는 BeanFactory이고 여기에 getBean()과 같은 메서드가 정의되어 있다.
빈 팩토리를 확장한 IoC 컨테이너다. 빈 팩토리의 빈을 등록하고 관리하는 기본적인 기능에서 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 빈 팩토리라고 부를 때는 주로 빈의 생성과 제어의 관점에서 이야기하는 것이고, 애플리케이션 컨텍스트라고 할 때는 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함해서 이야기하는 것이라고 보면 된다. 애플리케이션 컨텍스트가 구현해야 하는 기본 인터페이스는 ApplicationContext이고 이것은 BeanFactory를 상속한다.
애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 스프링의 설정정보는 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, 그보다 IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용된다. 애플리케이션 형상정보라고 부르기도 하고, 청사진이라고도 한다.
IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 한다. 후자는 주로 빈 팩토리의 관점에서 이야기하는 것이고, 그냥 컨테이너 또는 스프링 컨테이너라고 할 때는 애플리케이션 컨텍스트를 가리키는 것이라고 보면 된다. 컨테이너라는 말 자체가 IoC 개념을 담고 있기 때문에 애플리케이션 컨텍스트를 스프링 컨테이너라고 부르기도 한다. 때로는 컨테이너라는 말을 떼고 스프링이라고 부를 때도, 바로 이 스프링 컨테이너를 가리키는 것일 수 있다.
스프링 프레임워크는 IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다. 그냥 스프링이라고 줄여서 말하기도 한다.
스프링의 애플리케이션 컨텍스트는 기존에 직접 만들었던 오브젝트 팩토리와는 중요한 차이점이 있다.
먼저, DaoFactory의 userDao()를 여러 번 호출했을 때 동일한 오브젝트가 돌아오는가를 알아보자. 오브젝트를 직접 콘솔에 출력하면 오브젝트별로 할당되는 고유한 값이 출력되는데, 이 값이 같으면 동일한 오브젝트라는 뜻이다.
오브젝트의 동일성과 동등성
- 동일성
- 두 개의 오브젝트가 완전히 같은 오브젝트라는 뜻으로, == 연산자를 이용해 비교한다.
- 사실 하나의 오브젝트만 존재하는 것이고 두 개의 오브젝트 레퍼런스 변수를 갖고 있는 것이다.
- 동등성
- 두 개의 오브젝트가 동일한 정보를 담고 있다는 뜻으로, equals() 메서드를 이용해 비교한다.
- 두 개의 각기 다른 오브젝트가 메모리상에 존재하는 것이다.
DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();
System.out.println(dao1);
System.out.println(dao2);
실행 결과
com.springbook.user.dao.UserDao@1324409e
com.springbook.user.dao.UserDao@2c6a3f77
출력 결과에서 알 수 있듯이, 두 개는 각기 다른 값을 가진 동일하지 않은 오브젝트다. 즉, 오브젝트가 두 개가 생겼다는 사실을 알 수 있다.
ApplicationContext ac = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao3 = ac.getBean("userDao", UserDao.class);
UserDao dao4 = ac.getBean("userDao", UserDao.class);
System.out.println(dao3);
System.out.println(dao4);
실행 결과
com.springbook.user.dao.UserDao@5b38c1ec
com.springbook.user.dao.UserDao@5b38c1ec
두 오브젝트의 출력 값이 값으므로, getBean()을 두 번 호출해서 가져온 오브젝트가 동일하다는 사실을 알 수 있다.
우리가 만들었던 오브젝트 팩토리와 스프링의 애플리케이션 컨텍스트의 동작방식에 무엇인가 차이점이 있다. 스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다는 것인데, 왜 그럴까?
애플리케이션 컨텍스트는 우리가 만들었던 오브젝트 팩토리와 비슷한 방식으로 동작하는 IoC 컨테이너면서 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(singleton registry)이기도 하다.
스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다. 여기서 싱글톤이라는 것은 디자인 패턴에서 나오는 싱글톤 패턴과 비슷한 개념이지만 그 구현 방법은 확연히 다르다.
왜 스프링은 시싱글톤으로 빈을 만드는 것일까? 이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.
스프링이 처음 설계됐던 대규모의 엔터프라이즈 서버환경은 서버 하나당 최대로 초당 수십에서 수백 번씩 브라우저나 여타 시스템으로부터의 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경이었다. 그런데 매번 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다고 생각해보자. 아무리 자바의 오브젝트 생성과 가비지 컬렉션의 성능이 좋아졌다고 한들 이렇게 부하가 걸리면 서버가 감당하기 힘들다.
그래서 엔터프라이즈 분야에선 서비스 오브젝트라는 개념을 일찍부터 사용해왔고, 서블릿은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다. 스펙에서 강제하진 않지만, 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.
이렇게 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것이 싱글톤 패턴의 원리다. 따라서 서버환경에서는 서비스 싱글톤의 사용이 권장된다. 하지만 디자인 패턴에 소개된 싱글톤 패턴은 사용하기가 까다롭고 여러 가지 문제점이 있다.
싱글톤 패턴(Singleton Pattern)
싱글톤 패턴은 어떤 클래스를 애플리케이션 내에서 제한된 인스턴스 개수, 이름처럼 주로 하나만 존재하도록 강제하는 패턴이다. 이렇게 하나만 만들어지는 클래스의 오브젝트는 애플리케이션 내에서 전역적으로 접근이 가능하다. 단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다.
자바에서 싱글톤을 구현하는 방법은 보통 이렇다.
public class UserDao {
private static UserDao INSTANCE;
private static ConnectionMaker connectionMaker;
private UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public static synchronized UserDao getInstance() {
if (INSTANCE == null) INSTANCE = new UserDao(???);
return INSTANCE;
}
...
}
UserDao에 싱글톤 패턴을 도입함으로서 생기는 문제
일반적으로 싱글톤 패턴 구현 방식에 있는 문제
private 생성자를 갖고 있기 때문에 상속할 수 없다.
싱글톤 패턴은 생성자를 private로 제한한다. 오직 싱글톤 클래스 자신만이 자기 오브젝트를 만들도록 제한하는 것이다. 문제는 private 생성자를 가진 클래스는 다른 생성자가 없다면 상속이 불가능하다는 점이다. 객체지향의 장점인 상속과 이를 이용한 다형성을 적용할 수 없다. 또한 상속과 다형성 같은 객체지향의 특징이 적용되지 않는 스태틱 필드와 메서드를 사용하는 것도 역시 동일한 문제를 발생시킨다.
싱글톤은 테스트하기가 힘들다.
싱글톤은 테스트하기가 어렵거나 테스트 방법에 따라 아예 테스트가 불가능하다. 싱글톤은 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 때 목 오브젝트 등으로 대체하기가 힘들다. 싱글톤은 초기화 과정에서 생성자 등을 통해 사용할 오브젝트를 다이내믹하게 주입하기도 힘들기 때문에 필요한 오브젝트는 직접 오브젝트를 만들어 사용할 수밖에 없다. 이런 경우 테스트용 오브젝트로 대체하기가 힘들다.
서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라서 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다. 여러 개의 JVM에 분산돼서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.
싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
싱글톤은 사용하는 클라이언트가 정해져 있지 않다. 싱글톤이 스태틱 메서드를 이용해 언제든지 싱글톤에 쉽게 접근할 수 있기 때문에 애플리케이션 어디서든지 사용될 수 있고, 그러다 보면 자연스럽게 정역 상태(global state)로 사용되기 쉽다. 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태를 갖는 것은 객체지향 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다.
스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 이것이 바로 싱글톤 레지스트리(singleton registry)다. 스프링 컨테이너는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너이기도 하다. 싱글톤 레지스트리의 장점은 스태틱 메서드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 점이다. 평범한 자바 클래스라도 IoC 방식의 컨테이너를 사용해서 생성과 관계설정, 사용 등에 대한 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있다. 오브젝트 생성에 관한 모든 권한은 IoC 기능을 제공하는 애플리케이션 컨텍스트에게 있기 때문이다.
스프링의 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다. 따라서 테스트 환경에서 자유롭게 오브젝트를 만들 수 있고, 테스트를 위한 목 오브젝트로 대체하는 것도 간단하다. 생성자 파라미터를 이용해서 사용할 오브젝트를 넣어주게 할 수도 있다.
가장 중요한 것은 싱글톤 패턴과 달리 스프링이 지지하는 객체지향적인 설계 방식과 원칙, 디자인 패턴(싱글톤 패턴은 제외) 등을 적용하는 데 아무런 제약이 없다는 점이다. 스프링은 IoC 컨테이너일 뿐만 아니라, 고전적인 싱글톤 패턴을 대신해서 싱글톤을 만들고 관리해주는 싱글톤 레지스트리라는 점을 기억해두자. 스프링이 빈을 싱글톤으로 만드는 것은 결국 오브젝트의 생성 방법을 제어하는 IoC 컨테이너로서의 역할이다.
싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다. 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는, 상태정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야 한다. 다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변구를 수정하는 것은 매우 위험하다. 물론 읽기전용의 값이라면 초기화 시점에서 인스턴스 변수에 저장해두고 공유하는 것은 아무 문제가 없다.
상태가 없는 방식으로 클래스를 만드는 경우에는 각 요청에 대한 정보나, DB나 서버의 리소스로부터 생성한 정보는 파라미터와 로컬 변수, 리턴 값등을 이용해서 다뤄야 한다. 메서드 안에서 생성되는 로컬 변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어쓸 일은 없다.
public class UserDao {
private static ConnectionMaker 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;
}
}
스프링의 싱글톤 빈으로 사용되는 클래스를 만들 때 인스턴스 필드로 선언해도 되는 것
동일하게 읽기전용의 속성을 가진 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋다. 물론 단순한 읽기전용 값이라면 static final이나 final로 선언하는 편이 낫다.
스프링에서 빈의 스코프(scope)란 스프링이 관리하는 오브젝트, 즉 빈이 생성되고, 존재하고, 적용되는 범위라는 의미이다.
스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입(Dependency Injection)이라고 불린다. 물론 스프링이 컨테이너이고 프레임워크이니 기본적인 동작원리가 모두 IoC 방식이라고 할 수 있지만, 스프링이 여타 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 용어를 사용할 때 분명하게 드러난다. 그래서 지금은 스프링이 DI 컨테이너라고 더 많이 불리고 있다.
A가 B를 의존한다고 가정해보자. 의존한다는 건 의존대상, 즉 B가 변하면 그것이 A에 영향을 미친다는 뜻이다. B의 기능이 추가되거나 변경되거나, 형식이 바뀌거나 하면 그 영향이 A로 전달된다는 뜻이다.
대표적인 예는 A가 B를 사용하는 경우, 예를 들어 A에서 B에 정의된 메서드를 호출해서 사용하는 경우다. 이럴 땐 '사용에 대한 의존관계'가 있다고 말할 수 있다. 만약 B에 새로운 메서드가 추가되거나 기능이 변경되면 A의 기능이 수행되는 데 영향을 끼칠 수 있다.
UserDao는 ConnectionMaker 인터페이스에만 의존하고 있다. 따라서 ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 직접적으로 받게 된다. 하지만 ConnectionMaker 인터페이스를 구현한 클래스는 변화가 생겨도 UserDao에 영향을 주지 않는다. 이렇게 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태인 결합도가 낮은 상태가 된다.
UML에서 말하는 의존관계란 이렇게 설계 모델의 관점에서 이야기 하는 것이다. 그런데 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼 수 있다.
인터페이스를 통해 설계 시점에 느슨한 의존관계를 갖는 경우에는 UserDao의 오브젝트가 런타임 시에 사용할 오브젝트가 어떤 클래스로 만든 것인지 미리 알 수가 없다. 프로그램이 시작되고 UserDao 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트(dependent object)라고 말한다.
의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.
정리하면 의존관계 주입이란 다음과 같은 세 가지 조건을 충족하는 작업을 말한다.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다. DI에서 말하는 제3의 존재는 바로 관계설정 책임을 가진 코드를 분리해서 만들어진 오브젝트라고 볼 수 있다.
전략 패턴에 등장하는 클라이언트나 앞에서 만들었던 DaoFactory, 또 DaoFactory와 같은 작업을 일반화해서 만들어졌다는 스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제3의 존재라고 볼 수 있다.
UserDao에 적용된 의존관계 주입 기술을 다시 살펴보자.
public UserDao() {
this.connectionMaker = new DConnectionMaker();
}
문제 : 인터페이스를 사이에 두고 의존관계를 느슨하게 만들긴 했지만, UserDao가 설계 시점에서 사용할 구체적인 클래스를 알고 있다.
해결 : IoC 방식을 써서 UserDao로부터 제3의 존재에 런타임 의존관계 결정 권한을 위임한다. 그래서 만들어진 것이 DaoFactory였다.
여기서 두 오브젝트 사이의 런타임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재이자 IoC 방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너다. 즉, 의존관계 주입을 담당하는 컨테이너로 DI 컨테이너이다.
DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고 이 생성자의 파라미터로 오브젝트의 레퍼런스를 전달해준다. 이렇게 생성자 파라미터를 통해 전달받은 런타임 의존관계를 갖는 오브젝트는 인스턴스 변수에 저장해둔다.
private static ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레페런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다고 해서 이를 의존관계 주입이라고 부른다. DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다.
스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다. 의존관계를 맺는 방법이 외부로부터의 주입이 아닌 스스로 검색을 이용하기 때문에 의존관계 검색(Dependency Lookup)이라고 불리는 것도 있다. 의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메서드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.
public UserDao() {
DaoFactory daoFactory = new DaoFactory();
this.connectionMaker = daoFactory().connectionMaker();
}
이렇게 해도 UserDao는 여전히 자신이 어떤 ConnectionMaker 오브젝트를 사용할 지 미리 알지 못한다. 또한 의존관계 주입이 아닌 검색은 스스로 IoC 컨테이너인 DaoFactory에게 요청하는 것이다. DaoFactory의 경우엔 단순 메서드 호출로 보이겠지만 이런 작업을 일반화한 스프링의 애플리케이션 컨텍스트라면 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 된다. 그 대상이 런타임 의존관계를 가질 오브젝트이므로 의존관계 검색이라고 부르는 것이다. 스프링의 IoC 컨테이너인 애플리케이션 컨텍스트의 getBean() 메서드가 의존관계 검색에 사용된다.
public UserDao() {
ApplicationContext context =
new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
의존관계 검색은 방법만 조금 다를 뿐 기존 의존관계 주입의 거의 모든 장점을 갖고 있다.
하지만 위의 예에서는 사용자에 대한 DB 정보를 어떻게 가져올 것인가에 집중해야 하는 UserDao에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여 있는 것은 어색하다. 따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다.
의존관계 검색은 UserDaoTest같은 테스트 코드에서 사용하기 좋다. 또한 애플리케이션 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. 스태틱 메서드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다.
의존관계 검색과 의존관계 주입을 적용할 때는 중요한 차이점이 하나 있다. 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다. UserDao에 getBean()을 사용한 의존관계 검색 방법을 적용했다고 해보자. 이 경우 ConnectionMaker만 스프링의 빈이면 되지, UserDao는 어딘가에서 new UserDao() 해서 만들어서 사용해도 된다. 반면에 의존관계 주입에선 DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다. 즉, UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 한다.
DI 받는다
단지 외부에서 파라미터로 주입해줬다고 해서 다 DI가 아니고, 주입받는 메서드 파라미터가 이미 특정 클래스 타입으로 고정되어 있다면 DI가 일어날 수 없다. DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야 한다.
실제 운영에 사용할 DB는 매우 중요하기 때문에, 개발 중에는 절대 사용하지 말아야 한다. 대신 개발자 PC에 설치한 로컬 DB로 사용해야 한다고 해보자. 그리고 어느 시점이 되면 지금까지 개발한 것을 그대로 운영서버로 배치해서 사용할 것이다. 그런데 이때 만약 DI 방식을 적용하지 않았다고 해보자. LocalDBConnectionMaker을 사용하다가 이를 서버에 배치하는 시점에서 ProductionDBConnectionMaker라는 클래스로 변경해줘야 한다. DI를 안 했으니 모든 DAO에는 new LocalDBCOnnectionMaker() 라는 코드가 들어 있을 것이다. DAO가 100개라면 최소한 100군데의 코드를 new ProductionDBConnectionMaker() 로 수정해줘야 한다.
반면에 DI 방식을 적용하면, 모든 DAO는 생성 시점에 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공받는다.
@Bean
public ConnectionMaker connectionMaker() {
return new LocalDBConnectionMaker을();
}
@Configuration이 붙은 DaoFactory를 사용한다고 하면 위처럼 만들어서 사용하면 된다. 이를 서버에 배포할 때는 어떤 DAO 클래스와 코드도 수정할 필요 없이, 단지 딱 한 줄만 변경하면 된다.
@Bean
public ConnectionMaker connectionMaker() {
return new ProductionDBConnectionMaker();
}
개발환경과 운영환경에서 DI의 설정정보에 해당하는 DaoFactory만 다르게 만들어두면 나머지 코드에는 전혀 손대지 않고 개발 시와 운영 시에 각각 다른 런타임 오브젝트에 의존관계를 갖게 해줘서 문제를 해결할 수 있다.
DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 해보자. 모든 DAO의 makeConnection() 메서드를 호출하는 부분에 카운터를 증가시키는 코드를 넣는 것은 엄청난 노가다다. 또한 DB 연결횟수를 세는 일은 DAO의 관심사항이 아니다.
DI 컨테이너에서라면 아주 간단히 해결할 수 있다. DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하는 것이다. 기존 코드 수정 없이 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의해주면 된다.
먼저 ConnectionMaker 인터페이스를 구현해서 CountingConnectionMaker를 만든다. DAO가 의존할 대상이 될 것이다.
public class CountingConnectionMaker implements ConnectionMaker {
int counter = 0;
private ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
this.realConnectionMaker = realConnectionMaker;
}
public Connection makeConnection() throws ClassNotFoundException, SQLException {
this.counter++;
return realConnectionMaker.makeConnection();
}
public int getCounter() {
return this.counter;
}
}
새로운 의존관계를 컨테이너가 사용할 설정정보를 이용해 만들어보자.
@Configuration
public class CountingDaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new CountingConnectionMaker(realConnectionMaker());
}
@Bean
public ConnectionMaker realConnectionMaker() {
return new DConnectionMaker();
}
}
이제 커넥션 카운팅을 위한 실행 코드를 만든다.
public class UserDaoConnectionCountingTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext context =
new AnnotationConfigApplicationContext(CountingDaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);
//
// DAO 사용 코드
//
CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
System.out.println("Connection counter : " + ccm.getCounter());
}
}
DI의 장점은 관심사의 분리(SoC)를 통해 얻어지는 높은 응집도에서 나온다. 모든 DAO가 직접 의존해서 사용할 ConnectionMaker 타입 오브젝트는 connectionMaker() 메서드에서 만든다. 따라서 CountingConnectionMaker의 의존관계를 추가하려면 이 메서드만 수정하면 된다. 또한 설정 클래스도 DaoFactory 설정 클래스를 CountingDaoFactory로 변경하기만 하면 된다.
의존관계 주입 시 생성자가 아닌 일반 메서드를 사용하는 방법이 더 자주 사용된다.
일반 메서드를 이용해 의존 오브젝트와의 관계를 주입해주는 방법은 크게 두 가지가 있다.
스프링은 전통적으로 메서드를 이용한 DI 방법 중 수정자 메서드를 가장 많이 사용해왔다.
수정자 메서드 DI를 사용할 때는 메서드의 이름은 메서드를 통해 DI 받을 오브젝트의 타입 이름을 따르는 것이 가장 무난하고 이 관례를 따르도록 하자.
public class UserDao {
private ConnectionMaker connectionMaker;
public void setConnectionMaker(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setConnectionMaker(connectionMaker());
return userDao;
}
스프링은 DaoFactory와 같은 자바 클래스를 이용하는 것 외에도, 다양한 방법을 통해 DI 의존관계 설정정보를 만들 수 있다. 가장 대표적인 것이 바로 XML이다.
XML의 장점
이제 DaoFactory 자바 코드에 담겨 있던, DI를 위한 오브젝트 의존관계 정보를 XML을 이용해 만들어보자.
스프링의 애플리케이션 컨텍스트는 XML에 담긴 DI 정보를 활용할 수 있다. DI 정보가 담긴 XML 파일은 <beans>
를 루트 앨리먼트로 사용한다. <beans>
안에는 여러 가지 <bean>
을 정의할 수 있다. XML 설정은 @Configuration
과 @Bean
이 붙은 자바 클래스로 만든 설정과 내용이 동일하다.
@Configuration
을<beans>
,@Bean
을<bean>
에 대응해서 생각하면 이해하기 쉬울 것이다.
하나의 @Bean
메서드를 통해 얻을 수 있는 빈의 DI 정보
@Bean
메서드 이름이 빈의 이름이다. 이 이름은 getBean()
에서 사용된다.XML에서 <bean>
을 사용해도 이 세 가지 정보를 정의할 수 있다. XML은 자바 코드처럼 유연하게 정의될 수 있는 것이 아니므로, 핵심 요소를 잘 짚어서 그에 해당하는 태그와 애트리뷰트가 무엇인지 알아야 한다.
먼저 DaoFactory의 connectionMaker() 메서드에 해당하는 빈을 XML로 정의해보자.
connectionMaker()로 정의되는 빈은 의존하는 다른 오브젝트는 없으니 DI 정보 세 가지 중 두 가지만 있으면 된다.
- DI 정의 세 가지 중 빈의 이름과 빈 클래스(이름) 두 가지를
<bean>
태그의 id와 class 애트리뷰트를 이용해 정의할 수 있다.<bean>
태그의 class 애트리뷰트에 쩡하는 것은 자바 메서드에서 오브젝트를 만들 때 사용하는 클래스 이름이라는 것을 주의하자. (메서드의 리턴 타입X)- class 애트리뷰트에 넣을 클래스 이름은 패키지까지 모두 포함해야 한다.
<bean>
태그 전환@Bean --------------------------------> <bean>
public ConnectionMaker
connectionMaker() { -------------------> id="connectionMaker"
return new DConnectionMaker(); ---> class="springbook...DConnectionMaker" />
}
이번에는 userDao 메서드를 XML로 변환해보자.
userDao() 에는 DI 정보의 세 가지 요소가 모두 들어있다. 여기서 관심을 가질 것은 수정자 메서드를 사용해 의존관계를 주입해주는 부분이다. 자바빈의 관례를 따라서 수정자 메서드는 프로퍼티가 된다. 프로퍼티 이름은 메서드 이름에서 set을 제외한 나머지 부분을 사용한다.
XML에서는 <property>
태그를 사용해 의존 오브젝트와의 관계를 정의한다.
<property>
태그는 name과 ref라는 두 개의 애트리뷰트를 갖는다. 예를 들어, @Bean 메서드에서 다음과 같이 다른 @Bean 메서드를 호출해서 주입할 오브젝트를 가져온다면
userDao.setConnectionMaker(connectionMaker());
<property>
에 대응하면 <property>
name="connectionMaker"
ref="connectionMaker"
<beans>
<bean id="connectionMaker" class="springbook.user.dao.DConnectionMaker" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="connetionMaker" ref="connectionMaker" />
</bean>
</beans>
같은 인터페이스를 구현한 의존 오브젝트를 여러 개 정의할 경우
같은 인터페이스를 구현한 의존 오브젝트를 여러 개 정의해두고 그 중에서 원하는 걸 골라서 DI 하는 경우도 있다. 이 때는 각 빈의 이름을 독립적으로 만들어두고 ref 애트리뷰트를 이용해 DI 받을 빈을 지정해주면 된다.<beans> <bean id="localDBConnectionMaker" class="..localDBConnectionMaker" /> <bean id="testDBConnectionMaker" class="..testDBConnectionMaker" /> <bean id="productionDBConnectionMaker" class="..productionDBConnectionMaker" /> <bean id="userDao" class="springbook.user.dao.UserDao"> <property name="connetionMaker" ref="localDBConnectionMaker" /> </bean> </beans>
DTD와 스키마
XML 문서는 미리 정해진 구조를 따라서 작성됐는지 검사할 수 있다. XML 문서의 구조를 정의하는 방법에는 DTD와 스키마(schema)가 있다. 스프링의 XML 설정파일은 이 두 가지 방식을 모두 지원한다. 특별한 이유가 없다면 스키마를 사용하는 편이 바람직하다.
- DTD를 사용할 경우
<beans>
엘리먼트 앞에 다음과 같은 DTD 선언을 넣어준다.<!DOCTYPE brans PUBLIC "-//SPRING//DTD BRAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
- 스키마를 사용할 경우
스프링의 DI를 위한 태그들은 각각 별개의 스키마 파일에 정의되어 있고 독립적인 네임스페이스를 사용해야만 한다. 따라서 이런 태그를 사용하려면 DTD 대신 네임스페이스가 지원되는 스키마를 사용해야 한다.<beans>
태그를 기본 네임스페이스로 하는 스키마 선언은 다음과 같다.<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">
이제 애플리케이션 컨텍스트가 DaoFactory
대신 XML 설정정보를 활용하도록 만들어보자.
GenericXmlApplicationContext
를 사용한다.GenericXmlApplicationContext
의 생성자 파라미터로 XML 파일의 클래스패스를 지정한다.applicationContext.xml
이라고 만든다.applicationContext.xml
<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="springbook.user.dao.DConnectionMaker" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="connetionMaker" ref="connectionMaker" />
</bean>
</beans>
다음은 UserDaoTest
의 애플리케이션 컨텍스트 생성 부분을 수정한다.
클래스패스를 시작하는 /는 넣을 수도 있고 생략할 수도 없다. /가 없는 경우에도 항상 루트에서부터 시작하는 클래스패스라는 점을 기억해두자.
ApplicationContext ac = new GenericXmlApplicationContext(
"applicationContext.xml");
참고
GenericXmlApplicationContext
외에도ClassPathXmlApplicationContext
를 이용해 XML로부터 설정정보를 가져오는 애플리케이션 컨텍스트를 만들 수 있다.
GenericXmlApplicationContext
는 클래스패스뿐 아니라 다양한 소스로부터 설정파일을 읽어올 수 있다.ClassPathXmlApplicationContext
는 XML 파일을 클래스패스에서 가져올 때 사용할 수 있는 편리한 기능이 추가된 것이다.
DataSource
인터페이스로 변환DataSource
인터페이스 적용IoC와 DI의 개념을 설명하기 위해 직접 DB 커넥션을 생성해주는 connectioMaker
인터페이스를 정의하고 사용했지만, 사실 자바에서는 DB 커넥션을 가져오는 오브젝트의 기능을 추강화해서 비슷한 용도로 사용할 수 있게 만들어진 DataSource
라는 인터페이스가 이미 존재한다. 또한 이미 다양한 방법으로 DB 연결과 풀링(pooling) 기능을 갖춘 많은 DataSource
구현 클래스가 존재한다.
DataSource
인터페이스와 다양한 DataSource
구현 클래스를 사용할 수 있도록 UserDao
를 리팩토링해보자.
DataSource
를 사용하는 UserDao
import javax.sql.DataSource;
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws SQLException {
Connection c = dataSource.getConnection();
...
}
...
}
다음은 DataSource
구현 클래스가 필요하다. 스프링이 제공해주는 DataSource
구현 클래스 중 테스트 환경에서 간단히 사용할 수 있는 SimpleDriverDataSource
라는 클래스를 사용하자.
SimpleDriverDataSource는 DB 연결에 필요한 필수 정보를 제공받을 수 있도록 여러개의 수정자 메서드를 갖고 있다. 예를 들어 JDBC 드라이버 클래스, JDBC URL, 아이디, 비밀번호 등이다.
먼저 DaoFactory 설정 방식을 이용해보자.
connectionMaker()
수정connectionMaker()
메서드를 dataSource()
로 변경하고 SimpleDriverDataSource
의 오브젝트를 리턴하게 한다. userDao()
수정UserDao
는 이제 DataSource
타입의 dataSource()
를 DI 받는다.DataSource
타입의 dataSource
빈 정의 메서드@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
dataSource.setUrl("jdbc://mysql://localhost/springbook");
dataSource.setUsername("spring");
dataSource.setPassword("book");
return dataSource;
}
DataSource
타입의 빈을 DI 받는 userDao()
빈 정의 메서드@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setDataSource(dataSource());
return userDao;
}
이렇게 해서 UserDao
에 DataSource
인터페이스를 적용하고 SimpleDriverDataSource
의 오브젝트를 DI로 주입해서 사용할 수 있는 준비가 끝났다.
id
가 connectionMaker
인 <bean>
을 dataSource
라는 이름으로 변경한다. class
를 SimpleDriverDataSource
의로 변경한다.<bean id="dataSource"
class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />
여기서 문제는 <bean>
설정으로 SimpleDriverDataSource
의 오브젝트를 만드는 것까지는 가능하지만, dataSource()
메서드에서 SimpleDriverDataSource
오브젝트의 수정자로 넣어준 DB 접속정보는 나타나 있지 않다는 점이다. 그렇다면 XML에서는 어떻게 해서 dataSource()
메서드에서처럼 DB 연결정보를 넣도록 설정을 만들 수 있을까?
다른 빈 오브젝트의 레퍼런스가 아닌 단순 정보도 오브젝트를 초기화하는 과정에서 수정자 메서드에 넣을 수 있다. 이때는 DI에서처럼 오브젝트의 구현 클래스를 다이내믹하게 바꿀 수 있게 해주는 것이 목적은 아니다. 대신 클래스 외부에서 DB 연결정보와 같이 변경 가능한 정보를 설정해줄 수 있도록 만들기 위해서다. 예를 들어 DB 접속 아이디가 바뀌었더라도 클래스 코드는 수정해줄 필요가 없게 해주는 것이다.
텍스트나 단순 오브젝트 등을 수정자 메서드에 넣어주는 것을 스프링에서는 '값을 주입한다'고 말한다. 이것도 성격은 다르지만 일종의 DI라고 볼 수 있다. 사용할 오브젝트 자체를 바꾸지는 않지만 오브젝트의 특성은 외부에서 변경할 수 있기 때문이다.
스프링의 빈으로 등록될 클래스에 수정자 메서드가 정의되어 있다면 <property>
를 사용해 정보를 주입한다. 하지만 다른 빈 오브젝트의 레퍼런스(ref
)가 아니라 단순 값(value
)을 주입해주는 것이기 때문에 value
애트리뷰트를 사용한다.
dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
dataSource.setUrl("jdbc://mysql://localhost/springbook");
dataSource.setUsername("spring");
dataSource.setPassword("book");
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/springbook"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>
url, username, password는 모두 스트링 타입이니 원래 텍스트로 정의되는 value 애트리뷰트의 값을 사용하는 것은 문제없다. 그런데 driverClass
는 java.lang.Class
타입이다. 어떻게 이 "com.mysql.cj.jdbc.Driver"
라는 스트링 값이 Class
타입의 파라미터를 갖는 수정자 메서드에 사용될 수 있는 것일까?
이런 설정이 가능한 이유는 스프링이 프로퍼티의 값을, 수정자 메서드의 파라미터 타입을 참고로 해서 적절한 형태로 변환해주기 때문이다. setDriverClass()
메서드의 파라미터 타입이 Class임을 확인하고 "com.mysql.cj.jdbc.Driver"
라는 텍스트 값을 com.mysql.cj.jdbc.Driver.class
오브젝트로 자동 변경해주는 것이다. 내부적으로 다음과 같이 변환 작업이 일어난다고 생각하면 된다.
Class driverClass = Class.forName("com.mysql.cj.jdbc.Driver");
dataSource.setDriverClass(driverClass);
스프링은 value
에 지정한 텍스트 값을 적절한 자바 타입으로 변환해준다. Integer
같은 기본 타입은 물론 Class
, URL
같은 오브젝트로 변환할 수도 있다. 또한 값이 여러 개라면 List
같은 것이나 배열 타입으로도 값의 주입이 가능하다.
<?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="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/springbook"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>
</bean>
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
1장에서는 사용자 정보를 DB에 등록하거나 아이디로 조회하는 기능을 가진 간단한 DAO 코드를 만들고, 그 코드의 문제점을 살펴본 뒤, 이를 다양한 방법과 패턴, 원칙, IoC/DI 프레임워크까지 적용해서 개선해왔다.
스프링이란 '어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크'라는 사실을 꼭 기억해두자. 스프링의 관심은 오브젝트와 그 관계다. 하지만 오브젝트를 어떻게 설계하고, 분리하고, 개선하고, 어떤 의존관계를 가질지 결정하는 일은 스프링이 아니라 개발자의 역할이며 책임이다. 스프링은 단지 원칙을 잘 따르는 설계를 적용하려고 할 때 필연적으로 등장하는 번거로운 작업을 편하게 할 수 있도록 도와주는 도구일 뿐임을 잊지 말자.