오브젝트와 의존관계
CH1 에서는 오브젝트의 설계와 구현, 동작원리를 집중적으로 살펴보자.
엉망진창의 UserDao
- 사용자 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO를 만들어보자.
- DAO : DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트.
- 먼저 Dao에서 사용할 User객체를 생성한다. 위의 형식처럼 스프림 프레임워크에서는 자바빈 규약을 지켜 작성한다.
- 자바빈 : 디폴트 생성자와 프로퍼티를 가진 오브젝트
- 디폴트 생성자 : 파라미터가 없는 디폴트 생성자
- 프로퍼티 : 자바빈이 노출하는 이름을 가진 속성으로 getter, setter
- 자바빈 : 디폴트 생성자와 프로퍼티를 가진 오브젝트
public class User {
String id, name, password;
public String getId() { return id; }
public String getName() { return name;}
public String getPassword() { return password;}
public void setId(String id) { this.id = id; }
public void setName(String name) { this.name = name;}
public void setPassword(String password) { this.password = password;}
}
다음으로 UserDao클래스를 생성한다.
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/spring", "root", "1234");
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/spring", "root", "1234");
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.setPassword(rs.getString("password"));
user.setName(rs.getString("name"));
rs.close();
ps.close();
c.close();
return user;
}
}
위와 같은 코드에서는 코드의 변경이 일어나면 감당하지 못할 변경점들이 생길 가능성이 많다. 따라서 이런 변경 사항이 생길 때 필요한 작업을 최소화하고, 다른 코드에 문제를 일으키지 않도록 분리와 확장을 고려한 설계가 필요하다. 모든 변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어나기 때문에, 우리는 한 가지 관심이 한 군데에 집중되도록 해야 한다. 이를 관심사의 분리라고 한다.
UserDao 클래스에서의 관심사항은 총 3가지이다.
- DB와 연결을 위한 커넥션을 가져오는 것.
- 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것.
- 작업이 끝나면 사용한 리소스를 닫고 반환하는 것.
관심사의 분리
DB연결을 위한 Connection 오브젝트를 가져오는 과정은 다음과 같은 문제가 있다.
- 다른 관심사와 섞여서 같은 add() 메소드에 담겨져 있다.
- 같은 Connection 오브젝트를 가져오는 코드가 get() 메소드에도 중복되어 있다.
이를 해결하기 위해 가장 먼저 해야 할 일은 중복된 코드를 분리하는 것이다.
중복 코드 추출
public class UserDao {
public void add(User user) {
Connection c = getConnection();
...
}
public User get(String id) {
Connection c = getConnection();
...
}
public Connection getConnection() {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/spring", "root", "1234");
return c;
}
}
관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다.
이처럼 기능이 추가되거나 바뀐 것은 없지만 UserDao를 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드로 만드는 작업을 리팩토링이라고 하며, getConnection()처럼 공통의 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 메소드 추출 기법이라고 한다.
상속
다음과 같은 문제 상황을 생각하며 변화를 반기는 DAO를 만들어보자. N사와 D 사에서 사용자 관리를 위해 UserDao를 구매하겠다는 주문이 들어왔다. 문제는 N사와 D사가 각기 다른 종류의 DB를 사용하고 있고, DB커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어 한다. UserDao 소스코드를 N사와 D사에 제공해주지 않고도 고객 스스로 원하는 DB커넥션 생성 방식을 적용해가면서 사용할 수 있도록 코드를 바꿔보자.
public abstract class UserDao {
public void add(User user) { Connection c = getConnection(); }
public User get(String id) { Connection c = getConnection(); }
public abstract Connection getConnection() throws SQLException, ClassNotFoundException;
}
public class NUserDao extends UserDao {
public Connection getConnection() { }
}
위의 코드처럼 Connection 객체를 가져오는 부분을 추상클래스로 바꾸고 N사 또는 D사에서 이를 상속하여 구체부를 작성하도록 함으로써 변화에 용이한 코드를 작성할 수 있다.
이처럼 슈퍼클래스에 기본적인 로직의 흐름(커넥션 만들기, SQL 생성, 실행, 반환)을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하는 방법을 디자인 패턴에서 템플릿 메소드 패턴이라고 한다.
UserDao의 getConnection 메소드는 Connection 타입 오브젝트를 생성하는 기능을 정의해놓은 추상 메소드다. 그리고 UserDao 서브클래스의 getConnection() 메소드는 각각의 클래스 오브젝트를 어떻게 생성할 것인지를 결정하는 방법이라고 할 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 한다.
상속의 단점
자바는 다중상속을 허용하지 않기 때문에 커넥션 객체를 가져오는 방법을 분리하기 위해 상속구조를 사용하면 다음과 같은 문제가 생길 수 있다.
- 다른 목적으로 UserDao에 상속을 적용하기 힘들다.
- 상속을 통한 상하위 클래스 관계는 생각보다 밀접하다.
- 하위 클래스는 상위클래스의 기능을 직접 사용할 수 있기 때문에 상위 클래스 내부의 변경이 일어나면 모든 서브클래스를 수정해야 할 수도 있다.
- Ex ) 상위 클래스에서 메소드가 추가 되거나 삭제되면 모든 서브클래스를 업데이트 해야함.
- 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다.
이런 상속의 문제점들을 해결하기 위해 독립적인 클래스로 분리할 필요가 있다.
독립적인 클래스로 분리
완전히 독립적인 클래스로 만들기 위해서 DB커넥션과 관련된 부분을 별도의 클래스로 옮기고 이를 UserDao가 이용하도록 하자.
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) {
Connection c = simpleConnectionMaker.makeNewConnection();
}
public User get(String id) {
Connection c = simpleConnectionMaker.makeNewConnection();
}
}
public class SimpleConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException {}
}
public class SimpleConnectionMaker {
public Connection makeNewConnection()throwsClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby", "root", "1kingbank");
return c;
}
}
이 코드의 문제는 UserDao의 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되어 있기 때문에 UserDao 코드의 수정 없이 DB커넥션 생성 기능을 변경할 방법이 없다. 이렇게 클래스를 분리한 경우에도 상속을 이용했을 때와 마찬가지로 자유로운 확장이 가능하게 하려면 두 가지 문제점을 해결해야 한다.
- UserDao안의 소스코드를 변경해야 한다. -> simpleConnectionMaker = new SimpleConnectionMaker();
- SimpleConnectionMaker의 메소드 문제.
- D사에서 만든 DB 커넥션 제공 클래스는 openConnection()이라는 메소드 이름을 사용했다면 UserDao 내에 있는 커넥션을 가져오는 코드를 일일이 변경해야 한다.
- DB커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야 한다.
- SimpleConnectionMaker라는 구체적 클래스를 UserDao가 알고있음.
- 이런 문제의 근본적인 원인은 UserDao가 바뀔 수 있는 정보에 대해 너무 많이 알고 있기 때문이다. 따라서 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어 줄 필요가 있다. 이를 추상화라고 한다.
- 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업으로 이 때 인터페이스를 사용한다.
추상화
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
public DConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException {
...
}
}
public class UserDao() {
private ConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new DConnectionMaker();
}
}
위의 코드에서는 이제 DB커넥션을 얻는 코드를 아무리 바꿔도 UserDao의 코드를 뜯어 고칠 일은 없다. 하지만, UserDao 코드안에 DConnection이라는 클래스 이름이 보인다. 즉, UserDao가 구체적인 클래스까지 알아야 한다는 문제가 있다.
그렇다면 이런 문제가 발생한 이유는 무엇일까? 그 이유는 UserDao 안에 분리되지 않은, 또 다른 관심사항이 존재하고 있기 때문이다. UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 new DConnectionMaker()라는 코드가 있다. 이는 UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 것이다. 간단히 말하면 UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다.
UserDao의 클라이언트에서 UserDao를 사용하기 전에, 어떤 ConnectionMaker의 구현 클래스를 사용할지를 결정하도록 만들어보자. 직접 생성자를 호출해서 객체를 만들 수도 있지만, 외부에서 만들어준 것을 가져올 수도 있다.
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
public class DConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/spring", "root", "1234");
return c;
}
}
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
public class UserDaoTest {
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 dao = new UserDao(connectionMaker) 처럼 생성한 connectionMaker를 외부에서 dao로 넣어준다.
원칙과 패턴
개방 폐쇄 원칙이란?
클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다. 이 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발의 고전적인 원리로도 설명이 가능하다.
높은 응집도란?
하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있음.
낮은 결합도란?
책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결된 형태
전략 패턴이란?
필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
템플릿 메소드 패턴
슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 서브클래스에서 필요에 맞게 구현해서 사용하는 방법
팩토리 메소드 패턴
서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것
이번 장에서는
UserDao 클래스를 리팩토링(메소드 추출 → 상속 → 인터페이스)하면서 메소드 추출 기법과 상속의 단점, 템플릿 메소드 패턴, 팩토리 메소드 패턴에 대해서 공부하였다. 관심사를 분리함으로써 미래의 변화가 생겼을 때 잘 처리할 수 있게 되며, 리팩토링도 이를 기반으로 이루어졌다.