토비의 스피링 초난감 Dao
초난감 Dao
Dao 란 데이터 엑세스 오브젝트이다.
데이터를 조회 하거나 조작하는 기능을 말한다.
우리는 흔히 쓰는 자바빈 규약에 따른 오브젝트이다.
public class User {
String id;
String name;
String password;
public User() {
}
public User(String id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
데이터 베이스를 접근하는 Dao를 만들어 보자
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");
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 User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
}
코드를 살펴 보면 일단 좋은 코드는 아니다.
try catch fianlly 로 코드를 감싸주지 않았다. 만약 에러가 발생한다면 리소스를 반납하지 못할 것이다.
하지만 지금은 그게 중요한게 아니므로 일단은 넘어간다.
우리는 유저정보를 입력하고 또한 유저에 대한 정보를 가져 오는 Dao를 만들었다.
테스트를 해보자
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao userDao = new UserDao();
User user = new User();
user.setId("wonwoo");
user.setName("이원우");
user.setPassword("zaq12wsx");
userDao.add(user);
System.out.println(user.getId() + " 완료");
User user2 = userDao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공 ");
}
일단 잘 돌아간다. 이렇게 코딩을 하는 사람도 있겠지만 필자도 맘에 안들고 보는사람도 맘에 안드는 사람이 있을 것이다.
우리는 관심사를 분리 해보자.
중복코드의 메소드 추출
가장 먼저 해야 할 일은 중복코드를 메소드로 추출 하는 것이다.
//커넥션 정보를 불러오는 코드를 분리
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
return DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
우리는 커넥션 정보를 불러오는 코드를 메소드를 분리 시켰다.
테스트를 해도 잘 돌아간다.
아주 초보적인 관심사의 분리 작업이지만 메소드 추출만으로도 변화에 좀 더 유연하게 대처 할 수 있는 코드를 만들었다.
하지만 우리는 변화에 대응하는 수준이 아니라 반기는 Dao를 만들어 보자
상속을 통한 확장
우리는 N사와 D사의 각기 다른 데이터 베이스를 사용 하고 있다. DB 커넥션을 가져오는데 있어 독자적으로 만든 방법을 적용하고 싶어하는 점이다.
또한 종종 변경될 가능성이 있다.
다음 코드를 보자
public abstract class UserDao {
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
.. add
.. get(id)
}
UserDao를 추상화 하여 만들었다.
나머지는 구현체에서 구현하도록 템플릿 메서드 패턴을 이용하였다.
public class NUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
return DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");
}
}
이렇게 만들어도 나쁘지는 않다.
하지만 NUserDao 는 UserDao와 밀접한 관계에 있다.
UserDao 가 변경되면 NUserDao에도 영향을 미친다.
또한 자바는 다중상속을 지원하지 않는다.
조금더 확장 시켜보자.
클래스의 분리
public class UserDao {
SimpleConnectionMaker simpleConnectionMaker = new SimpleConnectionMaker();
.. add()
.. get(id)
}
SimpleConnectionMaker 클래스를 만들어 위임해주고 있다.
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
return DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");
}
}
하지만 여기에선 또 UserDao는 SimpleConnectionMaker와 너무 밀접한 관계에 있다. 또한 SimpleConnectionMaker의 makeNewConnection메소드는 보장 받지 못하고 있다.
만약 SimpleConnectionMaker이라는 클래스에 makeConnection 이라는 함수가 사용 되었을때는 메소드 또한 변경해야 된다.
이것은 다음과 같이 해결할 수 있다.
인터페이스의 도입
public interface ConnectionMaker {
Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}
우리는 위와 같이 인터페이스를 도입하였다.
public class SimpleConnectionMaker implements ConnectionMaker {
@Override
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
return DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");
}
}
그리고 SimpleConnectionMaker는 ConnectionMaker 인터페이스를 구현하고 있다.
public class UserDao {
ConnectionMaker connectionMaker = new SimpleConnectionMaker();
...add()
...get(id)
}
인터페이스 도입으로 우리는 makeNewConnection함수는 보장 받을수 있다.
하지만 아직도 UserDao은 SimpleConnectionMaker을 의존하고 있다.
아직도 맘에 들지 않는다.
의존성 주입
이제 많이 왔다. 아니 거의 다왔다.
마지막으로 UserDao는 SimpleConnectionMaker의존 하고 있다.
아직까지 커넥션 정보를 바뀌면 UserDao코드를 변경해야 된다.
우리는 이렇게 해결 할 수 있다.
public class UserDao {
final ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
...add()
...get(id)
}
우린 생성자를 통해 ConnectionMaker 주입받도록 하고 있다.
UserDao는 책임을 떠넘기고 외부에서 주입 받도록 하고 있다.
UserDao를 쓰는 클래스(클라이언트) 혹은 Service들은 Dao에 의존성을 주입해야 된다.
이렇게 UserDao를 전혀 손을 대지 않고 DB연결을 확장 시킬 수 있는 방법을 알아 냈다.
public static void main(String[] args) throws SQLException, ClassNotFoundException {
ConnectionMaker connectionMaker = new SimpleConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
...
...
}
우리는 이것을 의존성 주입이라고 한다. 별거 아닌 것처럼 느껴질 수 도 있다.
솔직히 별거 아닌건 아니다. 아주 중요하고 해보면 쉽지 않은 기술 일 수도 있다.
우리는 좀더 유연한 dao를 만들어 보았다.
실제 DI는 보시다 시피 스프링의 기술이 아니다. 굳이 스프링 컨테이너가 없어도 가능한 일이다.
또한 그렇다고 자바에만 있는 것도 아니다. 객체 지향 언어라면 DI란 기술을 마음껏 펴칠 수 있다.
우리는 이렇게 초난감 Dao에서 초슈퍼울트라캡짱 Dao를 만들어 봤다.