Spring Security OAuth2 ํ์ต์ ์ํ ์ํ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๋ฉด์ ์ฌ์ฉํ๊ฒ๋ ๊ฐ ๋ชจ๋์์ ํ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ ์ฉํ๊ธฐ ์ํด์ Spring JDBC๋ฅผ ์ฌ์ฉํ ๋ถ๋ถ์ ๋ํด์ ์ ๋ฆฌํด๋ณด๊ณ ์ ํฉ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ๋ฐ์ดํฐ ์ก์ธ์ค์ ๋ํด์๋ Mybatis ๋๋ JPA ์ด๋ผ๋ ๊ธฐ์ ์ ๋์ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ํ ๋ฐ ์คํ๋ง JDBC ๋ง์ผ๋ก๋ ์ถฉ๋ถํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ก์ธ์ค๊ฐ ๊ฐ๋ฅํ๋ฉฐ Spring Session ์ด๋ Spring Security ์์๋ JDBC ๊ธฐ๋ฐ์ผ๋ก ๊ด๋ จ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ ์์ต๋๋ค.
Data Access with JDBC โ
Spring JDBC๋ ๋ค์ํ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ํ ์ก์ธ์ค ๋ฐฉ๋ฒ์ ์ ๊ณตํ๋ฉฐ ์คํ๋ง ์ธ์ ์ด๋ ์คํ๋ง ์ํ๋ฆฌํฐ์ ํจ๊ป JDBC ๊ธฐ๋ฐ์ผ๋ก ๊ด๋ จ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด์๋ ๋ฐ๋์ Spring JDBC๊ฐ ํฌํจ๋์ด์ผ ํฉ๋๋ค. ์๋ง๋ ๋๋ถ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ํ ์ ๊ทผ์ด ํ์์ ์ด๋ฏ๋ก ๋ค์๊ณผ ๊ฐ์ JDBC ๋ชจ๋์ ๋ฐ๋์ ํฌํจํ๊ณ ์์ ๊ฒ ์ ๋๋ค.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'JdbcTemplate โ
์คํ๋ง ๋ถํธ์์๋ JdbcTemplateAutoConfiguration๋ฅผ ํตํด์ JdbcTemplate์ NamedParameterJdbcTemplate๋ฅผ ์๋์ผ๋ก ๋น์ผ๋ก ๊ตฌ์ฑํ๋ ๊ฒ์ ํ์ธํ ์ ์๋๋ฐ์. JdbcTemplate ๋ฟ๋ง ์๋๋ผ NamedParameterJdbcTemplate๋ฅผ ํจ๊ป ๊ตฌ์ฑํ๋ ์ด์ ๋ Spring Data JDBC์ ๊ฐ์ ๋ชจ๋์์ ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๋๋ก ๋์ด์๊ธฐ ๋๋ฌธ์ด๋ผ๊ณ ์๊ฐ๋ฉ๋๋ค.
JdbcUserDetailsManager โ
์คํ๋ง ์ํ๋ฆฌํฐ์์ JdbcUserDetailsManager๋ JDBC ๊ธฐ๋ฐ์ ์ฌ์ฉ์ ์ธ์ฆ ๊ตฌํ์ ์ํด์ JdbcDaoSupport๋ฅผ ํ์ฅํ๋ฉฐ ๋ด๋ถ์ ์ผ๋ก JdbcTemplate๊ณผ RowMapper๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ์์ฑ๋์ด ์์ต๋๋ค.
JdbcIndexedSessionRepository โ
์คํ๋ง ์ธ์ ์์์ JdbcHttpSessionConfiguration๋ JdbcTemplate๋ฅผ ํตํด์ JdbcIndexedSessionRepository๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๊ฒ ๋ฉ๋๋ค. JdbcIndexedSessionRepository๋ ๋ด๋ถ์ ์ผ๋ก JdbcOperations๋ฅผ ์ฌ์ฉํ์ฌ SQL๋ฅผ ์ํํ๋๋ฐ ์ธ์ ์ ๋ํ ์ ํธ๋ฆฌ๋ทฐํธ๋ฅผ ์ ์ฅํ ๋ JDBC Batch Operations๋ฅผ ํ์ฉํ๋ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.
JdbcTemplate๋ JdbcOperatrions ๊ตฌํ์ฒด์ ๋๋ค.
Stored Function with JDBC โ
ํ์ฌ ์กฐ์ง์์๋ ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ๋๋ Mybatis ๋๋ JPA๋ฅผ ๋์ ํ์ง ์๊ณ ์คํ ์ด๋ ํจ์(ํ๋ก์์ ์ ๋น์ทํ)๋ฅผ ์์ฑํด๋๊ณ ์คํ๋ง JDBC๋ฅผ ํตํด์ ํธ์ถํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๊ณ ์์ต๋๋ค. ๋ ๊ฑฐ์ ์์คํ ์ ๊ฒฝํํ์ง ์์๊ฑฐ๋ Mybatis ๋๋ JPA๋ผ๋ ๊ธฐ์ ๋ง์ ์ ํ ๊ฐ๋ฐ์๋ค์ ๊ถ๊ธํ ์ ์๋ ๋ถ๋ถ์ด๊ธฐ๋ ํ ๊ฒ ๊ฐ์ต๋๋ค. ์ฐ์ ์๋์ ๊ฐ์ ํจ์๊ฐ PostgreSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์๋์ด์๋ค๊ณ ๊ฐ์ ํ๊ฒ ์ต๋๋ค.
CREATE OR REPLACE FUNCTION users$find_by_username(v_username VARCHAR) RETURNS REFCURSOR AS
$$
DECLARE
    rtn_cursor REFCURSOR := 'rtn_cursor';
BEGIN
    OPEN rtn_cursor FOR
        SELECT username, password, enabled from users where username = v_username;
    RETURN rtn_cursor;
END;
$$ LANGUAGE plpgsql;
BEGIN;
select users$find_by_username('user');
FETCH ALL IN "rtn_cursor";
END;StoredProcedure โ
์คํ๋ง JDBC์ GenericStoredProcedure๋ RDBMS์์ ์ง์ํ๋ ์คํ ์ด๋ ํ๋ก์์ ๋ฅผ ํธ์ถํ ์ ์๋๋ก ๊ตฌํ๋ ํด๋์ค์ ๋๋ค. ์๋์ ๊ฐ์ด ์คํ ์ด๋ ํจ์๋ช ์ ์ง์ ํ์ฌ ํ๋ผ๋ฏธํฐ์ ํจ๊ป ์ ๋ฌํ๋ฉด ํ๋ก์์  ํธ์ถ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
@DisplayName("Call stored function using GenericStoredProcedure")
@Test
void testCallFunctionWithStoredProcedure() {
    String functionName = "users$find_by_username";
    jdbcTemplate.setResultsMapCaseInsensitive(true);
    GenericStoredProcedure storedProcedure = new GenericStoredProcedure();
    storedProcedure.setJdbcTemplate(jdbcTemplate);
    storedProcedure.setFunction(true);
    storedProcedure.setSql(functionName);
    storedProcedure.declareParameter(new SqlOutParameter("rtn_cursor", Types.REF_CURSOR, new ColumnMapRowMapper()));
    storedProcedure.declareParameter(new SqlParameter("v_username", Types.VARCHAR));
    Map<String, Object> inParams = new HashMap<>();
    inParams.put("v_username", "user");
    Map<String, Object> results = storedProcedure.execute(inParams);
    if (results.containsKey("rtn_cursor")) {
        Assertions.assertDoesNotThrow(() -> {
            List<Object> cursors = (List<Object>) results.get("rtn_cursor");
            for (Object cursor : cursors) {
                log.info("{}", cursor);
            }
        });
    }
}SimpleJdbcXXXX โ
SimpleJdbcInsert์ SimpleJdbcCall์ JdbcTemplate๋ฅผ ์ฌ์ฉํ์ฌ ๋ช๊ฐ์ง ์ํฉ์ ๋ํด ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ํ๋ฒ์ ๋ง์ ์์ฑ ์์ ์ด ํ์ํ ๊ฒฝ์ฐ์ SimpleJdbcInsert๋ฅผ ์ฌ์ฉํ ์ ์๊ณ ์คํ ์ด๋ ํ๋ก์์ (Stored Procedure) ๋๋ ์คํ ์ด๋ ํจ์(Stored Function)๋ฅผ ํธ์ถํ๊ณ ์ ํ๋ ๊ฒฝ์ฐ์๋ SimpleJdbcCall์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
@DisplayName("Call stored function using SimpleJdbcCall")
@Test
void testCallFunctionWithSimpleJdbcCall() {
    String functionName = "users$find_by_username";
    MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource();
    sqlParameterSource.addValue("v_username", "user");
    Map<String, Object> result = new SimpleJdbcCall(jdbcTemplate)
            .withFunctionName(functionName)
            .withoutProcedureColumnMetaDataAccess()
            .declareParameters(new SqlParameter("v_username", Types.VARCHAR))
            .returningResultSet("rtn_cursor", new ColumnMapRowMapper())
            .execute(sqlParameterSource);
    log.info("{}", result.get("rtn_cursor"));
}GenericStoredProcedure์ ๋น๊ตํด์ ์กฐ๊ธ์ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํจ์ ํ์ธํ ์ ์์ต๋๋ค.
JdbcTemplate โ
์คํ๋ง JDBC์์ ์ ๊ณตํ๋ ํด๋์ค๊ฐ ์๋๋๋ผ๋ ์คํ ์ด๋ ํ๋ก์์ ๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค. JdbcTemplate์์ ์ปค๋ฅ์ ์ ๊ฐ์ ธ์จ ํ prepareCall์ ์ฌ์ฉํด์ ์ง์  ํธ์ถํ ๊ฒฐ๊ณผ๋ฅผ RowMapper๋ก ๋ณํํ ์ ์์ต๋๋ค.
@DisplayName("Call stored function using connection")
@Test
void testCallFunctionWithConnection() {
    String functionName = "users$find_by_username";
    Assertions.assertDoesNotThrow(() -> {
        try (Connection connection = jdbcTemplate.getDataSource().getConnection();
                CallableStatement statement = connection.prepareCall(String.format("{call %s(?)}", functionName))) {
            statement.setString(1, "user");
            ResultSet resultSet = statement.executeQuery();
            while (resultSet.next()) {
                PgResultSet pgResultSet = (PgResultSet) resultSet.getObject(1);
                RowMapperResultSetExtractor<Map<String, Object>> extractor = new RowMapperResultSetExtractor<>(new ColumnMapRowMapper());
                log.info("{}", extractor.extractData(pgResultSet));
            }
        }
    });
}DataClassRowMapper ๋๋ BeanPropertyRowMapper๋ฅผ ์ฌ์ฉํด์ ๋ ๋ฒ์ฉ์ ์ธ ์ฝ๋๋ฅผ ์์ฑํ ์๋ ์์ต๋๋ค.
์ ์ฅ ํ๋ก์์ ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋ํ ์ฅ์ ๋ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ์ฅ ํ๋ก์์ ๋ฅผ ํธ์ถํ ์ ์๋ ๋ฐฉ๋ฒ์ ์๊ณ ์๋ ๊ฒ๋ ์ค์ํฉ๋๋ค. Spring JDBC์ ๋ํด์ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ์๊ฐํ์ง๋ ์์์ง๋ง JPA ๊ธฐ์ ์คํ์์๋ NamedStoredProcedureQuery์ ๊ฐ์ด ํ๋ก์์ ๋ฅผ ํธ์ถํ ์ ์๋๋ก ์ง์ํ๊ณ ์์ต๋๋ค.