JDBC는 “DB 연결 → SQL 실행 → 자원 해제”라는 뼈대가 강제되는 구조이다.
문제는 이 뼈대가 DAO 메서드마다 반복되면서 코드가 길어지고 실수 (특히 자원 해제 누락) 가능성이 커진다는 점이다.
그래서 스프링은 반복 뼈대를 프레임워크가 가져가고 개발자는 “변하는 부분 (SQL / 파라미터 / 매핑)”에만 집중하도록 JdbcTemplate을 제공한다.
1) Java JDBC에서 DAO가 매번 반복하는 4단계
DAO에서 DB 작업을 하면 기본적으로 아래 순서가 매번 등장한다.
- 드라이버 로드 (드라이버 메모리 적재)
- DB 연결 (connect)
- 쿼리 수행 (SQL 실행: read/write)
- 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) 왜 이게 더 좋은가
- 반복 뼈대가 사라져 DAO가 짧아진다.
- 자원 정리 / 예외 처리의 일관성이 올라간다.
- DataSource (커넥션 풀)의 개념으로 자연스럽게 확장된다.
- DI 기반이므로 구현 교체 (DAO 교체, DB 교체, 테스트)가 쉬워진다.
- 결과적으로 “DB 접근 코드의 품질”이 프레임워크 표준으로 맞춰진다.
'수업 복습' 카테고리의 다른 글
| JDBC DAO에서 MyBatis로 리팩토링 (0) | 2026.02.09 |
|---|---|
| JDBCTemplate + RowMapper (0) | 2026.01.28 |
| Spring AOP를 어노테이션 (@)으로 전환하는 흐름 정리 (0) | 2026.01.27 |
| 바인드 변수로 핵심관심 정보 뽑아오기 (0) | 2026.01.26 |
| 바인드 변수 (0) | 2026.01.26 |