JDBC란?
💡 JDBC: DB에 접근할수 있도록 Java에서 제공하는 API (Java Database Connectivity)
Spring JDBC
💡Spring JDBC: JDBC의 단점을 보완하여 더 편리한 기능을 제공
- Connection 열기와 닫기
- Statement 준비와 닫기
- Statement 실행
- ResultSet Loop처리
- Exception 처리와 반환
- Transaction 처리
JDBC 프로그래밍의 단점을 보완하는 스프링
구조적인 반복을 줄이기 위한 방법은 템플릿 메서드 패턴과 전략 패턴을 함께 사용하는 것이다. 스프링은 바로 이 두 패턴을 엮은 JdbcTemplate 클래스를 제공한다.
💡 JDBC Template:
Spring JDBC접근 방법중 하나로, 내부적으로 Plain JDBC API를 사용하지만 위와같은 문제점을 해결한 Spring에서 제공하는 클래스
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs , int rowNum) throws SQLException {
Member member = new Member(rs.getString("EMAIL"),
rs.getString("PASSWORD") ,
rs.getString("NAME"),
rs.getTimestamp("REGDATE"));
member.setld(rs.getlong ( 11 I D")) ;
return member;
}
} ,
email);
return results.isEmpty() ? null : results.get(O);
//람다 사용
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
(ResultSet rs , int rowNum) - > {
Member member = new Member(rs.getString("EMAIL"),
rs.getString("PASSWORD") ,
rs.getString("NAME"),
rs.getTimestamp("REGDATE"));
member.setld(rs.getlong ( 11 I D")) ;
return member;
} ,
email);
return results.isEmpty() ? null : results.get(O);
스프링이 제공하는 또 다른 장점은 트랜잭션 관리가 쉽다는 것이다. JDBC API로 트랜잭션을 처리하려면 다음과 같이 Connection의 setAutoCornrnit(false) 을 이용해서 자동 커밋을 비활성화하고 commit()과 rollback() 메서드를 이용해서 트랜잭션을 커밋하거나 롤백해야 한다.
conn = DriverManager.getConnection(
"jd bc : mysql : //local host/spring4fs?characterEncod ing=utf8" ,
"spring4", "spring4");
conn .setAutoCommit(false) ;
... (DB 쿼리 실행)
conn.commit();
스프링을 사용하면 트랜잭션을 적용하고 싶은 메서드에 @Transactional 애노테이션을 붙이기만 하면 된다.
@Transactional // 커밋과 롤백 처리는 스프링이 알아서 처리한다.
public void insert(Member member) {
}
프로젝트 준비하기
의존 모듈 추가
implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.0.2.RELEASE' // jdbc 연동에 필요한 기능 제공
implementation group: 'org.apache.tomcat', name: 'tomcat-jdbc', version: '9.0.70' //db 커넥션풀 기능 제공
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.26' // jdbc 드라이버 제공
- spring-jdbc : JdbcTemplate 등 JDBC 연동에 필요한 기능을 제공
- tomcat-jdbc : DB 커넥션풀 기능을 제공
- mysql- connector-java : MySOL 연결에 필요한 JDBC 드라이버를 제공
DataSource 설정
JDBC API는 DriverManager 외에 DataSource를 이용해서 DB 연결을 구하는 방법을 정의하고 있다.
Connection conn = null ;
try {
// dataSource 는 생성자나 설정 메서드를 이용해서 주입받음
conn = dataSource.getConnection();
스프링이 제공하는 DB 연동 기능은 DataSource를사용해서 DB Connection을 구한다. DB 연동에 사용할 DataSource를 스프링 빈으로 등록하고 DB 연동 기능을 구현한 빈 객체는 DataSource를 주입받아 사용한다.
@Configuration
@EnableTransactionManagement
//@Transactional 애노테이션이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능 활성화 PlatformTransactionManager 빈을 사용해서 트랜잭션을 적용
public class AppCtx {
@Bean(destroyMethod = "close") // 커넥션 풀에 보관된 connection을 닫는다.
public DataSource dataSource() {
DataSource ds = new DataSource(); // 객체 생성
ds.setDriverClassName("com.mysql.jdbc.Driver"); // jdbc 드라이버 클래스 지정, mysql 드라이버 클래스를 사용한다.
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8"); // jdbc url을 지정한다. 캐릭터셋을 UTF-8로 설정했으므로 지정행하낟.
ds.setUsername("spring5");//db연결할때 사용할 사용자 계정과 암호 지정
ds.setPassword("spring5");
ds.setInitialSize(2); // 최초에 생성할 커넥션 개수
ds.setMaxActive(10); // 커넥션 풀에서 가져올 수 있는 커넥션 max개수
// maxWait() -> 모든 커넥션이 활성상태일때 다른 커넥션이 반환될 때까지 대기하는 시간, 대기 시간 내에 반환된 커넥션이 없으면 익센셥
ds.setTestWhileIdle(true); // 유휴 커넥션 검사
ds.setMinEvictableIdleTimeMillis(60000 * 3); // 최소 유휴 시간 3분
ds.setTimeBetweenEvictionRunsMillis(10 * 1000); // 10초주기로 검사
return ds;
}
Tomcat JDBC 모듈의 org.apache.tomcatjdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스이다. DataSource 클래스는 커넥션을 몇 개 만들지 지정할 수 있는 메서드를 제공한다.
커넥션 풀은 커넥션을 생성하고 유지한다. 커넥션 풀에 커넥션을 요청하면 해당 커넥션은 활성 (active) 상태가 되고, 커넥션을 다시 커넥션 풀에 반환하면 유휴(idle) 상태가 된다. DataSource#getConnection()을 실행하면 커넥션 풀에서 커넥션을 가져와 커넥션이 활성 상태가 된다. 반대로 커넥션을 종료(close) 하면 커넥션은 풀로 돌아가 유휴 상태가 된다.
package org.example.chap08.dbQuery;
import org.apache.tomcat.jdbc.pool.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DbQuery {
private DataSource dataSource;
public DbQuery(DataSource dataSource) {
this.dataSource = dataSource;
}
public int count() {
Connection conn = null;
try {
conn = dataSource.getConnection(); //커넥션 풀에서 구함
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select count(*) from MEMBER")) {
rs.next();
return rs.getInt(1);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (conn != null)
try {
conn.close();
// 커넥션 풀에 반환
} catch (SQLException e) {
}
}
}
}
JdbcTemplate을 이용한 쿼리 실행
스프링을 사용하면 DataSource나 Connection, Statemement, ResultSet을 직접 사용하지 않고 JdbcTemplate을 이용해서 편리하게 쿼리를 실행할 수 있다.
JdbcTemplate 생성
public class MemberDao {
private JdbcTemplate jdbcTemplate;
// 생성자 의존 주입
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
.
.
.
public class AppCtx {
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
JdbcTemplate을 이용한 조회 쿼리 실행
Select 쿼리 실행을 위한 query()메서드 제공
- List<T> query(String sql , RowMapper<T> rowMapper)
- List<T> query(String sql , Object[] args , RowMapper<T> rowMapper)
- List<T> query(String sql , RowMapper<T> rowMapper , Object . . . args)
RowMapper를 이용해서 ResultSet의 결과를 자바 객체로 변환한다.
public class MemberDao {
.
.
.
public Member selectByEmail(String email) {
// 람다
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
new MemberRowMapper(), email); // ?에 들어갈 값 = email
return results.isEmpty() ? null : results.get(0);
}
MemberRowMampper 생성자 메서드는 코드의 중복을 막기 위함이다.
public class MemberDao {
private JdbcTemplate jdbcTemplate;
...생략
public List<Member> selectAII() {
List<Member> results = jdbcTemplate.query("select * from MEMBER",
//오버라이딩을 위해 이쪽 코드가 계속 중복됨
new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum)
throws SQLException {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME") ,
rs.getTimestamp("REGDATE").tolocalDateTime());
member.setld(rs.getlong("I D")) ;
return member;
}
////////////////////////////////////////
});
return results;
}
}
.
.
.
//RowMapper 구현 클래스를 만들어 코드 중복 막기
public class MemberRowMapper implements RowMapper<Member> {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("EMAIL") ,
rs.getString("PASSWORD") ,
rs.getString("NAME") ,
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
}
결과가 1행인 경우 사용할 수 있는 queryForObject() 메서드
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public int count() {
Integer count = jdbcTemplate.queryForObject( // 실행 결과가 1개일 경우 사용할 수 있는 메서드
"select count(*) from MEMBER", Integer.class);
return count;
}
}
쿼리 결과가 한 행이어야 한다. 쿼리 실행 결과 행이 없거나 두 개 이상이면 익센셥이 발생한다.
JdbcTemplate을 이용한 변경 쿼리 실행
INSERT, UPDATE, DELETE 쿼리는 update() 메서드를 사용한다.
public void update(Member member) {
jdbcTemplate.update(
"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(), member.getPassword(), member.getEmail());
}
PreparedStatementCreator를 이용한 쿼리 실행
lNSERT 쿼리 실행 시 KeyHolder를 이용해서 자동 생성 키값 구하기
public void insert(Member member) {
KeyHolder keyHolder = new GeneratedKeyHolder(); // GeneratedKeyHolder 객체를 생성한다. 이 클래스는 자동 생성된 키값을 구해주는 KeyHolder 구현 클래스이다.
jdbcTemplate.update((Connection con)-> {// update() 메서드는 PreparedStatementCreator 객체와 KeyHolder 객체를 파라미터로갖는다.
PreparedStatement pstmt = con.prepareStatement(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
"values (?, ?, ?, ?)",
new String[]{"ID"}); // 두번째 파라미터는 자동 생성되는 키 칼럼 목록을 지정할 때 사용한다.
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4,
Timestamp.valueOf(member.getRegisterDateTime()));
return pstmt;
}, keyHolder);
Number keyValue = keyHolder.getKey();
member.setId(keyValue.longValue());
}
트랜잭션 처리
📌 두 개 이상의 쿼리를 한 작업으로 실행해야 할 때 사용하는 것이 트랜잭션이다. 트랜잭션은 여러 쿼리를 논리적으로 하나의 작업으로 묶어준다. 트랜잭션으로 묶인 쿼리 중 하나라도 실패하면 롤백하고, 반면 모든 쿼리가 성공하면 커밋한다.
JDBC는 Connection의 setAutoCommit(false)를 이용해서 트랜잭션을 시작하고 commit()과 rollback()을 이용해서 트랜잭션을 반영 (커밋)하거나 취소(롤백)한다.
@Transactional 을 이용한 트랜잭션 처리
public class ChangePasswordService {
private MemberDao memberDao;
// 두 개 이상의 쿼리문을 하나의 작업으로 간주하여 쿼리문이 하나라도 실패하면 롤백함
// 트랜잭션 범위를 지정한다.
@Transactional
public void changePassword(String email, String oldPwd, String newPwd) {
Member member = memberDao.selectByEmail(email); // 쿼리가 트랜잭션에 묶인다.
if (member == null)
throw new MemberNotFoundException();
member.changePassword(oldPwd, newPwd); // 쿼리가 트랜잭션에 묶인다.
memberDao.update(member);
}
@Transactional 애노테이션이 제대로 동작하려면 다음의 두 가지 내용을 스프링 설정에 추가해야한다.
- 플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈 설정
- @Transactional 애노테이션 활성화 설정
@EnableTransactionManagement
//@Transactional 애노테이션이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능 활성화 PlatformTransactionManager 빈을 사용해서 트랜잭션을 적용
public class AppCtx {
@Bean
public PlatformTransactionManager transactionManager() { // PlatformTransactionManager: 스프링이 제공하는 트랜잭션 매니저 인터페이스
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
로그 메시지
트랜잭션과 관련 로그 메시지를 추가로 출력하기 위해 Logback를 사용해보자.
// build.gradle에 추가
implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
// src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %c{2} - %m%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="stdout"/>
</root>
<logger name="org.springframework.jdbc" level="DEBUG"/>
</configuration>
'Spring > 스프링5 프로그래밍 입문' 카테고리의 다른 글
Chapter12. MVC 2: 메시지, 커맨드 객체 검증 (0) | 2023.04.07 |
---|---|
Chapter 10. 스프링 MVC 프레임워크 동작 방식 (0) | 2023.04.03 |
Chapter7. AOP 프로그래밍 (0) | 2023.02.22 |
Chapter6. 빈 라이프사이클과 범위 (0) | 2023.02.21 |
Chapter5. 컴포넌트 스캔 (0) | 2023.02.21 |