토비의 스프링 [3.4장 - 3장 마무리] 스터디

43604 단어 toby-springtoby-spring

3.4. 컨텍스트와 DI

3.4.1. JdbcContext의 분리

전략패턴 구조상 UserDao의 메소드는 클라이언트 개념으로 구성되어진다.
UserDao메소드 내에 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고, jdbcContextWithStatementStrategy()메소드는 컨텍스트이다.

문제점

- 컨텍스트 메소드는 UserDao내에서만 동작할 수 있다.
- 다른 DAO에서도 범용성이 있는 로직 흐름이므로 분리할 필요가 있다.

클래스 분리

JdbcContext Class

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 = this.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) {} }
        }
    }
}

JdbcContext를 DI 받아서 사용하도록 만든 UserDao

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

	public void add(final User user) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
			() -> {...}
		);
	}

	public void deleteAll() throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
			() -> {...}
		);
	}
}

빈 의존관계 변경

UserDao가 JdbcContext에 의존하고 있는 상태이다.
그런데 JdbcContext는 인터페이스인 DataSource와는 달리 구체 클래스다.
스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔 사용하도록 하는게 목적인데 이 일반적인 관계와 다르게 작동되고 있다.
이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을뿐이고, 구현 방법이 바뀔 가능성이 없으므로 예외적으로 인터페이스를 구현하는 방식으로 만들지 않았다.

스프링의 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의된다.

3.4.2. JdbcContext의 특별한 DI

스프링 빈으로 DI

의존관계 주입이라는 개념을 충실히 따르자면, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이나믹하게 주입해주는 것이 맞지만 스프링의 DI는 넓게 보자면 객체의 생성과 관계 설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄하므로 JdbcContext를 스프링을 이용해 DI의 기본을 따르고 있다고 봐도 무방하다.

인터페이스를 사용하지 않는 구조로 JdbcContext를 UserDao와 DI 구조로 만들어야한 이유
- JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되도록 하기 위해서 이다.
- JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다.
JdbcContext는 dataSource 프로퍼티를 통해 DataSource 오브젝트를 주입 받도록 되어 있다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다.

코드를 이용하는 수동 DI

JdbcContext를 스프링 빈으로 등록해서 UserDao에 DI하는 대신 사용할 수 있는 방법으로 UserDao 내부에서 직접 DI를 적용하는 방법이 있다.
대신 JdbcContext를 싱글톤으로 만들려는 것을 포기해야 한다.

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

		this.jdbcContext.setDataSource(dataSource);
	}
}

3.5. 템플릿과 콜백

복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그 중 일부만 자주 바꿔 사용해야 하는 경우 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라 부른다.

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

콜백
실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다.
파라미터로 전달되지만 값을 참조하는 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다.
자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 펑셔널 오브젝트(functional object)라고도 한다.

3.5.1. 템플릿/콜백의 동작원리

템플릿/콜백의 특징

템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출 되는 것이 일반적이므로 템플릿/콜백 패턴의 콜백은 단일 메소드 인터페이스를 사용한다.

- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.
- 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
- 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.

3.5.2. 편리한 콜백의 재활용

콜백의 분리와 재활용

변하지 않는 부분을 분리시킨 deleteAll() 메소드

public void deleteAll() throws SQLException {
	executeSql("delete from users");
}

public void executeSql(final String query) throws SQLException {
	this.jdbcContext.workWithStatementStrategy(
		new StatementStrategy() {
			public PreparedStatement makePreparedStatement(Connection c) throws SQLException  { return c.prepareStatement(query); }
	);
}

콜백과 템플릿의 결합

JdbcContext로 옮긴 executeSql() 메소드

public class JdbcContext {
	...
	public void executeSql(final String query) throws SQLException {
		workWithStatementStrategy(
			new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); }
		}
	);
}

JdbcContext로 옮긴 executeSql()을 사용하는 deleteAll() 메소드

public void deleteAll() throws SQLException {
	this.jdbcContext.executeSql("delete from users");
}

3.5.3. 템플릿/콜백의 응용

템플릿/콜백 개념에 대해 조금 더 학습하기 위해 간단한 예제를 통해 알아보았다.

요구사항
- 파일을 열어 모든 라인의 숫자를 더한 합을 돌려주는 프로그램 작성

numbers.txt

1
2
3
4

계산기 컨텍스트

public class Calculator {
    ...
    public Integer calcLineSum(String filepath) throws IOException {
        LineCallback<Integer> sumCallback =
                (line, value) -> {
                    return value + Integer.parseInt(line);
                };

        return lineReadTemplate(filepath, sumCallback, 0);
    }

    public Integer calcLineMultiply(String filepath) throws IOException {
        LineCallback<Integer> multiplyCallback =
                (line, value) -> {
                    return value * Integer.parseInt(line);
                };

        return lineReadTemplate(filepath, multiplyCallback, 1);
    }

    public String concatenate(String filepath) throws IOException {
        LineCallback<String> concatenateCallback =
                (line, value) -> {
                    return value + line;
                };

        return lineReadTemplate(filepath, concatenateCallback, "");
    }

    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) { throw e; }
        finally {
            if (br != null) {
                try { br.close(); }
                catch (IOException e) { System.out.println(e.getMessage()); }
            }
        }
    }
}

CallbackStrategy

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

public interface BufferedReaderCallback {
    Integer doSomethingWithReader(BufferedReader br) throws IOException;
}

테스트 코드

import static org.hamcrest.MatcherAssert.*;

import org.hamcrest.CoreMatchers;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class CalcSumTest {bfggbv
    Calculator calculator;
    String numFilepath;

    @BeforeEach
   public void setUp() {
        this.calculator = new Calculator();
        this.numFilepath = getClass().getResource("/numbers.txt").getPath();
    }
    @Test
    public void sumOfNumbers() throws IOException {
        Calculator calculator = new Calculator();
        int sum = calculator.calcSum(this.numFilepath);

        assertThat(sum, Is.is(10));

        assertThat(calculator.calcSum(this.numFilepath), CoreMatchers.is(10));

        assertThat(calculator.calcMultiply(this.numFilepath), CoreMatchers.is(24));
    }
}

3.6. 스프링의 JdbcTemplate

JdbcTemplate란?

스프링이 제공하는 JDBC 코드용 기본 템플릿이다.
JDBC를 이용하는 DAO에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.

3.6.1. update()

insert/update/delete에 사용되는 템플릿 메소드이다.

deleteAll() 메소드

public void deleteAll() {
    this.jdbcTemplate.update("delete from users");
}

add() 메소드

public void add(final User user) throws SQLException {
    this.jdbcTemplate.update("insert into user(id, name, password) values(?, ?, ?)"
            , user.getId(), user.getName(), user.getPassword());
}

3.6.2. queryForObject()

public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
    }

query() 템플릿 이용하여 getAll() 구현

private RowMapper<User> userRowMapper = (rs, rowNum) -> {
    User user = new User();

    user.setId(rs.getString(1));
    user.setName(rs.getString(2));
    user.setPassword(rs.getString(3));
    return user;
};


public List<User> getAll() {
    return this.jdbcTemplate.query("select * from users", userRowMapper);
}

좋은 웹페이지 즐겨찾기