- UserDao 클래스는 사용자 정보를 DB에 넣고 관리할 수 있는 기능을 가지고 있다.
- 등록, 수정, 삭제, 조회등 기본적인 CRUD 구조를 가진다.
- JDBC 커넥션을 기반으로 한다.
- User.java
// Domain
public class User {
String id;
String name;
String password;
...
}
- BadUserDao.java
public class BadUserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection(
"jdbc:h2:tcp://localhost:1521/test", "sa", ""
);
PreparedStatement ps = c.prepareStatement(
"inser 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();
c.close();
}
public void update(User user) throws ClassNotFoundException, SQLException {
...
}
public void delete(User user) throws ClassNotFoundException, SQLException {
...
}
public User get() throws ClassNotFoundException, SQLException {
...
return null;
}
}
- 이 난감한 DAO가 제대로 동작된다는 가정하에, 각 메소드들은 add와 같은 구조로 동작한다고 생각하고 진행하도록 한다.
- 구현한 Dao를 테스트하는 클라이언트
@SpringBootTest
class TobySpringExApplicationTests {
@Test
void main() throws SQLException, ClassNotFoundException {
BadUserDao badUserDao = new BadUserDao();
User user = new User();
user.setId("test-01");
user.setName("wonjune");
user.setPassword("pwd01");
badUserDao.add(user);
}
}
- 이 DAO를 구성하는 메소드들은 각각 Connection을 가지고 있다.
- 현재는 4개의 메소드만 존재하지만, 미래에 추가적으로 더 생길수도 있다.
- Connection을 변경해야할 일이 있을 경우, 모든 메소드에 Connection 로직을 일괄 변경해줘야 한다.
- 중복코드가 발생한다.
- add 메소드의 경우 관심사는 DB에 User 정보를 INSERT 작업을 하는 것이다. 하지만 단순 INSERT 작업만을 수행하지 않고 DB를 연결하는 등 여러 서로 다른 관심사가 한 메소드 내에서 집중되어 있다.
1. DB 커넥션
2. 사용자 정보 바인딩 후, SQL 실행
3. 리소스 반환 (Connection.close())
- 가장 먼저 고려해볼 수 있는 것은 각 메소드 내에 중복되어 있는 Connection 부분을 가져와 메소드화 하여 사용하는 것이다.
public class BadUserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
PreparedStatement ps = c.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();
c.close();
}
public void update(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
}
public void delete(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
}
public User get() throws ClassNotFoundException, SQLException {
Connection c = getConnection();
return null;
}
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection(
"jdbc:h2:tcp://localhost:1521/test", "sa", ""
);
return c;
}
}
- 이와 같이 메소드로 Connection을 불러올 수 있게끔 해준다면 전보다는 나은 코드가 된다.
리팩토링
동작방식에는 변화 없이 내부 구조를 변경하여 재구성
코드 내부의 설계가 개선되어 코드를 이해하기 편하게 개선
변화에 효율적 대응
- 위의 과정은 변화에 대응하는 수준
- 해당 DAO의 로직은 건드리지 않고 Connection만 확장성을 고려하여 리팩토링 한다.
- 요구사항
- UserDao의 변경 없이, 두 가지 이상 DB 커넥션 타입을 가진다.
- UserDao.java(추상 클래스)
public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
- 구현 코드는 제거되고 추상 메소드로 바뀌었다.
- 메소드의 구현은 서브클래스가 담당한다.
- NUserDao.java
public class NUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
// NUserDao만의 커넥션 알고리즘
...
return connection;
}
}
- DUserDao.java
public class DUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
// DUserDao만의 커넥션 알고리즘
...
return connection;
}
}
- DAO의 핵심 기능인 등록, 수정, 삭제, 조회 기능은 UserDao에서 구현 (DAO 핵심 기능을 관심사로 담당)
- N, D UserDao는 각 각 Connection을 어떤 방식으로 가져갈 것인가에 대해 관심을 가지고 Overriding하여 해당 connection 관심사만 처리
- 이와 같은 설계 방식을 **템플릿 메소드 패턴**이라고 한다.
템플릿 메소드 패턴
슈퍼 클래스에 기본적인 로직의 흐름을 만든다.
즉, 공통된 기능을 구현해놓는 것이다.
그 중 서브 클래스에서 각자 필요에 맞게 구현 가능하도록 오버라이딩 가능한 protected 메소드 혹은 추상 메소드로 만드는 방식이다.
팩토리 메소드 패턴
객체를 만들어내는 부분을 서브 클래스에 위임하는 패턴
추상 클래스를 만들고 이를 상속한 서브 클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위함.
- 두 개의 관심사를 본격적으로 독립 시키면서 동시에 손쉽게 확장할 수 있는 방법
public abstract 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();
return null;
}
}
- 이렇게 된다면 두가지 문제가 생긴다.
1. 만약 connection에 대한 요구사항이 변경되어 openConnection() 메소드를 써야할 경우, DAO에 전체 Connection 변경 필요
2. DB 커넥션을 제공하는 클래스가 어떤 것인지 UserDao가 구체적으로 알고 있어야 한다.
따라서 UserDao는 DB 커넥션을 가져오는 구체적인 방법에 종속되어 버린다.
인터페이스란,
어떤 일을 하겠다는 기능만을 정의해놓은 것!!
- 클래스를 분리 하면서도 문제를 해결하기 좋은 방법은 두 개의 클래스가 서로 긴밀하게 연결되지 않도록 중간에 추상적인 느슨한 연결 고리를 만들어 주는 것
- ConnectionMaker.java
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException,
SQLException;
}
- 개선한 UserDao.java
public abstract 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();
...
}
}
- 문제점
- 인터페이스를 통해 추상화를 하여 커넥션을 상황에 따라서 구현 클래스를 바꿔 쓸 수 있게 되었지만, UserDao 내에서 직접 커넥션 구현체를 지정하여 선언하므로 UserDao와 ConnectionMaker가 여전히 높은 결합도를 가지고 있다.
- 여전히 UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지 결정하는 코드가 있다.
- 이 때문에 인터페이스를 이용한 분리에도 불구하고 여전히 UserDao의 변경 없이는 DB 커넥션 기능의 확장이 자유롭지 못하다.
- 여전히 DB 커넥션에 대한 관심사항이 UserDao에 남아있는 상태이다.
- 잠깐 정리를 해보자면, UserDao는 물론 독립적으로 실행될 수 있지만, 조금 더 넓은 개념으로 보자면 **어떤 클라이언트에 의해 사용되는 서비스** 역할을 한다고 볼 수 있다.
- 즉, 클라이언트에 의해 호출이 되므로, 클라이언트가 주 제어권자가 되어 UserDao에 어떤 DB 커넥션을 사용할지 결정하도록 해보자.
- UserDao가 외부에서 동적으로 Connection을 부여받을 수 있도록 코드 구조를 개선해보자.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
...
}
소프트웨어 개발의 지혜, 원칙, 디자인 패턴, 실천 방법
밥 마틴 저
- 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
- 높은 응집도와 낮은 결합도
- 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 한다.
- 하위 클래스는 반드시 상위 클래스와 대체 가능 해야 한다.
- 클라이언트의 세분화된 내용과 같은 세분화된 인터페이스를 만들자.
- A: 고수준(High-Level)의 모듈은 저수준(Low-Level)의 모듈에 의존하면 안된다. 둘 다 추상화에 의존해야한다.
- B: 추상은 세부사항에 의존해서는 안된다. 추상에 의존해야 한다.
- 자신의 기능 맥락(context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴