이 포스팅은 토비의 스프링 3.1을 읽고 글쓴이가 좀 더 이해하기 쉽도록 정리한 내용입니다.
이 포스팅의 대상은 토비의 스프링을 읽어보신 분들이 한 번쯤 다시 기억을 상기시키고자 하는 분들입니다.
글쓴이가 이미 알고 있는 내용에 대해서는 생략합니다. 또한 소스코드도 생략이 많으니 유의해서 읽어주시기 바랍니다.
스프링은 자바를 기반으로 한 기술이다. 스프링은 자바를 자바답게 사용하도록 명쾌한 기준을 마련해준다.
이 장에서는 자바를 이용해 오브젝트와 오브젝트의 의존관계를 구현하면서 문제가 되었던 부분을 스프링에서는 어떠한 초점을 가지고 오브젝트가 어떻게 설계와 구현, 사용되어야 하는지를 설명한다.
토비의 스프링을 읽다 보면 좋은 글귀가 많다. 1-2. DAO의 분리에서 설명하는 내용을 먼저 읽어보고 1장에 들어가자.
변화는 먼 미래에만 일어나는 게 아니다. 며칠 내에, 때론 몇 시간 후에 변화에 대한 요구가 갑자기 발생할 수 있다. 객체지향 설계와 프로그래밍이 이전의 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다. 객체 지향 기술은 흔히 실세계를 최대한 가깝게 모델링 해낼 수 있기 때문에 의미가 있다고 여겨진다. 하지만 그보다는 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는 데 더 의미가 있다.
- 토비의 스프링
1-1. 초난감 DAO
1-1장에서는 사용자의 정보를 표현할 User 클래스와 사용자의 정보를 실제로 저장할 DB 테이블인 USER 테이블, 그리고 사용자의 정보를 DB에 넣고 관리할 수 있는 UserDao 클래스를 생성한다.
UserDao 클래스는 JDBC를 이용한 가장 기본적인 코드 방식으로 add()와 get()를 구현하였다.
public class UserDao{
public void add(User user) throws ClassNotFoundException, SQLException {
// 1. DB 연결
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(...);
// 2. SQL 수행
PreparedStatement ps = c.prepareStatement("INSERT INTO USER(ID, NAME, PASSWORD) VALUES(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
ps.executeUpdate();
// 3. 리소스 반납
ps.close();
c.close();
}
...
테스트를 위해 main() 메서드를 구현하여 테스트하였다.
이렇게 JDBC를 이용한 가장 기본적인 코드 방식으로 작성하고, main() 메서드로 테스트를 하면 무엇이 문제가 될까?
하나씩 개선해보자.
1-2. DAO의 분리
관심사의 분리(Separation of Concerns)
- 프로그래밍의 기초 개념 중에 하나인 관심사의 분리
- 변화는 대체로 집중된 한 가지 관심에 대해서만 일어난다. 따라서 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리를 하게 되면 변화에 대처하기 쉽다.
UserDao.add()를 다시 봐보자. add의 관심사는 몇 개일까? 어찌 보면 사용자 정보를 DB에 저장하기 위한 관심사 하나로 생각할 수 있지만, 좀 더 작게 세분화하면 다음과 같다.
- DB와 연결을 위한 커넥션에 대한 관심사 : DB가 mysql이 아니라면? DB 접속 정보가 달라진다면?
- SQL 문장 : SQL 문장을 변경해야 한다면? 바인딩 변수가 달라진다면?
- 리소스 반납(close)
첫 번째 관심사부터 보자.
DB와 연결을 위한 커넥션에 대한 코드는 SQL을 수행하려는 곳에서는 항상 필요하다. 만약 중복으로 코드를 작성한 상태에서 DBMS를 변경하던지, DB 접속 정보가 달라진다면 모든 곳을 다 고쳐야 한다.
따라서 가장 기본적인 메서드 추출 기법으로 중복 코드를 제거한다.
public class UserDao{
public void add(User user) throws ClassNotFoundException, SQLException {
// 1. DB 연결
Connection c = getConnection()
...
}
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection(...);
}
중복 코드는 제거되었다. 하지만 DBMS정보나 접속 정보가 변경되면 UserDao를 수정해야 한다. 그렇다면 DB 연결 정보는 필요한 곳에서 직접 구현하도록 하면 어떨까?
간단하게 UserDao를 추상 클래스로 변경하면 가능하다.
public abstract class UserDao{
public void add(User user) throws ClassNotFoundException, SQLException {
// 1. DB 연결
Connection c = getConnection()
...
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
위와 같이 추상 클래스를 만들어서 서브 클래스에서 필요에 맞게 메서드를 구현해서 사용하도록 하는 방법을 템플릿 메서드 패턴(template method pattern)이라고 한다. 이렇게 변경하게 되면 DB와 연결을 위한 커넥션에 대한 관심사는 UserDao가 아닌 하위 계층 클래스로 독립되었다.
또한 위의 코드를 다시 보면 getConnection()이라는 메서드는 java.sql.Connection 인터페이스를 구현한 클래스를 하위 클래스로부터 리턴 받는다. 이렇게 하위 클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 방식을 팩토리 메서드 패턴(factory method pattern)이라고 한다.
팩토리 메서드 패턴과 템플릿 메서드 패턴은 둘 다 상속을 통해서 기능을 확장하게 하는 패턴이기 때문에 디자인 패턴을 처음 접하면 헷갈린다. 간단하게 구별하자면 팩토리 메서드 패턴은 객체를 생성하는 관심사를 메서드로 빼놓는 방식이며, 템플릿 메서드 패턴은 부모 클래스에서 구현한 기능에 대해서 추가적인 기능을 확장할 수 있도록 메서드로 빼놓는 방식이다. 따라서 템플릿 메서드 패턴에서 만약 추가적인 기능이라는 것이 객체를 생성하는 기능이라면 템플릿 메서드 패턴이면서 팩토리 메서드 패턴인 것이다.
하지만 자바를 조금 공부해봤다면 상속은 그다지 좋지 않은 구현 방식이라는 이야기를 들어본 적이 있을 것이다. 그 이유는 다음과 같다.
- 상속은 다중 상속을 허용하지 않으므로, 후에 다른 목적으로 상속을 적용하기 힘들다.
- 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다. 부모와 자식 계층으로 관심을 분리하긴 하였지만 결국엔 크게 보면 긴밀한 결합을 허용한다. 예를 들어 자식 클래스는 부모 클래스의 기능을 직접 사용할 수 있으므로 부모 클래스의 변경이 있을 때 자식 클래스도 수정하거나 다시 개발해야 할 수도 있다.
1-3. DAO의 확장
1-2장에서는 관심사를 분리하기 위해 DB와 연결을 위한 커넥션에 대한 관심사를 메서드로 추출해보았고, 이후에는 템플릿 메서드 패턴 또는 팩토리 메서드 패턴을 이용해 관심사를 분리하였다. 하지만 상속이라는 단점이 있으므로 개선해보자.
상속이 단점이 있다면, 상속관계가 아닌 완전히 독립적인 클래스로 나눠보자.
- UserDao.getConnection() 코드를 그대로 가지고와 SimpleConnectionMaker.makeNewConnection()으로 독립된 클래스를 만든다.
- UserDao에서 SimpleConnectionMaker를 인스턴스 변수로 관리하도록 선언하고, 생성자로 초기화하는 로직을 추가한다.
- 이후 UserDao.getConnection() 호출한 부분을 SimpleConnectionMaker.makeNewConnection()으로 변경한다.
public class UserDao {
private SimpleConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
// 1. DB 연결
Connection c = connectionMaker.makeNewConnection();
...
}
}
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
...
}
}
이제 UserDao는 SimpleConnectionMaker를 가지고 DB와 연결을 위한 커넥션을 가지고 온다. 하지만 만약 DB와 연결을 위한 커넥션이 변경되어 SimpleConnectionMaker가 아닌 NewConnectionMaker로 변경해야 한다면 UserDao 코드를 변경해야 한다. 또한 UserDao는 SimpleConnectionMaker의 어떠한 메서드가 DB와 연결을 위한 커넥션을 가지고 오는 메서드인지 알아야 하는 문제점이 있다.
감이 오는가? 자바를 공부한다면 항상 나오는 이야기이다. UserDao와 SimpleConnectionMaker는 강한 결합이 되어있으며, 메서드에 대한 정의를 알 수 없다. 따라서 이제 자바의 인터페이스를 사용해보자.
인터페이스는 자바가 추상화를 위해 제공하는 도구이다. 인터페이스를 사용하는 곳에서는 인터페이스만 알면 어떤 구현 클래스가 오던지 상관없이, 공통된 메서드를 호출하여 사용할 수 있다.
이제 인터페이스를 만들어보자.
- ConnectionMaker 인터페이스를 만들고 makeNewConnection() 메서드를 정의한다.
- SimpleConnectionMaker를 ConnectionMaker 인터페이스를 구현하도록 한다.
- UserDao에서 SimpleConnectionMaker를 인스턴스 변수로 관리하도록 선언했던 부분을 ConnectionMaker로 변경한다.
public interface ConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}
public class SimpleConnectionMaker implements ConnectionMaker {
@Override
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
...
}
}
public class UserDao {
private ConnectionMaker connectionMaker;
... // 이전과 동일
}
인터페이스를 적용해보았다. 하지만 위처럼 변경하였지만 앞에서 언급한 문제가 하나는 해결되지 않았다. SimpleConnectionMaker가 아니라 NewConnectionMaker를 사용하고 싶다면 객체를 생성했던 생성자 부분을 변경해줘야 한다.
위 문제는 어떻게 해결해야 할까? 사실 이 책이 스프링 책이라서 처음에는 어라 어떻게 해야 하지?라고 생각할 수도 있지만, 이 책이 자바 책이었다면 바로 생각할 수 있을 것이다. 간단하다. 생성자에 인자를 추가하여 ConnectionMaker 구현체를 입력받거나 setter를 통해 구현체를 할당하면 된다.
생성자를 통해 ConnectionMaker 구현체를 입력받는다는 의미는 UserDao가 어떤 구현체를 사용할지 결정하는 것이 아닌 UserDao를 사용하는 사람(클라이언트)이 정하도록 하는 것이다. 즉 이것도 어떤 구현체를 사용할지의 관심사를 UserDao가 아닌 다른 곳으로 분리하는 개념으로 보면 된다. 이것을 관계 설정 책임의 분리라고 한다.
관계 설정은 오브젝트들 간의 관계를 설정해주는 것을 의미하여 책임의 분리는 제 3자가 두 오브젝트 간의 관계를 설정한다는 의미이다.
한번 생성자를 통해 관계 설정 책임을 분리해보자.
- UserDao 생성자에 ConnectionMaker를 입력받도록 변경하고, 입력받은 값을 세팅한다.
- UserDao를 사용하는 클라이언트에서 원하는 구현체를 입력하여 UserDao를 객체를 생성한다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
... // 동일
public class UserDaoTest {
public static void main(String[] args) {
UserDao userDao = new UserDao(new SimpleConnectionMaker());
}
}
이 책은 스프링에 대해서만 다루지 않는다. 한 번쯤 알아두면 좋은 내용들에 대해서도 중간중간 설명을 끼워 넣는다.
지금까지 개선해온 내용을 가지고 객체지향 기술의 여러 가지 이론을 설명한다. 아래 내용들은 객체 지향을 설명하는 곳에서는 항상 나오는 내용이므로 잘 기억하도록 하자.
개방 폐쇄 원칙(Open-closed Principle)
- OCP로 줄여서 말하며 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다는 의미이다.
말은 쉽지만 어렵다.
- UserDao는 이제 DB 연결 설정 관점에서 OCP를 따른다. DB 연결 설정을 변경(확장)은 쉽게 가능하면(열려있으면)서 UserDao는 코드는 변경되지 않는다(닫혀있다).
- UserDao가 OCP를 따르도록 하는 주요한 이유는 추상화인 인터페이스를 사용하였기 때문이다. 일반적으로 인터페이스를 사용해 OCP 원칙을 따르게 할 수 있다.
- OCP는 객체지향 설계 원칙 다섯 개중 하나인 원칙이다. 객체지향 설계 원칙을 SOLID로 줄여서 말하며 각각은 다음과 같다.
- SRP(Single Responsibility Principle) : 단일 책임 원칙으로 한 클래스나 단 하나의 책임만을 가져야 한다.
- OCP(Open Closed Principle) : 개방 폐쇄 원칙으로 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
- LSP(Liskov Substitution Principle) : 리스 코프 치환 원칙으로 자식 클래스는 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다.
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙으로 인터페이스는 꼭 필요한 메소들만 선언되어 있어야 한다. 즉 크게 만들지 말고 작게 만들어야 한다.
- DIP(Dependency Inversion Principle) : 의존관계 역전 원칙
높은 응집도와 낮은 결합도(High coherence and low coupling)
- 응집도가 높으면, 변화가 일어날 때 변하는 부분이 크기 때문에 한 번에 확인할 수 있으므로, 어디를 고쳐야 할지도 알기 쉬우며 테스트도 그 해당 부분만 하면 된다.
- 1-1장에서 본 UserDao는 여러 관심사가 얽혀 있었다. 즉 같은 관심 사끼 리 응집해있지 않았으므로, DB 연결 설정에 대한 코드를 수정해야 한다면, 어디를 고쳐야 할지 알기 쉽지 않았고, 테스트도 전체적으로 다시 해야 했다.
- 하지만 관심사를 분리하여 UserDao는 SQL에만 관심을 가지도록 응집도를 높이고, ConnectionMaker 구현 클래스에서는 DB 연결 설정에 대한 관심을 가지도록 응집도를 높였다.
- 오브젝트의 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도(결합도)가 낮아야 한다. 결합도가 낮다면 다른 오브젝트가 변경이 일어난다 하더라도 다른 오브젝트는 변화가 없을 것이다.
- UserDao는 ConnectionMaker 인터페이스를 통해 결합도를 낮췄다. ConnectionMaker의 구현체가 변경되더라도 UserDao는 변화될 필요가 없다.
전략 패턴(Strategy Pattern)
- 전략 패턴이란 어떠한 행위를 할 때 사용할 전략을 쉽게 변경할 수 있도록 하기 위한 패턴이다. 예를 들어 전통 게임인 스타크래프트 테란으로 게임을 할 때, 건물을 짓는 순서를 하나의 행위라고 한다면, 어떠한 순서(전략)로 건물을 지을지는 게이머가 결정한다.
- 전략 패턴의 정의는 자신의 기능(행위)에서 필요에 따라 변경이 필요한 알고리즘(전략)을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘(전략) 클래스를 필요에 따라 외부에서 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
- UserDao는 DB 연결 설정이라는 기능을 ConnectionMaker 인터페이스를 통해 해당 구현체가 제공하는 알고리즘(전략)을 UserDaoTest(외부)에서 전달받기 때문에 전략 패턴에 해당한다고 볼 수 있다.
1-4. 제어의 역전(IoC)
이제 UserDaoTest-UserDao-ConnectionMaker 구조로 리팩터링을 하였다. 그런데 한번 UserDaoTest를 다시 봐보자!
UserDaoTest는 UserDao가 사용할 특정 ConnectionMaker의 구현체를 생성해서 UserDao 객체를 생성하고 나서 add(), get() 등을 테스트하는 클래스이다. 다시 보니 UserDaoTest는 객체를 생성하는 관심사와 테스트하는 관심사, 즉 관심사가 두 개다! 처음부터 해왔던 관심사의 분리인데.. 또다시 관심사가 모여있다니.. 어쩔 수 없다. 다시 관심사를 분리해보자. 메서드 추출보다는 독립된 클래스로 만들어보자.
일반적으로 객체지향에서 객체를 생성하는 목적으로 만들어진 클래스를 팩토리(Factory) 클래스라고 한다. 물건을 여러 가지 생산해내는 공장처럼 객체를 여러 가지 생성하기 때문에 붙여진 이름이다. UserDaoTest의 객체를 생성하는 관심사를 팩토리 클래스로 빼내어 만들어보자.
- DaoFactory 클래스를 만들고 DaoFactory.userDao() 메서드를 만들어 UserDaoTest의 UserDao 객체 생성 로직을 작성하자.
- UserDaoTest에서는 DaoFactory.userDao()를 이용하여 UserDao 객체를 얻는다.
public class DaoFactory {
public UserDao userDao() {
return new UserDao(new SimpleConnectionMaker());
}
}
public class UserDaoTest {
public static void main(String[] args) {
// 관계 설정
UserDao userDao = new DaoFactory().userDao();
// UserDao 테스트
}
}
현재 Dao는 UserDao 뿐이기 때문에 DaoFactory에는 메서드가 하나뿐이지만, Dao가 추가된다면 메서드를 추가하여 다른 Dao 객체를 만드는 로직을 넣으면 된다. 하지만 어떤 ConnectionMaker 구현체를 사용할지는 코드가 중복되므로 메서드로 추출해서 작성하자.
public class DaoFactory {
public UserDao userDao() {
return new UserDao(connectionMaker());
}
public DeptDao deptDao(){
return new DeptDao(connectionMaker());
}
public ConnectionMaker connectionMaker(){
return new SimpleConnectionMaker();
}
}
지금까지 객체를 생성해주는 DaoFactory를 구현해보았다. DaoFactory는 DaoFactory는 애플리케이션의 어떠한 비즈니스적인 로직은 담고 있지 않는다. 애플리케이션에서 사용할 UserDao와 ConnectionMaker의 관계를 설정하였다. 즉 DaoFactory는 객체 간의 관계를 정의한 설계도와 같은 역할을 하였다.
1-1장에서 구현한 UserDao를 보게 되면, UserDao는 직접적으로 자신이 어떠한 객체를 사용할지를 직접 결정(제어)하였다. 어찌 보면 이렇게 구현하는 게 이해하기 쉽고 직관적이다. 하지만 우리는 리팩터링 과정을 통해 이제는 DaoFactory가 그 역할을 대신(제어를 역전)하게 되었다.
이것이 바로 제어의 역전이다. 스프링을 공부한 적이 없다고 하더라도 스프링은 IoC(제어의 역전) 사용한다는 이야기는 많이 들어봤을 것이다. 그게 바로 이것이다(?)
사전적인 정의로는 다음과 같다.
제어의 역전이란 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임한다.
지금까지 짠 코드를 보면 스프링은 하나도 사용하지 않고 순수한 자바로만 작성하였다. 이렇듯 제어의 역전은 스프링이 나오기도 전에 사용된 용어이며 스프링이 아닌 일반적인 개념으로 보면 된다.
IoC를 적용함으로써 설계가 깔끔해지고 유연성이 증가하며 확장성이 좋아지기 때문에 필요할 때면 IoC 스타일의 설계와 코드를 만들어 사용하면 된다.
스프링은 IoC를 모든 기능의 기초가 되는 기반기술로 삼고 있으며, IoC를 극한까지 적용하고 있는 프레임워크이다.
- 토비의 스프링
1-5. 스프링의 IoC
지금까지 순수 자바로 IoC 방식대로 구현하였다. 이번장에서는 순수 자바로 구현한 코드를 기준으로 스프링에서는 어떻게 IoC를 구현(설정)하는지를 설명한다.
먼저 설명하기 전에 스프링에서 사용하는 용어를 나열한다. 일단 나열만 하고 설명은 하나씩 내용에서 하도록 한다.
스프링 IoC 용어 정리
- 빈(bean) : 스프링이 IoC 방식으로 관리하는 오브젝트
- 빈 팩토리(bean factory) : 스프링의 IoC를 담당하는 핵심 컨테이너. 빈 팩토리를 바로 사용하지 않고 이를 확장한 애플리케이션 콘텍스트를 이용한다.
- 애플리케이션 콘텍스트(application context) : 빈 팩토리를 확장한 IoC 컨테이너다.
- 설정 정보/설정 메타정보(configuration metadata) : IoC를 적용하기 위해 사용하는 메타정보
- 컨테이너(container) 또는 IoC 컨테이너 : IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 콘텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 한다.
스프링이 관리하는 오브젝트를 빈(Bean)이라고 한다. 그렇다면 우리가 관계를 맺어주기 위해 관리했던 클래스는 무엇인가? UserDao와 ConnectionMaker이다. 따라서 이 두 클래스는 이제 스프링에서 관리해야 하니 빈으로 만들어야 한다. 빈으로 만드는 방법은 간단하다. 해당 객체를 리턴하는 메서드에 @Bean 어노테이션만 추가하면 된다.
public class DaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public DeptDao deptDao(){
return new DeptDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker(){
return new SimpleConnectionMaker();
}
}
스프링은 오브젝트 간에 관계를 정의하는 내용을 담고 있는 빈 팩토리(Bean Factory) 또는 애플리케이션 콘텍스트(Application Context)을 가지고 있다. 그렇다면 관계를 담고 있는 빈 팩토리에 관계를 넣어주기 위해서는 어떻게 해야 할까? 당연하게도 관계 설정을 먼저 해야 한다. 그렇다면 우리가 관계를 맺어주도록 설정했던 클래스는 무엇인가? 바로 DaoFactory이다. 따라서 DaoFactory를 빈 팩토리에 넣어줘야 한다. 넣어주는 방법은 아래와 같다.
- DaoFactory 클래스에 @Configuration 어노테이션을 붙인다.
- 빈 팩토리 또는 ApplicationContext에 DaoFactory를 넣어준다.
@Configuration
public class DaoFactory {
...
public class UserDaoTest {
public static void main(String[] args) {
// 관계 설정
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao" , UserDao.class);
// UserDao 테스트
...
}
}
@Configuration 어노테이션은 해당 클래스가 스프링에서 사용할 어떠한 설정을 담당하고 있다고 표현하는 어노테이션이다. 여기서는 관계 설정을 담당하므로 해당 어노테이션이 붙여졌다.
위 소스를 보면 "userDao"로 하드 코딩된 부분이 있는데 이것은 빈의 이름이다. @Bean 어노테이션을 메서드에 붙였는데, 이때 메서드명이 빈의 이름이 된다.
최종적으로 ApplicationContext.getBean()을 통해서 userDao() 메서드가 호출되게 되고 UserDao 빈이 생성되어 얻게 된다.
스프링을 사용하면 더 편할 줄 알았지만, 오히려 구현한 순수 자바에 이것저것 추가되었다. 그렇다면 왜 스프링을 사용할까?
먼저 팩토리가 여러 개일 때를 생각해보자. DaoFactory뿐만 아니라 ServiceFactory 등 다양한 팩토리가 존재했을 때, 클라이언트는 자신이 필요한 객체를 얻기 위해 어느 팩토리에 해당 객체가 있는지 클래스를 알아야 한다. 하지만 스프링은 팩토리를 관리하는 것이 아닌 위에서 언급한 빈 팩토리 또는 애플리케이션 콘텍스트라는 설정을 모두 담고 있는 객체를 사용한다. 따라서 자신이 필요한 객체를 얻기 위해서 빈 팩토리 또는 애플리케이션 콘텍스트만 알면 된다.
DaoFactory는 단순하게 객체를 생성하는 역할만 제공한다. 하지만 스프링에서는 객체 생성뿐만 아니라, 이외의 부가적인 기능을 제공한다. 부가적인 기능들은 2장부터 설명하는 내용들이다.
DaoFactory는 객체를 생성하기 위해 메서드를 호출한다. 하지만 스프링은 빈을 검색하는 다양한 방법을 제공한다. 예를 들어 getBean()뿐만 아니라, 타입 또는 애노테이션만으로도 찾을 수 있다.
1-6. 싱글톤 레지스트리와 오브젝트 스코프
과연 스프링을 이용해서 getBean()으로 UserDao를 얻는 것과 이전 DaoFactory의 메서드를 호출하여 UserDao를 얻는 것의 차이는 무엇일까?
getBean()을 하면 결국에는 DaoFactory.userDao() 메서드가 호출되는 것이니 결국에는 동일한 것 아닌가? 라고 생각할 수 있다. 하지만 다르다. 무엇이 다른지 보자.
일반적으로 객체를 new 연산자를 사용해서 생성하면 논리적으로는(Object.equals()) 같을 수는 있어도 실제 메모리를 비교했을 때는 서로 다른 주소를 갖고 있는 객체이다. 아래 예제를 보면 값이 다르다.
public static void main(String[] args) {
DaoFactory daoFactory = new DaoFactory();
UserDao dao1 = daoFactory.userDao();
UserDao dao2 = daoFactory.userDao();
System.out.println(dao1); // spring.toby.study.chapter1.UserDao@15db9742
System.err.println(dao2); // spring.toby.study.chapter1.UserDao@6d06d69c
}
getBean()을 이용해도 DaoFactory.userDao()를 호출하는 것이니 위와 같은 결과가 나올 것으로 생각되지만 그렇지 않다. 예제를 보자.
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao1 = context.getBean("userDao", UserDao.class);
UserDao dao2 = context.getBean("userDao", UserDao.class);
System.out.println(dao1); // spring.toby.study.chapter1.UserDao@309e345f
System.out.println(dao2); // spring.toby.study.chapter1.UserDao@309e345f
}
dao1과 dao2가 동일한 주소 값을 가지고 있다. 결국 같은 객체이다. 그 이유는 다음과 같다.
- 스프링은 getBean()을 사용하여 호출할 때 어느 저장소에 해당 빈이 존재하는지 확인한다.
- 존재한다면 해당 빈을 전달한다.
- 존재하지 않는다면 빈을 생성한다.
마치 해당 객체는 애플리케이션에서 오직 단 하나만 존재할 수 있도록 관리하고 있는 것 같다. 싱글톤처럼!
그렇다. 스프링은 싱글톤을 좋아하고 지지한다. 그래서 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 1번에서 어느 저장소라는 것은 싱글톤 레지스트리라는 저장소이다.
싱글톤은 안티 패턴이라고 하는 사람도 있고, 옹호론자도 있다. 그 이유는 서로 각각 다양하다. 즉 단점이 있다는 것인데 스프링은 이런 단점을 보완하기 위해 직접 싱글톤을 관리한다. 따라서 우리는 싱글톤을 위해서 클래스를 구현할 때 어떠한 작업도 할 필요가 없다.
어떠한 추가적인 작업을 할 필요가 없긴 하지만, 클래스를 구현할 때 조심스럽게 구현해야 한다. 싱글톤이라는 말은 항상 같은 객체를 사용한다. 만약 A 스레드와 B 스레드가 getBean()을 통해 동일한 빈을 가지고 와서 해당 빈을 동시에 수정하거나 작업을 하게 되면 꼬일 수가 있다. 즉 싱글톤은 멀티스레드 환경에서 주의해야 한다.
멀리스 레드에서 빈에 대해 컨트롤하기가 힘들다면 클래스를 stateless 또는 immutable 하게 구현하면 된다. 그렇게 되면 해당 빈은 변경은 일어나지 않기 때문에 여러 스레드가 동시에 사용한다고 해도 문제가 없다. 마치 읽기 모드만 가능한 문서처럼 말이다.
하지만 가끔씩은 getBean()을 해오면 항상 새로운 객체를 받고 싶은 환경도 있을 것이다. 따라서 스프링에서는 이것을 설정으로 정할 수 있는데, 이를 스코프(Scope)라고 한다. 스코프 종류는 아래와 같다.
스코프(Scope) 종류 (카카오 면접 때 물어봤던 기억이 난다)
- 싱글톤(singleton) : 오직 컨테이너당 한 개의 객체만 생성하여 사용
- 프로토타입(prototype) : 요청할 때마다 매번 새로운 객체를 생성하여 사용
- 요청(request) : HTTP 요청마다 매번 새로운 객체를 생성하여 사용
- 세션(session) : 웹의 세션마다 객체를 생성하여 사용
1-7. 의존관계 주입(DI)
IoC와 마찬가지로 스프링을 공부한 적이 없다고 하더라도 스프링의 특징 중 DI(Dependency Injection), 즉 의존관계 주입이라는 이야기를 들어봤을 것이다.
IoC는 폭넓게 사용되는 용어이기 때문에 좀 더 스프링의 의도가 명확히 드러나는 DI라는 명칭을 스프링에서 사용하게 되었다.
그렇다면 의존 관계는 무엇인가? 아주 심플하다. 어떤 객체가 누구를 사용하냐에 따라 달려있다. 지금까지 구현한 예를 따지면 UserDao는 DB 연결을 위해 ConnectionMaker에 의존하는 관계를 맺고 있다.
그런데 ConnectionMaker는 인터페이스이며 어떤 구현체를 사용할지는 UserDao만을 봐서는 알 수가 없다. 즉 UserDao가 사용할 구현체는 런타임 시에 DaoFactory를 설정으로 사용한 애플리케이션 콘텍스트, 즉 제3의 존재가 결정하고 주입한다. 따라서 이를 DI라고 한다.
DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다. 스프링 컨테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져 있다. 그래서 스프링을 IoC 컨테이너 외에도 DI 컨테이너 또는 DI 프레임워크라고 부르는 것이다.
- 토비의 스프링
의존관계 주입뿐만 아니라, 의존관계를 맺는 방법을 스스로 검색을 이용하는 방법인 의존관계 검색이 있다.
의존관계 주입은 제 3자가 의존할 객체를 주입하는 것이다. 따라서 주입받을 때는 다양한 방식이 있겠지만 위 에제에서는 UserDao의 생성자에 ConnectionMaker를 인자로 받게 하여 제 3자로부터 주입받았다. 하지만 의존관계 검색은 제 3자가 주입하는 것이 아닌 자신이 직접 애플리케이션 콘텍스트에서 빈을 찾아 설정하는 방식이다. 여기서 의문이 생긴다. 지금까지 IoC와 DI의 장점을 설명하고, 관계를 외부에서 주입한다고 하였는데, 결국에는 다시 해당 클래스가 직접 빈을 찾아 설정한다니.. 처음 읽으면 이게 무슨 말인가 싶었다.
하지만 의존관계를 자신이 바로 설정하는 것이 아닌 빈을 검색해서 설정하는 방식이다. 즉 애플리케이션 콘텍스트에서 빈을 가져올 때, 그 빈에는 어떤 객체가 있는지는 알 수 없다. 검색해서 사용할 뿐이다.
의존 관계 검색은 사실 위 예제에서 해보았다. 바로 getBean()을 통해서 가져오는 방식이 의존관계 검색이었다.
public class UserDaoTest {
public static void main(String[] args) {
// 관계 설정
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao" , UserDao.class);
// UserDao 테스트
...
}
}
일반적으로는 UserDao와 ConnectionMaker처럼 의존관계 주입을 통해서 의존관계를 설정한다. 하지만 애플리케이션을 시작 처음 main에서는 main을 제 3자가 의존성 주입할 수는 없으므로, 의존관계 검색을 사용한다.
DI의 개념을 알아보았다. 그렇다면 제 3자가 의존관계를 주입해주면 무엇이 좋은가?
- 기능 구현의 교환 : 기능 구현을 변경해야 한다면 자신의 소스는 변경할 필요 없이 기능이 변경된 객체를 제 3자가 주입해주면 된다. UserDao에서 DB 접속 정보가 바뀌어도, connectionMaker()의 빈을 다른 객체로 변경해주면 UserDao는 아무 변경 없이 기능이 변경된다. 아래 예제는 기존 SimpleConnectionMaker 구현체를 리턴하는 빈을 NewConnectionMaker 구현체를 리턴하도록 변경한 예제이다.
public class DaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public DeptDao deptDao(){
return new DeptDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker(){
return new NewConnectionMaker();
}
}
- 부가기능 추가 : 위에 내용이랑 비슷하다. 추가적인 기능의 추가를 새로운 클래스를 만들어서 대체하는 것이 아닌 기존 객체를 인스턴스 변수로 활용해서 기능을 추가하는 방식이다. 예를 들어 ConnectionMaker.makeConnection()을 호출할 때마다 로깅을 하는 부가기능을 추가하고 싶다면 아래와 같다.
public class LoggingConnectionMaker implements ConnectionMaker {
private final ConnectionMaker connectionMaker;
public LoggingConnectionMaker(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
System.out.println("logging");
return connectionMaker.makeNewConnection();
}
}
의존관계 주입은 여러 가지 방식으로 할 수 있다. 지금까지는 아래와 같이 생성자를 사용해서 의존관계를 주입하였다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
... // 동일
일반 메서드로 가능하다. 아래는 setter 메서드로 변경하여 수정된 코드이다.
public class UserDao {
private ConnectionMaker connectionMaker;
public void setConnectionMaker(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
... // 동일
}
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setConnectionMaker(connectionMaker());
return userDao;
}
... // 동일
}
1-8. XML을 이용한 설정
우리는 DaoFactory에 @Configuration 어노테이션을 붙여 의존 관계 설정으로 사용하였다. 하지만 의존관계 설정을 위해 자바 코드로 하나씩 타이핑하기도 귀찮고, 빌드하고 다시 컴파일하기도 귀찮다.
그래서 스프링은 다양한 방법을 통해서 의존관계 설정 정보를 정의할 수 있는데, 그중 대표적인 게 XML이다.
XML 설정 방식에 대한 설명은 생략하고 DaoFactory에서 설정한 의존관계 설정을 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="myConnectionMaker" class="spring.toby.study.chapter1.SimpleConnectionMaker"></bean>
<bean id="newConnectionMaker" class="spring.toby.study.chapter1.NewConnectionMaker"></bean>
<bean id="userDao" class="spring.toby.study.chapter1.UserDao">
<property name="connectionMaker" ref="myConnectionMaker" />
</bean>
</beans>
빈은 <bean>으로 등록하고 의존 관계 주입은 <property>를 통해서 원하는 관계를 설정해주면 된다.
이제 UserDaoTest에서 DaoFactory가 아닌 위에서 만든 XML을 관계 설정으로 사용하도록 애플리케이션 콘텍스트 객체를 만드는 방법은 아래와 같다.
- XML 설정 방식을 작성한 파일을 applicationContext.xml로 저장한다.
- AnnotationConfigApplicationContext이 아닌 GenericXmlApplicationcontext 또는 ClassPathXmlApplicationContext를 사용한다.
public class UserDaoTest {
public static void main(String[] args) {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
...
}
}
GenericXmlApplicationcontext는 클래스 패스 기준으로 applicationContext.xml에 경로를 입력하면 된다.
ClassPathXmlApplicationContext는 특정 클래스의 위치를 힌트로 주어 경로를 모두 입력하지 않아도 찾을 수 있도록 한다.
new GenericXmlApplicationContext("spring/toby/study/chapter1/applicationContext.xml");
// UserDao.class의 패키지는 spring.toby.study.chapter1
new ClassPathXmlApplicationContext("applicationContext.xml", UserDao.class);
DataSource 인터페이스로 변환
위에서 구현한 ConnectionMaker를 대체할 수 있는 javax.sql.DataSource라는 인터페이스를 자바에서는 이미 제공한다. 따라서 ConnectionMaker를 DataSource로 변경해보자.
- ConnectionMaker를 사용했던 부분을 DataSource로 변경한다.
- DatsSource를 빈으로 설정한다.
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws SQLException {
// 1. DB 연결
Connection c = dataSource.getConnection();
//
}
<?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="myConnectionMaker" class="spring.toby.study.chapter1.SimpleConnectionMaker"></bean>
<bean id="newConnectionMaker" class="spring.toby.study.chapter1.NewConnectionMaker"></bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.Simp1eDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://loca1host/springbook" />
<property name="username" value="spring" />
<property name="password" value="book" />
</bean>
<bean id="userDao" class="spring.toby.study.chapter1.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
위 DataSource의 빈을 설정할 때 ref가 아닌 value 속성을 사용해서 일반 값도 주입할 수 있다. value 값은 스트링이지만, 자동으로 알맞은 형태로 변환해준다.