1장. 오브젝트와 의존관계
- 초난감 DAO
- DAO의 분리
- DAO의 확장
- 제어의 역전(IoC)
- 스프링의 IoC
- 싱글톤 레지스트리와 오브젝트 스코프
- 의존관계 주입(DI)
- 정리
오브젝트와 의존관계(1)에 이어서 DAO의 확장과 제어의 역전(IoC)에 대해 이야기 나눠봅시다.
DAO의 확장
오브젝트와 의존관계(1)에서 초난감 DAO를 팩토리 메서드 패턴과 템플릿 메소드 패턴으로 상속을 사용하여 DB 커넥션을 가져오는 코드를 클래스 계층으로 분리했습니다. 하지만 상속을 사용한다는 것이 단점으로 작용했습니다.
지금까지는 DB 커넥션을 가져오는 관심사를 분리할 때 독립된 메소드로 분리하고, 다음에는 상하위 클래스로 분리했습니다. 이번에는 DB 커넥션을 가져오는 관심사를 아예 독립적인 클래스로 만들어 보겠습니다. 이제 UserDao에서는 DB 커넥션을 생성할 때는 독립된 클래스를 이용하면 됩니다.
DB 커넥션을 가져오는 관심사를 독립된 클래스로 구현했습니다.
package springbook.user.dao;
...
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forname("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/springboot", "spring", "book");
return c;
}
}
...
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
// 상태를 관리하는 것도 아니니 생성자에서 한 번만 만들어 인스턴스 변수에 저장.
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMake.makeNewConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMake.makeNewConnection();
...
}
}
이제 상속을 사용하지 않고도 DB 커넥션이 필요할 때, add() 메소드와 get() 메소드에서 단순히 SimpleConnectionMaker 객체를 생성하고 makeNewConnection() 메소드를 호출하면 됩니다.
하지만 이 방법은 UserDao가 구체적으로 DB 커넥션을 제공하는 클래스를 알고 있기 때문에 UserDao가 SimpleConnectionMaker 클래스에 종속된다는 문제가 발생합니다.
이전 상속 방식은 N사와 D사에 UserDao만 주면 각자 상속을 통해 DB 커넥션 기능을 확장했지만, 지금은 DB 커넥션 기능이 변경되었을 때 UserDao의 수정이 필요해졌습니다.
우리는 UserDao 코드를 N사와 D사에 제공하지 않고 DB 커넥션 기능을 확장 가능하도록 만들어야 합니다.
이 문제를 해결할 방법은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것입니다. 여기서 느슨한 연결고리의 역할을 인터페이스가 하게 됩니다. 자바는 추상화를 위해서 인터페이스를 제공합니다.
인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보를 모두 숨깁니다. 오브젝트를 만들기 위해서 구체적인 타입(클래스)을 선택하겠지만 인터페이스로 추상화해 놓는다면 오브젝트를 만들 때 사용할 구체적인 타입은 몰라도 됩니다. 인터페이스를 통해 구체 타입을 몰라도 되기 때문에 실제 구현 클래스를 바꿔도 신경 쓸 일이 없습니다.
SimpleConnectionMaker 클래스로 DB 커넥션을 생성하는 관심을 분리해 UserDao가 구체 타입에 종속되게 하지 않고, ConnectionMaker 인터페이스로 DB 커넥션 생성 관심을 분리한 뒤 UserDao가 ConnectionMaker 인터페이스에 의존하게 만들면 UserDao가 특정 타입에 종속되지 않도록 만들 수 있습니다. 이제 UserDao는 자신이 사용할 클래스가 어떤 것인지 몰라도 됩니다. 단지 인터페이스를 통해 원하는 기능을 사용하기만 하면 됩니다.

인터페이스에 기능만 정의해 두고, 인터페이스를 구현한 클래스들이 기능을 어떻게 구현할지 관심을 가지면 됩니다.
이제 UserDao는 ConnectionMaker 인터페이스가 제공하는 기능에만 관심을 가지지 그 기능을 어떻게 구현했는지는 UserDao가 아닌 ConnectionMaker 인터페이스를 구현하는 구현 클래스에서 알아서 할 일입니다.
ConnectionMaker 인터페이스와 구현 클래스를 코드로 살펴봅시다.
package springbook.user.dao;
...
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
// ConnectionMaker 구현 클래스
public class DConnectionMaker implements ConnectionMaker {
...
public Connection makeConnection() throws ClassNotFoundException, SQLException {
// D사의 독자적인 방법으로 Connection을 생성하는 코드
}
}
이제 UserDao를 납품할 때 UserDao와 ConnectionMaker 인터페이스를 함께 전달하면, N사와 D사 각자에 어울리는 DB 커넥션 생성 방식을 ConnectionMaker 인터페이스의 구현 클래스에 추가하게 됩니다.
ConnectionMaker 인터페이스로 두 클래스 간의 결합을 느슨하게 했기 때문에 UserDao 코드에도 수정이 필요합니다.
public class UserDao {
private ConnectionMaker connectionMaker;
// 여전히 인터페이스가 아닌 구체 타입에 의존한다.
// public UserDao() {
// this.connectionMaker = new DConnectionMaker();
//}
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
...
}
이제 UserDao는 ConnectionMaker 인터페이스에만 의존하게 되었습니다. UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 DB 커넥션 생성과 관련된 어떤 클래스와도 관계를 가져서는 안 됩니다.
수정한 UserDao 클래스의 생성자를 보면 ConnectionMaker 타입의 객체를 주입받고 있습니다. 만약 외부에서 객체를 주입받지 않고 UserDao 클래스 내부에서 ConnectionMaker 인터페이스의 구현 클래스를 생성한다면 이는 구체 타입에 여전히 의존하고 있다고 볼 수 있습니다.
이렇게 UserDao가 의존할 구체 타입을 설정하는 관심사를 UserDao 외부로 분리하여 관계설정 책임을 분리했습니다.
UserDao를 사용하는 클라이언트 쪽에서 UserDao가 실제로 사용할 ConnectionMaker 인터페이스 구현체를 런타임 시점에 주입합니다.
UserDao 코드에서는 실제로 사용한 특정 타입을 전혀 알지 못합니다. 하지만 인터페이스를 사용했기 때문에 런타임 시점에 결정될 특정 타입을 인터페이스 타입으로 받을 수 있습니다. 이런 특징으로 우린 다형성이라 부릅니다.

나중에는 @Configuration이 붙은 설정 클래스를 만들어서 의존관계를 설정하겠지만 간단하게 UserDaoTest 클래스의 main() 함수를 클라이언트로 구현한 코드를 살펴봅시다.
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao dao = new UserDao(connectionMaker);
...
}
}
UserDaoTest는 UserDao와 ConnectionMaker 구현 클래스와의 런타임 오브젝트 의존 관계를 설정하는 책임을 담당하게 됩니다. N사라면 ConnectionMaker 인터페이스를 구현한 NConnectionMaker 구현 클래스를 만들고 UserDaoTest에서 UserDao를 생성할 때 NConnectionMaker 오브젝트를 주입하면 됩니다.
DB 커넥션을 가져오는 방법을 어떻게 바꾸더라도 UserDao 코드는 수정할 필요가 없습니다.
이번에는 초난감 DAO 코드를 개선한 결과를 객체지향 기술의 몇 가지 이론으로 설명하려 합니다.
먼저 개방 폐쇄 원칙(Open-Closed Principle)입니다.
개방 폐쇄 원칙이란 '클래스나 모듈은 확장에 열려 있어야 하고 변경에는 닫혀 있어야 한다'라는 원칙입니다. 우리가 앞서 UserDao를 개선하여 DB 커넥션을 가져오는 방법은 얼마든지 확장될 수 있고 UserDao에 전혀 영향을 주지 않게 만들었습니다. 이는 개방 폐쇄 원칙을 잘 지켰다고 볼 수 있습니다.
인터페이스를 통해 제공되는 확장 포인트는 확장을 위해 개방하고 인터페이스에 의존하는(사용하는) 클래스는 자신의 변화가 불필요하게 일어나지 않도록 폐쇄해야 합니다.
두 번째는 높은 응집도와 낮은 결합도(high coherence and low coupling)입니다.
응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 관심사에만 집중되어 있는 뜻입니다. 우리가 초난감 DAO에서 DB 커넥션 생성 코드를 분리하고 의존관계를 설정하는 코드를 분리하며 UserDao가 유저를 저장하고 조회하는 관심사만 집중할 수 있도록 구현했던 것입니다.
낮은 결합도는 느슨한 연결을 뜻합니다. 느슨한 연결은 두 클래스가 관계를 유지하는 데 필수적인 최소한의 방법(인터페이스)만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없도록 만드는 것입니다.
UserDao에서 ConnectionMaker 인터페이스를 통해 두 클래스 간의 결합을 느슨하게 했던 것처럼 낮은 결합도를 위해서는 인터페이스의 활용이 필수입니다. UserDao와 ConnectionMaker 구현 클래스는 꼭 필요한 관계만 ConnectionMaker 인터페이스를 통해 낮은 결합도로 최소한으로 연결됩니다.
낮은 결합도란 결국, 하나의 변경이 발생할 때 다른 모듈이나 객체로 변경에 대한 요구가 전파되지 않는 상태입니다.
반대로 결합도가 높다면 변경에 따르는 작업량이 많아지고, 버그가 발생할 가능성도 높습니다.
마지막으로 전략 패턴(Strategy Pattern)입니다.
개선한 UserDaoTest-UserDao-ConnectionMaker 구조를 전략 패턴이라 볼 수 있습니다.
전략 패턴이란 자신의 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴입니다.
스프링 책인데 왜 이렇게 스프링 이야기가 없지? 의문이 들 수 있습니다.
하지만 스프링은 초난감 DAO부터 개선하며 이야기 됐던 여러 객체지향적 설계 원칙과 디자인 패턴의 장점을 자연스럽게 우리 개발자들이 활용할 수 있게 돕는 프레임워크입니다.
제어의 역전(IoC)
지금까지 초난감 DAO를 깔끔하게 리팩터링 했습니다.
하지만 아직 부족한 점이 있습니다. 바로 UserDaoTest에서 UserDao와 ConnectionMaker 인터페이스의 의존관계를 설정하고 있다는 점입니다. UserDaoTest는 테스트를 하기 위한 목적이지 의존관계 설정이라는 관심은 어울리지 않습니다.
그래서 지금부터는 UserDaoTest에서 의존관계 설정의 책임을 분리하려고 합니다.
DaoFactory 클래스를 의존관계 설정의 책임을 가지도록 만들 텐데 이 클래스는 객체의 생성 방법을 결정하고 만들어진 오브젝트를 돌려주는 팩토리로서의 역할을 하게 됩니다.
아래 코드로 DaoFactory 클래스를 살펴봅시다.
package springbook.user.dao;
...
public class DaoFactory {
// 팩토리의 메소드는 UserDao 타입의 오브젝트를 어떻게 만들지 결정한다.
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
그리고 UserDaoTest에서 DaoFactory를 사용하도록 수정하겠습니다.
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// ConnectionMaker connectionMaker = new DConnectionMaker();
// UserDao dao = new UserDao(connectionMaker);
UserDao dao = new DaoFactory().userDao();
...
}
}
이제 UserDaoTest는 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경 쓰지 않고 팩토리로부터 UserDao 오브젝트를 받아서 테스트하기만 하면 됩니다. 본인의 관심사인 테스트에만 충실하게 된 것입니다.
이렇게 오브젝트의 생성과 의존관계 설정을 DaoFactory에서 하게 되면서 아래 그림과 같이 컴포넌트의 구조와 관계를 정의한 설계도 영역과 실질적인 로직을 담당하는 컴포넌트 영역으로 구분할 수 있게 됩니다.

이제 N사와 D사에 UserDao를 납품할 때 UserDao, ConnectionMaker와 함께 DaoFactory도 제공합니다. UserDao와 달리 DaoFactory는 소스까지 제공하여 각자 회사에 어울리는 오브젝트 의존관계를 설정할 수 있도록 합니다.
팩토리 클래스인 DaoFactory를 만들어서 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 구조를 결정하는 설계도 오브젝트를 분리했다는 데 큰 의미가 있습니다.
그렇다면 위에서 구현한 DaoFactory에는 개선할 부분이 있을까요? 있습니다.
DaoFactory 클래스에 UserDao를 생성하는 메소드 말고 다른 DAO를 생성하는 기능이 추가된다면 userDao() 메소드 속 DB 커넥션을 가져오는 코드가 중복될 것입니다. 따라서 DB 커넥션을 가져오는 코드는 별도의 함수로 분리하는 게 좋겠습니다.
public class DaoFactory {
public UserDao userDao() {
return new UserDao(connectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(connectionMaker());
}
public MessageDao messageDao() {
return new MessageDao(connectionMaker());
}
// ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 분리했다.
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
}
이제 제어의 역전이라는 개념에 대해 알아봅시다.
제어의 역전이란, 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것을 뜻합니다.
일반적으로 프로그램의 흐름은 main() 메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고, 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 호출하고, 그 오브젝트 메소드 안에서 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복됩니다.
모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언저 어떻게 그 오브젝트를 만들지를 스스로 결정합니다. 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조입니다.
제어의 역전은 이런 일반적인 제어 흐름을 거꾸로 뒤집는 것입니다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않습니다. 당연히 스스로 생성하지도 않습니다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하게 됩니다.
프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어집니다. @Configuration 설정 파일이 제어 권한을 위임받은 특별한 오브젝트로 볼 수 있습니다.
프레임워크도 제어의 역전 개념이 적용된 대표적인 기술입니다.
라이브러리는 개발자가 직접 사용하지만 프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용되며 프레임워크가 애플리케이션 흐름을 주도합니다. 그래서 프레임워크에는 반드시 제어의 역전 개념이 적용되어 있어야 합니다. 애플리케이션 코드는 프레임워크가 짜놓은 틀에서 수동적으로 동작해야 합니다.
앞서 구현한 UserDao와 DaoFactory에도 제어의 역전이 적용되었습니다.
DaoFactory가 런타임에 UserDao에서 사용할 ConnectionMaker 구현 클래스를 연결해 줍니다. UserDao 입장에서 본인이 사용할 오브젝트를 직접 선택하지 않고 외부에서 주입받아서 사용하게 됩니다.
이처럼 제어의 권한을 부여받은 특정 오브젝트(DaoFactory)를 제외한 나머지 오브젝트들은 수동적인 존재가 됩니다.
모두가 수동적인 존재이기 때문에 제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요합니다.
우리가 만들었던 DaoFactory는 단순한 IoC 컨테이너 또는 IoC 프레임워크라고 불릴 수 있습니다.
스프링은 IoC를 모든 기능의 기초가 되는 기반기술로 삼고 있습니다. IoC를 극한까지 적용하고 있는 프레임워크가 스프링이라 할 수 있습니다.
'기술 서적 > 토비의 스프링 3.1' 카테고리의 다른 글
1장 오브젝트와 의존관계(3) 스프링의 IoC, 싱글톤 레지스트리와 오브젝트 스코프 (0) | 2024.10.20 |
---|---|
1장 오브젝트와 의존관계(1) 초난감 DAO, DAO의 분리 (0) | 2024.10.20 |