주요글: 도커 시작하기
반응형
하나의 클래스가 테이블의 전체 행을 처리하는 테이블 데이터 게이트웨이 패턴의 구현방법을 살펴본다.

테이블 데이터 게이트웨이의 구현

본 글에서 소개하는 패턴은 '엔터프라이즈 어플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자는 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

'테이블 데이터 게이트웨이(Table Data Gateway)'
는 데이터베이스 테이블에 대한 게이트웨이 역할을 하는 객체로서, 하나의 테이블 데이터 게이트웨이 객체를 통해서 테이블의 모든 행을 처리하는 패턴이다. 테이블 데이터 게이트웨이는 테이블과 관련된 모든 SQL을 포함한다. 즉, 삽입(INSERT), 선택(SELECT), 갱신(UPDATE), 삭제(DELETE)와 관련된 SQL을 테이블 데이터 게이트웨이가 처리한다.

구현 방법

테이블 데이터 게이트웨이의 구현 방법은 매우 간단하다. CRUD(Create, Read, Update, Delete)와 관련된 SQL 쿼리를 실행하는 메소드를 구현하기만 하면 된다. 보통, 다음과 같이 4개의 메소드를 기본적으로 제공한다.

    public class TableGateWay {
        public void insert(Data data) { ... }
        public void update(Data data) { ... }
        public Data select(String key) { ... }
        public void delete(String key) { ... }
    }

이들 메소드는 이름에서 알 수 있겠지만 관련된 SQL 쿼리를 실행한다. 예를 들어, insert() 메소드의 경우는 파라미터로 전달받은 Data 객체가 제공하는 값을 테이블에 삽입한다. 비슷한 방법으로 update() 메소드는 테이블의 값을 변경한다. select() 메소드와 delete() 메소드는 선택 또는 삭제할 행의 키값을 전달받아 관련 기능을 수행하게 된다. Data 클래스는 테이블과 관련된 데이터를 저장하는 객체의 타입을 나타내며 테이블 데이터 게이트웨이에 따라 알맞은 타입을 선택하면 된다.

테이블 데이터 게이트웨의 코드를 예로 들면, delete() 메소드는 다음과 같은 형태를 갖는다.

    public void delete(String key) throws SQLException {
        ...
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("delete from MEMBER where MEMBER_ID = ?");
            pstmt.setString(1, key);
            int deletedCount = pstmt.executeUpdate();
            if (deletedCount == 0) {
                throw ...
            }
        } finally {
            if (pstmt != null) try { pstmt.close(); } catch(SQLException ex) {}
        }
    }

select() 메소드는 읽어오고자 하는 방식에 따라 여러가지 형태가 존재할 수 있다. 예를 들어, 다음과 같이 필드값에 따라서 데이터를 읽어오도록 다양한 select() 메소드를 작성할 수 있을 것이다.

    public Data select(String key) { ... }
    public List selectByRegDate(Timestamp date) { ... }
    public List selectByName(String name) { ... }

목록을 읽어오는 select() 메소드가 조건에 따라 다양하게 필요할 경우 위 코드처럼 일일이 select() 메소드를 만드는 것은 비슷한 코드를 중복해서 사용하는 비효율적인 코드를 양산하게 된다. 이런 경우 쿼리의 WHERE 절에 명시될 데이터를 저장하는 필터를 만들어서 필터를 인자로 전달받도록 select() 메소드를 구현하는 것이 좋다. 먼저 조건을 저장하는 필터는 다음과 같이 조건을 명시할 수 있는 메소드를 제공하면 된다.

    public class Filter {
        public void addEqualsFilter(String fieldName, Object value) { ... }
        public void addBetweenFilter(String fieldName, Object from, Object to) { ... }
        public void addGTFilter(String fieldName, Object value) { ... }
        public void addLTFilter(String fieldName, Object value) { ... }
        public void addINFilter(String fieldName, List values) { ... }
        public void addNullFilter(String fieldName) { ... }
        public void addNotNullFilter(String fieldName) { ... }
        public String getWherePart() { ... }
    }

예를 들어, 2004년 1월 1일 이후에 가입한 회원의 목록을 추출하고자 할 경우 다음과 같은 필터를 생성하면 된다.(회원 가입날짜는 REGDATE 필드에 저장된다고 가정한다.)

    Calendar cal = Calendar.getInstance();
    cal.set(2004, 0, 1, 0, 0, 0);
    Date date = new Date(cal.getTime().getTime())
    Filter filter = new Filter();
    filter.addGTFilter("REGDATE", date);

필터를 사용하는 메소드를 테이블 데이터 게이트웨이에 추가할 경우, 아래와 같은 메소드가 추가될 것이다.

    public List selectByFilter(Filter filter) {
        ...
        String query = "select * from MEMBER " + filter.getWherePart();
        
        Statememt stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            ...
        } finally {
            ...
        }
    }

Filter.getWherePart()는 SQL의 where 조건에 해당하는 부분의 쿼리를 생성하도록 하면 된다. 관리자 툴이나 게시판 등은 다양한 where 조건을 필요로 하는데, 이렇게 다양한 검색 조건을 필요로 하는 경우에는 각 조건마다 select 메소드를 만드는 것 보다는 Filter를 통해서 조건을 명시할 수 있도록 하는 것이 좋다.

데이터 베이스 커넥션 전달 방법

테이블 데이터 게이트웨이를 사용할 때 한가지 정해야 할 것이 커넥션의 전달 규칙이다. 테이블 데이터 게이트웨이는 데이터베이스 커넥션을 사용하게 되는데, 이를 전달하는 방법에 따라 테이블 데이터 게이트웨이의 사용방법이 달라지기 때문이다. 크게 다음과 같은 3가지 방법이 존재한다.

  • 테이블 데이터 게이트웨이를 매번 새롭게 생성하며, 생성할 때에 커넥션을 전달한다.
  • 메소드 마다 커넥션을 파라미터로 전달받는다.
  • 테이블 데이터 게이트웨이의 메소드에서 직접 커넥션을 생성한다.
먼저 첫번째 방법에 대해서 살펴보자. 첫번째 방법은 테이블 데이터 게이트웨이 클래스의 코드를 다음과 같이 작성한다.

    public class MemberTableDataGateWay {
        private Connection conn = null;
        public void setConnection(Connection conn) {
            this.conn = conn;
        }        
        public void releaseConnection() {
            conn = null;
        }        
        // insert, select, update, delete 메소드 정의
        public void insert() {
            ...
            pstmt = conn.prepareStatement(...);
            ...
        }
    }

테이블 데이터 게이트웨이를 사용하는 부분의 코드는 다음과 같은 형태를 띄게 된다.

    
    Connection conn = null;
    MemberTableDataGateWay gateway = null;
    try {
        conn = ... // 커넥션을 얻는 코드
        gateway = new MemberTableDataGateWay();
        gateway.setConnection(conn);
        
        List list = gateway.selectByFilter(someFilter);
        if (list.size() > 0) {
            gateway.update(data);
            ...
        }
    } finally {
        if (gateway != null) gateway.releaseConnection();
        if (conn != null) ... // 커넥션을 닫음
    }
    

게이트웨이가 필요할 때마다 매번 테이블 게이트웨이 객체를 생성하기 때문에 가비지콜렉션이 자주 발생할 수 있는 문제가 있다. 하지만, 객체 풀링을 사용해서 테이블 게이트웨이를 풀 속에 저장해서 필요할 때에만 꺼내 사용하게 되면 테이블 게이트웨이 객체 생성으로 인한 가비지 콜렉션 발생 빈도를 줄일 수 있다.

커넥션을 얻어오는 두번째 방법은 테이블 데이터 게이트웨이의 모든 메소드가 커넥션을 인자로 전달받는 형태이다. 예를 들면 다음과 같이 첫번째 인자를 데이터베이스 커넥션으로 전달받는 형태가 된다.

    public void insert(Connection conn, Data data) { ... }
    public void update(Connection conn, Data data) { ... }
    public Data select(Connection conn, String key) { ... }
    public void delete(Connection conn, String key) { ... }

테이블 게이트웨이를 사용하는 코드는 다음과 같이 게이트웨이의 메소드를 실행할 때 마다 매번 커넥션을 전달해주게 된다.

    
    Connection conn = null;
    try {
        conn = ... // 커넥션을 얻는 코드
        MemberTableDataGateWay gateway = new MemberTableDataGateWay();
        
        List list = gateway.selectByFilter(conn, someFilter);
        if (list.size() > 0) {
            gateway.update(conn, data);
            ...
        }
    } finally {
        if (conn != null) ... // 커넥션을 닫음
    }
    

커넥션을 얻어오는 세번째 방법은 테이블 데이터 게이트웨이가 직접 커넥션을 생성하는 형태이다. 즉, 게이트웨이의 메소드는 다음과 같은 형태의 코드를 사용하게 된다.

    public void insert(Data data) throws SQLException {
        Connection conn = null;
        try {
            conn = someResource.getConnection();
            ...
        } finally {
            if (conn != null) try { conn.close(); } catch(SQLException ex) {}
        }
    }

필자의 경우는 세번째 방법은 사용하지 말것을 권하고 싶다. 그 이유는 비즈니스 로직을 처리하는 데 여러 테이블 게이트웨이의 메소드를 사용할 경우 트랜잭션 처리가 어려워지기 때문이다. 예를 들어, 회원 가입을 처리하는 비즈니스 로직을 생각해보자. 회원 정보 테이블과 메일 관련 테이블이 따로 존재할 경우 첫번째 방식을 사용하게 되면 다음과 같이 코드를 생성할 것이다.

    MemberTableGateway memberGateway = null;
    MailAccountTableGateway mailAccountGateway = null;
    Connection conn = null;
    try {
        conn = someResource.getConnection();
        memberGateway = new MemberTableGateway();
        mailAccountGateway = new MailAccountTableGateway();
        
        memberGateway.setConnection(conn);
        mailAccountGateway.setConnection(conn);
        
        conn.setAutoCommit(false); // 트랜잭션 시작
        memberGateway.insert(data);
        mailAccountGateway.insert(mailAccountData);
        conn.commit(); // 트랜잭션 완료
    } catch(SQLException ex) {
        if (conn != null)
            try { conn.rollback(); } catch(SQLException ex) {}
        ...
    } finally {
        ...
    }
    new MemberTableGateway();

하지만, 세번째 방법처럼 테이블 게이트웨이의 각각의 메소드에서 커넥션을 개별적으로 얻어올 경우 여러 메소드 호출에 대한 트랜잭션 처리를 할 수 없다. 따라서, 테이블 데이터 게이트웨이가 사용할 데이터베이스 커넥션은 첫번째 또는 두번째 방법과 같이 전달해주는 것이 좋다.

정보 전달 객체의 선택 및 예외 처리

정보 전달 객체의 선택

테이블 데이터 게이트웨이의 insert() 메소드나 update() 메소드는 테이블에 적용할 값을 인자로 전달받게 된다. 또한, select() 메소드는 테이블에서 읽어온 데이터를 리턴하게 된다. 이렇게 테이블 데이터 게이트웨이와 테이블 데이터 게이트웨이를 사용하는 클래스 사이에 주고 받을 클래스는 어떤 걸 사용해야 좋을까?

다음과 같이 두 가지 타입중의 한가지를 선택하면 된다.

  • RecordSet과 같이 범용적인 타입 사용
  • 비즈니스 도메인에 알맞은 자바빈 객체 사용
해당 테이블 데이터 게이트웨이의 재사용성이 높지 않을 경우 RecordSet과 같은 범용적인 타입을 사용해서 데이터를 주고 받는 것이 좋다. 예를 들어, 회원 추가의 경우 테이블에 데이터를 삽입하는 insert() 메소드를 호출하게 되는데, RecordSet 클래스를 사용할 경우 다음과 같은 코드를 작성하게 된다.

    RecordSet record = new RecordSet();
    record.set("MEMBER_ID", id);
    record.set("NAME", name);
    ...
    gateway = new MemberTableGateway();
    gateway.insert(record);
    ...

RecordSet은 범용적이기 때문에 어떤 테이블 데이터 게이트웨이에서도 사용할 수 있다. 또한, RecordSet은 어떤 비즈니스 도메인 영역의 개체와도 연관되어 있지 않기 때문에, 모든 비즈니스 영역에 대해서 사용할 수 있다. 하지만 RecordSet 자체는 도메인 영역을 잘 표현하지 못하므로 소스 코드 분석 작업이 수월하지는 않다.

현재 프로젝트 뿐만 아니라 이후 프로젝트에서도 테이블 데이터 게이트웨이를 재사용할 가능성이 높다면 자바빈 객체를 사용하는 것도 나쁘지 않다. 자바빈 객체는 그 자체가 비즈니스 영역의 특정 개체를 나타내기 때문에 범용타입인 RecordSet을 사용할 때보다 소스 코드를 읽기가 더 쉬워진다. 예를 들면 다음과 같이 테이블 데이터 게이트웨이의 메소드에 도메인의 개체를 나타내는 자바빈 객체를 전달한다.

    MemberBean member = new MemberBean();
    MemberBean.setId(id);
    MemberBean.setName(name);
    ...
    gateway = new MemberTableGateway();
    gateway.insert(member);
    ...

예외 처리는 어떻게?

테이블 데이터 게이트웨이 클래스를 작성할 때에는 예외 처리에 대한 것도 미리 정의해 놓아야 한다. 예를 들어, update() 메소드나 delete()를 실행하는 데 변경된 행이 하나도 없을 경우 예외를 발생할 것인가? select() 메소드를 실행할 때 PK로 검색한 행이 존재하지 않을 경우 예외를 발생할 것인가? 이런 문제에 대해서 테이블 데이터 게이트웨이가 어떻게 행동할지에 대해 명확하게 규정짓는 게 좋다.

보통은 다음과 같은 규칙을 적용하면 대부분의 어플리케이션에서 큰 문제없이 사용할 수 있다.

  • insert() 메소드 수행시 이미 같은 PK를 갖는 객체가 존재할 경우 AleadySamePrimaryKeyInserted와 같은 예외를 발생시킨다.
  • update()와 delete() 메소드에서 변경되거나 삭제된 행이 존재하지 않을 경우 NotFoundRecord와 같은 예외를 발생시킨다.
  • select(key) 메소드에서 key로 검색한 행이 존재하지 않을 경우 NotFoundRecord와 같은 예외를 발생시킨다.
이렇게 상황에 따라 알맞은 예외를 발생시키도록 사전에 정의해 놓으면 테이블 데이터 게이트웨이의 사용자들은 보다 명확하게 로직을 구현할 수 있게 된다.

+ Recent posts