3장-템플릿

37 분 소요

3장 템플릿

바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법

3.1 다시 보는 초난감 DAO

  • 초난감 DAO는 많은 개선을 했지만, 예외상황에 대한 처리를 하지 않았다.

3.1.1 예외처리 기능을 갖춘 DAO

  • DB커넥션은 DB의 소중한 자원이므로, 어떤 이유로든 예외가 발생하더라도 리소스를 반드시 반환해야한다. 그렇지 않으면 시스템에 큰 문제를 일으킬 수 있다.

JDBC 수정 기능의 예외처리 코드

JDBC API를 이용한 DAO 코드인 deleteAll()

public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("delete from users");
    ps.executeUpdate();
    // 여기서 예외가 발생하면 바로 메소드 실행이 중단된다.

    ps.close();
    c.close();
}

위는 .deleteAll()의 코드다. 위 메소드에서는 정상적인 흐름의 경우에는 ps.close()c.close()가 잘 호출되어 리소스를 반환한다. 그런데 PreparedStatement를 처리하는 중에 예외가 발생하면 메소드 실행을 끝마치지 못하고 바로 메소드를 빠져나가게 되어 ConnectionPreparedStatement의 close() 메소드가 실행되지 않아 제대로 리소스가 반환되지 않을 수 없다.

서버는 DB커넥션을 풀로 관리하는데, 리소스가 반환되지 않으면 서버는 언젠가 리소스가 모자란다는 에러를 내며 중단될 것이다.

리소스 반환과 close() .close()라서 열린 것을 닫는 것으로 생각하기 쉽다. 하지만 엄밀히 말하면 리소스를 반환한다고 이해하는 것이 좋다.

예외 발생 시에도 리소스를 반환하도록 수정한 deleteAll()

  • 예외 상황에서도 리소스를 제대로 반환할 수 있도록 try/catch/finally를 적용해보자.
public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        ps = c.prepareStatement("delete from users");
        ps.executeUpdate();
        // 예외가 발생할 가능성이 있는 코드를 모두 try 블록으로 묶어준다.
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                // ps.close() 메서드에서도 SQLException이 발생할 수 있기 때문에 이를 잡아줘야 한다.
                // 그렇지 않으면 Connection을 close() 하지 못하고 메소드를 빠져나갈 수 있다.
            }
        }
        if (c != null) {
            try {
                c.close() //Connection 반환
            } catch (SQLException e) {

            }
        }
    }
}

예외가 발생할 수 있는 모든 시점을 고려해주어서 코드를 작성하였다.

  • getConnection()을 이용한 커넥션 생성지점에서 예외가 나면, cps 모두 null 상태이다.
    • 이 경우 널체크를 하지 않고 .close()를 호출하면 NullPointerException이 발생한다.
  • PreparedStatement를 생성하다가 예외가 발생하면 그 때는 psnull상태이다.
  • ps를 실행하다가 예외가 발생했다면 psc모두 null이 아니다.
  • finally에서는 두 변수가 null이 아닌지 체크한 뒤에 .close()를 호출하면 된다.
  • 문제는 .close()SQLException이 발생할 수 있다. .close()를 수행하는 도중 SQLException이 발생하면 .close()아래의 로직이 실행되지 않기 때문에, catch를 해주는 것이 좋다. - 마지막 close()try/catch는 혹시 예외 처리가 필요할 수도 있으니 해주었다.

    JDBC 조회 기능의 예외처리

  • 조회를 위한 JDBC 코드는 더 복잡해진다. ResultSet이 더 추가되기 때문이다. getCount()의 예외처리를 해보자.

JDBC 예외처리를 적용한 getCount() 메서드

public int getCount() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    try {
        c = dataSource.getConnection();

        ps = c.prepareStatement("select count(*) from users");

        rs = ps.executeQuery();
        rs.next();
        return rs.getInt(1);
    } catch (SQLException e) {
        throw e;
    } finally {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                // 만들어진 ResultSet을 닫아주는 기능. close()는 만들어진 순서의 반대로 하는 것이 원칙이다.
            }
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
            }
        }
        if (c != null) {
            try {
                c.close();
            } catch (SQLException e) {
            }
        }
    }
}

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

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

  • 복잡한 try/catch/finally 블록이 2중으로 중첩돼서 나오며, 모든 메소드마다 반복된다. 보통 이렇게 반복되는 코드에 대한 나쁜 습관은 따로 분리하지 않고 복사 붙여넣기로 처리하는 것이다.

  • 그러다가 어느 순간 한 줄을 빼먹고 복사하거나 몇 줄을 잘못 삭제하면 어떻게 될까? 괄호를 잘못 닫은게 아니라면 당장에 컴파일 에러가 나지는 않는다. 하지만 이렇게 별 문제 없어보이면서 리소스를 반환하지 않으면 더욱 심각한 문제가 된다.

  • 정상 동작하는 것처럼 보이는 서버가 며칠 주기로 리소스가 꽉찼다는 에러를 내면서 중지되는 상황이 올 수 있다.

  • 누군가 DAO 로직을 수정하려고 했을 때, 복잡한 try/catch/finally 블록 안에서 필요한 부분을 찾아서 수정해야하고, 언젠가 필요한 부분을 잘못 삭제했는데 코드가 정상적으로 동작하면 역시 같은 문제가 반복될 것이다.

  • 언제 터질지 모르는 폭탄과 같은 코드가 될 수 있다. 우리는 이전에 1장에서 이러한 코드를 효과적으로 분리하는 방법을 배웠었다.

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

UserDao.deleteAll() 메소드를 먼저 개선해보자.

개선할 deleteAll() 메소드

Connection c = null;
PreparedStatement ps = null;

try {
    c = dataSource.getConnection();

    ps = c.prepareStatement("delete from users"); // 변하는 부분

    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) {} }
}

deleteAll()에서 변경될 수 있는 부분은 단 한줄이다.

ps = c.prepareStatement("delete from users"); // 변할 수 있는 부분

저 부분만 유연하게 변경될 수 있으면 ResultSet이 필요없는 어떠한 쿼리도 같은 템플릿으로 처리할 수 있게 될 것이다.

그러면 우리가 정의한 변할 수 있는 부분변하지 않는 부분을 나누어보자.

add() 메소드에서 수정할 부분

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

메소드 추출

변하는 부분을 메소드로 추출한 후의 deleteAll()

public void deleteAll() throws SQLException {
    ...
    try {
        c = dataSource.getConnection();

        ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 만들었다.

        ps.executeUpdate();
    } catch (SQLException e) {
        ...
    }
    private PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps;
        ps = c.prepareStatement("delete from users");
        return ps;
    }
}
  • 보통 메서드 추출 리팩토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문이다.

템플릿 메소드 패턴의 적용

  • 이번엔 템플릿 메소드 패턴을 이용해서 분리해본다.
  • 템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다.

makeStatement()를 구현한 UserDao 서브클래스

public class UserDaoDeleteAll extends UserDao {
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}
  • 템플릿 메소드 패턴은 위와 같이 추상 클래스를 만들고 변화가 필요한 부분을 추상 메소드로 빼서, 해당 추상 클래스를 상속 받은 뒤 추상 메소드를 구현하는 방식이었다.

  • 확장 때문에 기존의 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙(OCP)은 그럭저럭 지킬 수 있지만, 이렇게 구현하면 각 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야한다. UserDao의 JDBC메소드가 4개일 경우, 4개의 서브 클래스가 필요해진다.

  • 또, 확장구조가 이미 클래스를 설계하는 시점에서 고정된다. 변하지 않는 코드를 가진 UserDao의 JDBC try/catch/finally 블록과 변하는 PreparedStatement를 담고 있는 서브 클래스들이 이미 클래스 레벨에서 컴파일 시점에서 그 관계가 결정되어 있다. 따라서 관계에 대한 유연성이 떨어져버린다.

전략 패턴의 적용

  • 개방 폐쇄 원칙(OCP)을 잘 지키면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다. 전략 패턴은 OCP관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.

전략패턴의적용1

  • 좌측에 있는 ContextcontextMethod()에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략을 클래스에 위임하는 것이다.

  • deleteAll()에서 변하지 않는 부분이라고 명시한 것이 바로 이 contextMethod()가 된다.
  • deleteAll()은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 컨텍스트를 갖는다.
    • DB 커넥션 가져오기
    • PreparedStatement를 만들어줄 외부 기능 호출하기
    • 전달받은 PreparedStatement실행하기
    • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
    • 모든 경우에 만들어진 PreparedStatementConnection을 적절히 닫아주기

PreparedStatement를 생성하는 전략을 호출할 때는 이 컨텍스트 내에서 만들어둔 DB커넥션을 전달해야한다는 점을 눈여겨봐야 한다.

StatementStrategy 인터페이스

package springbook.user.dao;
...
public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

deleteAll() 메소드의 기능을 구현한 StatementStrategy 전략 클래스

package springbook.user.dao;
...
public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Conneciton c) throws SQLException {
        PreparedStatement ps = c.preparedStatement("delete from users");
        return ps;
    }
}

전략 패턴을 따라 DeleteAllStatement가 적용된 deleteAll() 메소드

public void deleteAll() throws SQLException {
    ...
    try {
        c = dataSource.getConnection();

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

        ps.executeUpdate();
    } catch (SQLException e) {
        ...
    }
}
  • 전략 패턴은 적용했는데, 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서(OCP 폐쇄 원칙) 전략을 바꿔쓸 수 있다는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다.
  • 컨텍스트가 StatementStrategy인터페이스 뿐만 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없기 때문이다.

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

전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 앞단의 Client가 결정하는 것이 일반적이다.

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

이전 ConnectionMaker에 전략 패턴을 적용했을 때와 동일한 그림이 나왔다. 결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 ObjectFactory이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)이었다. 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.

StatementStrategy strategy = new DeleteAllStatement();
  • 컨텍스트에 해당하는 부분은 별도의 메소드로 독립시켜보자. 클라이언트는 DeleteAllStatement오브젝트 같은 전략 클래스의 오브젝트를 컨텍스트 메소드로 전달해야 한다.

  • 이를 위해 전략 인터페이스인 StatementStrategy를 컨텍스트 메소드 파라미터로 지정할 필요가 있다.

메소드로 분리한 try/catch/finally 컨텍스트 코드

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

    tyr {
        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) {} }
    }
}

클라이언트 책임을 담당할 deleteAll() 메소드

public void deleteAll() throws SQLException {
    StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
    jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달
}
  • 위의 구조로 볼 때, 완벽한 전략 패턴의 모습을 갖췄다. 비록 클라이언트와 컨텍스트를 분리하진 않았지만, 의존관계와 책임으로 볼 때는 이상적인 클라이언트/컨텍스트 관계를 갖고 있다.

  • 특히 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달한다는 면에서 DI구조라고 이해할 수도 있다. 아직까지는 이렇게 분리한 것에서 크게 장점이 보이지 않지만, 지금까지 해온 관심사 분리와 유연한 확장관계를 유지하도록 만드는 작업은 매우 중요하다.

마이크로 DI

  • 의존관계 주입(DI)는 다양한 형태로 적용할 수 있는데, 가장 중요한 개념은 제 3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다. 이 개념만 따르면 DI를 이루는 오브젝트와 구성요소의 구조나 관계는 다양하게 만들 수 있다.

  • 일반적으로 DI는 의존관계에 있는 두개의 오브젝트와 이 관계를 다이내믹하게 설정해주는 오브젝트 팩토리(DI 컨테이너), 그리고 이를 사용하는 클라이언트라는 4개의 오브젝트 사이에서 일어난다. 때로는 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다. 또는 클라이언트와 전략(의존 오브젝트)이 결합될 수도 있다. 심지어는 클라이언트와 DI 관계에 있는 두 개의 오브젝트가 모두 하나의 클래스 안에 담길 수도 있다.

  • DI는 꼭 클래스 단위로 일어나지 않고, 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다. 얼핏보면 DI 같아보이지 않지만, 세밀하게 관찰해보면 작은 단위지만 엄연히 DI가 이뤄지고 있음을 알 수 있다. 이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI라고도 한다. 또는 코드에 의한 DI라는 의미로 수동 DI라고 부를 수도 있다.

3.3 JDBC 전략 패턴의 최적화

기존까지의 컨텍스트와 전략

  • 자주 변하는 부분
  • 변하지 않는 부분

두 부분을 전략 패턴을 이용해 분리해냈다. 독립된 JDBC 작업 흐름이 담긴 jdbcContextWithStatementStrategy()는 DAO 메소드들이 공유할 수 있는 메소드다. 해당 메소드에 바뀌는 전략들에 대한 클래스를 추가하여 재활용할 수 있다.

여기서

  • 컨텍스트는 PreparedStatement를 실행하는 JDBC의 작업 흐름이다.
  • 전략은 PreparedStatement를 생성하는 것이다.

3.3.1 전략 클래스의 추가 정보

add() 메소드의 PreparedStatement 생성 로직을 분리한 클래스

public void AddStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        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()); // 그런데 user는 어디서 가져올까?

        return ps;
    }
}

User 정보를 생성자로부터 제공받도록 만든 AddStatement

package springbook.user.dao;
...
public class AddStatement implements StatementStrategy {
    User user;

    public AddStatement(User user) {
        this.user = user;
    }

    public PreparedStatement makePreparedStatement(Connection c) {
        ...
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());;
        ...
    }
}

user정보를 AddStatement에 전달해주는 add() 메소드

public void add(User user) throws SQLException {
    StatementStrategy st = new AddStatement(user);
    jdbcContextWithStatementStrategy(st);
}
  • add() 메소드에서는 deleteAll() 메소드와 다르게, User라는 정보가 필요했다. 그래서 생성자에 User 정보를 받는 부분을 추가했다.
  • add() 메소드의 코드가 훨씬 개선되었다. 테스트를 통해 테스트해보자. 앞으로 비슷한 기능의 DAO 메소드가 필요할 때마다 이 Statement 전략과 jdbcContextWithStatementStrategy() 컨텍스트를 활용할 수 있으니 try/catch/finally로 범벅된 코드를 만들다가 실수할 염려는 사라졌다.
  • DAO 코드도 간결해졌다. DAO 코드의 양을 많게는 70~80%까지 줄일 수 있다.

3.3.2 전략과 클라이언트의 동거

  • 현재 만들어진 구조에 두가지 불만이 있다.
    • 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야한다.
      • 클래스 파일의 개수가 너무 많아진다.
    • StatementStrategy에 전달할 부가 정보가 있는 경우, 클래스에 번거롭게 인스턴스 변수를 만들어줘야한다.

로컬 클래스

  • 클래스를 매번 독립된 파일로 만들지 않고 UserDao 클래스 내부 클래스로 정의하면 클래스 파일이 많아지는 문제를 해결할 수 있다.

add() 메소드 내의 로컬 클래스로 이전한 AddStatement

public void add(User user) throws SQLException {
    class AddStatement implements StatementStrategy {
        User user;

        public AddStatement(User user) {
            this.user = user;
        }

        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            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());

            return ps;
        }
    }
}
  • 위처럼 로컬 클래스로 구성하면, 따로 클래스 파일을 만들지 않아도 된다.

  • AddStatement가 사용될 곳이 add() 메소드 뿐이라면, 이렇게 사용하기 전에 바로 정의해서 쓰는 것두 나쁘지 않다. 덕분에 클래스 파일이 하나 줄고, add() 메소드 안에서 PreparedStatement 생성 로직을 함께 볼 수 있으니 코드를 이해하기도 좋다.

  • 로컬클래스에는 또 한가지 장점이 있는데, 내부 클래스이기 때문에 자신을 선언한 메소드의 로컬 변수에 접근할 수 있다는 점이다. 그래서 User를 생성자로 받을 필요가 사라진다.

add() 메소드의 로컬 변수를 직접 사용하도록 수정한 AddStatement

public void add(User user) throws SQLException {
    class AddStatement implements StatementStrategy {

        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            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());
            //로컬(내부) 클래스의 코드에서 외부의 메소드 로컬 변수에 직접 접근할 수 있다.

            return ps;
        }
    }

    StatementStrategy st = new AddStatement(); // 생성자 파라미터로 user를 전달하지 않아도 된다.
    jdbcContextWithStatementStrategy(st);
}
  • 단, final로 선언된 외부 로컬 변수에만 접근할 수 있다. 위 예제에서는 final을 붙이지 않았지만, Effectively final(참조링크1, 참조링크2) 때문에 자동으로 final이 붙은 효과가 나타난다. 내부 클래스 선언 이후에 User 객체가 가리키는 곳을 다른데로 바꾸면 에러가 날 것이다.

중첩 클래스의 종류

  • 다른 클래스 내부에 정의되는 클래스를 중첩 클래스(nested class)라고 한다.
    • static으로 선언되면, static class가 되어 독립적으로 오브젝트로 만들어질 수 있다.
    • static이 아니면, inner class로 내부에서만 생성될 수 있다.
  • inner class는 다시 범위에 따라 3가지로 나뉜다.
    • 오브젝트 레벨에 정의되는 멤버 내부 클래스(Memeber inner class)
    • 메소드 레벨에 정의되는 로컬 클래스(local class)
      • AddStatement는 여기에 해당된다.
    • 이름을 갖지 않는 익명 내부 클래스(anonymous inner class)
      • 익명 내부 클래스의 범위는 선언된 위치에 따라 다른다.

익명 내부 클래스 한가지 더 욕심을 내서 AddStatement 클래스의 이름도 제거해보자. 익명 내부 클래스를 이용하면 된다.

AddStatement를 익명 내부 클래스로 전환

StatementStrategy st = new StatementStrategy() {

    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        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());

        return ps;
    }
};

메소드 파라미터로 이전한 익명 내부 클래스

public void add(final User user) throws SQLException {
    jdbcContextWithStatementStrategy (
        new StatementStrategy() {
                public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                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());

                return ps;
            }
        }
    )
};
  • 메소드가 하나밖에 없는 인터페이스를 익명 내부 클래스로 구현하는 경우, 위와 같이 람다로 구현할 수 있다.

  • deleteAll() 메소드도 위와 같은 방식으로 간단히 정리해보면,

익명 내부 클래스를 적용한 deleteAll() 메소드

public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy (
        new StatementStrategy() {
                public PreparedStatement makePreparedStatement(Connection c)
                 throws SQLException {

                return c.prepareStatement("delete from users");
            }
        }
    );
}

위와 같이 줄일 수 있다.

이제 많은 클래스 파일이 생기지도 않고 메소드의 로컬 변수를 직접 이용하는데에 문제도 없어졌다.

3.4 컨텍스트와 DI

3.4.1 JdbcContext의 분리

이전의 방식을 전략패턴 구조로 보자면, 다음과 같다.

  • UserDao.deleteAll(), UserDao.add() : 클라이언트
    • 어떤 전략을 사용할지 의존성을 결정
  • 익명 내부 클래스 : 전략
    • 구체적인 전략
  • UserDao.jdbcContextWithStatementStrategy() : 컨텍스트
    • PreparedStatement를 실행하는 변하지 않는 부분

JDBC의 일반적인 작업 흐름을 담고 있는 jdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다. jdbcContextWithStatementStrategy()를 UserDao 클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해본다.

클래스 분리

  • JdbcContext 라는 클래스를 생성하여 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다. DataSource가 필요한 것은 UserDao가 아니라 JdbcContext가 된다.

  • JdbcContext가 DataSource에 의존하게 되므로 JdbcContext에 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.

JDBC 작업 흐름을 분리해서 만든 JdbcContext 클래스

package springboo.user.dao;
...
public class JdbcContext {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Conneciton c = null;
        PreparedStatement ps = null;

        try {
            c = this.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) {} }
        }
    }
}

JdbcContext를 DI 받아서 사용하도록 만든 UserDao

public class UserDao {
    ...
    private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext; // JdbcContext를 DI 받도록 만든다.
    }

    public void add(final User user) throws SQLException {
        this.jdbcContext.workWithStatementStrategy (
            new StatementStrategy() { ... }
        );
    }

    public void deleteAll() throws SQLException {
        this.jdbcContext.workWithStatementStrategy(
            new StatementStrategy() { ... }
        );
    }
}

빈 의존관계 설정

  • UserDao는 이제 JdbcContext에 의존하고 있다. 그런데, JdbcContext는 인터페이스인 DataSource와 달리 구체 클래스이다.

  • 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고, 구현 방법이 바뀔 가능성은 없다.

  • 따라서 인터페이스를 구현하도록 만들지 않았고, UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.

그림1

위 그림은 JdbcContext를 적용한 UserDao의 의존관계이다.

그림2

위 그림은 JdbcContext가 적용된 빈 오브젝트 관계이다. xml 파일은 아래와 같이 변경되면 된다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/toby_spring" />
    </bean>

    <bean id="jdbcContext" class="toby_spring.chapter1.user.jdbc_context.JdbcContext">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
        <property name="jdbcContext" ref="jdbcContext" />
    </bean>
</beans>
  • 아직은 UserDao 클래스의 모든 메소드가 jdbcContext를 사용하는 것은 아니라서 dataSource에 대한 의존성도 필요하다.

3.4.2 JdbcContext의 특별한 DI

JdbcContext의 인터페이스 없는 DI

  • UserDao와 JdbbcContext 사이에는 인터페이스를 사용하지 않고 DI를 적용했다. UserDao와 JdbcContext는 클래스 레벨에서 의존관계가 발생한다. 런타임 시에 DI방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수는 없다.

인터페이스 없이 스프링 빈으로 DI하면 문제가 있을까

  • 인터페이스를 사용하지 않는다면 엄밀히 말해서 온전한 DI라고 볼 수는 없다. 하지만 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다.

  • JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는건 DI의 기본을 따르고 있다고 볼 수 있다.

JdbcContext를 UserDao와 DI구조로 만들어야 할 이유를 꼽자면 어떤 것이 있을까?

  • 싱글톤 레지스트리에 등록된 싱글톤 빈으로 만든다는 것에 의미가 있다.
    • 많은 DAO에서 사용되더라도 JdbcContext는 1개의 빈으로 관리가 가능하다.
    • 변경되는 상태정보가 없기 때문에 서비스 오브젝트로서 의미가 있고 여러 오브젝트에 공유해 사용되는 것이 이상적이다.
  • JdcbcContext가 dataSource라는 다른 빈에 의존해야 하기 때문이다.

    • DI를 위해서 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록되어야 한다.
    • 다른 빈을 주입받기 위해서라도 스프링 빈에 등록되어야 한다.
  • 인터페이스가 없다는건 UserDao는 JdbcContext 클래스와 강한 결합을 갖고 있다는 의미이다. OOP의 설계 원칙에는 위배되지만, JdbcContext는 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없다.

  • 이런 경우는 굳이 인터페이스를 두지 않아도 상관 없다.

  • 단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임을 잊지 말자.

스프링 빈으로 DI

코드를 이용하는 수동 DI

  • JdbcContext를 빈으로 등록하지 않고, UserDao 내부에서 직접 DI를 적용할 수도 있다. 이 방법을 쓰려면 JdbcContext를 스프링 빈으로 등록해서 사용했던 첫번째 이유인 싱글톤으로 만드려는 것은 포기해야 한다.

  • 하지만 JdbcContext 자체는 싱글톤이 아니더라도, DAO 객체들은 빈으로 등록되어 싱글톤으로 관리될 것이기 때문에 JdbcContext도 DAO와 1:1로 형성될 것이다. 웬만큼 대형 프로젝트라도 수백개면 충분할 것이다.

  • UserDao가 직접 JdbcContext에 DataSource를 DI해주도록 코드를 변경해보자. 그림3

JdbcContext 생성과 DI 작업을 수행하는 setDataSource() 메소드

public class UserDao {
    ...
    private JdbcContext jdbcContext;

    public void setDataSource(DataSource dataSource) { // 수정자 메소드이면서 JdbcContext에 대한 생성, DI 작업을 동시에 수행한다.
        this.jdbcContext = new JdbcContext(); // JdbcContext 생성(IoC)

        this.jdbcContext.setDataSource(dataSource); // 의존 오브젝트 투입 (DI)

        this.dataSource = dataSource; // 아직 JdbcContext를 적용하지 않은 메소드를 위해 저장해둔다.
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/toby_spring" />
    </bean>

    <bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>
  • 이 방법의 장점은 굳이 인터페이스를 두지 않아도 될만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하기 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 점이다.

  • 이렇게 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI하는 것은 스프링에서도 종종 사용되는 기법이다.

정리

지금까지 JdbcContext와 같이 인터페이스를 사용하지 않고 DAO와 밀접한 관계를 갖는 클래스를 DI에 적용하는 방법 두가지를 알아보았다.

빈으로 등록하는 방법

- 장점 - 의존관계가 설정파일에 명확하게 드러난다. - 싱글톤 레지스트리에 등록 가능 - 단점 - DI의 근본적 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다.

수동으로 DI하는 방법

- 장점 - 관계를 외부에 드러내진 않는다. - 단점 - 싱글톤 불가능 - DI 작업을 위한 부가적인 코드가 필요하다. > 상황에 따라 적절한 방법을 선택해야하며, 왜 그렇게 선택했는지에 대한 근거가 있어야 한다. 분명하게 설명할 자신이 없다면 차라리 인터페이스를 만들어 평범한 DI구조로 만드는게 나을 수도 있다. ## 3.5 템플릿과 콜백 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고, 그 중 일부만 자주 바꿔서 사용하는 경우에 적합한 구조다. 스프링에서는 이러한 방식을 템플릿/콜백 패턴이라고 부른다. 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 한다. ### 3.5.1 템플릿/콜백의 동작원리 `템플릿`은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우 템플릿이라고 부른다. 이를테면 JSP는 HTML이라는 고정된 부분에 EL과 스크립릿이라는 변하는 부분을 넣은 일종의 템플릿 파일이다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼 클래스에 두고, 바뀌는 부분을 서브 클래스의 메소드에 두는 구조로 이뤄진다. `콜백`은 실행되는 것을 목적으로 다른 오브젝트의 메소드 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다. 자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를 전달해야한다. 그래서 functional object라고도 한다. > **템플릿/콜백의 특징** - 여러 메소드를 가질 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한번 호출되는 경우가 일반적이기 때문이다. 물론 하나 이상의 콜백 오브젝트를 사용하는 것도 가능하다. 콜백은 일반적으로는 하나의 메소드를 가진 인터페이스를 익명 내부 클래스로 구현한다. ![그림4](https://user-images.githubusercontent.com/37354978/148666071-4b5182b1-6541-4924-a41d-88e54a7512d9.png) 위 그림은 템플릿/콜백의 작업 흐름이다. - 클라이언트 : 콜백 오브젝트를 만들고, 템플릿에 전달 및 호출 (1),(2) - 템플릿 : 참조정보 생성 및 콜백의 오브젝트 메소드 호출 (3),(4),(5) - 콜백 : 클라이언트 메소드에 있는 정보와 템플릿이 가진 참조 정보를 이용하여 작업 수행 후 템플릿에 결과 반환 (6),(7),(8) - 템플릿 : 콜백이 돌려준 정보를 이용하여 나머지 작업 수행 후 경우에 따라 최종 결과를 다시 클라이언트에게 반환 (9),(10),(11) 클라이언트가 콜백 오브젝트를 만들고 템플릿에 전달하는 것은 메소드 레벨 DI이다. 일반적인 DI라면, 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 사용한다. 반면, 템플릿/콜백 방식에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달 받는다. 콜백 오브젝트가 내부 클래스로 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조하여 클라이언트와 콜백이 강하게 결합된다는 면에서도 일반적인 DI와 조금 다르다. 전략 패턴과 수동 DI를 이용한다. > **JdbcContext에 적용된 템플릿/콜백** > ![image](https://user-images.githubusercontent.com/37354978/148666182-acd4147c-de7c-42a1-a669-2b939ba46a29.png) - 템플릿의 작업 흐름이 복잡해지면, 한번 이상의 콜백을 호출하기도 하고, 여러개의 콜백을 클라이언트로부터 받아서 사용하기도 한다. ### 3.5.2 편리한 콜백의 재활용 - 기존 방식은 템플릿에 담긴 코드를 여기저기 반복적으로 사용해야하는 단점이 있었다. 템플릿/콜백 방식에서는 이러한 단점이 해결됐다. JdbcContext만 이용해도, 기존의 커넥션을 맺고 끊는 템플릿 코드를 매번 재작성할 필요가 사라지고, 비즈니스 로직에만 집중할 수 있게 되었다. 많이 개선된 지금의 코드에서 한가지 아쉬운 점이 있는데, 익명 내부 클래스를 사용하여 익숙하지 않은 스타일 때문에 가독성이 떨어진다는 것이다. > **콜백의 분리와 재활용** - 위에서 언급했던 문제인 복잡한 익명 내부 클래스의 사용을 최소화해보자. 코드를 깔끔하게 만들기 위해 가장 첫번째로 생각해보아야할 것은 분리를 통해 재사용이 가능한 부분을 찾아내는 것이다. 즉, 변화할 수 있는 부분과 변화하지 않을 부분을 구분하는 것이다. **기존의 deleteAll() 메소드** ```java public void deleteAll() throws SQLException { StatementStrategy strategy = c -> c.prepareStatement("delete from users"); // 선정한 전략 클래스의 오브젝트 생성 jdbcContext.workWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달 } ``` 여기서 변화할 수 있는 부분은 "delete from users" 뿐이다. 나머지는 '템플릿'으로 볼 수 있다. **익명 내부 클래스를 사용한 클라이언트 코드** **변경 후 deleteAll() 메소드** ```java public void deleteAll() throws SQLException { this.jdbcContext.workWithStatementStrategy { new StatementStrategy() { // 변하지 않는 콜백 클래스 정의와 오브젝트 생성 public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); } } } } ``` 변화하지 않는 템플릿을 executeSql()이라는 메소드로 만들어주었다. 이로 인해, .preparedStatement()를 사용하여 수행되는 쿼리는 전부 .executeSql()을 재활용할 수 있게 되었다. 이제는 콜백 익명 클래스 구현도 필요 없어졌다. > **콜백과 템플릿의 결합** - executeSql()은 위에서 언급한대로, deleteAll() 메소드에서만 사용되긴 아까우니, JdbcContext 클래스로 옮겨서 필요한 곳에 여기저기 쓰일 수 있도록 만들어주자. \*\*JdbcContext로 옮긴 executeSql() 메소드 ```java public class JdbcContext { ... public void executeSql(final String query) throws SQLException { workWithStatementStrategy ( new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); } } ) } } ``` **JdbcContext로 옮긴 executeSql()을 사용하는 deleteAll() 메소드** ```java public void deleteAll() throws SQLException { this.jdbcContext.executeSql("delete from users"); } ``` ![image](https://user-images.githubusercontent.com/37354978/148666794-a45bb692-05b1-4511-b583-736495743f64.png) - 객체지향 원칙 중 결합도가 낮고 응집도가 높아야 이상적이라는 원칙이 있다. 위 JdbcContext의 경우, 쿼리를 날리기 위한 JDBC 템플릿 코드가 뭉쳐있으니 응집도가 높은 경우의 예시이다. 또한, 외부에는 구체적인 구현을 감추고 필요한 기능을 제공하는 단순 메소드만 공개하였기 때문에 캡슐화(은닉화)가 잘되어있다. - 비단, .deleteAll() 뿐만 아니라, UserDao의 .add()도 위와 같이 템플릿을 쪼갤 수 있다. 다만, 바인딩할 파라미터 타입을 살펴서 적절한 설정 메소드를 호출해주는 작업이 조금 복잡해질 수는 있다. 그래도 한번 만들어 놓으면 매우 편리하게 재사용 가능하니 도전해볼만 하다. ### 3.5.3 템플릿/콜백의 응용 - 스프링이 제공하는 디자인 패턴을 이해하고 사용하는 것과 이해하지 못하고 사용하는 것에는 큰 차이가 있다. 스프링이 제공하는 기술의 구조를 이해하면 손쉽게 확장해서 쓸 수 있다. `전략 패턴`, `DI`, `템플릿/콜백` 패턴 모두 중요하다. - 코드에서 고정된 흐름을 찾았는데, 여기저기 반복되는 것을 보았다면 중복되는 코드를 분리할 방법을 생각해보는 습관을 길러야한다. 먼저 `메소드로 분리`해볼 수 있고, 일부 작업을 필요에 따라 바꾸어야 한다면, **인터페이스를 사이에 두고 분리하여 전략팬턴을 적용하여 DI로 의존관계를 관리**하도록 만들 수 있다. 바뀌는 부분이 **한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴을 적용하는 것을 고려**해볼 수 있다. > **테스트와 try/catch/finally** - 간단한 템플릿/콜백 예제를 하나 작성해보자. 파일을 열어서 모든 라인의 숫자를 더한 합을 돌려주는 코드를 만드는게 목표이다. **파일의 숫자 합을 계산하는 코드의 테스트** ```java package springbook.learningtest.template; ... public class CalcSumTest { @Test public void sumOfNumbers() throws IOException { Calculator carlculator = new Calculator(); int sum = calculator.clacSum(getClass().getResource ( "numbers.txt").getPath()); assertThat(sum, is(10)); } } ``` **처음 만든 Calculator 클래스 코드** ```java package springbook.learningtst.template; ... public class Calculator { public Integer calcSum(String filepath) throws IOException { BufferedReader br = new BufferedReader(new FileReader(filepath)); Integer sum = 0; String line = null; while((line = br.readLine()) != null) { sum += Integer.valueOf(line); } br.close(); return sum; } } ``` **try/catch/finally를 적용한 calcSum() 메소드** ```java public Integer calcSum(Stirng filepath) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filepath)); Integer sum = 0; String line = null; while((line = br.readLine()) != null) { sum += Integer.valueOf(line); } return sum; } catch (IOException e) { System.out.println(e.getMessage()); throw e; } finally { if (br != null ) { try { br.close(); } catch(IOException e) { System.out.prinln(e.getMessage()); } } } } ``` > **중복의 제거와 템플릿/콜백 설계** **문제제기** - 클라이언트가 모든 숫자의 곱도 얻을 수 있으면 좋겠다는 요구를 했다면, 그리고 그 후에 또 여러 연산을 더하고 싶다는 이야기가 들려온다면, 어떻게 해야할까? - 가장 간단히는 소스코드를 계속 복사, 붙여넣기 하고, calcMultiply, calcDivide, calc... 의 메소드를 계속 생성해나갈 수 있다. 그리고 우리가 고치는 부분은 그냥 +=을 \*= 정도로 고칠 것이다. 하지만 그 이후에 고객의 요구사항으로 예외가 났을 때, 예외처리 로그를 남겨달라는 요구사항이 또 생긴다면? 복사, 붙여넣기한 각각의 코드의 catch 블록에 일일이 로그를 남기는 부분을 추가해야할 것이다. - 이렇게 작성하면 실수하기도 쉽고 무엇보다 객체지향언어를 쓰는 이점이 하나도 없다. **문제해결** - 템플릿/콜백 패턴을 적용하여 조금 더 효율적인 문제 해결을 해보자. 늘 그렇듯, 반복되는 코드 흐림이 어떤 것인지 확인해보자. **반복되는 코드 흐름**은 **템플릿에 담길 것**이다. 그리고 반복되지 않는 코드 흐름이 어떤 것인지도 확인해보자. **반복되지 않는 코드 흐름**은 **콜백에 담길 것**이다. - 템플릿과 콜백에 담길 내용을 생각해보았다면, **템플릿이 콜백에게 전달해줄 내부 정보는 무엇**이고, **콜백이 템플릿에게 돌려줄 내용은 무엇**인지 생각해보자. **템플릿과 콜백의 메세징 내용이 중요하다.** - 템플릿/콜백 패턴을 적용할 때는, 템플릿과 콜백의 경계를 정하고, 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 가장 주용하다. 그에 따라 콜백의 인터페이스를 정의해야하기 때문이다. - 1단계로 다음과 같이 코드 흐름을 분리해보자. - 템플릿 : 파일을 열고 각 라인을 읽어올 수 있는 BufferedReader를 만들어 콜백에게 전달해준다. - 콜백 : BufferedReader를 받아 각 라인을 처리하고 최종 결과를 반환한다. 이것을 인테페이스 처럼 표현하면 아래와 같은 코드가 나온다. **콜백 인터페이스 정의하기 :** **BufferedReader를 전달받는 콜백 인터페이스** ```java package springbook.learningtest.tempalte; ... public interface BufferedReaderCallback { Integer doSomethingWithReader(BufferedReader br) throws IOException; } ``` **템플릿 메소드 정의하기 :** **BufferedReaderCallback을 사용하는 템플릿 메소드** ```java public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filepath)); int ret = callback.doSomethingWithReader(br); // 콜백 오브젝트 호출. 템플릿에서 만든 컨텍스트 정보인 BufferedReader를 전달해주고 콜백의 작업 결과를 받아둔다. return ret; } catch (IOException e) { System.out.println(e.getMessage()); throw e; } finally { if (br != null) { try { br.close(); } catch (IOException e) { System.out.println(e.getMessage()); } } } } ``` **템플릿/콜백 정의하기 :** **템플릿/콜백을 적용한 calcSum() 메소드** ```java public Integer calcSum(String filepath) throws IOException { BufferedReaderCallback sumCallback = new BufferedReaderCallback() { public Integer doSomethingWithReader(BufferedReader br) throws IOException { Integer sum = 0; String line = null; while ((line = br.readLine()) != null) { sum += Integer.valueOf(line); } return sum; } }; return fileReadTemplate(filepath, sumCallback); } ``` **새로운 테스트 메소드를 추가한 CalcSumTest** ```java package springbook.learningtest.template; ... public class CalcSumTest { Calculator calculator; String numFilepath; @Before public void setUp() { this.calculator = new Calculator(); this.numFilepath = getClass().getResource("numbers.txt").getPath(); } @Test public void sumOfNumbers() throws IOException { assertThat(calculator.calcSum(this.numFilepath), is(10)); } @Test public void multiplyOfNumbers() throws IOException { assertThat(calculator.calMultiply(this.numFilepath), is(24)); } } ``` **개선된 점 정리** - 이제 읽어들인 숫자에 다른 연산이 필요하더라도 BufferedReaderCallback의 구현만 바꿔주면 파일 입출력 코드는 전혀 신경쓰지 않고, 프로그램에 변화를 줄 수 있다. - 또한, +,-,\*,/ 등의 연산이 다 만들어진 이후에, 예외시 로그를 남기라는 추가 요청사항이 들어와도 변경해야할 지점은 단 한군데이다. **곱셈 연산기 만들어보기** - 이번에는 불러들인 숫자를 전부 곱하는 버전을 만들어보자. 1부터 10까지 곱하면 3628800이라는 결과가 나온다. **곱을 계산하는 콜백을 가진 calcMultiply() 메소드** ```java @Test public void multiplyOfNumbers() throws IOException { Integer multiply = calculator.calcMultiply(filePath); Assertions.assertEquals(multiply, 3628800); } ``` ```java public Integer calcMultiply(String filepath) throws IOException { BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() { public Integer doSomethingWithReader(BufferedReader br) throws IOException { Integer multiply = 1; String line = null; while ((line = br.readline()) != null) { multiply *= Integer.valueOf(line); } return multiply; } }; return fileReadTemplate(filepath, multiplyCallback); } ``` 위 테스트가 성립하도록 코드를 작성해보고, 테스트도 무사히 통과하는지 확인한다. > **템플릿/콜백의 재설계** - 사실 calcMultiply() 메소드는 이전의 calcSum() 메소드와 많은 부분이 공통된다. 단, 1부터 시작하며, 곱해나가는 점만 다르다. 공통적인 패턴은 또 템플릿으로 변화시킬 수 있다. - 템플릿/콜백을 찾아낼 때는 변화하는 경계를 찾고 그 경계에서 어떤 정보를 주고 받는지 확인하면 된다. - 위에서 바뀌었던 코드는 2가지로 볼 수 있다. - 초기값이 1로 시작됐다. - +=이 \*=이 됐다. **라인별 작업을 정의한 콜백 인터페이스** ```java package springbook.learningtest.template; ... public interface LineCallback { Integer doSomethingWithLine(String line, Integer value); } ``` **fileReadTemplate -> lineReadTemplate** **LineCallback을 사용하는 템플릿** ```java public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException { BufferedReader br = null; //initVal은 계산 결과를 저장할 변수의 초기값 try { br = new BufferedReader(new FileReader(filepath)); Integer res = initVal; String line =null; while(line = br.readLine() != null) { res = callback.doSomethingWithLine(line, res); } return ps; } cathch (IOException e) { ... } finally { ... } } ``` - 기존의 fileReadTemplate() 메소드를 지우고 lineReadTemplate()이라는 메소드를 생성해주었다. 이전에서 우리가 반복되는 부분을 callback에 넣었던 실수를 인지하고, 다시 반복되는 부분을 template로 자연스럽게 올려주었다. - 나머지 calcSum()과 calcMultiply()도 이에 맞게 변경해주자. **lineReadTemplate()을 사용하도록 수정한 calSum(), calMultiply() 메소드** ```java public Integer calcSum(String filepath) throws IOException { LineCallback sumCallback = new LineCallback() { public Integer doSomethingWithLine(String line, Integer value) { return value + Integer.valueOf(line); }}; return lineReadTemplate(filepath, sumCallback, 0); } public Integer calcMultiply(String filepath) throws IOException { LineCallback multiplyCallback = new LineCallback() { public Integer doSomethingWithLine(String line, Integer value) { return value*Integer.valueOf(line); }}; return lineReadTemplate(filepath, multiplyCallback, 1); } ``` - 람다 함수를 사용했더니 매우 적은 수의 라인으로 표현이 가능해졌다. - 테스트 코드를 작동시켜보면, 정상적으로 작동한다. **개선된 점** - 로우 레벨의 파일 처리 코드가 템플릿으로 분리되고 순수한 계산로직만 남게 되었다. 덕분에 해당 코드의 관심사가 명확하게 보인다. `Calculator` 클래스와 메소드는 데이터를 가져와 계산한다는 핵심 기능에 충실한 코드만 갖고 있게 됐다. - 코드의 특성이 바뀌는 경계를 잘 살피고 그것을 인터페이스를 사용해 분리한다는 가장 기본적인 객체지향 원칙에만 충실하면 어렵지 않게 템플릿/콜백 패턴을 만들어 활용할 수 있다. > **제네릭스를 이용한 콜백 인터페이스** - 현재는 결과가 Integer로 고정되어 있지만, 나눗셈을 하다보면 소수점도 나오고 다른 타입이 필요할 수 있다. 이럴 때는 제네릭스를 이용하면 된다. **타입 파라미터를 적용한 LineCallback** ```java public interface LineCallback { T doSomethingWithLine(String line, T value); } ``` **타입 파리미터를 추가해서 재네릭 메소드로 만든 lineReadTemplate()** ```java public lineReadTemplate(String filepath, LineCallback callback, T initVal) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filepath)); T res = initVal; String line = null; while ((line = br.readLine()) != null) { res = callback.doSomethingWithLine(line, res); } return res; } catch(IOException e) {...} finally {...} } ``` **문자열 연결 기능 콜백을 이용해 만든 concatenate() 메소드** ```java public String concatenate(String filepath) throws IOException { LineCallback concatenamteCallback = new LineCallback() { public String doSomethingWithLine(String line, String value) { return value + line; }}; return lineReadTemplate(filepath, concatenateCallback, "") } } ``` - 위와 같이 파라미터에서 받은 타입을 이용하여 제네릭스를 적용하는 방식을 썼는데, 위와 같이 설정하면 메소드에서는 LineCallback callback 파라미터에서 받은 타입으로 T 타입을 설정하게 된다. - 파라미터 중, callback과 initValue 모두 같은 T 타입을 사용하므로 두 파라미터의 타입은 일치해야한다. > 참고 : 클래스를 만들 때, 타입 파라미터의 타입을 설정하고 싶다면 Calculator와 같이 작성하면 된다. **테스트해보기** **concatenate() 메소드에 대한 테스트** ```java @Test public void concatenateStrings() throws IOException { assertThat(calculator.concatenate(this.numFilepath), is("1234")); } ``` ## 3.6 스프링의 JdbcTemplate 이번엔 스프링이 제공하는 템플릿/콜백 기술을 살펴보자. 거의 모든 종류의 JDBC 코드에 사용 가능한 템플릿/콜백을 제공할 뿐만 아니라, 자주 사용되는 패턴을 가진 콜백은 다시 템플릿에 결합시켜서 간단한 메소드 호출만으로 사용가능하도록 만들어져 있기 때문에 템플릿/콜백 방식의 기술을 사용하는지 모르고도 쓸 수 있을 정도로 편리하다. 스프링이 제공하는 JDBC 코드용 기본 템플릿은 JdcbcTemplate이다. 앞에서 만들던 JdbcContext와 유사하지만 훨씬 강력하고 편리한 기능을 제공한다. 기존 UserDao 클래스에 있던 코드를 JdbcTemplate을 이용해 단계적으로 변경시켜보자. **JDBCTemplate 초기화** ```java public class UserDao { ... private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.dataSource = dataSource; } } ``` - 위와 같이 초기화를 해주면, 이제 JdbcTemplate를 사용할 준비가 됐다. ### 3.6.1 update() **Jdbc Template을 적용한 deleteAll() 메소드** ```java public void deleteAll() { this.jdbcTempalte.update ( new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection) return con.prepareStatement("delete from users"); } ) } ``` - `deleteAll()`을 변경해보자. 기존 `deleteAll()` 메소드는 connection에 있는 `.prepareStatement()`에 sql구문인 "delete from users"를 이용하여 구성했었다. - 이전에는 다양한 sql 구문을 이용하여 `StatementStrategy` 인터페이스 내부의 `makePreparedStatement()` 추상 메소드를 구현해 `.preparedStatement()`의 결과인 `PreparedStatement` 타입의 객체를 만드는 것이 **전략**이었으며, - `PreparedStatement` 내부 메소드인 `.executeUpdate()`를 수행하고, 커넥션을 잘 회수해주는 것이 **템플릿**이었다. - 스프링에서 제공하는 `JdbcTemplate`도 마찬가지로 `.update()` 메소드 내부에서 `Connection` 객체를 통해 `PreparedStatement`객체를 반환하는 추상메소드를 구현하면 동일하게 활용할 수 있다. ```java //re jdbcTemplate.update(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection con) throws SQLException { PreparedStatement deleteFromUsers = con.prepareStatement("delete from users"); return deleteFromUsers; } }); ``` - `PreparedStatementCreator()`라는 추상 메소드는 `makePreparedStatement()`와 동일한 역할을 하는 추상메소드 이다. 사실 스프링 `JdbcTemplate`의 `update()`메소드도 이전에 우리가 작성했던 `executeSql()`메서드와 같이, sql만 넘겨주어도 내장 템플릿으로 콜백을 만들어 넘겨줄 수 있다. - 최종적으로 `.deleteAll()` 메소드를 아래와 같이 변경할 수 있다. ```java //re public void deleteAll() { jdbcTemplate.update("delete from users"); } ``` ### add() - `.update()`의 기능은 단순히 `sql`문자열을 받아 DB에서 수행해주는 것을 넘어서 파라미터를 순서대로 바인딩 해줄 수 도 있다. ```java //re public void add(User user) throws SQLException { this.jdbcTemplate.update("insert into users(id, name, password) values (?, ?, ?)" , user.getId() , user.getName() , user.getPassword() ); } ``` - 사실 이전에 JdbcContext에서 구현했던 `.executeSql()`메소드는 `JdbcTemplate`에 이미 `.update()`란 이름으로 구현되어 있는 메소드였다. 다만, `.update()`의 구현이 더욱 풍부하다. ### queryForObject()와 ResultSetExtractor 콜백 - 다음은 아직 템플릿/콜백 방식을 적용하지 않았던 메소드에 JdbcTemplate을 적용해볼 것이다. - 이전까지는 단순히 `update()`에 대한 쿼리만 날려보았다. `update()`에 들어가는 쿼리는 사실 보통 성공, 실패 외에 딱히 결과가 없는 쿼리이다. 결과가 있는 쿼리는 `query()` 메소드를 사용한다. - `query()` 메소드는 결과를 받아야 하기 때문에 2가지 콜백을 받아야 한다. - 1번째는 쿼리에 대한 `PreparedStatement`를 생성하는 `PreparedStatementCreator`콜백이다. - 2번째는 반환받은 결과를 추출, 매핑해주는 `ResultSetExtractor` 콜백이다. - query() 메소드를 getCount() 메소드 내부에서 이용하여 전체 데이터 숫자 결과를 돌려받아보자. ```java //re public int getCount() throws SQLException { return jdbcTemplate.query("select count(*) from users", new ResultSetExtractor() { public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException { resultSet.next(); return resultSet.getInt(1); } }); } ``` 개선할 필요성이 보이지만, 일단은 위와 같이 작성했다. - 첫번째 콜백은 앞서 설명했던 것과 같이, 문자열로 된 sql을 넘기면 `PreparedStatement`를 만들어준다. - 앞서 만들었던 `lineReadTemplate()`와 유사하게 두번째 콜백에서 리턴하는 값은 결국 템플릿 메소드의 결과로 다시 리턴된다. 클라이언트/템플릿/콜백의 3단계 구조이니, 콜백이 만들어낸 결과는 템플릿을 거쳐야만 클라이언트인 `getCount()`메소드로 넘어오는 것이다. - 또 한가지 눈여겨 볼 점은 `ResultSetExtractor`는 재네릭스 타입 파라미터를 갖는다는 점이다. `lineReadTemplate()`와 `LineCallback`에 적용했던 방법과 유사하다. 파라미터로 설정된 인터페이스가 갖는 제네릭 타입을 기준으로 클래스 내부 메소드의 타입이 설정될 것이다. - 사실 JdbcTemplate은 위와 같이 특정한 타입의 결과를 출력하는 경우에 대해 `queryForObject()`라는 편리한 메소드를 제공한다. 결과를 반환하는 SQL문장과 반환하는 타입의 정보만 클래스 형태로 넘겨주면 된다. ```java //re public int getCount() throws SQLException { return jdbcTemplate.queryForObject("select count(*) from users", Integer.class); } ``` - `getCount()`는 이전에 34줄로 복잡하게 작성되어 있던 코드였지만, 이제는 한줄로 바뀌었으며 그 의미도 몇 배는 명확해졌다. 여러 메소드에 반복되며 변화하지 않는 부분은 템플릿으로 빼고, 변화하는 부분은 콜백으로 만들어 멋지게 코드를 변경하였다. ### queryForObject()와 RowMapper 콜백 - 이번엔 `get()`메소드에 `JdbcTemplate`을 적용해보자. `get()`메소드에서 하는 일을 정리해보면, - sql을 이용하여 `PreparedStatement`를 만들어주어야 한다. - id로 검색하기 때문에 파라미터에 대한 처리도 해주어야 한다. - 결과로 `User`객체를 만들어야 하기 때문에, 결과로 받은 `ResultSet`에 대한 처리도 해주어야 한다. - 이전의 `count()`와 같은 단순 개수 조회가 아닌, 어떠한 객체로 매핑해야할 때는 `ResultSetExtractor`와 같은 콜백 대신에 `RowMapper`와 같은 콜백을 사용해야한다. 코드로 보면 다음과 같다. ```java //re public User get(String id) { return jdbcTemplate.queryForObject("select * from users where id = ?", new RowMapper() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; } }, id); } ``` `.queryForObject()` 메소드 하나로 코드가 상당히 짧아졌다. 2개의 콜백을 이용함에도 그다지 복잡하지 않다. 그러나 `get()` 메소드에는 한가지 더 고려해야할 것이 있는데, 기존에 조회 결과가 없을때 `EmptyResultDataAccessException`을 던지도록 만들었다. 사실 해당 예외는 `queryForObject()`에서 결과가 1개가 아니라면, 즉 2개 이상이거나 없을 때, 원래 던지던 예외이다. 그래서 별달리 예외처리를 추가하지 않아도, 기존의 예외 테스트는 잘 작동할 것이다. ### query()

기능 정의와 테스트 작성

- 여태까지는 단일 row에 대해서만 데이터를 조회해보았다. `getAll()`과 같은 메소드는 users 테이블에 존재하는 모든 row를 가져와야한다. `get()`으로 단일 row를 조회하는 것에 대해서는 User객체 자체가 결과값이었다면, `getAll()`으로 모든 row를 조회할 때는 `List`가 결과값이 되면 좋을 것이다. 그리고 정렬은 id를 기준으로 한다고 기능을 정의해보자. `user1, user2, user3`을 등록하고 id 순서대로 가져올 것이다. 매 유저를 등록 시에 `.getAll()`메소드로 조회를 하고 올바른 id 순서로 가져왔는지 확인할 것이다. ```java //re @Test @DisplayName("전체 유저 추가 및 불러오기") public void getAll() { userDao.deleteAll(); userDao.add(user1); // id: user1 List users = userDao.getAll(); assertEquals(users.size(), 1); checkSameUser(user1, users.get(0)); userDao.add(user2); // id: user2 users = userDao.getAll(); assertEquals(users.size(), 2); checkSameUser(user1, users.get(0)); checkSameUser(user2, users.get(1)); userDao.add(user3); // id: user2 users = userDao.getAll(); assertEquals(users.size(), 3); checkSameUser(user1, users.get(0)); checkSameUser(user2, users.get(1)); checkSameUser(user3, users.get(2)); } private void checkSameUser(User user1, User user2) { assertEquals(user1.getId(), user2.getId()); assertEquals(user1.getName(), user2.getName()); assertEquals(user1.getPassword(), user2.getPassword()); } ``` - `checkSameUser()`와 같이 테스트에서 반복되는 부분을 따로 분리하고 재사용하는 것은 좋은 습관이다. 여러 테스트 클래스에 걸쳐 재사용되는 코드라면 별도의 클래스로 분리하는 것도 고려해볼 수 있다. - 현재 위의 테스트는 성공하지 않으니 위의 테스트를 성공할 수 있도록 `.getAll()` 메소드를 구성해보자. ### query() 템플릿을 이용하는 getAll() 구현 ```java //re public List getAll() { return this.jdbcTemplate.query("select * from users", (rs, rowNum) -> { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; }); } ``` - `queryFor...()`메소드는 일반적으로 쿼리의 결과가 하나일 때 사용하고, `query()`는 일반적으로 여러 개의 로우가 결과로 나오는 경우에 사용한다. 리턴타입은 제네릭 타입을 가진 `List`이며, `RowMapper`콜백 오브젝트에서 결정된다. - `RowMapper`는 쿼리의 결과로 반환된 모든 Row에 대해 매핑 작업을 수행 후에 List 형태로 반환한다. 위의 코드를 작성하면, 이전에 동작하지 않았던 코드가 올바르게 동작하는 것을 확인할 수 있다. ### 테스트 보완 **성공적인 테스트 결과를 보면 빨리 다음 기능으로 넘어가고 싶겠지만, 너무 서두르는 것은 좋지 않다. 항상 꼼꼼하게 빠진 것은 없는지 더 개선할 부분은 없는지 한번 쯤 생각해보는 것이 좋다.** - 네거티브 테스트라고 불리는 예외 상황에 대한 테스트는 항상 빼먹기 쉬우므로 주의해야한다. `getAll()` 메소드의 결과가 없다면 어떻게 해야할까? 조회용 메소드의 조회 결과가 없을 때는 null 반환, 사이즈가 0인 리스트 반환, 예외 던지기 등 다양한 방식으로 처리될 수 있다. - `JdbcTemplate`의 `query()`메소드는 결과가 없을 때 단순히 `사이즈가 0인 리스트`를 반환한다. 우리가 구현한 `getAll()`은 그냥 그것을 그대로 반환하게 만들자. ```java //re @Test @DisplayName("조회할 유저가 존재하지 않는 경우") public void getAllWithoutUser() { userDao.deleteAll(); List users = userDao.getAll(); assertEquals(users.size(), 0); } ``` - 우와 같은 테스트를 새로 생성했다. 이러한 테스트 코드를 볼 때, 왜 query()의 결과에 손댈 것도 아니면서 굳이 검증코드를 추가할까? 라는 생각이 들 수 있다. - 그러나, 우리가 만든 것은 `.getAll()`메소드이며, 이 메소드가 `query()`의 결과를 반환하는지는 코드를 개발한 개발자 말고는 알 수 없다. 이전에 말했듯, 예외를 던질수도 있고, null을 반환할 수도 있다. - UserDao를 사용하는 입장에서는 JdbcTemplate으로 구현됐는지, JDBC코드를 직접 사용했는지 알 수도 알 필요도 없다. getAll()이라는 메소드가 어떻게 동작하는지에만 관심이 있다. - 위와 같은 면에서 `query()`의 결과와 상관없이 `getAll()` 메소드의 예외상황에 대한 테스트는 반드시 필요하다. - 실상 `getAll()` 메소드의 구현을 아는 개발자라도 `query()`메소드에 대한 학습 테스트로서의 의미도 있다. ### 재사용 가능한 콜백의 분리 - 이제 코드에는 핵심적인 SQL문장, 파라미터, 생성되는 결과의 타입 정보만 남기고 모든 템플릿 코드는 제거되었다. 그러나 아직 몇가지 할 일이 남았다. ### DI를 위한 코드 정리 - 이제 `DataSource`를 직접 사용할 일은 없으니 `UserDao`에서 정리하자. - 단, 수정자에서는 JdbcTemplate을 초기화하는데 필요하니 그대로 두자. ```java //re public class UserDao { JdbcTemplate jdbcTemplate; public UserDao() { } public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } ... ``` - 만일 `JdbcTemplate`을 직접 스프링 븐으로 등록하는 방식으로 변경하고 싶다면, `setDataSource`를 `setJdbcTemplate`으로 바꿔주면 딘다. ```xml ``` ```java //re public class UserDao { JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } ... ``` - 위와 같이 JdbcTemplate 빈을 직접 등록해주고, userDao로 주입해주어도 아무런 문제가 없다. ### 중복 제거 - User를 매핑시키는 `RowMapper` 콜백의 구현이 `get()`메소드와 `getAll()`메소드에서 동일하다. ```java public class UserDao { JdbcTemplate jdbcTemplate; RowMapper userRowMapper; public UserDao() { this.userRowMapper = (rs, rowNum) -> { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; }; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void add(User user) { this.jdbcTemplate.update("insert into users(id, name, password) values (?, ?, ?)" , user.getId() , user.getName() , user.getPassword() ); } public User get(String id) { return jdbcTemplate.queryForObject("select * from users where id = ?", userRowMapper, id); } public void deleteAll() { jdbcTemplate.update("delete from users"); } public int getCount() { return jdbcTemplate.queryForObject("select count(*) from users", Integer.class); } public List getAll() { return this.jdbcTemplate.query("select * from users", userRowMapper); } } ``` - 위와 같이 `userRowMapper`를 필드로 빼서, 필요한 다양한 메소드에서 활용하도록 코드를 정리할 수 있다. - 현재는 `get()`과 `getAll()`에서 밖에 쓰이지 않았지만, 언제라도 조회와 관련된 메소드가 추가될 때, 사용될 수 있다. 혹여나 조회와 관련된 메소드가 추가되고 User에 컬럼이 몇개 추가된다면 복사 붙여넣기 한 부분을 전부 찾아다니면서 수정해야 하는데 끔찍하다. ### 응집도/결합도 관점에서의 UserDao - 현재 작성된 UserDao코드에는 User의 정보를 DB에 넣거나 가져오거나 조작하는 방법에 대한 핵심적인 로직만 담겨있다. 만약 사용할 테이블과 필드정보가 바뀌면 UserDao의 거의 모든 코드가 함께 바뀔 것이다. 따라서 응집도가 높다고 볼 수 있다. - 반면에 JDBC API를 사용하는 방식, 예외처리, 리소스의 반납, DB연결을 어떻게 가져올지에 대한 책인은 모두 JdbcTemplate에게 있다. - 따라서 위 내용에 대한 변경이 일어난다고 해도 UserDao의 소스코드에는 아무런 영향을 미치지 않는다. 그런 면에서 책임이 다른 코드와는 낮은 결합도를 유지하고 있다. - 다만, JdbcTemplate이라는 템플릿 클래스를 직접 이용한다는 면에서는 특정 템플릿/콜백 구현에 강한 경합을 갖고 있다고 할 수 있다. - 그래도 더 낮은 결합도를 유지하고 싶다면 JdbcOperations라는 인터페이스를 통해 JdbcTemplate을 DI 받도록 해도 된다. ### UserDao에서 더 개선할 점은? - `userRowMapper`가 인스턴스 변수로 설정되어 있고, 한번 만들어지면 변경되지 않는 프로퍼티와 같은 성격을 띠고 있으니, 아예 UserDao 빈의 DI용 프로퍼티로 만들어버리면 어떨까 ? UserMapper를 독립된 빈으로 만들고 XML 설정에 User 테이블의 필드 이름과 User 오브젝트 프로퍼티의 매핑 정보를 담을 수도 있을 것이다. UserMapper를 분리할 수 있다면, **User의 프로퍼티와 User 테이블의 필드 이름이 바뀌거나 매핑 방식이 바뀌는 경우에 UserDao 코드를 수정하지 않고도 매핑정보를 변경할 수 있다는 장점**이 있다. - **DAO 메소드에서 사용하는 SQL문장을 UserDao 코드가 아니라 외부 리소스에 담고 이를 읽어와 사용하게 만들면 어떨까?** **이렇게 해두면 DB테이블의 이름이나 필드 이름을 변경하거나 SQL 쿼리를 최적화해야할 때도 UserDao코드에는 손을 댈 필요가 없다.** 어떤 개발팀은 정책적으로 모든 SQL 쿼리를 DBA들이 만들어서 제공하고 관리하는 경우가 있다. 이럴 때 SQL이 독립된 파일에 담겨있다면 편리할 것이다. > 스프링에는 JdbcTemplate 외에도 십여가지 템플릿/콜백 패턴을 적용한 API가 존재한다. 클래스 이름이 Template로 끝나거나 인터페이스 이름이 Callback으로 끝난다면, 템플릿/콜백이 적용된 것이라고 보면 된다. ### 정리 - JDBC나 파일처리와 같이 예외가 발생할 가능성이 있으며, 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야한다. - 일정한 작업 흐름이 반복되며 그 중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분은 컨텍스트, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다. - 같은 애플리케이션 안에서 여러가지 종류의 전략을 다이내믹하게 구성하고 사용해야 한다면, 컨텍스트를 이용하는 **클라이언트 메소드에서 직접 전략을 정의하고 제공**하게 만든다. - 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다. - 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면, 클래스를 분리해서 공유하도록 만든다. - 컨텍스트는 별도의 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해서 사용한다. 클래스 내부에서 컨텍스트를 사용할 때 컨텍스트가 의존하는 외부의 오브젝트가 있다면 코드를 이용해서 직접 DI해줄 수 있다. - 단일 전략 메소드를 갖는 전략 패터이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다. - 콜백의 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다. - 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다. - 스프링은 JDBC 코드 작성을 위해 JdbcTemplate을 기바으로 하는 다양한 템플릿과 콜백을 제공한다. - 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러번 호출할 수도 있다. - 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 **주고받는 정보**에 관심을 둬야한다. > 템플릿/콜백은 스프링이 객체지향 설계와 프로그래밍에 얼마나 가치를 두고 있는지를 보여주는 예이다. ### 전략 패턴과 템플릿 메소드 패턴의 차이 - 전략 패턴과 흡사하나 인터페이스를 익명 내부 클래스로 구현했다는 차이가 있다. - 또한, 익명 내부 클래스로 구현되어야 하기 때문에, 전략 패턴의 전략 내부에 메소드가 1개만 있을때 말 그대로 '콜백 함수'와 같은 개념으로 사용된다고 볼 수 있겠다. 참고 링크 https://github.com/team-tancheon/book-lounge/issues/8 https://n00nietzsche.gitbook.io/jake-seo/books/toby-spring/3/3.7

카테고리:

업데이트:

댓글남기기