DAO의 분리와 확장
초난감 DAO
- DAO: DB를 사용해 데이터를 조회하거나, 조작하는 기능을 전담하도록 만든 오브젝트
- 자바 빈: 두 가지의 관례를 따라 만들어진 오브젝트
- 디폴트 생성자: 자바 빈 은 파라미터가 없는 디폴트 생성자를 가지고 있어야 한다.(툴 또는 프레임워크 에서 리플렉션을 사용하여 오브젝트를 만들기 위해)
- 프로퍼티: 자바 빈 이 노출하는 이름을 가진 속성, 프로퍼티는 set 으로 시작하는 수정 메서드와 get 으로 시작하는 접근자 메서드를 사용해 수정 또는 조회가 가능하다.
User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| 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;
}
}
|
- id, name, password 프로퍼티를 가지고 있는 User 오브젝트
- get, set 이 붙은 메서드틀 통해 조회와 수정이 가능하다.
- 즉 위에 오브젝트가 앞에서 설명한 자바 빈 이라고 할 수 있다.
UserDAO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/toby_spring"
);
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("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/toby_spring"
);
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;
}
}
|
- User 오브젝트를 DB 에 넣고 관리하는 DAO 오브젝트
여담으로 코드스쿼드에서 순수 JDBC를 사용한 프로젝트를 진행할때 작성했던 코드와 매우 유사하다. 조금은 반가웠다 ㅋ.ㅋ- 위에 작성된 코드를 현업 개발자가 작성하였다면 쫒겨났을꺼라는 토비님의 말씀이 있었다.
- 위에 코드가 어떤점이 문제인지, 이를 개선하면 앞으로 어떤 측면에서 좋은점을 갖는지 이걸 고민하는게 스프링을 공부하는 방법이라 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao userDao = new UserDao();
User user = new User();
user.setId("crispin");
user.setName("crispindeity");
user.setPassword("molru");
userDao.add(user);
System.out.println("등록 성공 = " + user.getId());
User user2 = userDao.get(user.getId());
System.out.println("user2.getName() = " + user2.getName());
System.out.println("user2.getPassword() = " + user2.getPassword());
System.out.println("조회 성공 = " + user2.getId());
}
|
- 위에 작성한 UserDao 오브젝트가 정상적으로 동작하는지 확인해보는 테스트용 main 메서드
- 물론 우리가 생각했던대로 등록과 조회가 매우 잘 되는걸 알 수 있다.
- 하지만, 기능이 잘 된다 해서 좋은 코드는 아니라는것
DAO의 분리
관심사의 분리
- 관심이 같은 것 끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리해야한다.
- 만약 관심사를 따로 분리하지 않는다면, 어떠한 요구사항에 있어 변경이 일어나게 되면 코드에 많은 변경이 발생하게 된다.
UserDao 의 관심사항
- DB 와 연결을 위한 커넥션을 어떻게 가져올까 라는 관심
- 더 세부적으로 어떤 DB 를 사용하고, 어떤 드라이버를 사용하며, 어떤 로그인 정보를 쓰고, 그 커넥션은 어떻게 만들것인지 이렇게 더 세부적으로 관심사를 분리 시킬수도 있다.
- 사용자 등록을 위해 DB 에 보낼 SQL 문장을 담을 statement 를 만들고 실행하는 관심
- 어떤 SQL 을 사용할지와, 파라미터를 어떻게 바인딩 시킬지 이것도 다른 관심사로 분리가 가능하다.
- 공유 리소스를 반환하는 관심
중복 코드의 메서드 추출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public void get(String id) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/toby_spring"
);
return c;
}
|
- 커넥션을 가져오는 부분의 관심을 getConnection() 메서드로 분리하였다.
- 앞으로 DB 가 변경되어 커넥션을 변경해야 하면 다른 메서드는 건드릴 필요없이 getConnection() 메서드만 변경하면 되어, 수정이 매우 편리해졌다.
상속을 통한 UserDao 확장
- 각기 다른 형식으로 커넥션을 만들 필요가 발생하였을때, getConnection() 메서드만 변경하여 각기 다른 커넥션을 만들 수 있도록 상속을 통해 UserDao 를 확장시켜 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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;
}
|
- UserDao 를 추상클래스로 만들어 get() 과 add() 의 메서드는 기존의 로직대로 되어있고, 필요에따라 커넥션을 만드는 메서드만 따로 구현하여 사용할 수 있게 만든다.
1
2
3
4
5
6
7
| public class NUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
// N DB 커넥션 생성코드
...
}
}
|
1
2
3
4
5
6
7
| public class DUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
// D DB 커넥션 생성코드
...
}
}
|
- 위와 같이 두 가지의 다른 커넥션을 만들어야 할때 UserDao 객체 는 어떠한 수정도 필요없이, UserDao 를 상속받아서 구현만 하면 된다.
- 즉, 어떻게 데이터를 DB 에 등록하고, 등독되어 있는 데이터를 가져올것인가 에 대한 관심을 갖고있는 UserDao와 DB 연결 방법을 어떻게 할 것인가 에 대한 관심을 갖고있는 NUserDao, DUserDao 가 클래스 레벨로 구분이 되고 있다.
- 변경이 쉽다라는 수준을 넘어서 이제는 확장까지 손 쉽게 해결할 수 있는 단계가 되었다.
상속을 통한 UserDao 확장 단점
- 상속을 통해 관심이 다른 기능을 분리하였지만, 상속관계는 두 가지의 다른 관심사에 대해 긴밀한 결합을 허용한다.
- 서브 클래스의 경우 슈퍼 클래스의 기능을 그대로 사용이 가능하기 때문에 슈퍼 클래스의 변경이 있을 경우 모든 서브 클래스의 수정이 불가피 하다.
- 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용 시킬 수 없다. UserDao 외에 DAO 클래스가 계속 만들어진다면 상속을 통해 만들어진 getConnection() 메서드의 구현 코드가 매 DAO 클래스 마다 중복돼서 나타날수 있다.
DAO의 확장
클래스의 분리
- 상속을 활용하여 관심사를 분리하는 것이 아니라 그냥 클래스 자체를 나눠서 관심사를 분리시켜 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao(SimpleConnectionMaker simpleConnectionMaker) {
this.simpleConnectionMaker = simpleConnectionMaker;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.getConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.getConnection();
...
}
}
|
- 생성자 주입을 통해 simpleConnectionMaker 를 주입 받아 변수에 저장하여, 메소드에서 사용하면 된다.
1
2
3
4
5
6
7
8
9
| public class SimpleConnectionMaker {
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/toby_spring"
);
return c;
}
}
|
- 상속을 할 필요가 없으니 메서드는 abstract 로 만들 필요도 없다.
- 기존의 코드가 많이 변경되었지만, 기능상의 변경은 전혀 없다.
- 관심사의 경우는 완벽하게 분리가 되었지만, 이전 처럼 여러 DB 커넥션을 생성하려 한다면 UserDao 의 수정 없이 되는 것이 아니라, UserDao 가 수정되게 된다.
인터페이스의 도입
- 클래스를 분리하면서, 위에 상황처럼 긴민한 관계를 끊어 주기 위해 중간에 추상적인 연결고리가 필요하다.
- 추상적인 연결고리를 만들기에 자바에서 제공하는 가장 유용한 도구가 인터페이스 이다.
1
2
3
| public interface ConnectionMaker {
Connection makeConnection() throws ClassNotFoundException, SQLException;
}
|
- ConnectionMaker 라는 이름의 인터페이스를 정의한다.
- 이 인터페이스를 사용하는 UserDao 입장에서는 어떤 클래스로 만들었는지 상관없이 ConnectionMaker 타입의 오브젝트라면 makeConnection() 메서드를 호출하기만 하면 Connection 타입의 오브젝트를 반환해줄것으로 기대할 수 있다.
1
2
3
4
5
6
7
| public class DConnectionMaker implements ConnectionMaker {
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
// D DB 커넥션 생성코드
...
}
}
|
- ConnectionMaker 를 구현한 DConnectionMaker 클래스
- 필요에 맞게 D DB 커넥션을 생성하도록 메서드를 구현만 해주면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
this.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();
...
}
}
|
- ConnectionMaker 를 받아서 그 안에 있는 makeConnection() 메서드를 사용하기 때문에 UserDao 의 입장에서는 어떤 클래스로 ConnectionMaker 가 구현되었는지 알 필요가 없다.
- 그러나 여기서 문제점이 하나 발생하는데, 초기에 생성자를 통해 DConnectionMaker 오브젝트를 사용하도록 결정하는 코드가 여전히 UserDao 에 남아 있다.
- 결국 다른 ConnectionMaker 로 변경을 하려할때 이 부분에서 UserDao 의 코드에 수정이 일어나게 된다.
관계설정 책임 분리
- UserDao 에서 생성자를 통해 DConnectionMaker 오브젝트를 사용하도록 결정하는 코드가 남아 있는 문제를 해결하기 위해 관계설정 책임을 분리시켜 보자.
- 현재로는 UserDao 의 생성자에서 관계를 설정하고 있어 그 책임이 UserDao 에 있다.
- 이 책임을 UserDao 가 아닌 UserDao 를 사용하는 쪽에 둔다면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection();
...
}
}
|
- 책임을 분리하기 위해 기존에 생성자에서 사용할 오브젝트를 결정하는 것이 아니라 매개변수를 통해 사용할 오브젝트가 결정 되도록 변경하였다.
1
2
3
4
5
| public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
...
}
|
- UserDao 를 사용하는 main 메서드에서 이제 어떤 ConnectionMaker 를 사용할 것인지 설정을 하고 그걸 인자값으로 넘겨 UserDao 와 ConnectionMaker 간의 관계를 설정해준다.
- 이렇게 되면 UserDao 의 경우 어떤 클래스로 ConnectionMaker 를 구현하였는지 알 필요가 없게 된다.
- 이게 바로 다형성이 가지는 큰 힘이다.
- 앞으로 ConnectionMaker 를 구현하는 클래스가 변경되더라도 UserDao 의 코드는 전혀 수정할 필요없이, UserDao 를 사용하는 쪽에서 변경된 클래스와 관계를 설정해주기만 하면 된다.
- 다른 DAO 클래스에서도 ConnectionMaker 를 구현한 클래스를 그대로 가져다 사용이 가능하기 때문에 상속에 비해 확장에 있어 매우 유연하게 변경되었다.
REFERENCE
토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리