2022. 5. 25. 13:55ㆍ토비의 스터디
앞서 1.2장에서 "DB 연결을 어떤 방법으로 할 것인가"라는 관심사를 중심으로 UserDao를 상하위 클래스로 분리하였다. 추상클래스로 선언하고 이를 상속한 서브클래스에서 구체적인 getConnection()메소드를 구현했다.
하지만 자바는 단일 상속만을 허용하기때문에 상속은 비효율적이며, 상속 관계가 의외로 결합도가 높아 부모의 코드가 변경되는 경우 자식 클래스의 코드가 변경되는 일이 빈번하다는 단점이 있었다.
1.3.1 클래스의 분리
그래서 아예 다른 클래스로 화끈하게 분리한다. 상속관계도 아닌 완전히 독립적인 클래스로 만들어본다.
SimpleConnectionMaker라는 새로운 클래스를 만들고, DB 생성 기능을 그 안에 넣는다.
UserDao에서는 생성자를 통해 인스턴스 변수로 SimpleConnectionMaker를 저장해두고 이를 계속 사용한다.
public class SimpleConnectionMaker {
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/sys", "kiseo", null );
return c;
}
}
public class UserDao {
private SimpleConnectionMaker connectionMaker;
public UserDao(SimpleConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.getConnection();
...
}
}
그러나 이렇게 클래스로 분리한 경우에도 문제점이 발생한다. UserDao가 SimpleConnectionMaker라는 특정 클래스에 종속되어 있으므로 DB커넥션 생성 기능이 변경될 경우 UserDao의 코드 수정이 불가피하다.
즉, 다른 방식으로 DB 커넥션을 제공하는 클래스를 사용하려면
Connection c = connectionMaker.getConnection();
이 코드가 변경되어야 한다.
만약 D사에서 DB 커넥션 제공 클래스가 openConnection() 메소드를 사용한다면,
UserDao내에 있는 커넥션을 가져오는 코드가
Connection c = connectionMaker.openConnection();으로 전부 다 수정되어야 한다.
구체적인 클래스에 의존하고 있기 때문이다. 어떤 클래스가 쓰일지, 커넥션을 가져오는 메소드가 구체적으로 무엇인지 일일이 알고 있어야 하기 때문이다.
1.3.2 인터페이스의 도입
클래스를 분리하면서, 중간에 추상적인 연결고리를 만들어 주면 위의 문제를 해결 가능하다.
추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.
자바는 추상화를 위해 인터페이스를 제공한다.
인터페이스는 "역할"을 정의해 놓은 것이다. 따라서 구체적인 구현 방법은 나타나지 않는다. 그것은 구현한 클래스가 할 일이다. 따라서 UserDao가 인터페이스에 의존한다면, 인터페이스의 메소드를 통해 알 수 있는 기능에만 관심을 가지면 된다. 그 기능이 어떻게 구현되었는지는 알 필요 없다.
먼저 ConnectionMaker 라는 인터페이스를 정의하고, DB 커넥션을 가져오는 메소드 이름을 makeConnection()이라고 정했다.
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
N사는 ConnectionMaker를 구현한 클래스를 만들고, 자신들의 DB 연결 기술을 이용해 커넥션을 가져오도록 메소드를 작성한다.
public class NUserDao implements ConnectionMaker{
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
// N사의 독자적인 방법으로 Connection을 생성하는 코드
}
}
이제 UserDao에서는 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보를 알 필요가 없다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(SimpleConnectionMaker connectionMaker) {
this.connectionMaker = new NConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection();
...
}
}
하지만 여전히 UserDao의 생성자에서 NConnectionMaker라는 구체적인 클래스에 의존하는 모습이 보인다.
초기에 한 번 어떠한 클래스의 오브젝트를 사용할지 결정하는 생성자의 코드라 제거가 쉽지 않아보인다.
이대로면 D사가 UserDao를 이용할때 생성자 부분을 this.connectionMaker = new DConnectionMaker() 처럼 일일이 수정해야 한다.
1.3.3 관계설정 책임의 분리
여전히 확장이 자유롭지 못한 이유는, "UserDao에서 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정"하는 분리되지 않은 또다른 관심사가 존재하기 때문이다.
이 관심사는 어디에 두어야 할까?
바로 UserDao를 사용하는 오브젝트, 즉 클라이언트 오브젝트에서 구체적인 구현 클래스를 결정하면 된다.
오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 참조값을 가지고 있는 방식으로 만들어진다.
connectionMaker = new DConnectionMaker();
이 코드는 DConnectionMaker 오브젝트의 참조값을 UserDao의 connectionMaker 변수에 넣어 사용하게 함으로써, 두 개의 오브젝트가 '사용'이라는 관계를 맺게 해준다.
UserDao는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안되게 해야 한다. 현재는 UserDao와 DConnectionMaker와 직접적인 관계가 있어 확장이 자유롭지 않다.
UserDao가 DConnectionMaker를 사용하려면 런타임 사용관계(의존관계)를 맺어주면 된다. 코드에는 보이지 않던 관계가 런타임 시에(오브젝트로 만들어진 후에) 생성되는 것이다. 이는 클라이언트 오브젝트에서 진행한다.
클라이언트 오브젝트로 UserDaoTest를 생성하고 관계설정 책임을 추가하였다.
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new NConnectionMaker();
//UserDao가 사용할 ConnectionMaker의 구현클래스를 결정하고 오브젝트를 생성한다.
UserDao dao = new UserDao(connectionMaker);
//UserDao를 생성하고 사용할 ConnectionMaker 타입의 오브젝트를 제공한다.
//두 오브젝트 사이의 의존관계를 설정한다.
}
}
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
...
}
이제 UserDao는 인터페이스에만 의존하고 있다(구체적인 클래스가 사라졌다).
만약 D사가 사용한다면, UserDaoTest에서
ConnectionMaker connectionMaker = new NConnectionMaker();
이 한 줄만 수정하면 된다.
이렇게 인터페이스를 도입하면 훨씬 유연한 설계가 가능하다. 다른 DAO 클래스가 생겨도 ConnectionMaker의 구현 클래스들을 그대로 적용할 수 있기 때문이다. DAO가 아무리 많아져도 DB 접속 방법에 대한 관심은 오직 한 군데에 집중되며, DB 접속 방법을 변경해야 할 때도 오직 한 곳의 코드만 수정하면 된다.
1.3.4 원칙과 패턴
객체지향의 원리와 패턴에 대해 다룬다.
개방 폐쇄 원칙(OCP, Open-Closed Principle)
- 깔끔한 설계를 위해 적용 가능한 객체지향 설계 원칙 중 하나
- 클래스나 모듈은 확장 에는 열려있고, 변경 에는 닫혀 있어야 한다.
- UserDao에서는
- 인터페이스를 통해 제공되는 확장 포인트는 확장을 위해 활짝 개방되어 있다.
- 인터페이스를 이용하는 클래스는 자신의 변화가 불필요하게 일어나지 않도록 폐쇄되어있다.
이외에도 객체지향 설계 원칙(SOLID)은
- 단일 책임 원칙(SRP, The Single Responsibility Principle)
- 한 클래스는 하나의 책임만 가져야 함
- 변경이 있을 때 파급 효과가 적으면 단일 책임의 원칙을 잘 따른 것이다.
- 리스코프 치환 원칙(LSP, The Liskov Substitution Principle)
- 프로그램의 객체는 프로그램의 정확성을 꺠뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 함
- 인터페이스 분리 원칙(ISP, The Interface Segregation Principle)
- 여러 개의 인터페이스가 범용 인터페이스 하나보다 낫다.
- 인터페이스가 명확해지며 대체 가능성이 높아짐
- 의존관계 역전 원칙(DIP, The Dependency Inversion Principle)
- 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다
- 구현 클래스에 의존하지 말고 인터페이스에 의존해야 한다.
'토비의 스터디' 카테고리의 다른 글
[토비의 스프링 3.1] 1.5 스프링의 IoC (0) | 2022.05.27 |
---|---|
[토비의 스프링 3.1] 1.4 제어의 역전(IoC) (0) | 2022.05.26 |
[토비의 스프링 3.1] 1.2 DAO의 분리 - 포포 (0) | 2022.05.24 |
[토비의 스프링 3.1] 1.1 초난감 DAO - 포포 (0) | 2022.05.23 |
들어가며 (0) | 2022.05.23 |