[토비의 스프링 3.1] 오브젝트와 의존관계

1.1 초난감 DAO

DAO란?
Data Access Object의 줄임말로 DB에 접근해 데이터를 조회하거나 저장하는 등의 역할을 수행하는 객체를 의미한다.
//UserDao.java

public void addUser(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");

        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/toby_spring", "root", "xxx");

        PreparedStatement ps = connection.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();
        connection.close();
}

public User getUser(String id) throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");

        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/toby_spring", "root", "xxx");

        PreparedStatement ps = connection.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet resultSet = ps.executeQuery();
        resultSet.next();

        User user = new User();
        user.setId(resultSet.getString("id"));
        user.setName(resultSet.getString("name"));
        user.setPassword(resultSet.getString("password"));

        resultSet.close();
        ps.close();
        connection.close();

        return user;
}

위 코드의 문제점은 무엇일까? 일단 중복된 코드가 많다.

  • Driver를 초기화하고
  • DB 커넥션을 맺고
  • 자원을 사용하는 객체를 해제하고

위와 같은 일들은 데이터를 조회, 저장하는 기능에 모두 공통적으로 필요한 작업이다. 그런데 각 메소드에 중복되어 작성되어 있으므로 분리가 가능하다. 그렇다면 분리라는 것은 무엇의 분리를 의미할까?

1.2 DAO 분리

1.2.1 관심사의 분리

위에서 중복된 코드가 하는 일들의 목록을 작성했다. 이것 하나하나가 관심사이다. 책에서 나온 말을 인용하자면 "미래를 준비하는데 있어 가장 중요한 과제는 변화에 어떻게 대비할 것인가"이다. 변화란 사용자의 요구사항 같은 것인데 요구사항은 굉장히 복합적으로 구성되어 있어 기능 전반에 영향을 미칠 가능성이 높다. 이 때 관심사를 잘 분리해놓았다면 해당 관심사에 해당하는 것만 변경하면 되므로 영향도를 최소한으로 줄일 수 있다. 즉 관심사의 분리란 관심이 같은 것끼리는 가까이에 두고 관심이 다른 것은 멀리 두는 것을 의미한다.

위 초난감 DAO를 관심사에 따라 하나씩 분리해보자.

1.2.2 커넥션 만들기 추출

public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/toby_spring", "root", "xxx"
}

각 메소드에 중복되어 존재했던 DB 커넥션을 가져오는 작업을 getConnection()이라는 메소드로 분리해냈다. 각 메소드에서는 이 메소드를 이용하면 된다. 만약 DB명이 바뀌었다거나 비밀번호가 바뀌었다거나 하는 변경이 발생했을 때 기존에는 각 메소드별로 모두 수정해줬어야 했다면 이제는 getConnection()만 수정해주면 된다.

1.2.3 DB 커넥션 만들기의 독립

만약 UserDao를 납품한다고 가정해보자. 다만 납품받는 회사에서 DB 커넥션을 각자 다른 방법으로 적용하려고 한다. 우리는 소스코드 자체를 제공하지 않고 컴파일된 바이너리 파일만 제공하려고 한다. 어떻게 하면 좋을까

1. 추상 클래스로 분리 (상속을 통한 확장)

public User getUser(String id) throws ClassNotFoundException, SQLException {
        Connection connection = this.getConnection();

        ...
    }

public abstract Connection getConnection() throws ClassNotFoundException, SQLException;

이 방법은 커넥션을 만드는 관심을 추상클래스를 상속받는 자식 클래스에 할당한 것이다. 즉 우리는 추상클래스만 컴파일해 제공해주고 실제 구현은 납품받는 곳에서 상속받아 하는 것이다.

이렇게 부모 클래스에서 기본적인 기능의 틀을 정의해놓고 구체적인 작동방식은 추상 메소드 등으로 만들어 자식 클래스에서 정의하도록 하는 디자인 패턴을 템플릿 메소드 패턴이라 한다.

또한 상위 클래스는 하위 클래스에서 어떤 Connection 객체를 구현하는지 관심이 없다. 하위 클래스는 그저 Connection 인터페이스의 구현체만 전달하면 된다. 이처럼 하위 클래스에서 구체적인 객체를 생성하는 방식을 팩토리 메소드 패턴이라 한다.

사실 책을 읽을 때 static 메소드로 객체를 생성하는 역할을 하는 메소드를 정적 팩토리 메소드라 하는데 이와 이름이 똑같아 헷갈렸는데 책에서 의미가 달라 혼동하지 말라는 내용이 있어 이해하게 됐다.

 

그런데 이처럼 상속을 통한 방법은 단점이 있다. 이는 상속이 가지는 한계와 비슷한 점을 공유한다.

  • java는 다중상속을 지원하지 않으므로 만약 UserDao 클래스가 다른 목적을 위해 상속을 사용하고 있었다면 커넥션 만들기 분리를 위한 상속을 사용할 수 없다.
  • 상속을 한다면 하위 클래스는 부모 클래스의 메소드를 사용할 수 있다. 만약 부모 클래스의 메소드가 변경됐다면 하위 클래스에도 영향이 간다. 즉 결합도가 너무 강하다.

1.3 DAO의 확장

2. 별도 클래스로 분리

public class SimpleConnectionMaker {

    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/toby_spring", "root", "xxx"
    }
}
public class UserDao {

    private final SimpleConnectionMaker connectionMaker = new SimpleConnectionMaker();

    public void addUser(User user) throws ClassNotFoundException, SQLException {
        Connection connection = connectionMaker.getConnection();

        ...
    }
 }

커넥션을 만드는 역할을 아예 별도의 클래스로 분리했다. UserDao에서는 해당 클래스를 인스턴스 변수로 생성하고 이를 이용하면 된다. 그러나 이 방식은 큰 문제가 있다. 납품받는 곳에서 각자의 방식으로 커넥션 만들기를 구현할 방법이 없어졌다. 직접 UserDao를 수정하지 않는 이상, 즉 인스턴수 변수를 직접 수정해야만 하는 상황이다. 이는 간단하게 표현하면 "UserDao 클래스가 커넥션 만들기의 관심사에 대해 너무 많이 의존하고 있다". UserDao는 SimpleConnectionMaker라는 구체적인 타입까지 알고 있는 상황이다. 이를 어떻게 개선할 것인가?

3. 인터페이스 도입

public interface ConnectionMaker {
    Connection getConnection() throws ClassNotFoundException, SQLException;
}
public class AConnectionMaker implements ConnectionMaker{
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/toby_spring", "root", "xxx");
    }
}
public class UserDao {

    private ConnectionMaker connectionMaker;

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

    ...  
 }

인터페이스는 추상화를 할 때 사용한다. 추상화란 구체적인 것에서 일반적인 것을 뽑아내는 것이다. Connection 객체는 여러 구체적인 것이 존재할 수 있지만 UserDao에서 알고 싶은 것은 그저 Connection 객체이다. 따라서 Connection 객체를 만드는 메소드를 인터페이스에서 정의하고, UserDao는 해당 인터페이스의 메소드를 이용하고 실제 구현은 ConnectionMaker를 구현하는 구현체에서 하면 된다.

그런데 이렇게 해도 아주 핵심적인 문제가 있다. UserDao에서 구현체를 알아야 한다는 것이다. 근본적인 문제는 해결되지 않았다.

1.3.3 관계설정 책임의 분리

클래스 간의 관계와 오브젝트 간의 관계의 차이는 무엇일까? 클래스 간에 관계는 클래스 내부에 다른 클래스가 등장할 때를 의미한다. 예를 들어 A class에서 B class를 사용하고 있다면 이는 A, B class 간에 관계가 있다고 할 수 있다. 오브젝트 간의 관계는 A class에서 B interface를 구현한 객체를 사용하고 있다면 이는 오브젝트 간 관계가 있다고 할 수 있다. A class를 사용하려면 A class 객체를 만들어야 하고 A class는 B interface의 구현체 클래스가 무엇인지는 알 필요 없이 구현체의 객체(reference)만 알면 된다. 이처럼 클래스 간 관계와 오브젝트 간 관계는 차이가 있다. 이것이 객체지향 프로그래밍에서 말하는 다형성에서 기인한다.

위에서 UserDao class와 AConnection class 간에 불필요한 관계가 문제가 됐다. 이를 오브젝트 간 관계로 바꿔주면 UserDao는 ConnectionMaker의 구현 클래스가 무엇인지 알 필요 없이 커넥션을 가져와 기능을 수행할 수 있다. 그럼 여기서 중요한 건 ConnectionMaker의 구현체를 주입해주는 무언가가 필요하다. 일단 그 역할을 수행하는 존재는 UserDao를 사용하는 클라이언트라 할 수 있다. 간단하게 main 메소드가 있는 Main class가 해당 역할을 수행할 수 있을 것이다.

public class Main {

    public static void main(String[] args) throws Exception {

        ConnectionMaker connectionMaker = new AConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);

        ...
    }
}

이렇게 하니 UserDao class에 아무런 수정이 없이 납품 받은 곳에서 각자 구현하고 싶은대로 커넥션 만들기 기능을 구현하고 UserDao의 기능을 사용할 수 있게 됐다.

전략 패턴

지금까지 UserDao에서 Connection을 만드는 작업을 인터페이스로 분리하고 UserDao는 해당 인터페이스를 사용하는 방식으로 코드를 개선했다. 이처럼 어떤 기능에서 변경이 필요한 부분을 인터페이스로 분리하고 인터페이스를 구현한 클래스를 필요에 따라 바꿔서 사용하게끔 설계하는 방식을 디자인 패턴에서 전략 패턴이라 한다. 스프링에서 사용하는 대표적인 디자인 패턴이 바로 전략 패턴이다. 아래 사진은 전략 패턴을 도식화 한 것이다.

 

전략패턴

(출처: https://upload.wikimedia.org/wikipedia/commons/4/45/W3sDesign_Strategy_Design_Pattern_UML.jpg)