IOC의 필요성 - 3. 클래스의 분리 및 인터페이스 도입, 관계설정 책임

2022. 1. 9. 16:55북리뷰/토비의 봄

728x90

클래스의 분리

처음에는 독립된 메서드를 만들어서 분리했고, 그 다음에는 상하위 클래스의 상속을 통해 분리했다. 이번에는 아예 상속관계도 아닌 완전히 독립적인 클래스로 만들어보겠다.

그림에 나와있는 것 처럼 SimpleConnectionMaker라는 새로운 클래스를 만들고 DB 생성 기능을 그 안에 넣는다 . 그리고 UserDao에서 SimpleConnectionMaker클래스의 오브젝트를 만들어두고, 이를 각 메서드에서 사용하면 된다.

package springbook.user.dao;

import springbook.user.domain.User;
import springbook.util.SimpleConnectionMaker;

import java.sql.*;

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 users(id, name, password) values (?,?,?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.execute();

        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;
    }
}
package springbook.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby_mysql", "root", "<password>");

        return c;
    }
}

분리를 하고나서 보니까 기존에 네ㅇ버와 카ㅋ오에게 상속을 통해 DB커넥션 기능을 확장해서 사용하게 했던 것이 불가능해졌다. 왜냐하면 DB커넥션을 제공하는 클래스를 변경하려면 UserDao의 simpleConnectionMaker 부분을 직접 수정해야하기 때문이다.
이렇게 클래스를 분리한 경우에도 두 가지 문제를 해결해야 한다. 첫쨰는 SimpleConnectionMAker메서드가 문제다. 현재 우리는 makeNewConnection()을 사용하는데 만약 카ㅋ오는 openConnection()이라는 메서드를 통해 DB커넥션을 가져온다면? 현재 UserDao의 makeNewConnection을 호출하는 부분을 전부 수정해야 하는 대참사가 발생한다.
두 번째 문제는 DB커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야 한다는 점이다. UserDao에 SimpleConnectionMaker라는 클래스 타입의 인스턴스 변수까지 정의해놓고 있으니 네ㅇ버에서 SimpleConnectionMaker 대신 NeverConnectionMaker 클래스를 구현하면 어쩔 수 없이 UserDao 자체를 다시 수정해야 한다.

인터페이스 도입

위 분제의 해결책으로는 대표적으로 인터페이스를 도입하는 것이다. 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 인터페이스를 넣어서 추상적인 느슨한 연결고리를 만들어 주는 것이다. 결국 오브젝트를 만들려면 구체적인 클래스 하나를 선택해야겠지만 인터페이스로 추상화해놓은 최소한의 통로를 통해 접근하는 쪽에서 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다.

인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다. 따라서 인터페이스는 어떻게 하겠다는 구현 방법은 나타나있지 않다. 그것은 인터페이스를 구현한 클래스들이 할 일이다.

package springbook.util;

import java.sql.Connection;
import java.sql.SQLException;

public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
package springbook.util.impl;

import springbook.util.ConnectionMaker;

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("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby_mysql", "root", "<password>");

        return c;
    }
}
package springbook.user.dao;

import springbook.user.domain.User;
import springbook.util.ConnectionMaker;
import springbook.util.SimpleConnectionMaker;
import springbook.util.impl.NConnectionMaker;

import java.sql.*;

public class UserDao {
    private ConnectionMaker connectionMaker;

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

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();

        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.execute();

        ps.close();
        c.close();
    }
    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();

        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를 수정할 일은 없을 것 같다. 하지만 UserDao를 자세히 보면 아직까지도 NConnectionMaker를 직접 호출하는 부분이 남아있다.

  public UserDao() {
        connectionMaker = new DConnectionMaker();
    }

이렇게 되면 다시 원점이다. 여전히 UserDao 코드를 함께 제공해서 고객에세 메서드를 직접 수정하라고 지시하는 수 밖에 없다 ㅠㅠ 어떻게 해결하면 좋을까?

관계설정 책임의 분리

현재 UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지 결정하는 코드가 있다.

 connectionMaker = new DConnectionMaker();

어떤 ConnectionMaker를 이용할지 결정하는 것은 UserDao가 책임질 관심사가 아니다. 따라서 해당 책임을 다른 클래스에게 전가할 필요가 있다. UserDao를 사용하는 클라이언트가 관계설정에 대한 책임을 전가하는 것이 좋다. 현재 UserDao를 사용하는 클래스는 UserDaoTest 클래스 밖에 없다.

 package springbook.user.dao;

import springbook.user.domain.User;
import springbook.util.ConnectionMaker;


import java.sql.*;

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 users(id, name, password) values (?,?,?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.execute();

        ps.close();
        c.close();
    }
    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();

        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;
    }
}
package springbook.user.dao;

import springbook.user.domain.User;
import springbook.util.ConnectionMaker;
import springbook.util.impl.NConnectionMaker;

import java.sql.SQLException;

public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        ConnectionMaker connectionMaker = new NConnectionMaker();

        UserDao dao = new UserDao(connectionMaker);

        User user = new User();
        user.setId("whiteship");
        user.setName("test");
        user.setPassword("1234");

        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의 변경 없이도 네ㅇ버와 카ㅋ오는 자신들을 위한 DB 접속 클래스를 만들어서 UserDao가 사용하게 할 수 있다.
개방 폐쇄 원칙(OCP, Open-Closed Principle)을 이용하면 지금까지 해온 리펙토링 작업의 특징과 최종적으로 개선된 설계와 코드의 장점이 무엇인지 효과적으로 설명할 수 있다. 개방 폐쇄 원칙은 깔끔한 설계를 위해 적용 가능한 객체지향 설계 원칙 중의 하나다. 이 원칙을 간단히 정의하자면 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다 라 할 수 있다. UserDao는 DB 연결 방법이라는 기능을 확장하는데 열려 있다. UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있게 되어있다. 동시에 자신의 핵심기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 변경에는 닫혀 있다고 말할 수 있다.

전략 패턴

개선한 UserDaoTest - UserDao - ConnectionMaker 구조를 전략 패턴(Strategy Pattern)이라 한다. 전략패턴은 자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.UserDao는 전략패턴의 컨텍스트에 해당한다. DB 연결 방식 알고리즘을 ConnectionMaker라는 인터페이스에 정의하고 이를 구현한 클래스, 즉 전략을 바꿔가면서 사용할 수 있게 분리했다.

728x90