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์™€ ๊ฐ™์ด ํ”„๋กœ์‹œ์ €๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ