Home 예제로 살펴보는 객체지향의 원칙
Post
Cancel

예제로 살펴보는 객체지향의 원칙

다음과 같은 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
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 String setPassword(String password) {
        this.password = password;
    }
}

이제 이 사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어보자

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
public class UserDao{
    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");

        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.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");

        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;
    }
}

관심사 분리하기

개발자는 객체를 설계할 떄 미래의 변화를 고려해야 한다 요구사항의 변화가 생기면 코드를 수정해야 할 것이고 개발자는 매번 필요한 작업을 최소화해야 하며 변경이 다른 곳에 문제를 일으키지 않게 해야 할 것이다

이러한 설계는 바로 분리와 확장을 고려한 설계에서 나온다 먼저 관심사의 분리를 고려하도록 코드를 변경해보자

위의 UserDao에서 add() 메서드와 get() 메서드는 DB와의 Connection을 가져오는 동일한 코드가 존재한다 중복된 DB연결을 getConnection()이라는 이름의 독립적인 메서드로 추출해보자

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
public class UserDao{
    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 User get(String id) throws ClassNotFoundException, SQLException{
        Connection c = getConnection();
        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;
    }

    public Connection getConnection() throws ClassNotFoundException, SQLException{
      Class.forName("com.mysql.jdbc.Driver");
      return DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
    }
}

이제 DB 연결과 관련된 부분에 변경이 일어났을 경우, getConnection() 메서드의 코드만 수정하면 된다

이제 또다른 상황을 가정해보자 getConnection()을 수정해 사용할 일이 많아지고, UserDao 클래스를 수정하지 않도록 해야 한다

상속을 통한 확장

getConnection() 메서드를 수정하도록 추상 메서드로 변경해서 이를 상속하도록 해 보자

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
48
public static class UserDao{
    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 User get(String id) throws ClassNotFoundException, SQLException{
        Connection c = getConnection();
        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;
    }

    public static Connection getConnection() throws ClassNotFoundException, SQLException{
        // 수정할 코드
    }
}

public class NUserDao extends UserDao {
    public static Connection getConnection() throws ClassNotFoundException, SQLException{
      // N사가 사용할 Connection 구현
    }
}

public class DUserDao extends UserDao {
  public static Connection getConnection() throws ClassNotFoundException, SQLException{
    // D사가 사용할 Connection 구현
  }
}

이제 UserDao 클래스를 수정하지 않고도 상속을 통해 손쉽게 확장이 가능해졌다 이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드로 만들어 서브클래스에서 이러한 메서드를 필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메소드 패턴이라고 한다 또한 서브클래스에 Connection 클래스의 생성 방법을 결정하기 때문에 팩토리 메소드 패턴이라고도 할 수 있다

이렇게 만들어진 UserDao 클래스에는 또 다른 몇 가지 문제가 존재한다 먼저, 상속을 통해 만들어진 클래스와 UserDao 클래스는 긴밀한 결합을 허용한다 서브클래스는 슈퍼클래스의 기능을 사용할 수 있고, 만약 슈퍼클래스의 수정이 일어나면 서브클래스에도 영향을 줄 것이다 또한, 상속, 구현한 getConnection() 메서드를 다른 클래스에서 사용할 수 없다는 것도 문제이다 만약 다른 클래스에서도 동일한 연결을 사용하고 싶다면 중복된 코드를 작성해야 할 것이다

클래스의 분리

이번엔 관심사가 다르고 변화의 성격이 다른 두 코드를 화끈하게 분리해보자

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
public static 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 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{
        Connection c = simpleConnectionMaker.makeNewConnection();
        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;
    }
}

public class SimpleConnectionMaker{
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
      Class.forName("com.mysql.jdbc.Driver");
      return DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
    }
}

이제 위에서 언급했던 문제들이 해결되었다 상속이 아닌 새로운 클래스를 사용했기 때문에 결합의 정도가 낮아졌고 한 번 작성한 Connection 생성 방법을 다른 클래스에서도 사용할 수 있다

하지만 또다른 큰 문제가 발생하였다 UserDao 클래스가 SimpleConnectionMaker 클래스에 종속되어 있기 때문에 다른 DB Connection을 제공하는 클래스를 제공하기 위해서는 UserDao를 수정해야 한다

또한 UserDao 클래스가 SimpleConnectionMaker 클래스의 정보를 너무 많이 알고 있다는 것도 문제이다 어떤 클래스가 쓰일지, 그 클래스에서 커넥션을 가져오는 메소드의 이름이 뭔지 알고 있어야 한다

인터페이스의 도입

두 클래스 간의 긴밀한 연결 관계가 느슨하도록 만들 수 있는 가장 좋은 방법은 추상화가 있다 추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업을 말한다 자바에서 추상화를 제공하는 가장 유용한 도구는 바로 인터페이스이다 바로 위에서 작성했던 코드에서 SimpleConnectionMaker 클래스를 인터페이스로 수정하면 다음과 같다

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
48
49
50
51
52
53
54
public static class UserDao{
    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        simpleConnectionMaker = new NConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        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{
        Connection c = simpleConnectionMaker.makeNewConnection();
        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;
    }
}

public interface SimpleConnectionMaker{
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}

public class NConnectionMaker {
  public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
    // N사 구현 Connection 생성 코드
  }
}

public class DConnectionMaker {
  public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
    // D사 구현 Connection 생성 코드
  }
}

이제 모든 문제를 해결한 듯 보인다 하지만 아직 하나의 문제가 남아있다 생성자에서 어떤 SimpleConnectionMaker 구현체를 사용할 것인지 지정해 주어야 한다는 것이다 이는 UserDao 클래스에서 SimpleConnectionMaker 클래스를 생성하는 책임을 가지고 있기 때문에 발생한 것이다 따라서 이 책임을 외부로 던져버린다면 다음과 같이 문제를 해결할 수 있을 것이다

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
48
49
50
51
52
53
54
public static class UserDao{
    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao(SimpleConnectionMaker simpleConnectionMaker) {
        this.simpleConnectionMaker = simpleConnectionMaker;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        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{
        Connection c = simpleConnectionMaker.makeNewConnection();
        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;
    }
}

public interface SimpleConnectionMaker{
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}

public class NConnectionMaker {
  public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
    // N사 구현 Connection 생성 코드
  }
}

public class DConnectionMaker {
  public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
    // D사 구현 Connection 생성 코드
  }
}

이제 생성한 UserDao를 사용하는 클라이언트가 있다고 생각해보자 이를 UserDaoTest 클래스라고 하면, 이 클래스에는 Connection의 종류를 결정하여 UserDao 객체를 생성하는 책임이 있다 이러한 책임 또한 분리시켜 생각할 필요가 있으므로 DaoFactory 클래스를 만들어 보자

1
2
3
4
5
6
7
8
9
public class DaoFactory{
    public UserDao userDao() {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
}

만약 UserDao가 아닌 AccountDao, MessageDao 등의 클래스가 추가될 경우, 매번 DConnectionMaker 생성 코드를 작성해야 한다 따라서 중복을 제거한다면 다음과 같은 코드가 작성된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DaoFactory{
    public UserDao userDao() {
      UserDao userDao = new UserDao(connectionMaker());
      return userDao;
    }

    public UserDao accountDao() {
      AccountDao accountDao = new AccountDao(connectionMaker());
      return userDao;
    }

    public MessageDao messageDao() {
      MessageDao messageDao = new MessageDao(connectionMaker());
      return userDao;
    }

    public ConnectionMaker connectionMaker() {
      return new DConnectionMaker();
    }
}

원칙과 패턴

개방 폐쇄 원칙 (Open-Closed Principle)

클래스나 모듈은 확장에는 열려 있고 변경에는 닫혀 있어야 한다

작성한 UserDao의 경우 DB 연결 방법이라는 기능은 얼마든지 추가가 가능하므로 확장에는 열려 있고 자신은 변화에 영향을 받지 않는다는 점에서 변경에는 닫혀 있다고 할 수 있다

높은 응집도와 낝은 결합도

개방 폐쇄 원칙은 높은 응집도와 낮은 결합도로도 설명 가능하다

높은 응집도

응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임, 또는 관심사에만 집중되어 있다는 뜻이다 만약 최초의 UserDao처럼 여러 관심사와 책임이 얽혀 있는 복잡한 코드에서는 변경이 필요한 부분을 찾는 것도 번거롭고 변경으로 인해 변경되지 않는 부분에는 다른 영향을 주지 않는지 확인해야 하는 이중의 부담이 생긴다

낮은 결합도

결합도란 하나의 오브젝트가 변경될 때 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도를 말한다 UserDao에 인터페이스를 도입한 후 DB 연결 기능을 구현한 클래스가 바뀌더라도 DAO의 코드는 변경될 필요가 없게 된다

전략 패턴

자신의 context 에서 변경이 필요한 부분을 인터페이스를 통해 외부로 분리시키는 디자인 패턴

UserDao는 전략 패턴의 컨텍스트에 해당한다 컨텍스트는 자신의 기능 중 변경이 필요한 DB 연결 부분을 SimpleConnectionMaker 인터페이스를 통해 외부로 분리시켰다

제어관계 역전 (Inversion of Control, IoC)

제어의 역전이란 제어의 권한을 자신이 아닌 다른 대상에게 위임하는 것

예를 들어 최초의 UserDao 클래스는 자신이 직접 어떤 Connection을 만들 것인지 선택하고 생성했다 하지만 getConnection() 메서드를 추출함으로써 Connection을 선택하고 생성하는 제어권을 위임하였다

이제 정말 깔끔한 코드로 리팩토링을 완료하였다 다음은 스프링을 사용하여 좀더 일반적인 IoC를 구현해볼 것이다

This post is licensed under CC BY 4.0 by the author.