토비의 스프링 책 내용을 정리한 포스팅입니다. 전체 코드는 여기서 확인하실 수 있습니다.
스프링에서 가장 중요하게 가치를 두는 것은 바로 객체지향 프로그래밍이 가능하다는 점이다. 다시 말해 스프링에서 제공하는 기능은 자바를 활용하여 객체지향 프로그래밍을 할 수 있도록 하기 위해서, 다른 부분들을 추상화하여 본질적인 비즈니스 로직에만 집중할 수 있게 하는 것이다.
스프링의 동작이나 원리에 대해서 잘 아는 것도 중요하겠지만 객체지향적으로 프로그래밍 할 수 있는 역량이 필요한 것이다. 다행인 것은 객체지향적으로 프로그래밍하는 능력이 스프링의 동작 방식에도 그대로 스며들어 있다. 응집성을 높이고 결합도를 낮추는 작업, OCP를 적용하는 작업, 관심사를 분리하는 작업 등의 작업이 스프링에도 내부적으로 적용되어 있다.
1장에서는 스프링 코드 없이 객체지향적으로 프로그래밍 하는 사례를 먼저 보여주고 해당 사례를 스프링을 적용하여 변경한다. 전체 과정을 잘 따라온다면 객체지향에 대한 이해 그리고 나아가 스프링의 IoC/DI와 객체 지향의 관계에 대해서도 느낄 수 있을 것이다.
아래의 코드들이 리팩토링 되는 과정에서 가장 중요하게 생각되는 포인트이기에 미리 설명하는 것이 좋을 것 같다. 어떻게 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있었을까? - 관심사의 분리를 통해 가능하다.
변화는 대체로 집중된 한 가지 관심에 대해 일어나지만 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다는 점이다. 변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한 가 지 관심이 한 군데에 집중되게 하는 것이다. 즉 관심이 같은 것끼리는 모으고, 관심이 다 른것은따로떨어져있게하는것이다. 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것 이라고 생각할 수 있다.
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:h2:tcp://localhost/~/test", "sa", ""
);
PreparedStatement ps = connection.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();
connection.close();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection connection = getConnection();
PreparedStatement ps = connection.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();
connection.close();
}
protected abstract Connection getConnection();
}
public class DUserDao extends UserDao{
@Override
protected Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:h2:tcp://localhost/~/test", "sa", ""
);
return connection;
}
}
public class UserDao {
private final SimpleConnectionMaker connectionMaker = new SimpleConnectionMaker();
public void add(User user) throws ClassNotFoundException, SQLException {
Connection connection = connectionMaker.openConnection();
PreparedStatement ps = connection.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();
connection.close();
}
}
public class SimpleConnectionMaker {
public Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:h2:tcp://localhost/~/test", "sa", ""
);
return connection;
}
}
public class UserDao {
private final ConnectionMaker connectionMaker = new DConnectionMaker();
}
public interface ConnectionMaker {
Connection openConnection() throws ClassNotFoundException, SQLException;
}
public class DConnectionMaker implements ConnectionMaker{
public Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
return DriverManager.getConnection(
"jdbc:h2:tcp://localhost/~/test", "sa", ""
);
}
}
public class UserDaoTest { // 클라이언트로 생성 이동
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new UserDao(new DConnectionMaker());
}
}
public class UserDao { // UserDao는 직접적으로 자신이 사용할 클래스를 모른다!
private final ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
public class DaoFactory {
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
return new UserDao(connectionMaker);
}
}
public class UserDaoTest {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new DaoFactory().userDao();
}
}
public UserDao userCountingDao() {
ConnectionMaker connectionMaker = new CountingConnectionMaker(new DConnectionMaker());
return new UserDao(connectionMaker);
}
}
public class CountingConnectionMaker implements ConnectionMaker{
private int count = 0;
private final ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
this.realConnectionMaker = realConnectionMaker;
}
@Override
public Connection openConnection() throws ClassNotFoundException, SQLException {
count ++;
return realConnectionMaker.openConnection();
}
public int getCount() {
return count;
}
}
인터페이스 분리 시점
오브젝트 관계 매핑까지 분리한 시점
스프링을 사용했을 때 시점
2번과 3번 비슷해보이지 않는가? 우리가 2번에서 오브젝트 사이에 연관관계를 맺고 적절한 객체를 생성하여 주입하던 DaoFactory의 역할을 스프링의 ApplicationContext로 치환하면 구조가 동일하다. 즉 오브젝트의 생명주기를 관리하고 의존성 주입 작업을 대신해 주고 있는 것이다.
관심사의 분리를 통해 자신이 관할하던 기능을 다른 오브젝트 혹은 누군가에게 넘김으로써, 자신을 해당 관심사로부터 수동태로 만드는 것을 의미한다. 다시 말해 자신이 스스로 제어권을 가지고 있지 않으며 누군가가 설정하는 것에 따라 달라질 수 있는 형태를 의미한다. 프레임워크도 우리의 코드가 프레임워크가 동작하는 방식으로 작동한다는 것을 전제로 하기 때문에 대표적인 IoC의 사례이다.
범용적이고 유연한 방법으로 IoC 기능을 확장하기 위해서 스프링을 사용한다. 스프링 컨테이너를 사용했을 때의 장점은 아래와 같다.(자신이 직접 구현한 IoC형태가 아닌)
왜 스프링은 싱글톤으로 빈을 만드는 것일까? 이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다. 대규모의 엔터프라이즈 서버환경은 서버 하나당 최대로 초 당 수십에서 수백 번씩 브라우저나 여타 시스템으로부터의 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경 - 매번 새로운 객체를 생성한다면 부하를 감당할 수 없다.
하지만 싱글톤 패턴은 아래와 같은 큰 단점들이 존재한다. 그렇다면 스프링이 제공하는 싱글톤 레지스트리는 이러한 단점을 어떻게 극복했을까?
스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 이것이 싱글톤 레지스트리이다. 위의 조건에 맞게 싱글톤 형태를 만들어 빈을 등록하는 것이 아니라, 일반적인 자바 오브젝트를 스프링에게 제어권을 넘기면 싱글톤의 형태로 생성 삭제 관리를 해준다. 즉 해당 객체는 자유롭게 사용할 수 있지만 해당 객체를 스프링 컨테이너에서 가져오면 싱글톤으로 가져오는 것이다. 이를 통해 스프링은 위의 단점들을 극복하며 아래의 기능을 가능하게 한다.
스프링 싱글톤 레지스트리의 사용
IoC가 매우 느슨하게 정의돼서 폭넓게 사용되는 용어라는 점이다. 때문에 스프링을 IoC 컨테이너라고만 해 서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다. 스프링이 서블릿 컨 테이너처럼 서버에서 동작하는 서비스 컨테이너라는 뜻인지, 아니면 단순히 IoC 개념이 적용된 템플릿 메소드 패턴을 이용해 만들어진 프레임워크인지, 아니면 또 다른 IoC 특 징을 지닌 기술이라는 것인지 파악하기 힘들다. 물론 스 프링이 컨테이너이고 프레임워크이니 기본적인 동작원리가 모두 IoC 방식이라고 할 수 있지만, 스프링이 여타 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라 는 새로운 용어를 사용할 때 분명하게 드러난다.
정의
의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클 라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다. 의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도 와주는 제3의 존재가 있다는 것이다
DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동 적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다. 스프링 컨 테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져 있다. 그래서 스프링 을 IoC 컨테이너 외에도 DI 컨테이너 또는 DI 프레임워크라고 부르는 것이다.
사용자에 대한 DB 정보를 어떻게 가져올 것인가에 집중해야 하는 UserDao에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여 있는 것(의존관계 검색)은 어색하다. 따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다.
한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. 다행 히 이런 서블릿은 스프링이 미리 만들어서 제공하기 때문에 직접 구현할 필요는 없다.
장점
스프링이란 ‘어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크’라는 사실을 꼭 기억해두자. 스프링의 관심은 오브젝트와 그 관계다. 하지만 오브젝트를 어떻게 설계하고, 분리하고, 개선하고, 어떤 의존관계를 가질지 결정하는 일은 스프링이 아니라 개발자의 역할이며 책임이다.
먼저 책임이 다른 코드를 분리해서 두 개의 클래스로 만들었다(관심사의 분리, 리팩토링).
그중에서 바뀔 수 있는 쪽의 클래스는 인터페이스를 구현하도록 하고, 다른 클래스에서 인터
페이스를 통해서만 접근하도록 만들었다. 이렇게 해서 인터페이스를 정의한 쪽의 구현 방법
이 달라져 클래스가 바뀌더라도, 그 기능을 사용하는 클래스의 코드는 같이 수정할 필요가 없
도록 만들었다(전략 패턴).
이를통해 자신의 책임자체가 변경되는 경우 외에는 불필요한 변화가 발생하지 않도록 막아
주고, 자신이 사용하는 외부 오브젝트의 기능은 자유롭게 확장하거나 변경할 수 있게 만들었
다(개방 폐쇄 원칙).
결국 한쪽의 기능 변화가 다른 쪽의 변경을 요구하지 않아도 되게 했고(낮은 결합도),자신의 책
임과 관심사에만 순수하게 집중하는(높은 응집도) 깔끔한 코드를 만들 수 있었다.
오브젝트가 생성되고 여타 오브젝트와 관계를 맺는 작업의 제어권을 별도의 오브젝트 팩토
리를 만들어 넘겼다. 또는 오브젝트 팩토리의 기능을 일반화한 IoC 컨테이너로 넘겨서 오브
젝트가 자신이 사용할 대상의 생성이나 선택에 관한 책임으로부터 자유롭게 만들어줬다(제어
의 역전/IoC).
전통적인 싱글톤 패턴 구현 방식의 단점을 살펴보고, 서버에서 사용되는 서비스 오브젝트로
서의 장점을 살릴 수 있는 싱글톤을 사용하면서도 싱글톤 패턴의 단점을 극복할 수 있도록
설계된 컨테이너를 활용하는 방법에 대해 알아봤다(싱글톤 레지스트리).
설계 시점과 코드에는 클래스와 인터페이스 사이의 느슨한 의존관계만 만들어놓고, 런타임
시에 실제 사용할 구체적인 의존 오브젝트를 제3자(DI 컨테이너)의 도움으로 주입받아서 다이
내믹한 의존관계를 갖게 해주는 IoC의 특별한 케이스를 알아봤다(의존관계 주입/DI).
의존 오브젝트를 주입할 때 생성자를 이용하는 방법과 수정자 메소드를 이용하는 방법을 알
아봤다(생성자 주입과 수정자 주입).