[토비의 스프링 3.1] 3.3 JDBC 전략 패턴의 최적화

2022. 7. 7. 09:59토비의 스터디

이전 장(3.2)에서는 전략 패턴을 사용해서 변하는 부분과 그렇지 않은 부분을 깔끔하게 분리했다.

public void deleteAll() throws SQLException {
    StatementStrategy strategy = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
    jdbcContextWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}

클라이언트 코드에서 구체적인 전략을 정하고, 컨텍스트에 그 전략을 파라미터로 전달해주었다. 
컨텍스트도 jdbcContextWithStatementStrategy라는 메소드로 분리하였더니 다른 DAO에서도 PreparedStatement를 실행하는 JDBC의 공통 작업 흐름을 가져다 쓸 수 있게 되었다.

3.3.1 전략 클래스의 추가 정보

지난 장 deleteAll()에 이어 이번에는 add() 메소드에도 전략 패턴을 적용해보자.

가장 먼저 했던 작업은 변하는 부분과 그렇지 않은 부분을 가려내는 것이다.
먼저 변하는 부분인 PreparedStatement를 만드는 코드를 AddStatement 클래스에 옮겨 담았다.

public class AddStatement implements StatementStrategy{
	 private 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;
    }
}

deleteAll() 메소드와는 다른 점이, add() 메소드는 user를 파라미터로 전달 받아야 한다는 것이다.
이 user 정보는 클라이언트가 제공해주면 된다. User 타입 오브젝트를 전달받을 수 있도록 위와 같이 생성자를 작성한다.

이제 add 클라이언트 코드를 살펴보자. 

public void add(User user) throws SQLException{
	StatementStrategy st = new AddStatement(user);  // 변하는 부분(전략)
	jdbcContextWithStatementStrategy(st); // 이전에 분리한 컨텍스트 메소드(공통 부분)
}

이렇게 해서 deleteAll()과 add() 메소드 두 군에서 모두 PreparedStatement를 실행하는 JDBC try/catch/finally 컨텍스트를 공유해서 사용할 수 있게 되었다.

3.3.2 전략과 클라이언트 동거

아직 코드에 개선의 여지가 남아있다.

  • 먼저, DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점이다. --> 기존 UserDao때보다 클래스 파일의 개수가 늘어난다. 

  • 또 다른 하나는 DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우(위의 add 메소드 등), 이를 위해 오브젝트를 전달받는 생성자와 저장해 둘 인스턴스 변수를 번거롭게 만들어야 한다는 점이다.

로컬 클래스

클래스 파일이 많아지는 문제는 UserDao 클래스 안에 내부 클래스로 정의하면 해결이 가능하다.

public void add(User user) throws SQLException { // user에 접근 가능하다.
        class AddStatement implements StatementStrategy{ //add()메소드 내부에 선언된 로컬 클래스
            private 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;
            }
        }
		StatementStrategy st = new AddStatement(user);
        jdbcContextWithStatementStrategy(st);
    }

구체적인 전략이었던 AddStatement 클래스를 add() 메소드 안에 집어넣은 것이다. AddStatement는 UserDao 밖에서는 사용되지 않으며, 메소드와 강하게 결합되어있기 때문에 내부클래스로 정의해도 무방하다. 

로컬 클래스의 또 다른 장점은 내부클래스이므로 자신이 선언된 곳의 정보에 접근할 수 있다는 점이다. AddStatement는 파라미터로 User타입의 변수가 필요했다. 이를 위해 생성자와 저장할 인스턴스 변수를 직접 추가로 작성했었는데, 내부 클래스로 정의하면 번거롭게 오브젝트를 전달해 줄 필요가 없다.

단, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해야 한다. 지금 여기서는 user 파라미터를 변경하지 않으므로(getter로 값을 가져오는 로직만 있다) final로 선언한다.

public void add(final User user) throws SQLException {
...

 

익명 내부 클래스

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

익명 클래스는 인터페이스 타입의 변수에만 저장할 수 있다. 이름이 없으므로 자신의 타입을 가질 수 없다.

익명 클래스는 딱 한번만 사용할 것이므로 변수에 저장하지 말고 jdbcContextWithStatementStrategy() 메소드의 파라미터에서 바로 생성하자. 참고로 jdbcContextWithStatementStrategy() 메소드는 JDBC의 PreparedStatement를 실행하는 try/catch/finally 컨텍스트다.

public void add(final User user) throws SQLException {

	jdbcContetWithStatementStrategy(
		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(User user) throws SQLException{
	StatementStrategy st = new AddStatement(user);  // 변하는 부분(전략)
	jdbcContextWithStatementStrategy(st); // 이전에 분리한 컨텍스트 메소드(공통 부분)
}

이제 아까 언급한 두가지 문제점

  • DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점, 

  • DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우 이를 위해 오브젝트를 전달받는 생성자와 저장해 둘 인스턴스 변수를 번거롭게 만들어야 한다는 점)

둘 다 해결했다.