Today I Learned_230619

4 분 소요

토비의 스프링 3.1

그런데 코드를 짜다보니.. 코드에 중복이 너무 많다 → 분리와 재사용을 위해 코드를 분리하자!

코드를 분리할 때는 변하는 성격이 다른 것을 찾아내도록 한다.

변하는 부분과 / 자주 변하지 않는 부분으로 나눌 것

public void deleteAll() throws SQLException {
// 변하지 않는 부분 시작 ----------------------------------
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
// 변하지 않는 부분 끝 ----------------------------------
// 변하는 부분 시작 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        ps = c.prepareStatement("delete from USER");
// 변하는 부분 끝 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 변하지 않는 부분 시작 ----------------------------------
        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null) {
            try { ps.close(); }
            catch (SQLException e) {}   // 이거 잡아주지 않으면 connection을 close하지 않고 빠져나갈 수 있다.
        }
        if( c != null ) {
            try { c.close(); }
            catch (SQLException e) {}
        }
    }
// 변하지 않는 부분 끝 ----------------------------------
} 

변하는 부분을 메소드로 뺀다면?

메소드 추출 리팩토링은 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이렇게 하고 나면 분리시키고 남은 메소드가 재사용이 필요한 부분이 된다.

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴

템플릿 메소드 패턴

  • 상속을 통해 기능을 확장해서 사용하는 부분
  • 변하지 않는 부분은 슈퍼클래스에, 변하는 부분은 추상 메소드로 정의

상속을 통해 자유롭게 확장할 수 있고, 상위 클래스에 불필요한 변화는 생기지 않도록 할 수 있다

→ 단, 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 제한이 있다. 장점보다 단점이 더 많다.

서브클래스 구조가 클래스 레벨에서 컴파일 시점에 관계가 이미 결정되어 있다 → 관계에 대한 유연성 떨어짐

전략 패턴의 적용

전략 패턴

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

확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임

전략에 해당하는 인터페이스

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

전략, 바뀌는 부분에 해당하는 클래스.

PreparedStatement를 생성하는 클래스다.

public class DeleteAllStatement implements StatementStrategy{

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

전략 패턴이 적용된 deleteAll() 메소드

try {
        c = dataSource.getConnection();

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

        ps.executeUpdate();
    } 
catch (SQLException e) {
        throw e;
}

그런데 코드 상에서 컨텍스트가 특정 구현체인 DeleteAllStatement를 알고 있다.

→ DI로 주입해야 하지 않을까?

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

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

→ 컨텍스트 코드와 클라이언트의 코드를 분리하면 전략 패턴의 장점 사용 가능

메소드로 분리한 컨텍스트 코드

// 전략 파라미터인 stmt 받는다
    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) {}
            }
        }
    }

클라이언트로부터 전략 오브젝트를 제공받고 컨텍스트 내에서 작업을 수행한다.

제공받은 전략 오브젝트는 PreparedStatement 생성이 필요한 시점에 호출하여 사용한다.

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

// 전략 패턴에서 클라이언트 코드에 해당.
    // 컨텍스트인 jdbcContextWithStatementStrategy를 호출하여 사용한다.
    public void deleteAll() throws SQLException {
        StatementStrategy st = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(st);
    }

전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다.

마이크로 DI

DI의 핵심 → 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만드는 것

일반적인 DI는 아래 4개의 오브젝트 사이에서 일어남

  • 의존관계에 있는 두 개의 오브젝트
  • 관계를 다이나믹하게 설정해 주는 오브젝트 팩토리(DI 컨테이너)
  • 이를 사용하는 클라이언트

때에 따라서는 이 오브젝트들간 결합될 수 있으며, 이런 경우 DI가 매우 작은 단위인 코드와 메소드 단에서 일어나기도 한다.

전략 클래스의 추가 정보

AddStatement에 적용

public class AddStatement implements StatementStrategy{
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("insert into USER(id, name, password) values (?, ?, ?)");
        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;
    }

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("insert into USER(id, name, password) values (?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        return ps;
    }
}

add 메소드 수정

public void add(User user) throws SQLException { //예외는 메소드 밖으로 던지기
        StatementStrategy st = new AddStatement(user);
        jdbcContextWithStatementStrategy(st);
    }

문제점 → DAO 메소드마다 새로운 구현 클래스를 만들어야 한다. 또 User와 같은 부가적인 정보가 있는 경우, 이를 저장할 인스턴스 변수를 번거롭게 만들어야 한다.

로컬 클래스

특정 메소드 내에서만 사용되는 것이라면 로컬 클래스로 만들어도 된다.

public void add(final User user) throws SQLException { //예외는 메소드 밖으로 던지기

        //로컬 클래스로 선언
        class AddStatement implements StatementStrategy{
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into USER(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();
        jdbcContextWithStatementStrategy(st);
    }

로컬 클래스는 선언된 메소드 내에서만 사용할 수 있다.

또한 로컬 클래스틑 내부 클래스이므로 자신이 선언된 곳의 정보에 접근할 수 있다

→ 자신이 정의된 메소드의 로컬 변수에 직접 접근할 수 있다. (user 접근 가능)

단, 내부 클래스에서 외부 변수를 사용할 때는 변경되면 안되므로 final을 사용해야 한다.

익명 내부 클래스

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

이름이 없기 때문에 클래스 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장할 수 있다.

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

add, deleteAll의 Statement를 익명 클래스로 전환

public void add(final User user) throws SQLException { //예외는 메소드 밖으로 던지기
        jdbcContextWithStatementStrategy(
                new StatementStrategy() {
                    @Override
                    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                        PreparedStatement ps = c.prepareStatement("insert into USER(id, name, password) values (?, ?, ?)");
                        ps.setString(1, user.getId());
                        ps.setString(2, user.getName());
                        ps.setString(3, user.getPassword());

                        return ps;
                    }
                }
        );
    }

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

카테고리:

업데이트: