본문 바로가기

Spring Framework

토비의 스프링 1.3

DAO를 확장하기 전 현재까지의 상황DAO를 확장하기 전 현재까지의 상황


현재의 설계 상태는 객체지향 설계 원칙 중 하나인 '관심사의 분리'를 적용하여, 두 가지 주요 관심사를 명확히 구분한 상태입니다. 이 두 관심사는 각각 '데이터베이스(DB) 연결 관리'와 '데이터 액세스 오브젝트(DAO)의 비즈니스 로직 처리'입니다. 이러한 분리의 목적은 변화의 성격에 기인합니다. 즉, 변화의 이유, 시기, 주기 등이 서로 다르기 때문에 이들을 별도로 관리하는 것이 유리합니다.

1. 데이터베이스 연결 관리: 이 관심사는 어떤 종류의 데이터베이스에 연결할 것인지, 접속 정보(예: 호스트 주소, 사용자 이름, 비밀번호 등)를 어떻게 설정할 것인지에 초점을 맞춥니다. 이는 애플리케이션의 구성 요소 중 하나로, 종종 환경이나 배포 설정에 따라 변경될 수 있습니다.

2. DAO의 비즈니스 로직 처리: 이 관심사는 특정 테이블에 어떠한 SQL 쿼리를 날릴 것인지에 관한 것입니다. 이는 애플리케이션의 핵심 기능과 직접 관련되며, 비즈니스 요구사항의 변경에 따라 자주 수정될 수 있습니다.

현재의 설계는 추상 클래스를 사용하여 서브 클래스에서 필요에 따라 변화할 수 있는 부분을 오버라이드할 수 있도록 구성되어 있습니다. 이러한 접근 방식은 관심사의 독립성을 확보하고 유연한 변화 관리를 가능하게 합니다. 그러나 상속을 사용함으로써 드러난 단점들도 존재합니다:

- 강한 클래스 계층구조: 상속은 강한 상하위 클래스 관계를 형성합니다. 이는 슈퍼클래스의 변경이 서브클래스에 영향을 미치며, 슈퍼클래스의 자유로운 수정을 제한할 수 있습니다.
- 코드 중복의 문제: 동일한 슈퍼클래스를 여러 DAO에서 상속할 경우, 비슷한 코드의 중복이 발생할 수 있으며, 이는 유지보수를 복잡하게 만듭니다.

이러한 문제들을 해결하기 위한 대안으로는 디자인 패턴의 적용을 고려할 수 있습니다. 예를 들어, 컴포지션(Composition)과 전략 패턴(Strategy Pattern)을 사용하여 데이터베이스 연결 로직을 더 유연하게 관리할 수 있습니다. 이 방식은 상속 대신 객체의 구성을 사용하여 필요에 따라 다른 데이터베이스 연결 전략을 쉽게 교체할 수 있게 합니다.

이와 같은 접근은 객체지향 설계의 근본적인 목표, 즉 시스템의 유연성, 확장성 및 유지보수성을 향상시키기 위한 방법론입니다.

 

이는 또한 SOLID 원칙 중 개방-폐쇄 원칙(Open-Closed Principle)과 의존 역전 원칙(Dependency Inversion Principle)을 충족시키는 설계로 이어질 수 있습니다.

 

클래스의 분리
이번엔 아예 상속 관계로 이어지는 것이 아니라 독립적인 클래스로 만들어보면 어떨까? DB 커넥션과 관련된 부분을 아예 별도의 클래스에 담아보자. UserDao는 새롭게 만들어진 별도의 클래스를 이용하여 커넥션을 맺게 될 것이다.

 
public class UserDao {

    // 상속의 단점을 해결하고자, 상속을 피하고 클래스를 나누는 방식으로 해결 시도.
    // 커넥션 때문에 상속을 적용하면 추후에 다른 이유로 상속을 할 수 없음. (다중상속을 지원 안 함)
    // 슈퍼 클래스가 변경되면 하위 클래스를 모두 변경해야 함.
    // 애초에 변화가 있어도 독립적으로 서로 영향을 주지 않는 디자인을 원했는데, 상속 자체가 변화를 불편하게 만듦.
    // 위와 같은 이유 때문에 클래스를 분리함
    SimpleConnectionMaker simpleConnectionMaker;

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

    public void add(User user) throws SQLException, ClassNotFoundException {
        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 SQLException, ClassNotFoundException {
        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("org.postgresql.Driver");

        String user = "postgres";
        String password = "iwaz123!@#";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );

        return c;
    }
}

 

 수행된 리팩토링은 기능적 변경 없이 내부 코드 구조의 개선에 중점을 두었습니다. 이러한 종류의 리팩토링은 시스템의 기능적 측면을 그대로 유지하면서도, 내부 구현을 보다 효율적이고 관리하기 쉬운 형태로 변환하는 것을 목표로 합니다. 그러나 기능적 변경이 없더라도, 리팩토링 후에는 항상 테스트를 통해 코드가 여전히 올바르게 동작하는지 확인하는 것이 필수적입니다. 이는 리팩토링의 성공을 검증하고, 예상치 못한 오류를 방지하는 데 중요한 역할을 합니다.

한편, UserDao의 설계 변경에도 불구하고, 여전히 makeNewConnection() 메소드의 구현은 한 가지 방식으로 고정되어 있어, 다양한 연결 설정이 필요한 경우를 처리할 수 없는 문제가 있습니다. 이는 UserDao가 데이터베이스 연결을 생성하는 구체적인 방법(즉, 구현체)에 대해 너무 많이 알고 있기 때문에 발생하는 문제입니다. 좋은 객체지향 설계는 구체적인 구현보다는 인터페이스에 의존하는 것을 지향합니다. 즉, UserDao는 구체적인 연결 생성 방법을 알고 싶어하지 않으며, 어떠한 방식으로든 Connection을 제공받기만 하면 그에 따라 동작할 수 있어야 합니다.

이 문제를 해결하기 위한 방법으로는 디자인 패턴을 적용할 수 있습니다. 예를 들어, 팩토리 메소드 패턴이나 의존성 주입(Dependency Injection)과 같은 패턴은 UserDao로 하여금 연결 생성 방법에 대한 지식 없이도 다양한 `Connection` 구현체를 유연하게 사용할 수 있도록 할 수 있습니다. 이러한 접근 방식은 코드의 결합도를 낮추고, 확장성 및 유지보수성을 높이는 데 기여합니다.

결론적으로, 이러한 리팩토링 과정은 단순히 코드를 개선하는 것 이상의 의미를 가집니다. 시스템의 유연성을 높이고, 향후 변화에 더 쉽게 대응할 수 있는 구조를 만드는 것이 핵심입니다. 이 과정에서 중요한 것은 기능적인 측면을 유지하면서도 내부 설계를 개선하는 것이며, 이를 위해 적절한 디자인 패턴의 적용과 철저한 테스트가 수반되어야 합니다.

 

인터페이스 도입

UserDao가 데이터베이스 연결을 생성하는 클래스에 대한 구체적인 정보를 직접 알 필요 없이, 이를 위한 인터페이스를 통해 필요한 연결을 얻을 수 있도록 구성.

 
// 1.3.2 인터페이스의 사용, 1.3.3 관계 설정의 책임의 분리를 이용하여
// UserDao 는 생성자에서 해당 인터페이스를 주입 받는 방식으로 변경됨
// 어떤 Connection 을 이용할 것인지는 UserDao 의 관심사가 아니라 클라이언트의 관심사가 됨
// UserDao 는 SimpleConnectionMaker 라는 인터페이스에 맞는 오브젝트로 Connection 을 생성하는 것만이 관심사
// 구체적으로 어떤 SimpleConnectionMaker 가 들어올 것인지에 대해서는 관심이 없다.
// 이렇게 구현함으로써 훨씬 유연해진다.
public interface SimpleConnectionMaker {
    Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}

 

SimpleConnectionMaker를 인터페이스로 전환함으로써, 관심사의 분리 원칙에 따라 UserDao는 이제 오직 SimpleConnectionMaker 인터페이스가 제공하는 .makeNewConnection() 메소드를 통해 Connection 객체를 획득하는 것에만 집중합니다.

이 인터페이스의 내부 구현은 UserDao의 관심 영역 밖에 있으며, 

따라서 UserDao는 SimpleConnectionMaker의 구현 세부 사항을 알 필요가 없습니다. 이러한 접근 방식은 코드의 결합도를 낮추고, 시스템의 각 부분이 독립적으로 변화하고 발전할 수 있도록 합니다.

 

생성자 사용 불가능

UserDao 클래스에 발생한 한 가지 문법적 오류는 SimpleConnectionMaker가 이제 인터페이스로 전환되었기 때문에, 이전과 같은 방식으로 생성자를 사용하여 인스턴스를 생성할 수 없다는 점입니다. 만약 new DConnectionMaker()나 new NConnectionMaker()와 같이 구체적인 클래스 이름으로 인스턴스를 생성한다면, 이는 이전의 접근 방식과 다를 바 없으며, 이는 결국 인터페이스의 도입 목적인 결합도를 낮추고 유연성을 증가시키는 원칙과 상반됩니다.

    public UserDao() {
        // 문법적 오류. SimpleConnectionMaker는 인터페이스임
        this.simpleConnectionMaker = new SimpleConnectionMaker();
    }

 

 

세부적인 Connection 책임은 클라이언트에게 넘기기

UserDao의 생성자를 다음과 같이 변경.

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

 

 

이 변경으로 인해, UserDao를 사용하는 클라이언트는 SimpleConnectionMaker의 구체적인 구현 객체를 생성하고 이를 UserDao에 주입하는 책임을 맡게 됩니다. 이로써 UserDao는 데이터베이스 연결에 관한 모든 책임으로부터 해방되며, SQL 쿼리 작성 및 실행에 집중할 수 있게 됩니다. 이제 데이터베이스 연결 방식에 어떠한 변화가 있더라도 UserDao의 코드를 수정할 필요가 없어집니다.

이와 같은 설계는 객체간의 관계가 설계 시점이 아닌 런타임에 결정되도록 합니다. 즉, 클라이언트는 UserDao가 사용할 SimpleConnectionMaker의 레퍼런스를 보유하고 있으며, UserDao의 인스턴스를 생성할 때 이를 전달합니다.

이러한 유형의 관계 설정은 '다이내믹한 관계(Dynamic Relationship)'라고 불리며, 객체지향 프로그래밍의 다형성을 활용하여 구현됩니다. 또한, 이러한 관계는 '의존 관계(Dependency Relationship)'로도 알려져 있습니다.

인터페이스의 도입과 클라이언트에 의한 런타임에의 동적인 관계 형성은 상속을 사용했을 때보다 훨씬 더 유연한 설계를 가능하게 합니다.

 

 
원칙과 패턴
 
개방 폐쇄 원칙 (OCP, Open-Closed Principle)
 
객체지향 SOLID 원칙
 
 
높은 응집도와 낮은 결합도
개방-폐쇄 원칙은 소프트웨어 개발에서 고전적인 원리인 '높은 응집도와 낮은 결합도'로 설명될 수 있습니다.

높은 응집도는 모듈이나 클래스가 단일 책임이나 관심사에 집중되어 있는 상태를 의미한다. 이는 한 클래스나 모듈 내에서 공통된 관심사가 집중되어 있으며, 불필요한 외부 책임과 관심사로부터 분리되어 있다는 것을 나타낸다. 응집도가 높은 시스템은 클래스, 패키지, 컴포넌트, 모듈 등 다양한 수준에서 적용되며, 변경 시 해당 모듈 내부에서 대부분의 변경이 이루어지는 특성을 가진다. 즉, 변경이 필요할 때 해당 모듈 내에서 대다수의 부분이 함께 변경되는 것이 이상적이다. 반면, 변경이 모듈의 일부에만 국한되면, 어떤 부분이 변경되어야 하는지 파악하는 부담과 변경으로 인한 영향을 확인해야 하는 이중적인 부담이 발생한다. 높은 응집도는 모듈의 변경이 명확하고, 이로 인한 다른 클래스의 수정 요구가 없으며, 기능에 미치는 영향이 적어야 함을 의미한다.

낮은 결합도는 서로 다른 책임과 관심사를 가진 객체나 모듈 간의 결합을 최소화하는 것을 목표로 한다. 낮은 결합도는 시스템의 유연성과 확장성을 높이며, 변화에 빠르게 대응할 수 있게 한다. 결합도는 한 객체의 변경이 관련된 다른 객체에 얼마나 큰 영향을 미치는지를 나타내는 지표로, 낮은 결합도는 한 객체의 변경이 다른 객체에 미치는 영향을 최소화한다.

예를 들어, ConnectionMaker 인터페이스의 도입은 UserDao와 데이터베이스 연결 방식 간의 낮은 결합도를 보여줍니다. DB 연결 방식의 변경이 발생하더라도, UserDao는 그 자체로 변경될 필요가 없습니다. 필요한 ConnectionMaker의 구현체는 클라이언트 코드에서 결정되며, 이로 인해 UserDao와 ConnectionMaker 간의 관계는 인터페이스를 통해 느슨하게 연결되어 있습니다. 이러한 설계는 런타임에 구체적인 구현체를 선택함으로써 유연성을 제공하며, UserDao의 코드 수정 없이도 새로운 연결 방식을 채택할 수 있게 합니다.
 
 
 
개선된 UserDaoTest, UserDao 및 ConnectionMaker 구조는 디자인 패턴의 관점에서 전략 패턴(Strategy Pattern)의 예시로 간주될 수 있다. 전략 패턴은 특정 컨텍스트(여기서는 UserDao) 내에서 필요에 따라 변경 가능한 '알고리즘'을 인터페이스를 통해 완전히 외부로 분리하고, 이를 구현한 구체적인 알고리즘 클래스(여기서는 ConnectionMaker를 구현한 클래스)를 상황에 따라 교체할 수 있게 하는 디자인 패턴이다. 이러한 이유로 이를 '전략'이라고 부르며, 이 패턴의 명칭이 전략 패턴으로 지어졌다.

'알고리즘'이라는 용어는 이 경우 단순히 독립적인 책임으로 분리 가능한 기능을 의미한다. 전략 패턴에서는 컨텍스트(예: UserDao)가 실행할 전략(예: ConnectionMaker의 구현체)을 클라이언트(예: UserDaoTest)가 결정하고 제공한다. 이는 일반적으로 컨텍스트의 생성자를 통해 이루어진다. 이러한 접근 방식은 컨텍스트가 전략의 구체적인 구현에 의존하지 않게 하여, 시스템의 유연성을 높이고 확장성을 개선한다. 결과적으로, 컨텍스트는 다양한 전략을 쉽게 교체하며 사용할 수 있게 되며, 이는 결합도를 낮추고, 책임을 명확히 분리하는 효과를 가져온다.

'Spring Framework' 카테고리의 다른 글

토비의 스프링 1.5  (0) 2024.04.09
토비의 스프링 1.4  (0) 2024.04.09
토비의 스프링 1. 오브젝트와 의존관계  (0) 2024.04.09
Spring – JDBC Template  (0) 2024.04.09
PlatformTransactionManager  (0) 2024.04.09