티스토리 뷰

스프링/토비의 스프링 정리

3장 - 템플릿

구름뭉치 2021. 8. 20. 12:50

OCP - 개방 폐쇄 원칙

변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어준다.

템플릿은 이런 OCP를 구현하기 위해 변경이 거의 일어나지 않고 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로 부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.

3.1 다시보는 초난감 DAO

예외상황에 대한 처리

public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("delete from users");
    ps.executeUpdate();

    ps.close();
    c.close();
}

서버는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리

  • 예외가 발생해도 커넥션을 끊을 수 있게 해야한다.
  • try-catch-finally구조로 변경하면 예외가 발생해도 close()할 수 있다.
  • 끊을 때도 null 체크를 해줘야한다.

리팩토링 후

public int getCount() throws SQLException {
        ...
        try {
            c = dataSource.getConnection();
            ps = c.prepareStatement("select count(*) from users");
            rs = ps.executeQuery();
            rs.next();

            return rs.getInt(1);
        } catch (SQLException e) {...}
        finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                }
            }
            if (c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                }
            }
        }
    }

3.2 변하는 것과 변하지 않는 것

JDBC tyr-catcth-finally의 문제점

  • 중복이 많으므로 실수가 발생하기 쉽다.
  • 중복을 제거해서 하나의 템플릿으로 만든다.

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

  • 변하는 성격이 다른 것을 찾아내자.
  • 로직에 따라서 변하는 부분을 변하지 않는 나머지 코드에서 분리한다. 그렇게 변하지 않는 부분을 재사용한다.

메소드 추출

public void deleteAll() throws SQLException {
        ...
        try {
            c = dataSource.getConnection();

            ps = makeStatement(c);

        } catch (SQLException e)
        ...
    }

    private PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        ps.executeUpdate();
        return ps;
    }
  • 재사용이 필요한 부분이 아닌 변화가 발생하는 부분을 추출하고 있다.
  • 필요한 로직이 있을 때마다 새로 메소드를 만들어 줘야 한다. 이는 OCP를 제대로 적용하지 않은 것이다.

템플릿 메소드 패턴의 적용

  • 상속을 통해 기능을 확장해서 사용하는 것
  • 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상메서드로 정의해서 쓰는 방법
public class UserDaoDeleteAll extends UserDao {
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        ps.executeUpdate();
        return ps;
    }
}
  • UserDao 클래스를 추상클래스로 만들고 기능의 확장이 필요한 부분은 오버라이드하여 구현.

단점

  • DAO로직마다 상속을 통해 새로운 클래스를 만들어야 한다.
  • 만약 UserDao의 JDBC 메소드가 4개일 경우 4개의 서브 클래스를 만들어서 사용해야된다.
    Untitled
  • 확장 구조가 클래스를 설계하는 시점에 고정되므로 변하지 않는 코드를 가진 userDao의 JDBC try-catch-finally 블록PreparedStatement를 담고있는 서브 클래스들이 컴파일 시점에 클래스의 관계가 결정되어 있다.
    • 관계에 대한 유연성이 떨어진다.

전략 패턴의 적용

  • OCP 구조를 잘지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 방법.
  • 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 하는 만든다.
스크린샷_2021-08-19_오후_4 39 20
  • Context의 contextMethod()에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 구조
  • deleteAll()메소드에서 변하지 않는 부분이 contextMethod()가 된다. 이 메소드는 JDBC를 이용해 DB를 업데이트하라는 변하지 않는 맥락(context)를 갖는다.

deleteAll()의 맥락

  • DB커넥션 가져오기
  • PreparedStatement를 만들어줄 외부 기능 호출하기
  • 전달받은 PreparedStatement실행하기
  • 예외가 발생할 경우 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만든 PreparedStatement와 Connection을 닫아주기

이 중에서 PreparedStatement를 만들어줄 외부 기능이 전략 패턴에서 말하는 전략이 된다.

인터페이스

스크린샷_2021-08-19_오후_4 48 21
  • PreparedStatement를 만드는 전략의 인터페이스: 컨텍스트가 만들어둔 Connection을 전달받아서 PreparedStatement를 만들어서 반환.

구현체 (전략)

스크린샷_2021-08-19_오후_4 50 47
  • PreparedStatement의 전략 DeleteAllStatement : contextMethod()에 해당하는 UserDao의 deleteAll()메소드에서 사용.
스크린샷_2021-08-19_오후_4 53 44
  • 현재 코드에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다.
  • 전략 패턴은 필요에 따라 컨텍스트는 유지하고 전략을 바꿔사용하는 OCP원칙을 따라한다. 따라서 이미 고정이 되어있지 않도록 변경한다.

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

Context가 어떤 전략을 사용할지는 Context를 사용하는 앞단의 Client가 결정하게 한다.
Client가 구체적인 전략을 하나 선택하고 객체로 만들어서 Context에 전달하는것이다. Context는 전달받은 Strategy 구현 클래스의 오브젝트를 사용하게 된다.

스크린샷_2021-08-19_오후_4 57 33
  • 전략 오브젝트 생성컨텍스트의 전달을 담당하는 책임을 분리시킨 것이 ObjectFactory이고 이를 일반화한 것이 의존관계 주입 DI인 것.
  • 결국 DI란 전략패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조.

StatementStrategy strategy = new DeleteAllStatement();

컨텍스트에 해당하는 부분은 별도의 메소드로 추출.

  • 클라이언트는 전략 클래스 객체DeleteAllStatement의 메소드를 호출하며 전달해야한다. 이를 위해 전략 인터페이스 StatementStrategy를 컨텍스트 메소드 파라미터로 지정한다.
스크린샷_2021-08-19_오후_5 12 17

컨텍스트 코드

  • 클라이언트로부터 StatementStrategy타입의 전략 오브젝트 stmt를 제공받고 JDBC try-catch-finally구조로 만들어진 컨텍스트 안에서 작업을 수행한다.
  • 제공받은 전략 오브젝트 stmtPreparedStatement생성이 필요한 시점에 호출해서 사용하면 된다.
스크린샷_2021-08-19_오후_5 21 44

deleteAll()

  • 컨텍스트를 별도의 메소드로 분리했으므로 deleteAll()이 클라이언트가 된다.
  • 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고있다.
  • 사용할 전략 클래스 DeleteAllStatement 오브젝트를 생성하고, 컨텍스트로 분리한 jdbcContextWithStatementStrategy("주입할 오브젝트")메소드를 호출한다.

결론

  • 전략패턴의 모습을 갖추게 리팩토링했다.
  • 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달하는 면에서 DI 구조라고 이해할 수 있다.
  • 관심사를 분리하고 유연한 확장관계를 유지하도록 만드는 작업을 했고 이는 매우 중요하다.

3.3 JDBC 전략 패턴의 최적화

  • deleteAll() 메소드에 담겨있던 변하지 않는 부분 / 변하는 부분을 전략패턴을 이용해서 분리했다.
  • jdbcContextWithStatementStrategy()는 DAO 메소드들이 공유할 수 있게 됐다.
  • DAO 메소드는 전략 패턴의 클라이언트로서 컨텍스트에 해당하는 jdbcContextWithStatementStrategy("여기로 주입")메소드에 적절한 전략(바뀌는 로직)을 제공해주는 방법으로 사용할 수 있다.

전략 클래스의 추가정보

add() 메소드에도 적용

스크린샷_2021-08-19_오후_5 33 19

add() 메소드에서 변하는 부분인 PreparedStatement를 만드는 코드를 AddStatement 클래스로 옮기기.

  • User를 add()해야되는데 유저가 없다. 따라서 유저를 AddStatement 클래스의 생성자를 통해서 제공받게 한다.
스크린샷_2021-08-19_오후_5 51 05
  • 클라이언트인 UserDao의 add()메소드에 user정보를 생성자를 통해 전달
스크린샷_2021-08-19_오후_5 55 11
  • deleteAll(), add() 모두 preparedStatement를 실행하는 JDBC try - catch - finally 구문을 공유해서 사용할 수 있게 됐다.
  • 앞으로 DAO 관련 메소드가 필요할 때마다 Statement 전략jdbcContextWithStatementStrategy() 컨텍스트를 활용할 수 있다.

전략과 클라이언트의 동거

불만사항

  1. DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다.
    • 이런식이면 기존 UserDao때보다 클래스 파일의 개수가 많이 늘어난다. 그러면 템플릿 메소드 패턴을 적용한 방법보다 딱히 나은게 없다.
  2. DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있다면, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스를 만들어야 한다.

단계 1) 로컬 클래스

스크린샷_2021-08-19_오후_6 08 51 스크린샷_2021-08-19_오후_6 09 04

내부 클래스로 해결

  • StatementStrategy클래스를 독립된 파일로 만들지 않고 UserDao 클래스의 내부 클래스로 정의할 수 있다.
  • DeleteAllStatement, AddStatement는 UserDao밖에서 사용되지 않고 UserDao에서만 사용되고, UserDao 메소드 로직에 강하게 결합되어있다. 따라서 내부 클래스로 만들어서 사용할수 있다.

중첩 클래스

  • 다른 클래스 내부에 정의되는 클래스를 말한다.

  • 독립적인 오브젝트로 만들어질 수 있는 스태틱 클래스와, 저신이 정의된 클래스의 오브젝트 안에서만 만들어 질 수 있는 내부 클래스로 구분한다.

장점

  • 클래스 파일을 줄일 수 있고 add()메소드 내에서 PreparedStatement 생성 로직을 함께 볼 수 있어서 코드 이해에 좋다
  • 로컬 클래스는 내부 클래스이므로 자신이 선언된 곳의 정보에 접근할 수 있다. 따라서 굳이 생성자로 User 정보를 넘겨주지 않아도 된다.
스크린샷_2021-08-19_오후_7 46 50

단계 2) 익명 내부 클래스

add()메소드에서만 사용할 용도이므로 이름도 제거한 익명 내부 클래스로 만들 수 있다.

익명 내부 클래스란?

  • 이름을 갖지 않는 클래스로 클래스 선언과 오브젝트 생성이 결합 된 형태이다. 상속한 클래스나 구현할 인터페이스를 생성자 대신 사용해서 new 인터페이스_이름() { 클래스 본문 };형태로 사용한다.
  • 클래스를 재사용하지 않고 구현할 인터페이스 타입으로만 사용할 경우 유용하다.

AddStatement를 익명 내부클래스로 바꾼다.

스크린샷 2021-08-19 오후 8 07 16

만들어진 익명 내부 클래스를 변수가 아닌 jdbcContextWithStatementStrategy() 메소드의 파라미터에서 바로 생성하게 변경.

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 users(id, name, password) values (?, ?, ?)");

                ps.setString(1, user.getId());
                ps.setString(2, user.getName());
                ps.setString(3, user.getPassword());
                return ps;
            }
        });
    }

deleteAll() 메소드도 익명 내부 클래스로 처리

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

3.4 컨텍스트와 DI

jdbcContextWithStatementStrategry()를 UserDao클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 변경

클래스 분리

  • JdbcContext의 workWithSatementStrategy()메소드에 UserDao내에 있던 컨텍스트 메소드를 옮긴다.
  • JdbcContext가 DataSource를 주입받을 수 있게 변경한다.
public class JdbcContext {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void workWithStatementStrategy(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) {
                }
            }
        }
    }
}

빈 의존관계 변경

UserDao -> (추가, jdbcContext) -> DataSource

  • 의존관계의 중간에 jdbcContext가 추가된다
  • UserDao와 JdbcContext는 인터페이스를 사용하지 않고 클래스간에 DI가 적용되어있다.
  • 클래스 레벨에서 의존관계가 정해진다.

스프링 빈으로 DI

인터페이스를 사용하지 않고 클래스간에 DI를 하는것이 문제가 있지 않을까?

  • 인터페이스를 사이에 두지 않았으므로 온전한 DI는 아니다.
  • 객체의 생성과 관계 설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC 기법은 적용했다.
  • 결국 DI의 기본은 따르고 있는 것

DI구조를 해야하는 이유는?

  • JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 때문이다. jdbccontext는 JDBC컨텍스트 메소드를 제공해주는 일종의 서비스 오브젝트로서 의미를 갖는다.
  • JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다. DI를 위해서는 주입되는 오브젝트, 주입받은 오브젝트 둘다 스프링 빈으로 등록돼야 하기 때문.

근데 왜 인터페이스를 두지 않을까?

  • 인터페이스가 없다는건 UserDao와 JdbcContext간의 결합도가 매우 높다는 뜻
  • UserDao는 항상 JdbcContext와 함께 사용되어야 하는 높은 응집도를 가진다. UserDao가 JPA ORM을 사용한다면 어짜피 JdbcContext도 변경되어야 한다. 이런 구조라면 굳이 인터페이스를 두지 않고 강력한
    관계를 허용하고 DI되게 해도 좋다.

코드를 이용하는 수동 DI

DI를 스프링 빈으로 등록하지 않고 UserDao 내부에서 직접 DI할 수 있다.

  • DAO마다 하나의 JdbcContext 오브젝트를 갖게한다.
    • 대형 플젝의 경우 많아야 수백개가 생성될 것이므로 부담이 적다.
  • JdbcContext 내부에는 상태 정보가 없다. 따라서 수백개가 만들어져도 메모리에 대한 부담은 없다.
  • 자주 생성/삭제 되는게 아니므로 gc부담도 적다.

생성과 초기화를 담당할 객체가 필요하므로 UserDao가 하도록 한다. 또한 JdbcContext에 대한 DI도 담당하게 한다. (빈이 아니면 DI를 받을 수 없으므로)

  • UserDao가 DI 컨테이너로서 역활을 하게 된다.

결론

인터페이스를 사용하지 않고 DAO와 밀접한 관계를 갖는 클래스를 DI에 적용하는 방법 2가지

  1. 인터페이스 없이 클래스로 DI관계를 갖게한다.
    • 빈으로 등록할 수 있다는 장점이 있다.
    • DI의 근본적인 원칙에는 부합하지 않는다.
  2. DAO의 코드를 통해 수동 DI한다.
    • UserDao 내부에 만들어지고 외부에 드러나지 않는다.
    • 싱글톤이 아니고 DI를 위한 부가적인 코드가 필요하다.

3.5 템플릿과 콜백

전략패턴의 기본구조에 익명 내부 클래스를 활용한 방식을 템플릿 / 콜백 패턴이라고 한다.

템플릿

템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 말한다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분은 서브 캘래스의 메소드에 두는 구조가 된다.

콜백

콜백은 실행되는 것을 목적으로 다른 객체의 메소드에 전달되는 객체를 말한다. 자바는 메소드 자체를 전달할 방법이 없으므로 메소드가 담긴 오브젝트를 전달해야한다. 따라서 functional object라고도 한다.

템플릿/콜백 동작원리

  • 보통 단일 메소드 인터페이스를 사용한다
  • 콜백은 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다.

템플릿 콜백 작업 흐름

  • 클라이언트
    • 템플릿안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공
    • 만든 콜백은 클라이언트가 템플릿 메소드를 호출할 때 파라미터로 사용
  • 템플릿
    • 콜백 오브젝트의 메소드를 호출한다
    • 콜백은 클라이언트 메소드에 있는 정보 + 템플릿이 제공한 참조정보를 이용해서 수행하고 결과를 템플릿에 리턴
    • 콜백이 리턴한 값을 사용해서 작업을 마저 수행
    • 결과를 클라이언트에 다시 되돌려주기도 한다.

JdbcContext에 적용된 템플릿 / 콜백

  • JdbcContext의 workWithStatementStrategy() 템플릿은 리턴값이 없는 구조이지만 보통 조회 작업에서는 템플릿의 작업 결과를 클라이언트에 리턴해준다.
  • 더 복잡한 구조의 경우 한번 이상 콜백을 호출하기도 하고 여러개의 콜백을 클라이언트로 받아 사용하기도 한다.

편리한 콜백의 재활용

템플릿 / 콜백의 장점

  • 클라이언트인 DAO의 메소드가 간결해지고 최소한의 데이터 액세스 로직만 갖게된다.

댠점

  • DAO 메소드에서 매번 익명 내부 클래스를 사용하므로 코드를 작성하고 읽기가 불편하다.

콜백의 분리와 재활용

복잡한 익명 내부 클래스의 사용을 최소화 해보자.

public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(new StatementStrategy() {
        @Override
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            return c.prepareStatement("delete from users");
        }
    });
}
  • 코드 중 자주 바뀌지 않는 부분은 query 부분이므로 해당 부분을 매개변수로 받게 바꾸고 별도로 분리.
    public void deleteAll() throws SQLException {
        executeSql("delete from users");
    }

    private void executeSql(final String query) throws SQLException {
        this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                return c.prepareStatement(query);
            }
        });
    }
  • 바뀌는 sql문만 따로 받아서 사용하도록 변경.
  • executeSql() 메소드 처럼 UserDao만 사용하기에는 아까운 재사용 가능한 콜백을 담고 있는 메소드는 DAO가 공유할 수 있도록 템플릿 클래스로 옮기는게 낫다.

UserDao 클래스에서 JdbcContext로 executeSql() 메소드를 옮김

    public void executeSql(final String query) throws SQLException {
        workWithStatementStrategy(new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                return c.prepareStatement(query);
            }
        });
    }
  • 이제 UserDao의 모든 메소드에서 executeSql()메소드를 사용할 수 있게 됐다.
  • 익명 내부 클래스의 사용으로 복잡한 구조가 깔끔하고 단순해졌다.

템플릿 / 콜백의 응용

txt파일을 읽어서 덧셈을하는 코드를 템플릿 / 콜백으로 분리하고, 곱셈기능을 추가시켜 보았다.
@Before를 사용해서 테스트를 좀더 직관적으로 볼 수 있게 했다.

추가적으로 +, * 연산 로직이 매우 유사하므로 이 또한 템플릿으로 만들어서 재사용하도록 할 수 있다.

제네릭스를 이용한 콜백 인터페이스

    private <T> T fileLineReadTemplate(String filepath, LineCallback<T> callback, T initValue) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            T result = initValue;
            String line = null;

            while ((line = br.readLine()) != null) {
                result = callback.doSomethingWithReader(line, result);
            }
            return result;
        } catch (IOException e) {
            ...
        } finally {
            ...
        }
    }
public interface LineCallback<T> {
    T doSomethingWithReader(String line, T result) throws IOException;
}
  1. 제네릭스를 사용해서 원하는 타입의 초기값으로 initValue를 받아는다.
  2. T타입의 변수 result를 정의하고, T타입 파라미터로 선언된 LineCallback의 메소드를 호출한다.
  3. 메소드에서 처리한 후 T타입 결과를 반환한다.
  • 템플릿 / 콜백을 이용하면 파일을 라인 단위로 처리하는 다양한 기능을 편리하게 만들 수 있다.

3.6 스프링의 JdbcTemplate

스프링이 제공하는 템플릿 / 콜백 : JdbcTemplate

update

jdbcTemplate.update("query문");

쿼리문 : "delete from users"

  • 전체 삭제

쿼리문 : "insert into users(id, name, password) values(?,?,?)", user.getId(), ..

  • add() 메소드로 사용

queryForInt

jdbcTemplate.queryForInt("selec count(*) from users")'
  • Integer 타입의 결과를 가져올 수 있는 SQL문장만 전달하면 int값을 받을 수 있다.

queryForObject

jdbcTemplate.queryForObject("select * from users where id = ?",
    new Object[] {id}, this.userRowMapper);

private RowMapper<User> userRowMapper = new RowMapper<User>() {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
        return user;
    }
};
  • 쿼리문에 맞는 객체를 찾는다. 위에서는 id가 갖은 객체를 조회해서 반환

query

jdbcTemplate.query("select * from users order by id", this.userRowMapper);

private RowMapper<User> userRowMapper = new RowMapper<User>() {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
        return user;
    }
};
  • 모든 유저를 id 오름차순으로 가져오는 쿼리이다.

테스트 보완

get(), getAll() 등 모든 메소드에서도 예외적인 조건에 대한 테스트를 빼먹지 말아야한다. 레코드 몇개 추가하고 그 결과만 맞는지 확인할게 부정적인 자세가 필요하다.

  • 네거티브 테스트 : 예외상황에 대한 테스트

    • get()일 경우 id가 없다면, getAll()일 경우 결과가 하나도 없다면 등
    • 생일에 음수값도 넣어보고, 아무것도 입력안한채 form 저장버튼 누르기, 엉터리 검색 조건 등
  • 일관성있는 에러처리가 중요하다.

    • 데이터가 없을 때 어디서는 null을 반환, 빈 리스트 or 오브젝트 반환, 예외 던지기, 런타임 에러 등 전부 제각각으로 처리하면 테스트도 처리도 힘들다.
    • 미리 기준을 정해둬야 한다.
  • 중복제거

    • RowMapper를 빼서 따로 만든다. (위 예시에 적용)

추가 리팩토링

  • userMapper가 인스턴스 변수로 설정되어 있고, 변경되지 않는 프로퍼티와 같은 성격을 띠고 있으므로 UserDao 빈의 DI용 프로퍼티로 만들수 있다.
  • DAO메서드에서 사용하는 SQL 문장을 UserDao 코드내부에서 작성하지 않고 외부 리소스에 작성하고 이를 가져와서 사용하게 할 수 있다.
    • SQL쿼리를 최적화하거나 DB 테이블의 이름이나 필드 이름을 변경해도 UserDao코드에 손을 대지 않고 SQL 파일만 고치면 된다.
반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday