3. 템플릿 - JDBC 전략 패턴의 최적화

이유석·2022년 5월 24일
0

Book - Toby's Spring

목록 보기
16/20
post-thumbnail

JDBC 전략 패턴의 최적화

지금까지 기존의 deleteAll() 메소드에 담겨 있던 변하지 않는 부분, 자주 변하는 부분을 전략 패턴을 사용해 깔끔하게 분리해냈다.

여기서 클라이언트는 DAO 메소드로서 컨텍스트 메소드에게 적절한 전략, 즉 바뀌는 로직을 제공해준다.
컨텍스트는 PreparedStatement를 실행하는 JDBC의 작업 흐름이다.
전략은 PreparedStatement를 생성하는 것 이다.

3.3.1 전략 클래스의 추가 정보

이번에는 add() 메소드에도 적용해보자.

먼저 add() 메소드의 자주 변하는 부분인 PreparedStatement를 생성하는 부분을 클래스로 분리한다.

public class AddStatement implements StatementStrategy {

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement(
                "insert into users(id, name, password) values(?,?,?)");
        
        // 그런데 user 는 어디서 가져올까?
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        return ps;
    }
}

이렇게 클래스를 분리하고 나면, user라는 부가 정보가 필요하기 때문에 컴파일 에러가 발생한다.

클라이언트로부터 User타입 오브젝트를 AddStatement의 생성자를 통해 제공받을 수 있도록 만들자.

public class AddStatement implements StatementStrategy {
    User user;

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

이제 컴파일 에러는 발생하지 않을 것 이다.

다음은 클라이언트인 UserDao의 add() 메소드를 아래와 같이 수정하자.

public void add(User user) throws SQLException {
    StatementStrategy stmt = new AddStatement(user);
    jdbcContextWithStatementStrategy(stmt);
}

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

이전까지 다양한 문제점을 해결하면서 코드도 깔끔해졌지만, 두 가지 개선할 점이 있다.

  1. DAO 메소드마다 새로운 StatmentStrategy 구현 클래스를 만들어야 한다는 점이다.
    • 이렇게 되면 기존 UserDao 보다 클래스 파일의 개수가 많아진다.
  1. DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 생성해주어야 한다는 점이다.

이 두 가지 문제를 해결할 수 있는 방법을 생각해보자.


로컬 클래스

1번의 클래스파일이 많아지는 문제는 간단한 해결 방법이 있다.

StatementStrategy 구현 클래스를 매번 독립된 파일로 만들지 않고, UserDao 클래스 안에 내부 클래스로 정의해버리는 것이다.

DeleteAllStatement나 AddStatement는 UserDao 밖에서는 사용되지 않기 때문에 가능한 것 이다.

public void add(User user) throws SQLException {
	
    // add() 메소드 내부에 선언된 로컬 클래스이다.
    class AddStatement implements StatementStrategy {
        User user;

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

		@Override
        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 stmt = new AddStatement(user);
    jdbcContextWithStatementStrategy(stmt);
}

위 코드는 AddStatement 클래스를 로컬 클래스로서 add() 메소드 안에 집어넣은 것이다.

AddStatment가 사용될 곳이 add() 메소드 뿐이라면, 이렇게 사용하기 전에 바로 정의해서 쓰는 것도 나쁘지 않다.

이렇게 로컬 클래스로 작성하면 장점이 존재한다.

  • 클래스 파일이 하나 줄어들게 된다.
  • 코드를 이해하기도 좋다.
    • add() 메소드 안에서 PreparedStatement 생성 로직을 함께 볼 수 있기때문이다.
  • 로컬 클래스는 자신이 선언된 곳의 정보에 접근할 수 있다.
    • 즉, 생성자를 통해 User 오브젝트를 전달해줄 필요가 없다.
    • 다만, 내부 클래스에서 외부의 변수를 사용할 때, 해당 변수는 final로 선언해줘야 한다.
public void add(final User user) throws SQLException {

    class AddStatement implements StatementStrategy {
        @Override
        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 stmt = new AddStatement();
    jdbcContextWithStatementStrategy(stmt);
}

익명 내부 클래스

좀 더 간결하게 바꿔보자.
AddStatement 클래스는 add() 메소드에서만 사용할 용도로 만들어졌다.

그렇다면 익명 내부 클래스를 이용하여 클래스 이름을 제거할 수 있다.

익명 내부 클래스 (anonymous inner class)
이름을 갖지 않는 클래스이다.
클래스 선언과 오브젝트 생성이 결합된 형태로 만들어진다.
상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 만들어진다.

new 인터페이스이름() {클래스 본문};

클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.

AddStatement를 익명 내부 클래스로 만들어보자.

public void add(final User user) throws SQLException {

    StatementStrategy stmt = 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;
        }
    };

    jdbcContextWithStatementStrategy(stmt);
}

만들어지 StatementStrategy는 딱 한 번만 사용할 테니 굳이 변수에 담아두지 말고 jdbcContextWithStatement() 메소드의 파라미터에서 바로 생성하는 편이 낫다.

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

마찬가지로 DeleteAllStatement도 deleteAll() 메소드로 가져와서 익명 내부 클래스로 처리하면 아래 코드와 같이 만들 수 있다.

public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(
        new StatementStrategy() {
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("delete from users");
                return ps;

            }
        }
    );
}

소스코드 : github

profile
https://github.com/yuseogi0218

0개의 댓글