IOC의 필요성 - 2. 클래스 상속을 통한 관심사 분리

2022. 1. 9. 15:42북리뷰/토비의 봄

728x90

DB Connection 제작 메서드 독립

직전 포스팅에서 제작한 UserDao 코드가 엄청난 인기를 얻어 네ㅇ버와 카ㅋ오에서 UserDao를 구매하겠다고 주문이 들어왔다.
그런데, 납품 과정에서 문제가 발생했다. 문제는 네ㅇ버와 카ㅋ오에서 각기 다른 종류의 DB를 사용하고 있고, DB 커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다. 더욱 큰 문제는 UserDao를 구매한 이후에도 DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 높다는 점이다.
물론 고객한테 직접 UserDao 소스코드를 제공하여 직접 수정해서 쓰라고 하고 싶지만, 초 특급 비밀기술이 적용된지라 고객에게 소스코드를 직접 공개하고 싶지 않다. 이런 경우 어떻게 해야 고객 스스로 원하는 DB커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?

상속을 통한 확장

기존 UserDao코드를 한 단계 더 분리하면 어떨까? 일단 우리가 만든 UserDao에서 메서드의 구현코드를 제거하고, getConnection()을 추상 메서드로 만들어 놓는다. 추상 메서드라서 메서드 코드는 없지만 메서드 자체는 존재한다. 따라서 add(), get() 메서드에서 getConnection()을 호츨하는 코드는 그대로 유지할 수 있다.

package springbook.user.dao;

import springbook.user.domain.User;

import java.sql.*;

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

        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.setPass word(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
package springbook.user.dao.impl;

import springbook.user.dao.UserDao;

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

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

        return c;
    }
}

이 추상클래스인 UserDao를 각 네ㅇ버와 카ㅋ오에 판매한다. UserDao를 구입한 회사들은 각각 UserDao 클래스를 상속해서 NUserDao, KUserDao라는 서브클래스를 만든다. 서브클래스 내에서 UserDao에 추상 메서드로 선언한 getConnection()메서드를 원하는 방식대로 구현할 수 있다. 이렇게 하면 UserDao의 소스코드를 수정해서 쓰지 않아도 getConnection()메서드를 원하는 방식으로 확장한 후에 UserDao의 기능과 함께 사용할 수 있다.
기존에는 같은 클래스에 다른 메서드로 분리했던 DB커넥션 연결이라는 관심을 이번에는 상속을 통해 서브클래스로 분리해버리는 것이다.

이렇게 슈퍼클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환)을 만들고, 그 기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤 서브클래스에서 이런 메서드를 필요에 맞게 구현해서 사용하는 디자인 패턴을 템플릿 메서드 패턴(template method pattern) 이라 한다.
그리고 UserDao의 getCOnnection() 메서드는 Connection타입의 오프젝트를 생성하는 기능을 정의해놓은 추상 메서드다. 또한 UserDao의 서브클래스(NUserDao)의 서브 클래스의 getConnection()메서드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지를 결정하는 방법이라고 볼 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메서드 패턴(factory method pattern) 라고 한다.

템플릿 메서드 패턴

상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. 슈퍼클래스에서는 미리 추상 메서드 또는 오버라이드 가능한 메서드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메서드를 만든다. 슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메서드를 훅(hook)메서드라고 한다. 서브클래스에서는 추상메서드를 구현하거나, 훅 메서드를 오버라이드 하는 방법을 이용해 기능의 일부를 확장한다.

public abstract class Super {
    public void templateMethod() {
        //기본 알고리즘 코드
        hookMethod();
        abstractMethod();
    }
    protected void hookMethod() {} // 선택적으로 오버라이드 가능한 훅 메서드
    public abstract void abstractMethod(); // 서브클래스에서 반드시 구현해야 하는 추상메서드
 }

팩토리 메서드 패턴

팩토리 메서드 패턴도 템플릿 메서드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 그래서 구조도 비슷하다. 슈퍼클래스 코드에서는 서브클래스에서 구현할 메서드를 호출해서 필요한 타임의 오브젝트를 가져와 사용한다. 이 메서드는 주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에서 알지 못한다. 사실 관심도 없다. 서브클래스는 다양한 방법으로 오브젝트를 생성하는 메서드를 재정의할 수 있다. 이렇게 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메서드를 팩토리 메서드라고 하고, 이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법을 팩토리 메서드 패턴이라고 한다.

위 코드의 한계점

이렇게 템플릿 메서드 패턴 또는 팩토리 메서드 패턴으로 관심사항이 다른 코드를 분리해내고, 서로 독립적으로 변경 또는 확장할 수 있도록 만드는 것은 간단하면서도 매우 효과적인 방법이다.
하지만, 이 방법은 상속을사용했다는 단점이 있다. 상속 자체는 간단해 보이고 사용하기도 편리하게 느껴지지만 사실 많은 한계점이 있다.
만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있으면? 자바는 클래스의 다중상속을 허용하지 않는다. 단지, 커넥션 객체를 가져오는 방법을 분리하기 위해 상속구조로 만들어버리면 후에 다른 목적으로 UserDao에 상속을 적용하기가 힘들다.
또 다른 문제는 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다는 점이다. 상속을 통해 관심이 다른 기능을 분리하고 필요에 따라 다양한 변신이 가능하도록 확장성도 줬으나, 여전히 서브 클래스는 슈퍼클래스의 기능을 직접 사용할 수 있어 만약 슈퍼클래스 내부가 변경되면 모든 서브클래스를 함께 수정해야 한다.
확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다는 것도 단점이다. 만약 UserDao외에 다른 DAO클래스들이 계속 만들어진다면 그때는 상속을 통해서 만들어진 getConnection() 구현 코드가 매 DAO클래스마다 중복해서 나타날 것이다.

728x90