수업 복습

스프링에서의 JDBC 정리

_김영인 2026. 1. 28. 12:18

 

JDBC는 “DB 연결 → SQL 실행 → 자원 해제”라는 뼈대가 강제되는 구조이다.

문제는 이 뼈대가 DAO 메서드마다 반복되면서 코드가 길어지고 실수 (특히 자원 해제 누락) 가능성이 커진다는 점이다.

그래서 스프링은 반복 뼈대를 프레임워크가 가져가고 개발자는 “변하는 부분 (SQL / 파라미터 / 매핑)”에만 집중하도록 JdbcTemplate을 제공한다.


 

1) Java JDBC에서 DAO가 매번 반복하는 4단계

 

DAO에서 DB 작업을 하면 기본적으로 아래 순서가 매번 등장한다.

  1. 드라이버 로드 (드라이버 메모리 적재)
  2. DB 연결 (connect)
  3. 쿼리 수행 (SQL 실행: read/write)
  4. DB 연결 해제 (disconnect)

이 4단계 자체는 정석이다.

하지만 메서드가 늘어날수록 (1)(2)(4)가 계속 복붙된다.

 

이 반복이 유지보수를 어렵게 만들고 실무에서는 커넥션 누수 같은 장애로도 이어진다.


 

2) 반복되는 로직 (1,2,4)을 ‘모듈화 (Util)’하는 이유

 

반복되는 연결 / 해제 로직을 JDBCUtil 같은 Util로 빼내는 것은 “공통 로직을 모듈화”하는 작업이다.

  • 공통 로직을 한 군데로 모아 응집도 (관련된 코드가 함께 모여 있는 정도)를 높인다.
  • DAO 코드에서 “DB 연결 / 해제”라는 복잡한 뼈대를 숨기고 “SQL 실행”에 집중하게 만든다.
  • 반복이 아니더라도 너무 복잡해서 숨기고 싶은 로직 (예외 처리, 자원 정리 등)도 Util로 분리하는 경우가 많다.

여기까지는 “자바 JDBC 문법과 OOP 관점”에서 정리 가능한 영역이다.

 

직접 객체 설계 / 모듈화로 해결하는 방식이다.


 

3) 공통 로직 / 횡단 관심 / 어드바이스 (Advice) 감각 잡기

 

연결 / 해제 / 로깅처럼 여러 메서드에 걸쳐 반복되는 처리는 흔히 “횡단 관심사 (cross-cutting concern)”라고 부른다.

  • Util로 빼는 방식: 개발자가 직접 호출해서 공통을 재사용한다.
  • AOP의 Advice 방식: 공통 로직을 메서드 실행 시점에 프레임워크가 자동으로 끼워 넣는다 (weaving)

“공통 로직을 다룬다”는 관점은 같지만 Advice는 “자동 삽입 (프레임워크 레벨)”이라는 점에서 더 발전된 방식이다.


 

4) 템플릿 패턴의 출발점이 JDBC인 이유

 

JDBC 작업은 본질적으로 “변하지 않는 뼈대 + 변하는 부분”으로 나뉜다.

  • 변하지 않는 뼈대: 연결 / 자원정리 / 예외처리 / 실행 흐름
  • 변하는 부분: SQL, 파라미터 바인딩, ResultSet → DTO 매핑

이 구조는 템플릿 패턴 (Template Method Pattern)과 매우 잘 맞는다.

그래서 JDBC 영역은 템플릿 패턴이 가장 많이 활용되는 영역 중 하나이다.

 

스프링 JdbcTemplate은 이름 그대로 이 반복 뼈대를 템플릿으로 제공하는 대표 구현이다.


 

5) Java JDBC 방식: JDBCUtil + BoardDAO

(1) JDBCUtil.java

package com.example.biz.common;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JDBCUtil {
	private static final String driverName = "com.mysql.cj.jdbc.Driver";
	private static final String url = "jdbc:mysql://localhost:3306/teemo";
	private static final String user = "root";
	private static final String password = "1234";
	
	public static Connection connect() {
		Connection conn = null;
		try {
			System.out.println("[로그] JDBC URL = " + url);
			Class.forName(driverName);
			conn = DriverManager.getConnection(url, user, password);
		} catch (Exception e) {
			e.printStackTrace();
		}		
		return conn;
	}
	
	public static void disconnect(Connection conn,PreparedStatement pstmt) {
		try {
			pstmt.close();
			conn.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

 

여기서 핵심은 “DAO가 직접 connect/disconnect를 하지 않게” 공통 로직을 Util로 빼냈다는 점이다.

하지만 DAO에서는 여전히 PreparedStatement, executeQuery/executeUpdate, disconnect() 호출이 반복된다.

 

(2) BoardDAO.java

 
package com.example.biz.board.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import com.example.biz.board.BoardDTO;
import com.example.biz.common.JDBCUtil;

public class BoardDAO {
    private static final String SELECT_ALL = "SELECT * FROM BOARD ORDER BY BID DESC";
    private static final String SELECT_ONE = "SELECT * FROM BOARD WHERE BID=?";

    private static final String INSERT = "INSERT INTO BOARD(TITLE,WRITER,CONTENT) VALUES(?,?,?)";
    private static final String UPDATE = "UPDATE BOARD SET CNT=CNT+1 WHERE BID=?";
    private static final String DELETE = "DELETE FROM BOARD WHERE BID=?";

    public boolean insertBoard(BoardDTO dto) {
        Connection conn = JDBCUtil.connect();
        PreparedStatement pstmt = null;

        try {
            pstmt = conn.prepareStatement(INSERT);
            pstmt.setString(1, dto.getTitle());
            pstmt.setString(2, dto.getWriter());
            pstmt.setString(3, dto.getContent());
            int rs = pstmt.executeUpdate();
            if(rs <= 0) {
                return false;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        JDBCUtil.disconnect(conn, pstmt);

        return true;
    }

    public boolean updateBoard(BoardDTO dto) {
        Connection conn = JDBCUtil.connect();
        PreparedStatement pstmt = null;

        try {
            pstmt = conn.prepareStatement(UPDATE);
            pstmt.setInt(1, dto.getBid());
            int rs = pstmt.executeUpdate();
            if(rs <= 0) {
                return false;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        JDBCUtil.disconnect(conn, pstmt);

        return true;
    }

    public boolean deleteBoard(BoardDTO dto) {
        Connection conn = JDBCUtil.connect();
        PreparedStatement pstmt = null;

        try {
            pstmt = conn.prepareStatement(DELETE);
            pstmt.setInt(1, dto.getBid());
            int rs = pstmt.executeUpdate();
            if(rs <= 0) {
                return false;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        JDBCUtil.disconnect(conn, pstmt);

        return true;
    }

    public BoardDTO getBoard(BoardDTO dto) {
        BoardDTO data = null;

        Connection conn = JDBCUtil.connect();
        PreparedStatement pstmt = null;

        try {
            pstmt = conn.prepareStatement(SELECT_ONE);
            pstmt.setInt(1, dto.getBid());
            ResultSet rs = pstmt.executeQuery();
            if(rs.next()) {
                data = new BoardDTO();
                data.setBid(rs.getInt("BID"));
                data.setTitle(rs.getString("TITLE"));
                data.setWriter(rs.getString("WRITER"));
                data.setContent(rs.getString("CONTENT"));
                data.setRegdate(rs.getDate("REGDATE"));
                data.setCnt(rs.getInt("CNT"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        JDBCUtil.disconnect(conn, pstmt);

        return data;
    }

    public List<BoardDTO> getBoardList(BoardDTO dto){
        List<BoardDTO> datas = new ArrayList<BoardDTO>();

        Connection conn = JDBCUtil.connect();
        PreparedStatement pstmt = null;

        try {
            pstmt = conn.prepareStatement(SELECT_ALL);
            ResultSet rs = pstmt.executeQuery();
            while(rs.next()) {
                BoardDTO data = new BoardDTO();
                data.setBid(rs.getInt("BID"));
                data.setTitle(rs.getString("TITLE"));
                data.setWriter(rs.getString("WRITER"));
                data.setContent(rs.getString("CONTENT"));
                data.setRegdate(rs.getDate("REGDATE"));
                data.setCnt(rs.getInt("CNT"));

                datas.add(data);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        JDBCUtil.disconnect(conn, pstmt);

        return datas;
    }
}

여기서 “insert가 true/false로 끝나는 구조”가 왜 자주 쓰이나

executeUpdate() 결과는 “영향받은 row 수”이다.

  • 0 이하이면 실패로 보고 false
  • 0 초과이면 성공으로 보고 true

이 “결과값으로 성공 여부를 판단하는 패턴”이 이후 JdbcTemplate.update()로 넘어가도 그대로 유지된다. (JdbcTemplate.update()도 row 수(int)를 반환한다.)


 

6) 스프링 JdbcTemplate로 넘어가면 무엇이 달라지나 

 

DAO를 “아이폰”이라고 보면 기존에는 아이폰이 직접 DB 작업 (연결 / 실행 / 해제)을 다 했다.
JdbcTemplate을 쓰면 “애플워치 (= JdbcTemplate)”에게 SQL 실행을 맡기고 DAO는 결과만 받는다.

  • DAO는 JdbcTemplate.update (sql, params...)에 SQL과 파라미터를 던진다.
  • update()는 “영향받은 row 수”를 반환한다.
  • DAO는 이 값을 보고 true/false로 성공을 판단한다.

 

7) (의존성) pom.xml에 필요한 라이브러리 넣기

 

JdbcTemplate을 쓰려면 관련 라이브러리가 필요하다. 업로드된 pom.xml에 이미 포함되어 있다.

  • spring-boot-starter-jdbc : 스프링 JDBC 모듈 (= JdbcTemplate 포함)
  • mysql-connector-j : MySQL 드라이버
  • commons-dbcp:1.4 : BasicDataSource (DBCP) 사용을 위한 라이브러리

정확한 의미 정리
pom.xml은 “DI를 한다”가 아니라 의존성 (라이브러리)을 가져오는 설정이다.
DI는 다음 단계 (스프링 컨테이너가 Bean을 주입)에서 발생한다.


 

8) 왜 @Autowired만 붙였는데 안 되었나?

 

@Autowired는 “스프링 컨테이너가 관리하는 Bean”을 주입한다.

스프링 레퍼런스는 @Autowired가 Bean 구성 / 주입 규칙에 따라 동작한다고 명시한다.

 

  • PlusBoardDAO가 컴포넌트 스캔 대상 (예: @Repository)으로 Bean이 되어야 하고
  • JdbcTemplate도 컨테이너에 Bean으로 등록되어 있어야 한다.

그런데 JdbcTemplate은 스프링이 제공하는 클래스라 코드에 직접 @Component를 붙일 수 없다.
그래서 XML (또는 Java Config @Bean)로 Bean 등록을 해줘야 한다.


 

9) applicationContext.xml에서 DataSource + JdbcTemplate Bean 등록하기

 

applicationContext.xml에 아래 설정이 들어 있다.

  • BasicDataSource에
    • driverClassName / url / username / password
  • JdbcTemplate에
    • dataSource 주입

BasicDataSource가 어떤 프로퍼티를 받는지는 Apache Commons DBCP 공식 문서에 명확히 정리되어 있다.

 
<bean class="org.apache.commons.dbcp.BasicDataSource" id="dataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/teemo"/>
    <property name="username" value="root"/>
    <property name="password" value="1234"/>
</bean>

<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

 

이 구성이 완성되면 흐름은 아래처럼 정리된다.

 

Client → Service → DAO → JdbcTemplate → DataSource (드라이버 / URL / 계정 / 비번) → DB


 

10) PlusBoardDAO: JdbcTemplate로 “실행 뼈대”를 위임

 

(1) 핵심: update는 int (row 수)를 반환 → true / false로 변환

 

JdbcTemplate.update()는 JDBC update / insert / delete를 수행하고 “영향받은 row 수”를 반환한다.
이 반환값을 이용해 기존 JDBC에서 하던 것처럼 성공 / 실패를 boolean으로 판단하는 구조가 그대로 유지된다.


(2) 핵심: select는 RowMapper로 매핑

RowMapper는 ResultSet의 각 row를 객체로 매핑하기 위한 인터페이스이고 JdbcTemplate이 이를 이용해 결과를 객체 / 리스트로 만든다.

package com.example.biz.board.impl;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import com.example.biz.board.BoardDTO;

@Repository
public class PlusBoardDAO {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    private static final String SELECT_ALL = "SELECT * FROM BOARD ORDER BY BID DESC";
    private static final String SELECT_ONE = "SELECT * FROM BOARD WHERE BID=?";

    private static final String INSERT = "INSERT INTO BOARD(TITLE,WRITER,CONTENT) VALUES(?,?,?)";
    private static final String UPDATE = "UPDATE BOARD SET CNT=CNT+1 WHERE BID=?";
    private static final String DELETE = "DELETE FROM BOARD WHERE BID=?";

    public boolean insertBoard(BoardDTO dto) {
        if(jdbcTemplate.update(INSERT, dto.getTitle(), dto.getWriter(), dto.getContent()) <= 0) {
            return false;
        }
        return true;
    }

    public boolean updateBoard(BoardDTO dto) {
        if(jdbcTemplate.update(UPDATE, dto.getBid()) <= 0) {
            return false;
        }
        return true;
    }

    public boolean deleteBoard(BoardDTO dto) {
        if(jdbcTemplate.update(DELETE, dto.getBid()) <= 0) {
            return false;
        }
        return true;
    }

    public BoardDTO getBoard(BoardDTO dto) {
        return jdbcTemplate.queryForObject(SELECT_ONE, new BoardRowMapper(), dto.getBid());
    }

    public List<BoardDTO> getBoardList(BoardDTO dto){
        return jdbcTemplate.query(SELECT_ALL, new BoardRowMapper());
    }
}

class BoardRowMapper implements RowMapper<BoardDTO> {

    @Override
    public BoardDTO mapRow(ResultSet rs, int rowNum) throws SQLException {
        BoardDTO data = new BoardDTO();
        data.setBid(rs.getInt("BID"));
        data.setTitle(rs.getString("TITLE"));
        data.setWriter(rs.getString("WRITER"));
        data.setContent(rs.getString("CONTENT"));
        data.setRegdate(rs.getDate("REGDATE"));
        data.setCnt(rs.getInt("CNT"));
        return data;
    }
}

 

정리하면

  • DAO에서 Connection / PreparedStatement / ResultSet / close 뼈대가 사라지고
  • SQL과 매핑만 남는다.
    이게 “JdbcTemplate = JDBC 템플릿 패턴 구현체”라는 말이 실감 나는 지점이다.

 

11) ServiceImpl에서 DAO 교체: BoardDAO → PlusBoardDAO

 

DAO 구현이 바뀌었으니 Service가 사용하는 DAO도 바뀐다.

업로드된 BoardServiceImpl.java는 이미 PlusBoardDAO로 주입받도록 수정되어 있다.

 
package com.example.biz.board.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.biz.board.BoardDTO;
import com.example.biz.board.BoardService;

@Service("bs")
public class BoardServiceImpl implements BoardService {
    @Autowired
    private PlusBoardDAO boardDAO;

    @Override
    public boolean insertBoard(BoardDTO dto) {
        return boardDAO.insertBoard(dto);
    }

    @Override
    public boolean updateBoard(BoardDTO dto) {
        return boardDAO.updateBoard(dto);
    }

    @Override
    public boolean deleteBoard(BoardDTO dto) {
        return boardDAO.deleteBoard(dto);
    }

    @Override
    public BoardDTO getBoard(BoardDTO dto) {
        return boardDAO.getBoard(dto);
    }

    @Override
    public List<BoardDTO> getBoardList(BoardDTO dto) {
        return boardDAO.getBoardList(dto);
    }
}

 

Service 입장에서는 “DAO가 내부에서 JDBC로 하든, JdbcTemplate로 하든” 인터페이스가 유지되는 한 크게 달라질 게 없다.

DI로 주입받는 구조이기 때문에 이 교체가 자연스럽게 가능해진다.


12) 최종 흐름 정리

수업 흐름을 그대로 한 줄로 정리하면 아래 구조이다.

 

Client → Service → DAO → JdbcTemplate → DataSource → JDBC 정보 4종 (driver / url / user / password) → DB

  • 기존: DAO가 connect/disconnect 뼈대를 직접 수행
  • 변경: DAO는 JdbcTemplate에게 SQL 실행을 맡기고 결과 (row 수 or 객체)를 받음

 

13) 왜 이게 더 좋은가

  1. 반복 뼈대가 사라져 DAO가 짧아진다.
  2. 자원 정리 / 예외 처리의 일관성이 올라간다.
  3. DataSource (커넥션 풀)의 개념으로 자연스럽게 확장된다.
  4. DI 기반이므로 구현 교체 (DAO 교체, DB 교체, 테스트)가 쉬워진다.
  5. 결과적으로 “DB 접근 코드의 품질”이 프레임워크 표준으로 맞춰진다.