[Spring] 전략패턴 (토비의 스프링 p.216)

Coastby·2022년 10월 24일
0

LIKELION Back-End School

목록 보기
45/61

전략패턴 (p.216)

❌ 이전의 코드의 문제점

  • try/catch/finally 예외상황 처리는 완료되었다. 그러나 UserDao의 모든 메소드마다 복잡한 블록이 나온다.
  • 이러한 복잡한 코드의 반복은 Human Error의 가능성을 높이고, 유지/보수 또한 어렵게한다.

💡 문제 해결의 핵심은 변하지 않으며 많은 곳에서 중복되는 코드와 로직에 따라 확장되고 변하는 코드를 분리해내는 것이다.

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

  • PreparedStatement를 만들어서 업데이트용 쿼리를 실행하는 메소드라면 deleteAll() 메소드와 구조의 거의 비슷할 것이다.
  • 아래의 deleteAll(), add() 두 메서드에서 노란색 박스 안의 부분은 동일한 구조를 갖는다.
  • deleteAll()에서 가운데 부분만 수정하면 add()를 만들 수 있다.

⌨️ deleteAll()

⌨️ add()

○ 전략 패턴이란?

  • 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.
  • 확장에 해당하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.
  • Context의 contextMethod()에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전량 클래스에 위입하는 것이다.

○ 전략 패턴의 장점

개방 폐쇄 원칙 (OCP)을 잘 지키는 구조이면서도 유연하고 확장성이 뛰어나다.

○ 전략 패전의 적용

⌨️ Strategy interface

  • 이를 deleteAll()에 적용하면, 앞에서 보았듯이 deleteAll()의 context 중에서 Preparement를 만드는 부분만이 변하는 부분이다.
  • 따라서 이를 외부 기능 (전략) 으로 만들어서 호출할 것이다.
  • 변하는 부분을 인터페이스로 만들어두고 인테페이스의 메소드를 통해 PreparedStatement 생성을 호출한다.
  • context가 만들어 둔 connection을 받아서 PreparedStatement 오브젝트를 반환하는 메소드를 가진 인터페이스를 만들다.
public interface StatementStrategy {
    PreparedStatement makePreparedStatement (Connection c) throws SQLException;
}

⌨️ DeleteAllStatement

    public class DeleteAllStatement implements StatementStrategy{
        @Override
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            PreparedStatement ps = c.prepareStatement("delete from Users");
            return ps;
        }
    }

⌨️ 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 Users(id, name, password) VALUES(?, ?, ?)");
            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());
    
            return ps;
        }
    }

⌨️ Context method

  • context가 어떤 전략을 사용할 것인지는 context를 사용하는 앞단의 client가 결정하는 것이 일반적이다.
  • client가 구체적인 전략을 선택하고 오브젝트로 만들어서 context에 전달한다.
  • Context method는 clinet로부터 StatementStrategy 타입의 전략 오브젝트를 제공받는다. 그리고 제공받은 오브젝트는 PreparedStatement 생성이 필요한 시점에 호출해서 사용한다.
  • ⌨️ 메소드로 분리한 context code
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
    Connection conn = null;
    PreparedStatement ps = null;
    try {
        conn = connectionMaker.makeConnection();

        ps = stmt.makePreparedStatement(conn);
        ps.executeUpdate();

    } catch (SQLIntegrityConstraintViolationException e) {
        throw new SQLIntegrityConstraintViolationException(e);
    } catch (SQLException e) {
        throw e;
    } finally {
        if(ps != null){
            try {
                ps.close();
            } catch (SQLException e) {
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
            }
        }
    }
}

⌨️ client

  • 컨텍스트를 별도의 메소드로 분리했으니 deleteAll() 메소드가 클라이언트가 된다.
  • deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 가지고 있다.
public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(new DeleteAllStatement());
}
  • add() 메소드는 user 정보를 생성자를 통해 전달해준다.
public void add(User user) throws SQLException {
    AddStatement addStatement = new AddStatement(user);
    jdbcContextWithStatementStrategy(addStatement);
}

💡 비록 클라이언트와 컨텍스트는 클래스를 분리하지 않았지만, 의존관계과 책임으로 볼 때 이상적인 클라이언트/컨택스트 관계를 갖고 있다.
특히, 클라이언트가 컨택스트가 사용할 전략을 정해서 전달한다는 면에서 DI 구조라고도 할 수 있다.

profile
훈이야 화이팅

0개의 댓글