본문 바로가기
spring/토비의 스프링

토비의 스프링 Vol. 1 정리 - 3장 템플릿 - 1

by Empering 2020. 4. 7.
반응형

토비의 스프링 Vol.1 정리 - 3 - 1

3장 템플릿


개방 폐쇄 원칙 (OCP)은 코드에서 어떤 부분은 변경을 통해 그 기능을 확정하려고 하는 성질이 있고, 어떤 부분은 변하지 않으려는 성질이 있음을 말해준다. 변화의 특성이 다른 부분을 구분해주고 각각의 목적과 이유에 의해 독립적으로 변경될 수 있는 효율적이 구조를 만들어 주는 것이 개방 폐쇄 원칙이다.
템플릿이란 이렇게 바뀌는 성질이 다른 코드 중에서 변경이 일어나지 않으며 특정한 패턴으로 유지되는 특성을 가진 부분을 독립시켜 효과적으로 활용할 수 있도록 하는 방법이다.

<문제의 코드>

package com.empering.springdemo.user.dao;

import com.empering.springdemo.user.domain.User;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDao {
    private DataSource dataSource;

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

    public void add(User user) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();

            // 변하는 부분
            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());
            // 변하는 부분

            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

package com.empering.springdemo.user.dao;

import com.empering.springdemo.user.domain.User;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public abstract class UserDao {
    private DataSource dataSource;

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

    public void add(User user) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();

            ps = this.makeStatement(c, user);

            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) {

                }
            }
        }
    }

	// 변하는 부분을 추상메소드로 정의
    abstract protected PreparedStatement makeStatement(Connection c, User user) throws SQLException;

}

 

- 서브클래스 UserDaoAdd

package com.empering.springdemo.user.dao;

import com.empering.springdemo.user.domain.User;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDaoAdd extends UserDao {
    @Override
    protected PreparedStatement makeStatement(Connection c, User user) 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;
    }
}

UserDao의 기능을 확장 하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있고, 확장때문에 상위 DAO 클래스는 변화가 생기지 않도록 되어 OCP를 어느 정도 만족하는 구조가 되었다.

 

하지만 템플릿 메소드 패턴의 문제로 DAO 로직 마다 상속을 통해 새로운 클래스를 만들어야 한다는 문제점이 발생한다. 예를 들어 add 뿐만아닌, get, update, delete 등 새로운 로직이 필요하다면 4개의 클래스를 만들어야 하는 문제점이 있다.

 

또 슈퍼클래스와 서브클래스의 확장구조가 컴파일 시점에 이미 관계가 결정되어 있다. 따라서 관계에 대한 유연성이 떨어진다.

 

템플릿 메소드 패턴 클래스 다이어그램

 

 


전략 패턴 적용

확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다. 특정한 Context에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 것이다.

 

전략 패턴 클래스 다이어그램

 

문제의 코드에서 변하는 부분인 PreparedStatement를 만들어주는 외부기능이 전략패턴의 전략 부분이다. 이 기능을 인터페이스로 만들어두고 인터페이스의 메소드를 통해 생성전략을 호출한다.

 

- UserDao 전략 패턴 적용

package com.empering.springdemo.user.dao;

import com.empering.springdemo.user.domain.User;

import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDaoStrategy {

    private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }

    public void add(final User user) throws SQLException {
    	// AddStatement 를 익명 내부 클래스로 변경
        this.jdbcContext.workWithStatementStrategy(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;
        });
    }

    public void deleteAll() throws SQLException {
        this.jdbcContext.workWithStatementStrategy(c -> {
            PreparedStatement ps = c.prepareStatement("DELETE FROM USERS");

            return ps;
        });
    }
}

 

- JdbcContext

package com.empering.springdemo.user.dao;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

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) {

                }
            }
        }
    }
}

AddStatement 클래스를 로컬클래스로 add() 메소드에 집어 넣은 것이다. UserDao 와 AddStatement가 강하게 결합되어 있고, UserDao에서 밖에 사용하지 않기 때문에 별도의 클래스에서 합친 것이다. 로컬클래스로 합치는 과정에서 AddStatement에는 user 정보를 받기 위해 생성자와 인스턴스 변수를 제거할 수 있으므로 코드 또한 간결해진다. AddStatement 클래스는 add()에서만 사용되므로 메소드 클래스 이름또한 없앤 익명 내부 클래스로 선언 할 수 있다.

 

하지만 아직 중복이 존재한다. 실제로 익명 내부 클래스에서 sql 문장을 제외한 나머지 부분은 변하지 않는 부분이다. sql 문장만 파라미터로 받고 나머지 부분은 분리 해 별도의 메소드로 만든다.

 

- UserDao

package com.empering.springdemo.user.dao;

import com.empering.springdemo.user.domain.User;

import java.sql.SQLException;

public class UserDaoStrategy {

    private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }

    public void add(final User user) throws SQLException {
        this.jdbcContext.executeSql("INSERT INTO USERS(id, name, password) VALUES(?, ?, ?)", user.getId(), user.getName(), user.getPassword());
    }

    public void deleteAll() throws SQLException {
        this.jdbcContext.executeSql("DELETE FROM USERS");
    }
}

 

- JdbcContext

package com.empering.springdemo.user.dao;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcContext {
    private DataSource dataSource;

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

    public void executeSql(final String query, String... params) throws SQLException {
        workWithStatementStrategy(c -> {
            PreparedStatement ps = c.prepareStatement(query);

            for (int i = 1; i <= params.length; i++) {
                ps.setString(i, params[i]);
            }

            return ps;
        });
    }

    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) {

                }
            }
        }
    }

}

 

 

토비의 스프링 3.1 Vol. 1: 스프링의 이해와 원리, 에이콘출판 토비의 스프링 3.1 Vol. 2: 스프링의 기술과, 에이콘출판 토비의 스프링 3.1 세트:스프링의 이해와 원리 + 스프링의 기술과, 에이콘출판

 

토비의 스프링 3.1 세트:스프링의 이해와 원리 + 스프링의 기술과

COUPANG

www.coupang.com

“파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음"

반응형

댓글