토비의 스프링 3.1 - 객체 지향 설계 과정 - 관심사의 분리2
21 Dec 2020 | Spring 스터디 객체지향 디자인패턴책 토비의 스프링 3.1의 1장 오브젝트와 의존관계를 정리한 내용입니다.
지금까지 내용을 정리해보면, 관심사를 분리시키는 방법으로
- 독립된 메소드를 만들어 분리
- 상속을 통한 분리
가 있었다. 여기서는 상속이 아닌 인터페이스를 통해 완전히 독립적인 클래스로 분리시켜보자.
1.3.1 클래스의 분리
책에서는 먼저 DB 커넥션과 관련된 부분을 서브 클래스가 아닌 별도의 클래스(ConnectionMaker)에 담는다. UserDao에서는 이 클래스의 객체를 직접 생성하고 클래스에 정의된 메소드를 호출하여 DB 연결 정보를 가져오게된다. 하지만 문제가 있다. UserDao가 ConnectionMaker라는 특정 클래스에 종속되어 버린다는 문제이다. 자유로운 확장이 가능해지려면 특정 클래스에 종속되면 안된다.. 현재 UserDao 구조는 ‘언제든지 변경될 수 있는 정보’인 DB 커넥션을 가져오기 위해 어떤 클래스를 사용해야하는지, 그 클래스에서 커넥션을 가져오는 메소드 이름이 무엇인지까지 일일이 알아야 하는 구조이다. 이는 메소드 이름이 바뀌면 메소드 이름을 일일이 변경해줘야 하는 것처럼 UserDao가 DB 커넥션을 제공하는 특정 클래스와 그 코드에 종속되어 있음을 의미하며 곧 DB 커넥션을 가져오는 방법을 자유롭게 확장하기가 힘든 구조임을 의미한다!!!
1.3.2 인터페이스의 도입
그렇다면 클래스를 분리하면서도 이런 문제를 해결할 수는 없을까? 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주면 가능하다.
추상화란, 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다. 이 추상화 작업을 할 수 있는 도구가 바로 인터페이스이다.
인터페이스란 어떤 일을 하겠다는 기능만 정의해 놓은 것이다. 인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보는 모두 감춰버린다. 그러므로 UserDao가 인터페이스를 사용한다면 인터페이스를 통해 오브젝트에 접근하므로, 구체적인 클래스 정보를 알 필요가 없게 된다. 인터페이스에 정의된 메소드를 사용하므로 구현 클래스가 바뀐다 해도 메소드 이름이 변경될 걱정은 없다.
인터페이스를 도입한 결과
코드를 보자
/* ConnectionMaker 인터페이스를 사용하도록 개선한 UserDao */
public class UserDao {
// 특정 클래스 이름이 아니라 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보를 알 필요가 없게 된다.
ConnectionMaker connectionMaker;
public UserDao() {
// !!!!!! 그런데 여기에는 특정 클래스 이름이 나오네;; (또다른 문제점이 발생했군)
this.connectionMaker = new NConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
// 인터페이스에 정의된 메소드를 사용하므로 '클래스가 바뀐다 해도' 메소드 이름이 변경될 걱정은 없다.
Connection c = connectionMaker.makeConnection();
……
}
}
/* ConnectionMaker 인터페이스 */
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
/* ConnectionMaker 구현 클래스 */
public class NConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
return c;
}
이렇게 UserDao가 인터페이스를 이용하게 만들어서 DB 커넥션을 제공하는 클래스에 대한 구체적인 정보 제거는 가능했지만, 어떤 클래스의 오브젝트를 사용할지 결정하는 생성자 코드가 제거되지 않고 남아 있다.
connectionMaker = new NConnectionMaker();
여전히 UserDao 소스코드 수정 없이는 자유로운 DB 커넥션 확장 기능을 가진 UserDao를 제공할 수가 없다.
1.3.3 관계 설정 책임의 분리
책에서는 문제가 되는 위 코드를, UserDao 안에 아직 분리되지 않은, 또 다른 관심사항으로 바라본다. 이 관심사가 담긴 코드를 UserDao에서 분리시키려면 어떻게 해야 할까? 먼저 저 코드에 담긴 관심사가 무엇인지 생각해보자. 그것은 UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 대한 관심이다. 즉, 오브젝트 간에 ‘사용’이라는 관계를 맺게 하는 관심이다.
오브젝트 간에 관계가 만들어지려면 일단 만들어진 오브젝트가 있어야 하는데 이처럼 직접 오브젝트를 만드는 방법도 있지만, 외부에서 만들어 준 것을 가져오는 방법도 있다. 즉, 외부에서 만든 오브젝트를, 메소드 파라미터나 생성자 파라미터로 전달받는 것이다. 이때, 파라미터의 타입을 전달받을 오브젝트의 인터페이스로 선언하면 된다.
이는 객체지향의 다형성이라는 특징을 이용하는 것이다. 코드에서 특정 클래스(구현 클래스들 ex.N사, D사)를 전혀 알지 못하더라도 그 특정 클래스가 구현한 인터페이스(ConnectionMaker)를 코드에서 사용하고 있다면, 해당 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있게 된다. 따라서, 모델링시에는 없었던, 그래서 코드에는 보이지 않던 관계가 런타임 시점에 오브젝트로 만들어진 후에 생성되는 것이다.
다형성이란,
객체지향에서는 ‘여러가지 형태를 가질 수 있는 능력’을 의미하며, 자바에서는 ‘한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 하는 것’을 의미한다. 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조하는 경우를 예로 들 수 있다. 여기서는 인터페이스의 타입으로 그 인터페이스를 구현한 객체를 참조하는 것이 해당된다. 자바에서 다형성은 상속과 인터페이스를 통해 이루어지며 인터페이스가 상속보다 다형성에 더 큰 유연함을 제공한다.
생성자를 통해 의존관계를 느슨하게 한다(의존관계 주입)
public class UserDao {
ConnectionMaker connectionMaker;
// 생성자 파라미터를 통해 ConnectionMaker 구현체를 전달 받는다. new NConnectionMaker() 코드가 사라졌다!
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
/* 관계 설정 책임이 추가된 클래스 */
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// UserDao가 사용할 ConnectionMaker 구현 클래스를 결정하고 오브젝트를 만든다.
ConnectionMaker connectionMaker = new NConnectionMaker();
// UserDao를 생성해서, 사용할 ConnectionMaker 타입의 오브젝트를 생성자 주입을 통해 두 오브젝트간의 의존관계를 설정한다.
UserDao dao = new UserDao(connectionMaker);
……
}
}
이렇게 UserDao는 자신의 관심사이자 책임인 사용자 데이터 접근을 위해 SQL을 생성하고, 이를 실행하는 데만 집중할 수 있게 됐다. 더 이상 DB 커넥션을 가져오는 방법이 어떻게 변경되든 UserDao 코드는 아무런 영향을 받지 않는다.