ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Study] 토비의 스프링 2주차 스터디 회고: JDBC 리팩토링과 JdbcTemplate의 발전
    Spring 2025. 11. 5. 16:58

     

    "스프링의 철학은 결국 코드 중복을 줄이고, 변하지 않는 부분과 변하는 부분을 명확히 분리하는 것."이다. 

    이번 스터디에서는 이 철학이 JDBC 리팩토링 과정을 통해 어떻게 코드로 녹아드는지를 살펴보았다.

     

    배경: JDBC 리팩토링

    JDBC를 이용해 DAO를 작성하다 보면 항상 비슷한 코드가 반복된다.

    Connection c = dataSource.getConnection();
    PreparedStatement ps = c.prepareStatement(sql);
    ps.executeUpdate();
    ps.close();
    c.close();

     

    이 구조의 문제점은 명확하다.

    • 반복되는 코드 (Connection, Statement, close())
    • 예외 처리 복잡도 (try/catch/finally 중첩)
    • 유지보수 어려움 (SQL이 바뀌면 모든 DAO 수정)

    토비의 스프링 3장은 바로 이 문제를 출발점으로, 중복을 제거하면서도 유연하게 확장 가능한 구조를 찾아가는 과정을 다룬다.

     


     

     

    템플릿 메서드 패턴 (Template Method)

    공통 흐름은 부모 클래스에 두고, 세부 구현은 자식 클래스가 맡는다.
    abstract class UserDao {
        public void execute() throws SQLException {
            Connection c = dataSource.getConnection();
            PreparedStatement ps = makeStatement(c);
            ps.executeUpdate();
            ps.close();
            c.close();
        }
    
        protected abstract PreparedStatement makeStatement(Connection c) throws SQLException;
    }
    
    class UserDaoDeleteAll extends UserDao {
        protected PreparedStatement makeStatement(Connection c) throws SQLException {
            return c.prepareStatement("delete from users");
        }
    }

     

    장점

    • 중복된 JDBC 코드 제거
    • 공통 로직 재사용 가능

    단점

    • 상속에 의한 강한 결합
    • 새로운 기능마다 하위 클래스 추가 필요 → 확장성 낮음

     

    즉, 확장은 상속이 아닌 조립(Composition)으로 가야 한다는 결론으로 이어진다.

     

     


     

    전략 패턴 (Strategy Pattern)

     

    변할 수 있는 부분을 인터페이스로 추상화하고, 컨텍스트(UserDao)는 그 인터페이스에만 의존한다.

    interface StatementStrategy {
        PreparedStatement makeStatement(Connection c) throws SQLException;
    }
    
    class DeleteAllStatement implements StatementStrategy {
        public PreparedStatement makeStatement(Connection c) throws SQLException {
            return c.prepareStatement("delete from users");
        }
    }
    
    class UserDao {
        private DataSource dataSource;
    
        public void execute(StatementStrategy strategy) throws SQLException {
            try (Connection c = dataSource.getConnection();
                PreparedStatement ps = strategy.makeStatement(c)) {
                ps.executeUpdate();
            }
        }
    }
    dao.execute(new DeleteAllStatement());
     
     
    장점
    • 상속 제거 → 결합도 감소 (OCP 실현)
    • 런타임에 전략 객체 교체 가능 (DI로 주입)
    • UserDao는 SQL이 바뀌어도 수정 불필요

     

    여전히 남은 문제

    • 매번 전략 객체를 새로 만들어야 함
    • try/catch/finally 구조는 여전히 반복

     


     

    템플릿 / 콜백 패턴 (Template / Callback)

     

    템플릿은 변하지 않는 흐름을, 콜백은 변하는 로직을 맡는다.

    public class JdbcContext {
        private DataSource dataSource;
    
        public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
            try (Connection c = dataSource.getConnection();
                PreparedStatement ps = stmt.makeStatement(c)) {
                ps.executeUpdate();
            }
        }
    }
    
    public class UserDao {
        private JdbcContext jdbcContext;
    
        public void deleteAll() throws SQLException {
            jdbcContext.workWithStatementStrategy(
                c -> c.prepareStatement("delete from users")  // 콜백 전달
            );
        }
    }

     

    장점

    • 템플릿 (불변 구조): Connection 관리, 예외 변환, 트랜잭션 동기화
    • 콜백 (가변 부분): SQL이나 매핑 로직만 담당
    • 람다식 덕분에 익명 클래스 없이 깔끔

    즉, 스프링의 JdbcTemplate이 바로 이 구조의 완성체다.

     

    이제 템플릿/콜백 구조가 실제 스프링 코드(JdbcTemplate)에서 어떻게 구현되는지 살펴보자.

     


     

    JdbcTemplate

    JdbcTemplate은 스프링이 제공하는 템플릿/콜백 패턴의 실질적 구현체다.

    // 내부적으로 PreparedStatementCreator 콜백을 자동 생성
    public void deleteAll() {
        this.jdbcTemplate.update("delete from users");
    }

    JdbcTemplate은 다음과 같은 불변 흐름을 관리한다.

    1. DataSource로부터 Connection 획득
    2. PreparedStatement 생성
    3. SQL 실행
    4. 자원 반환
    5. 예외 변환 (SQLException → DataAccessException)

    개발자는 SQL 자체만 작성하면 된다. 즉, 변하지 않는 흐름은 템플릿이 처리하고, 변하는 SQL 로직만 콜백으로 넘긴다.

     
     

    JdbcTemplate 내부의 콜백 구조

    JdbcTemplate은 내부적으로 여러 종류의 콜백 인터페이스를 사용한다.

     

    대표적으로 두 가지가 있다.

    PreparedStatementCreator SQL 준비 단계 (Statement 생성)
    ResultSetExtractor / RowMapper 결과 처리 단계 (ResultSet 매핑)

    예시:

    public int getCount() {
        return this.jdbcTemplate.query(
            con -> con.prepareStatement("select count(*) from users"),
            rs -> {
                rs.next();
                return rs.getInt(1);
            }
        );
    }

     

    첫 번째 콜백(PreparedStatementCreator)로 SQL을 준비한 뒤, SQL 실행 후 두 번째 콜백(ResultSetExtractor)을 호출한다.

     

    즉, 콜백을 통해 SQL 생성과 결과 매핑을 완전히 분리한 구조다. 이게 스프링이 말하는 관심사의 분리 철학이 코드로 구현된 대표적인 예다.

     

     

    람다로 더 간결하게

    토비의 스프링이 쓰이던 시절(자바 6~7)에는 이런 콜백을 모두 익명 내부 클래스로 작성해야 했다.

    jdbcTemplate.update(new PreparedStatementCreator() {
        public PreparedStatement createPreparedStatement(Connection c) throws SQLException {
            return c.prepareStatement("delete from users");
        }
    });

     

     

    하지만 자바 8부터는 함수형 인터페이스(추상 메서드가 1개인 인터페이스)에 람다식을 적용할 수 있게 되면서 코드가 훨씬 간결해졌다.

    jdbcTemplate.update(con -> con.prepareStatement("delete from users"));

    이 람다는 사실상 new PreparedStatementCreator() { } 익명 클래스의 축약 문법이다.

    즉, 람다는 스프링의 콜백 구조를 더 직관적으로 표현할 수 있게 만든 문법적 진화단계이다. 콜백의 개념은 그대로 유지되지만, 표현 방식이 훨씬 깔끔해졌다.

     

    이처럼 스프링의 템플릿/콜백 철학은 자바 8의 함수형 프로그래밍과 자연스럽게 결합되며 발전한다.

     

     


     

     

    PreparedStatement의 내부 동작: Server vs Client

    JdbcTemplate이 내부적으로 PreparedStatement를 생성할 때, 이 문이 실제로 서버에서 준비되는지 또는 클라이언트에서 준비되는지는 JDBC 드라이버 설정에 따라 달라진다.

      ServerPreparedStatement ClientPreparedStatement
    SQL 컴파일 위치 MySQL 서버 클라이언트 (JVM)
    네트워크 요청 횟수 2회 (PREPARE + EXECUTE) 1회 (단일 요청)
    서버 부하 높음 (세션별 캐시) 낮음
    기본 설정 X 기본 설정 값 (useServerPrepStmts=false)

     

    스터디 중 나온 핵심 논의

    서버가 미리 컴파일하면 더 빠를 줄 알았는데, 실제로는 웹 환경에서는 오히려 느릴 수도 있다.

     

    이유는 다음과 같다

    • 대부분의 웹 요청은 단발성 SQL이다. 즉, 동일 세션 내에서 같은 SQL을 반복 실행할 일이 거의 없다.
    • ServerPreparedStatement는 세션 단위 캐시를 사용한다. 커넥션 풀 환경에서는 세션이 자주 바뀌므로 캐시를 재활용할 수 없다.
    • 오히려 네트워크 왕복 + 서버 메모리 오버헤드가 발생한다.

    그래서 다음 설정이 권장된다.

    useServerPrepStmts=false
    cachePrepStmts=true
    prepStmtCacheSize=250
    prepStmtCacheSqlLimit=2048

     

    이 조합은 ClientPreparedStatement + 클라이언트 캐시 전략이다. 서버 부하를 줄이면서도 JVM 내부에서 PreparedStatement를 캐싱해 성능을 높인다.

     

    즉, JdbcTemplate이 내부적으로 어떤 PreparedStatement를 쓰든 그 동작 방식은 드라이버 설정에 의해 결정되는 전략이며,
    스프링 구조는 그대로 유지된다.

     

    이 과정을 통해 ServerPreparedStatement의 캐싱 구조를 이해했고, 실제로는 ClientPreparedStatement가 기본 설정인 이유를 명확히 알 수 있었다. 즉, 스프링의 구조적 철학뿐 아니라, 하위 JDBC 드라이버 레벨에서도 불변의 흐름(템플릿)과 전략의 선택(콜백/옵션)이 동일하게 작동한다는 점이 흥미로웠다.

     

     

     


     

     

     

    JdbcTemplate vs MyBatis vs JPA 

    JdbcTemplate은 SQL을 직접 작성하지만, 중복 코드는 없애는 구조였다면, MyBatis와 JPA는 그 위에서 더 높은 추상화를 제공한다.

      JdbcTemplate MyBatis JPA
    패러다임 SQL 중심 SQL 매퍼 ORM (객체 중심)
    추상화 수준 낮음 중간 높음
    SQL 관리 코드 내부 문자열 XML/어노테이션 자동 생성 (JPQL)
    결과 매핑 RowMapper resultMap 엔티티 매핑
    장점 단순, 빠름, 제어 쉬움 동적 SQL 강력 생산성, 일관성, 도메인 중심
    단점 SQL 반복 XML 관리 복잡 러닝커브, N+1
    스프링 철학적 위치 템플릿/콜백의 원형 SQL 재사용/매핑 확장 데이터 추상화 완성체

    즉,

    • JdbcTemplate은 스프링의 템플릿/콜백 철학을 가장 직접적으로 보여주는 예제이고,
    • MyBatis는 SQL 중심의 확장형,
    • JPA는 객체 중심의 자동화된 템플릿이라고 볼 수 있다.

     

    결국 이 셋은 경쟁 관계가 아니라 데이터 접근의 추상화 수준이 다른 세대의 기술들이다. 스프링은 이 모든 방식을 아우르며, 상황에 따라 가장 적절한 도구를 선택할 수 있도록 설계되었다.

     

     


     

     

    정리

    토비의 스프링 3장은 결국 이 한 문장으로 정리된다.

    불변의 흐름은 템플릿으로, 변하는 로직은 콜백으로

     

    JdbcTemplate의 구조 속에는 스프링이 추구하는 핵심 원칙인 중복 제거, 유연한 확장, 안정된 일관성이 담겨 있다.

    이 철학은 MyBatis, JPA, Spring Data JPA 등 스프링의 모든 데이터 접근 기술에 일관되게 이어지고 있다.

     

     

     

     

     

     

Designed by Tistory.