DB
CREATE TABLE "계정"."USER_TEST"
("ID" VARCHAR2(10) NOT NULL ENABLE,
"NAME" VARCHAR2(20) NOT NULL ENABLE,
"PASSWORD" VARCHAR2(20) NOT NULL ENABLE,
CONSTRAINT "USER_TEST_PK" PRIMARY KEY ("ID"))
폴더구조
┬springbook.user
├dao
│ └UserDao.java
├domain
│ └User.java
└UserTest.java
STEP1
User.java
package springbook.user.domain;
public class User {
String id;
String name;
String 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;
}
}
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
PreparedStatement ps = c.prepareStatement("insert into user_test(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("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
PreparedStatement ps = c.prepareStatement("select * from user_test 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;
}
}
UserTest.java
package springbook.user;
import java.sql.SQLException;
import springbook.user.dao.UserDao;
import springbook.user.domain.User;
public class UserTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new UserDao();
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
DAO의 분리
- 변경에 대한 요청
- Oracle 에서 MySql 로 변경해야 한다면.. 모든 메서드를 수정해야 한다.
- 암호를 변경해야 하는 경우도 전체 메서드를 수정해야 한다.
STEP2
중복코드의 메소드 추출
- 관심의 분리로 하나의 메소드만 수정하면 된다.
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("insert into user_test(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 {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("select * from user_test 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;
}
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
return c;
}
}
STEP3
폴더구조
┬springbook.user
├dao
│ ├UserDao.java
│ ├DUserDao.java
│ └NUserDao.java
├domain
│ └User.java
└UserTest.java
이번에는 상속을 통해 서브클래스로 분리한다.
getConnection() 기능을 자유롭게 확장할 수 있다.
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("insert into user_test(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 {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("select * from user_test 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;
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
DUserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
// D 사 DB connection
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
return c;
}
}
NUserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class NUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
// N 사 DB connection
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
return c;
}
}
UserTest.java
package springbook.user;
import java.sql.SQLException;
import springbook.user.dao.DUserDao;
import springbook.user.dao.UserDao;
import springbook.user.domain.User;
public class UserTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new DUserDao();
// UserDao dao = new NUserDao();
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
위의 코드는 디자인 패턴에서 템플릿 메소드 패턴이라고 한다.
하지만 이 방법은 상속을 사용했다는 단점이 있다.
상속자체는 간단해 보이고 사용하기도 편리하지만 한계점이 많다.
만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면 어떤가?
자바는 클래스의 다중 상속을 허용하지 않는다.
후에 다른 목적으로 UserDao에 상속을 적용하기 힘들다.
또 다른 문제는 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다는 점이다.
상속관계는 두가지 다른 관심사에 대해 긴밀한 결합을 허용한다. 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다. 반대로 그런 변화에 따른 불변을 주지 않기 위해 슈퍼클래스가 더 이상 변화하지 않도록 제약을 가해야 할지도 모른다.
만약 UserDao 외에 DAO 클래스들이 계속 만들어진다면 그 때는 상속을 통해서 만들어진 getConnection()의 구현 코드가 매 DAO 클래스마다 중복돼서 나타나는 심각한 문제가 발생할 것이다.
STEP4
클래스의 분리
이번에는 상속관계가 아닌 독립적인 클래스로 분리 한다.
폴더구조
┬springbook.user
├dao
│ ├SimpleConnectionMaker.java
│ └UserDao.java
├domain
│ └User.java
└UserTest.java
SimpleConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
return c;
}
}
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement("insert into user_test(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 {
Connection c = simpleConnectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement("select * from user_test 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;
}
}
UserTest.java
package springbook.user;
import java.sql.SQLException;
import springbook.user.dao.UserDao;
import springbook.user.domain.User;
public class UserTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new UserDao();
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
내부 설계를 변경해서 좀 더 나은 코드로 개선했을 뿐이지만, 문제가 있다. UserDao의 코드가 SimpleConnectionMaker 라는 특정 클래스에 종속되어 있기 때문에 상속을 사용했을 때 처럼 UserDao 코드의 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없다.(N사, D사)
STEP5
인터페이스의 도입
클래스를 분리하면서도 이전의 문제를 해결하는 방법이 있을까?
가장 좋은 해결책은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리르 만들어 주는 것이다.
결국 오브젝트를 만들려면 구체적인 클래스 하나를 선택해야겠지만 인터페이스로 추상화 해놓은 최소한의 통로를 통해 접근하는 쪽에서 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다.
인터페이스를 통해 접근하게 하면 실제 구현 클래스를 바꿔도 신경 쓸 일이 없다.
폴더구조
┬springbook.user
├dao
│ ├ConnectionMaker.java
│ ├DConnectionMaker.java
│ ├NConnectionMaker.java
│ └UserDao.java
├domain
│ └User.java
└UserTest.java
ConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
DConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
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:@아이피:포트:ORA8", "유저명", "비밀번호");
return c;
}
}
NConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class NConnectionMaker implements ConnectionMaker {
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection c = DriverManager.getConnection("jdbc:oracle:thin:@아이피:포트:ORA8", "유저명", "비밀번호");
return c;
}
}
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new DConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement("insert into user_test(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 {
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement("select * from user_test 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;
}
}
이제 아무리 N 사와 D 사가 DB 접속용 클래스를 다시 만든다고 해도 UserDao의 코드를 뜯어 고칠 일은 없을 것 같다. 그런데 UserDao 클래스를 자세히 살펴보면 DConnection 이라는 클래스 이름이 보인다. 생성자의 코드가 제거되지 않았다. 이렇게 하면 고객에게 자유로운 DB 커넥션 확장 기능을 가진 UserDao를 제공할 수 없다. 필요할 때 마다 UserDao의 생성자 메소드를 고객에게 직접 수정하라고 할 수 밖에 없다.
STEP5
관계설정 책임의 분리
어떤 DB 커넥션을 사용할지에 대한 책임은 UserTest.java 클래스에 맡긴다.
폴더구조
┬springbook.user
├dao
│ ├ConnectionMaker.java
│ ├DConnectionMaker.java
│ ├NConnectionMaker.java
│ └UserDao.java
├domain
│ └User.java
└UserTest.java
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement("insert into user_test(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 {
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement("select * from user_test 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;
}
}
UserTest.java
package springbook.user;
import java.sql.SQLException;
import springbook.user.dao.ConnectionMaker;
import springbook.user.dao.DConnectionMaker;
import springbook.user.dao.UserDao;
import springbook.user.domain.User;
public class UserTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao dao = new UserDao(connectionMaker);
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
드디어 UserDao 에 있으면 안되는 다른 관심사항, 책임을 클라이언트로 떠넘기는 작업이 끝났다.
이제는 UserDao의 변경 없이도 자유롭게 N 사와 D 사는 자신들을 위한 DB접속 클래스를 만들어서 UserDao가 사용하게 할 수 있다.
앞에서 사용했던 상속을 통한 확장 방법보다 더 깔끔하고 유연한 방법으로 UserDao 와 Connectionmaker 클래스들을 분리하고, 서로 영향을 주지 않으면서도 필요에 따라 자유롭게 확장할 수 있는 구조가 됐다.