토비의 스프링 정리 프로젝트 #3.3 JDBC 전략 패턴의 최적화

Jake Seo·2021년 7월 19일
0

토비의 스프링

목록 보기
19/29

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

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

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

여기서

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

UserDao.add() 메소드 개선

    public void add(User user) throws SQLException {
        jdbcContextWithStatementStrategy(new AddStatement(user));
    }
public class AddStatement implements StatementStrategy{
    private 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;
    }
}

add() 메소드에서는 deleteAll()메소드와 다르게, User라는 정보가 필요했다. 그래서 생성자에 User 정보를 받는 부분을 추가했다.

add() 메소드의 코드가 훨씬 개선되었다. 테스트를 통해 테스트해보자. 앞으로 비슷한 기능의 DAO 메소드가 필요할 때마다 이 Statement 전략과 jdbcContextWithStatementStrategy() 컨텍스트를 활용할 수 있으니 try/catch/finally로 범벅된 코드를 만들다가 실수할 염려는 사라졌다. DAO 코드도 간결해졌다. DAO 코드의 양을 많게는 70~80%까지 줄일 수 있다.

미리 준비해둔 테스트덕에 마음껏 코드를 수정해도 정상적으로 동작한다는 확신을 가질 수 있다.

전략과 클라이언트의 동거

사실 현재 만들어진 구조에 두가지 불만이 있다.

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

로컬 클래스

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

    public void add(User user) throws SQLException {
        class AddStatement implements StatementStrategy{
            private 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;
            }
        }

        jdbcContextWithStatementStrategy(new AddStatement(user));
    }

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

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

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

public void add(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;
        }
    }

    jdbcContextWithStatementStrategy(new AddStatement());
}

단, final로 선언된 외부 로컬 변수에만 접근할 수 있다. 위 예제에서는 final을 붙이지 않았지만, Effectively final(참조 링크1, 참조 링크2) 때문에 자동으로 final이 붙은 효과가 나타난다. 내부 클래스 선언 이후에 User 객체가 가리키는 곳을 다른데로 바꾸면 에러가 날 것이다.

중첩 클래스의 종류

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

익명 내부 클래스

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

public void add(User user) throws SQLException {
        StatementStrategy stmt = c -> {
            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);
    }

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

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

    public void deleteAll() throws SQLException {
        StatementStrategy strategy = c -> c.prepareStatement("delete from users"); // 선정한 전략 클래스의 오브젝트 생성
        jdbcContextWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
    }

위와 같이 줄일 수 있다.

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

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글