본문 바로가기

Spring/토비의 스프링 3.1

3.2 변하는 것과 변하지 않는 것


3.2.1 JDBC try/catch/finally 코드의 문제점

복잡한 try/catch/finally 블록이 2중으로 중첩되며, 모든 메소드마다 반복된다.

  • Copy & Paste는 미친 짓이다.
  • 실수를 해도 테스트가 통과할 가능성이 높다.
  • 잘 닫혔는지 테스트 코드를 만드는 것도 불가능에 가깝다.

많은 곳에서 자주 중복되는 코드와 로직에 따라 확장되고 자주 변하는 코드를 분리해야 한다.

3.2.2 분리와 재사용을 위한 디자인 패턴 적용

deleteAll(), add() 메소드를 살펴보면 고정되어 있는 부분과 변하는 부분이 구분된다.

  • 변하지 않는 부분 : 커넥션 생성, 예외처리
  • 변하는 부분 : PreparedStatement

메소드 추출

일단 변하는 부분을 메소드로 빼는 것이 가장 간단할 것 같지만, 변하는 부분을 변하지 않는 부분이 감싸고 있기 때문에 반대로 해보자.

UserDaoTest.java

public class UserDao {
    // ...
    public PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps;
        ps = c.prepareStatement("DELETE FROM users WHERE 1=1");
        return ps;
    }

    public void deleteAll() throws SQLException {
        // ...
        try { 
            c = dataSource.getConnection();
            ps = makeStatement(c);
            ps.executeUpdate();
        }
        // ...
    }
}

자주 바뀌는 부분을 메소드로 독립시켰지만 별 이득이 없다.

  • 분리시키고 남은 메소드가 재사용이 필요
  • 분리된 메소드는 확장되어야 함

반대로 됐다.

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴 : 상속을 통해 기능을 확장

  • 슈퍼 클래스 : 변하지 않는 부분
  • 서브 클래스 : 변하는 부분을 오버라이드

템플릿 메소드 패턴을 적용한 UserDao (임시)

public abstract class UserDao {
    // ...
    abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
}

class UserDaoDeleteAll extends UserDao {
    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("DELETE FROM users WHERE 1=1");
        return ps;
    }
}

DeleteAllStatement.java

public class DeleteAllStatement implements StatementStrategy {
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("DELETE FROM users WHERE 1=1");
        return ps;
    }
}

UserDaoTest.java

public class UserDaoTest {
    public void setUp() {
        dao = new UserDaoDeleteAll();
    }
}

기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장 가능

  • OCP를 그럭저럭 지키는 구조
  • 상속은 싫어
  • DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 함
  • 또한 확장구조가 클래스 설계시점에 고정되어 버림

전략 패턴의 적용

오브젝트를 아예 둘로 분리하고 클래스 레벨에서 인터페이스를 통해서만 의존하도록 만듦

  • Context의 contextMethod()에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Starategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임

deleteAll()의 컨텍스트

  • DB 커넥션 가져오기
  • PreparedStatement를 만들어 줄 외부 기능 호출하기
  • 전달받은 PreparedStatement 실행하기
  • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만들어진 PreparedStatement와 Connection 닫아주기

PreparedStatement 만들어 주는 기능 : 전략패턴에서의 전략

  • 해당 기능을 인터페이스로 만들어두고 인터페이스의 메소드를 통해 PreparedStatement를 생성하는 전략을 호출
  • 호출할 때 DB 커넥션을 전달해 주어야 함

StatementStrategy.java

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
public class DeleteAllStatement implements StatementStrategy {
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("DELETE FROM users WHERE 1=1");
        return ps;
    }
}

deleteAll()

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        StatementStrategy strategy = new DeleteAllStatement();
        ps = strategy.makePreparedStatement(c);

        ps.executeUpdate();
    }
    //...
}

코드의 문제점

  • 컨텍스트 안에서 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정

DI 적용을 위한 클라이언트/컨텍스트 분리

Context가 어떤 전략을 사용하게 할 것인가?

  • Client가 결정하는 것이 일반적
  • JDBC try/catch/finally 구문을 StatementStrategy를 만드는 부분에서 독립시켜야 함
  • deleteAll()의 나머지 코드는 컨텍스트 코드이므로 분리해야함

컨텍스트에 해당하는 부분을 별도의 메소드로 독립

  • 클라이언트 : 전략 클래스의 오브젝트를 컨텍스트 메소드로 전달해야함
  • 전략 인터페이스인 StatementStrategy를 컨텍스트 메소드 파라미터로 지정

UserDao.java

public class UserDao {
    // ...

    public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = stmt.makePreparedStatement(c);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                }
            }

            if (c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                }
            }
        }
    }

    // ...
}

컨텍스트의 핵심적인 내용을 잘 담고있눈 메소드

  • 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고, try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행
  • 제공받은 전략 오브젝트는 PreparedStatement 생성이 필요한 시점에 호출해서 사용

클라이언트(deleteAll)

  • 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임
  • 사용할 전략 클래스 : DeleteAllStatement 생성
  • jdbcContextWithStatementStrategy() 메소드를 호출
public void deleteAll() throws SQLException {
    StatementStrategy st = new DeleteAllStatement();
    jdbcContextWithStatementStrategy(st);
}

구조적으로 완전한 전략 패턴의 모습을 갖춤

  • 아직 클라이언트와 컨텍스트가 클래스로 분리되지는 않음
  • 의존관계와 책임으로 볼 때 이상적인 클라이언트/컨텍스트 관계
  • DI 구조이기도 함(클라이언트가 컨텍스트가 사용할 전략을 정해서 전달)

마이크로 DI
의존관계 주입(DI)은 다양한 형태로 적용할 수 있다. DI의 가장 중요한 개념은 제 3자의 도움을 통해 두 오브젝트 사이의 관계가 유연한 관계가 되도록 설정되도록 만드는 것
일반적으로 DI는 두 개의 오브젝트, 오브젝트 팩토리(DI 컨테이너), 그리고 클라이언트 4개의 오브젝트 사이에서 일어남
그러나 때로는 클라이언트가 오브젝트 팩토리의 책임을 함께 지는 경우도 있음. 이런 경우 DI가 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 함
마이크로 DI, 코드에 의한 DI, 수동 DI라고 부를 수 있음


' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

3.4 컨텍스트와 DI  (0) 2019.01.22
3.3 JDBC 전략 패턴의 최적화  (0) 2019.01.18
3.1 다시보는 초난감 DAO  (0) 2019.01.17
3.0 템플릿  (0) 2019.01.17
2.6 정리  (0) 2019.01.17