1.3 DAO의 확장
데이터 액세스 로직 + DB 연결 : 상하위 클래스로 분리
- UserDao
- 데이터 액세스 로직을 담당
- 어떤 API를 사용하여 DB와 통신할 것인가
- 어떤 SQL을 만들 것인가
- 어떤 오브젝트를 통해 DB에 저장할 정보를 전달받고 넘겨줄 것인가
- NUserDao / DUserDao
- DB 연결을 담당
- DB 연결 방법이 그대로면 코드의 변경이 없음
- DB 연결 방법이 바뀌면 NUserDao, DUserDao의 코드만 바뀜
그러나 여전히 상속이라는 방법은 불편하다.
1.3.1 클래스의 분리
두 개의 관심사를 본격적으로 독립시키면서 동시에 손쉽게 확장 가능하도록 분리하자.
일단 두 개의 클래스를 아예 별도의 클래스로 분리하자.
- SimpleConnectionMaker 클래스를 생성 - DB 생성 기능
- UserDao는 new를 사용하여 SimpleConnectionMaker 오브젝트를 생성
- add(), get() 메소드에서 사용
- 각각의 메소드에서 매번 새로운 오브젝트를 생성하는 것이 아니라 한번만 생성하면 된다.
UserDao는 다음과 같이 변경한다.
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
// 한 번만 만들어 인스턴스 변수에 저장해두고 메소드에서 사용하게 한다.
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
// ...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
// ...
}
public static void main(String[] args) throws ClassNotFoundException, SQLException{
// ...
}
}
SimpleConnectionMaker는 다음과 같다.
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe", "spring", "book");
return c;
}
}
main() 메소드 실행 후 테스트를 통과하는 지 확인해보자.
1.3.1의 문제점
- DB 커넥션 기능을 확장해서 사용하는 것이 불가능
- SimpleConnectionMaker가 특정 클래스에 종속
- 문제점
- SimpleConnectionMaker의 메소드 문제
- 만약 makeNewConnection() 메소드명이 openConnection()으로 바뀐다면?
- DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알아야한다.
- 클라이언트가 다른 클래스를 구현하면 UserDao를 수정해야 한다.
- SimpleConnectionMaker의 메소드 문제
1.3.2 인터페이스의 도입
추상화 : 어떤 것들의 공통적인 성격을 뽑아내어 따로 분리해내는 작업
- 두 개의 클래스가 긴밀하게 연결되어 있지 않도록
- 추상적인 느슨한 연결고리를 생성(인터페이스)
- 최소한의 통로를 통해 접근하는 쪽에서 오브젝트를 만들 때 사용할 클래스를 몰라도 됨
ConnectionMaker 인터페이스
- makeConnection()을 호출하기만 하면 Connection 타입의 오브젝트를 리턴할 것이라 기대
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
ConnectionMaker 구현 클래스
public class DConnectionMaker implements ConnectionMaker{
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException{
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe", "spring", "book");
return c;
}
}
UserDao
public class UserDao {
// 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보 알 필요 없음
private ConnectionMaker connectionMaker;
public UserDao() {
// 그러나 여기에 클래스 이름이 나옴
connectionMaker = new DConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
// 인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀌어도 걱정없음
Connection c = connectionMaker.makeConnection();
// ...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection();
// ...
}
public static void main(String[] args) throws ClassNotFoundException, SQLException{
// ...
}
}
1.3.2의 문제점
N사와 D사가 DB 접속용 클래스를 다시 만들더라도 UserDao의 코드를 수정하지 않아도 되!ㄹ 줄 알았지?
최초에 한 번은 어떤 클래스를 사용할 지 결정하는 생성자의 코드에 클래스의 이름이 등장한다.
즉, UserDao 코드를 함께 제공해야만 한다.
1.3.3 관계 설정 책임의 분리
문제점
- 문제의 원인
- UserDao 내에 분리되지 않은 또 다른 관심사항이 존재
- new DConnection() : UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용할지를 결정
- UserDao 내에 분리되지 않은 또 다른 관심사항이 존재
- 해결책
- UserDao의 클라이언트 오브젝트에 해당 기능을 분리
- 클래스와 클래스의 관계 설정이 아닌, 오브젝트와 오브젝트의 관계를 설정
- 오브젝트의 관계 설정 방법 : 직접 생성이 아닌 메소드 파라미터로 전달
UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안 된다.
- UserDao의 클라이언트
- UserDao 오브젝트를 사용하는 main() 메소드
- ConnectionMaker 구현 클래스를 선택하고, 오브젝트를 생성하여 UserDao와 연결
- 관심과 책임이 다르므로 새로운 클래스로 분리(UserDaoTest)
- UserDao
- 생성자에 ConnectionMaker 오브젝트를 전달받을 수 있는 파라미터를 추가
UserDao
- SQL 생성, 실행에만 집중
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
// 구체적인 구현 클래스의 이름이 사라짐
this.connectionMaker = connectionMaker;
}
// ...
}
UserDaoTest
- 런타임 오브젝트 의존관계를 설정하는 책임을 담당
- 원래 자신의 책임이던 테스트 작업 수행
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException{
// UserDao가 사용할 ConnectionMaker 구현 클래스를 결정하고 오브젝트를 생성
ConnectionMaker connectionMaker = new DConnectionMaker();
// 1. UserDao 생성
// 2. 사용할 ConnectionMaker 타입의 오브젝트 제공
// 결국 두 오브젝트 사이의 의존관계 설정 효과
UserDao dao = new UserDao(connectionMaker);
// ...
}
}
1.3.3 결론
- 인터페이스를 사용함으로써 상속에 비해 훨씬 유연해 짐
- 각각의 클래스는 자신의 책임만을 다루고 있음
- DAO가 아무리 많아져도, DB 연결이 그대로면 ConnectionMaker는 유지됨
- DB가 아무리 변경되어도, UserDao는 그대로임
- N사, D사 외 다른 고객들이 생겨도 확장에 유연함
1.3.4 원칙과 패턴
개방 폐쇄 원칙(OCP; Open-Closed Priciple)
'클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다.'
객체지향 설계 원칙 (SOLID)
객체 지향의 특징을 잘 살릴 수 있는 설계의 특징
- 단일 책임 원칙(SRP; The Single Responsibility Principle)
- 개방 폐쇄 원칙(OCP; The Open Closed Principle)
- 리스코프 치환 원칙(LSP; The Liskov Substitution Principle)
- 인터페이스 분리 원칙(ISP; The Interface Segregation Principle)
- 의존관계 역전 원칙(DIP; The Dependency Inversion Principle)
높은 응집도와 낮은 결합도
- 높은 응집도
- 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다.
- 작업은 항상 전체적으로 일어나고 무엇을 변경할지 명확
- 다른 클래스의 수정을 요구하지 않음
- 기능에 영향을 주지 않음
- 낮은 결합도
- 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결되어야 한다.
- 결합도 : 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에 변화를 요구하는 정도
- 변경에 대한 요구가 전파되지 않는 상태
UserDao는 자신의 책임에 대한 응집도가 높으며, ConnectionMaker와는 인터페이스를 통해 매우 느슨하게 연결되어 있다.
전략 패턴(Strategy Pattern)
- UserDaoTest-UserDao-ConnectionMaker의 구조
- 전략 패턴 : 디자인 패턴의 꽃
- 자신의 기능 맥락(Context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 함
- 컨텍스트(UserDao)를 사용하는 클라이언트(UserDaoTest)는 컨텍스트가 사용할 전략(ConnectionMaker를 구현한 DConnectionMaker)을 컨텍스트의 생성자 등을 통해 제공해주는 것이 일반적
' Spring > 토비의 스프링 3.1' 카테고리의 다른 글
1.6 싱글톤 레지스트리와 오브젝트 스코프 (0) | 2019.01.09 |
---|---|
1.5 스프링의 IoC (0) | 2019.01.07 |
1.4 제어의 역전(IoC) (0) | 2019.01.07 |
1.2 DAO의 분리 (0) | 2019.01.03 |
1.1 초난감 DAO (0) | 2019.01.03 |