티스토리 뷰

 

 

 

[이전글]

더보기

볶음밥 1장 - 1,2,3: 자바빈, 디자인 패턴(템플릿 메소드, 팩토리 메소드, 전략 패턴), 관심사의 분리, SOLID 및 객체지향 약간

볶음밥 1장 - 4,5,6: 제어의 역전, 프레임워크 vs 라이브러리, 스프링 IoC 및 용어 정리, 싱글톤, 동일성 vs 동등성, 빈의 스코프

볶음밥 1장 - 7: 의존관계 주입(DI), DL, IoC

볶음밥 2장: 테스트, TDD, jUnit

 

 

[3장] 템플릿

객체지향의 핵심 개념인 OCP => 코드 중 어떤 부분은 변경을 통해 기능을 확장하려는 성질이 있고, 또 어떤 부분은 고정되어있고 변하지 않으려는 성질이 있다. 이에 변화의 특성이 다른 부분을 구분하고 각기 다른 시점에 독립적으로 변경될 수 있는 구조를 만들어 주는 것이다. 

템플릿: 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가지는 부분으로 독립시켜서 효과적으로 활용할 수 있도록 하는 방법. ?? 뭔소린지 하나도 모르겟따

=> 변하는 부분과 변하지 않는 부분을 구분하자는 것!

스프링에 적용된 템플릿 기법을 알아보자!

 

 

* JDBC 사용을 마무리하고 나면 꼭 리소스를 반환 해줘야한다. 

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

- DB 풀에서 꺼내간 커넥션은 사용을 마무리 하고 나면 리소스를 반환해서 다시 커넥션 풀에 돌려놔야 한다

- 그런데 오류 등의 상황으로 반환되지 못한 커넥션이 쌓이게 될 경우 커넥션 풀에 여유가 없어지고, 리소스가 부족해지는 상황을 초래할 수 있다. 

- 장시간 운영되는 웹서버의 경우 위와같은 상황은 매우 치명적이다. 따라서 리소스 반환을 잘 해줘야한다

 

예외 처리를 어떻게 해주는 것이 좋을까?

1. try-catch-finally 사용

	try {
            conn = connectionMaker.makeConnection();
            conn.prepareStatement("delete from users");
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

윽.. 보기만 해도 어질어질하다. 너무 복잡하고, 중복 또한 많다

-> 변하는 부분 (.prepareStatement())과 변하지 않는 부분(try-catch-finally) 를 분리하자!!

    public void deleteAll() throws SQLException, ClassNotFoundException {
        Connection conn = null;
        PreparedStatement ps = null;

        try {
            conn = connectionMaker.makeConnection();

            //// 메소드 마다 변하는 부분 ////
            PreparedStatement ps = conn.prepareStatement("delete from users");
            ////

            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

- 변하지 않는 부분을 공용으로 사용하고 변하는 부분만 갈아끼워 사용할 수 있도록 하자!!

-> 어떻게?? =  "전략패턴"을 사용하자

 

[전략 패턴, Strategy Pattern]

자신의 기능 컨텍스트에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔 사용할 수 있도록 하는 디자인 패턴.

- 오브젝트를 아예 둘로 분리하고, 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만든다

 

    public void deleteAll() throws SQLException {
        StatementStrategy st = new DeleteAllStatement(); // 전략 선택
        jdbcContextWithStatementStrategy(st);
    }

    // 공통 메소드 분리!!!
    public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = connectionMaker.makeConnection();
            //// 변하는 부분
            ps = stmt.makePreparedStatement(c);
            ////
            ps.executeUpdate();
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

-> JDBC 작업 흐름이 담긴 부분을 독립적 메소드로 분리 / 클라이언트는 컨텍스트가 사용할 전략을 정해서 전달한다.

근데 책에서 자꾸 컨텍스트 컨텍스트 하는데 컨텍스트가 대체 뭔지 정확하게 개념이 안잡히고 아리까리하다.. 흑  

 

    public void add(final User user) {
         // 로컬 클래스로 통합
         class AddStatement implements 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;
            }
        }
        StatementStrategy st = new AddStatement();
        jdbcContextWithStatementStrategy(st);
    }

- DAO의 메소드마다 전략 구현 클래스를 새로 만들어야 한다는 것은 매우 부담스러운 일이다.

- 따라서 메소드 내에 로컬 클래스를 만들어서 해당 메소드 내에서만 사용할 수 있도록 한다

- 내부 메소드는 자신의 정의된 메소드의 로컬 변수에 직접 접근할 수 있다

- 내부 클래스에서 외부의 변수를 사용할 때에는 외부 변수는 반드시 final으로 설정해 줘야한다.

 

여기서 한발 더 나가면 어차피 한 메소드 내에서만 사용될 것이기 때문에 익명 클래스로 만들어 버릴 수 있다

    public void add(final User user) {
        StatementStrategy st = 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;
            }
        };
        jdbcContextWithStatementStrategy(st);
    }

 

클라이언트: UserDao의 각 메소드

개별 전략: 익명 내부 클래스

컨텍스트: jdbcContextWithStatementStrategy()

컨텍스트 메소드는 클라이언트가 공유해서 사용할 수 있다.

 

public class UserDao {

    private JdbcContext jdbcContext;

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


public class JdbcContext {
    private DataSource dataSource;

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

///////////// 설정파일
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="jdbcContext" class="springbook.user.dao.JdbcContext">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:tcp://localhost/~/study/toby"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>
</beans>

JdbcContext는 스프링 컨테이너에 의해 DataSource 빈을 DI 받는다

-> UserDao는 스프링 컨테이너에 의해 JdbcContext 빈을 DI받는다

 

DI의 정석은 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 동적으로 주입해주는 것이다.

- 인터페이스를 사용하지 않은 DI는 온전한 DI는 아니다.

-> 다만 스프링의 DI는 넓게 보면 객체의 생성과 관계설정에 대한 제어권한을 제3자씨인 스프링 컨테이너에 위임한다는 IoC의 개념을 포함한다. 그렇기에 인터페이스 없이 클래스 레벨에서 의존관계가 고정되었더라도 DI를 위반한다고 보지는 않는다.

 

* 인터페이스를 사용해 클래스를 자유롭게 변경할 수 없는데 굳이 DI 구조로 만든 이유는?

1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리하는 싱글톤 빈이 되기 때문.

2. JdbcContext는 DI를 통해 다른 빈에 의존하고 있기 때문. -> 다른 빈을 DI받기 위해서는 자신도 스프링 빈이어야 한다.

(DI를 위해서는 주입 받는 대상과 주는 대상 모두가 스프링 빈이어야 함)

 

* 그럼 왜 인터페이스를 사용하지 않았나?

- 인터페이스가 없다는 것은 UserDao와 JdbcContext가 매우! 긴밀한 관계에 있고 강하게 결합되어 있다는 것.

-> 클래스는 서로 구분되어 있으나 서로 강하게 결합되어 있는 경우 (OK)

--> 그냥 이유 없이 귀찮아서 클래스를 사용하는 경우 (XXXXXX)

 

jdbcContext와 같이 스프링 빈으로 DI를 받지만 인터페이스를 사용하지는 않고 클래스 레벨에서 강하게 결합된 경우

-> jdbcContext에 대한 제어권을 의존관계에 있는 클래스(예에서는 UserDao)에 대신 맡겨버릴 수 있다

public class UserDao {

    private DataSource dataSource;
    private JdbcContext jdbcContext;

    public void setDataSource(DataSource dataSource) {
        this.jdbcContext = new JdbcContext();
        jdbcContext.setDataSource(dataSource);

        this.dataSource = dataSource;
    }
    ...
}

- 스프링 빈인 UserDao가 대신 스프링 컨테이너에게 DI 받고, JdbcContext에 대해 대신 수동 DI 해주는 그림

 

 

방법 1 - 인터페이스를 사용하지 않는 클래스와의 의존관계지만 스프링의 DI를 이용해 빈으로 등록해 사용하자

장점: 스프링 DI를 사용하기 때문에 오브젝트 간의 의존관계가 설정파일 만으로도 명확하게 드러남

단점: DI의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다.

 

방법 2 - 인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 오브젝트를 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 DI도 받게 하자

장점: 의존관계를 외부에 드러내지 않는다

단점: JdbcContext를 싱글톤으로 만들 수 없고 DI를 위한 추가 작업이 필요하다.

 

-> 명확한 이유가 없으면 "1번"을 하거나, 인터페이스를 만들어서 평범한 DI 구조를 갖게 하자.

 

 

[3.5] 템플릿과 콜백

복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고, 그 중 일부분만 자주 바꿔서 사용해야 하는 경우

-> 전략 패턴의 기본 구조 + 익명 내부 클래스 활용 == "템플릿/콜백 패턴"

 

[템플릿]: 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀. 

[콜백]: 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용.

- 자바는 메소드 자체를 파라미터로 전달할 수 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. (functional object)

 

===> 고정된 작업 흐름을 가진 코드를 재사용 하자!! (템플릿 안에서 콜백 메소드를 실행하게 하자)

 

* 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다.

* 템플릿 하나에 하나 이상의 콜백을 사용할 수 있고, 하나의 콜백을 여러번 호출하는 것도 가능하다.

 

사실 이 내용은 글로 정리하는것 보다 코드로 보는게 훨신 이해가 빨라서 포스팅은 빠르게 스킵..

 

[콜백]

public interface LineCallback<T> {
    T doSomethingWithLine(String line, T value);
}

[템플릿]

 public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
        BufferedReader br = null; 
        try {
             br = new BufferedReader(new FileReader(filepath));
             T res = initVal;
             String line = null;
             while ((line = br.readLine()) != null) {
                 res = callback.doSomethingWithLine(line, res);
             }
             return res; 
        } catch (IOException e) {...}
        finally {...}
 }

[클라이언트]

public Integer calcSum(String filepath) throws IOException {
    LineCallback<Integer> sumCallback = new LineCallback<>() {
        @Override
        public Integer doSomethingWithLine(String line, Integer value) {
            return value + Integer.valueOf(line);
        }
    };
    return lineReadTemplate(filepath, sumCallback, 0);
}

 

 

[결론]

1. JDBC와 같이 예외가 발생할 가능성이 있으면서 공유 리소스의 반환이 필요한 코드의 경우는 try-catch-finally로 공유 리소스 반환을 보장 해줘야한다

2. 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면 클래스를 분리하고 공유해서 사용하도록 만들자

3. 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부클래스를 사용해 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 부른다

4. 템플릿과 콜백의 리턴 타입이 다양하다면 제네릭스를 활용하자

5. 네거티브 테스트를 만드는게 더더더 중요하다

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함