이 프로젝트의 개발 환경
- 개발 언어 및 개발 환경
- OpenJDK 12
- Spring: spring-context 5.0.2.RELEASE
- Spring: spring-jdbc: 5.0.2.RELEASE
- Tomcat: tomcat-jdbc: 8.5.27
- MySQL: mysql-connector-java: 8.0.22
- Gradle 7.3
- 기타 환경
- macOS Sonoma 14.1.1
- IntelliJ IDEA 2020.3 Ultimate Edition
- MySQL 8.2.0
예제에 앞서 이 블로그의 문서: Chapter 08A. JdbcTemplate을 사용한 DB 연동을 선행합니다.
JdbcTemplate을 사용한 Update 쿼리
Insert Update Delete 쿼리는 update() 메소드를 사용합니다.
이전 예제의 sp5-chap08 프로젝트 chap08 패키지에서 MemberDao 클래스를 수정합니다.
public class MemberDao
{
public void update(Member member)
{
jdbcTemplate.update("update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(),
member.getPassword(),
member.getEmail());
}
}
PreparedStatementCreator를 사용한 쿼리 실행
지금까지 예제에서 작성한 코드는 쿼리 실행을 위한 인자를 함수의 인자로 전달했습니다.
jdbcTemplate.update("update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(),
member.getPassword(),
member.getEmail());
PreparedStatement를 사용하면 쿼리 실행을 위한 인자를 대신 전달 할 수 있습니다.
PreparedStatement를 사용하려면 PreparedStatemenCreator인터페이스를 람다로 구현 할 수 있습니다.
chap08 패키지의 MemberDao 클래스를 수정합니다.
public class MemberDao
{
public void insert(Member member)
{
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con)
throws SQLException
{
PreparedStatement pstmt = con.prepareStatement("insert into MEMBER(EMAIL, PASSWORD, NAME, REGDATE) values (?, ?, ?, ?)");
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
return pstmt;
}
});
}
}
코드 | 비고 | |
PreparedStatement는 update() 메소드와 query() 메소드에서 사용 할 수 있습니다.
@Override
public int update(PreparedStatementCreator psc) throws DataAccessException { ... }
@Override
public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder) { ... }
@Override
public <T> List<T> query(PreparedStatementCreator psc, RowMapper<T> rowMapper) throws DataAccessException { ... }
위 메소드에서 KeyHolder는 자동 생성되는 키 값을 구할 때 사용합니다.
KeyHolder를 사용한 Insert 쿼리 작성
MySQL의 AUTO_INCREMENT 컬럼은 행이 추가되면 자동으로 값이 할당됩니다.
create table MEMBER(
ID int auto_increment primary key,
EMAIL varchar(255),
PASSWORD varchar(100),
NAME varchar(100),
REGDATE datetime,
unique key(EMAIL)
) engine=InnoDB character set=utf8;
따라서 코드에서 Insert 쿼리를 실행할 때는 MEMBER.ID 인자로 AUTO_INCREMENT 컬럼의 값을 할당하지 않습니다.
이때 쿼리 실행 결과로는 ID 컬럼의 값을 구할 수 없습니다. update() 메소드는 변경된 행의 개수를 리턴하기 때문입니다.
JdbcTemplate에서 자동으로 생성된 키 값을 구하려면 KeyHolder를 사용해야 합니다.
public class MemberDao
{
public void insert(Member member)
{
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con)
throws SQLException
{
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());
}
}
코드 | 비고 | |
이 클래스는 자동 생성된 키 값을 구하는 |
||
예제 프로젝트를 실행합니다.
앱에서 사용 가능한 명령문에 따라서 MySQL에 데이터가 생성되고 업데이트 되는 과정을 확인합니다.
스프링의 트랜잭션 처리
어떤 작업은 SQL이 연속으로 실행되어야 합니다. SQL이 연속 실행되는 작업은 하나의 트랜잭션(Transaction)으로 관리해야합니다.
만약 하나의 SQL이라도 실패하면 모든 SQL을 실패로 간주하고 처음 상태로 되돌리는 롤백(Rollback)을 수행합니다.
반면 모든 SQL이 성공하면 DB에 실제로 반영되는 커밋(Commit)을 수행합니다.
Connection conn = null;
try {
conn = ds.getConnection();
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select count(*) from MEMBER"))
{
rs.next();
return rs.getInt(1);
}
conn.commit();
} catch (SQLException e) {
if (null != conn)
try { conn.rollback();} catch (SQLException ew) { }
throw new RuntimeException(e);
} finally {
if (null != conn) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
코드 | 비고 | |
트랜잭션을 위해 |
||
SQL이 성공하면 |
||
SQL이 실패하면 |
스프링에서는 트랜잭션 처리를 위한 @Transactional 어노테이션을 제공합니다. @Transactional 어노테이션을 메소드에 등록해 트랜잭션 범위를 지정합니다.
chap08 패키지의 ChangePasswordService 클래스를 다음과 같이 수정합니다.
@Transactional
public void changePassword(String email,
String oldPassword,
String newPassword)
{
Member member = memberDao.selectByEmail(email);
if (null == member)
throw new RuntimeException();
member.changePassword(oldPassword, newPassword);
memberDao.update(member);
}
코드 | 비고 | |
메소드를 |
||
메소드의 두 쿼리가 |
||
@Transactional 어노테이션이 제대로 동작하려면 플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈을 등록하고 @Transactional 어노테이션을 활성화합니다.
@Configuration
@EnableTransactionManagement
public class DbConfig
{
@Bean(destroyMethod = "close")
public DataSource dataSource()
{
...
}
@Bean
public PlatformTransactionManager transactionManager()
{
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
}
코드 | 비고 | |
설정 클래스의 |
||
스프링에서 제공하는 트랜잭선 매니저 인터페이스입니다. JDBC 환경에서는 |
스프링의 트랜잭션 테스트
트랜잭션이 제대로 동작하는지 확인하기 위해 JDBC 관련 실행을 로그로 출력합니다.
스프링 5 버전은 자체 로깅 모듈인 spring-jcl을 사용합니다. spring-jcl은 직접 로그를 남기는대신 다른 로깅 모듈을 사용해 로그를 기록합니다.
예를 들어 클래스 패스에 Logback이 존재하면 Logback을 사용하고 Log4j2가 존재하면 Log4j2를 사용하는 방식입니다.
따라서 사용할 로깅 모듈을 클래스 패스에 추가해야 합니다.
build.gradle 파일에서 다음 의존성을 추가합니다.
dependencies {
implementation group: 'org.springframework', name: 'spring-context', version: '5.0.2.RELEASE'
implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.0.2.RELEASE'
implementation group: 'org.apache.tomcat', name: 'tomcat-jdbc', version: '8.5.27'
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.22'
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>
코드 | 비고 | |
JDBC 관련 모듈에서 출력하는 로그 메시지를 |
애플리케이션을 실행하고 트랜잭션으로 수행되는 비밀번호 변경 명령문을 사용합니다.
정상적으로 비밀번호를 변경합니다. 그리고 출력되는 콘솔 로그를 살펴봅니다.
2023-12-18 18:12:18,191 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]] for JDBC transaction
2023-12-18 18:12:18,194 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]] to manual commit
2023-12-18 18:12:18,214 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL query
2023-12-18 18:12:18,214 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement [select * from MEMBER where EMAIL = ?]
2023-12-18 18:12:18,251 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL update
2023-12-18 18:12:18,251 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement [update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?]
2023-12-18 18:12:18,253 DEBUG o.s.j.c.JdbcTemplate - SQL update affected 1 rows
2023-12-18 18:12:18,255 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction commit
2023-12-18 18:12:18,255 DEBUG o.s.j.d.DataSourceTransactionManager - Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]]
2023-12-18 18:12:18,263 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]] after transaction
2023-12-18 18:12:18,263 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
코드 | 비고 | |
이번에는 비밀번호를 잘못 입력합니다. 그리고 출력되는 콘솔 로그를 살펴봅니다.
2023-12-18 18:32:32,874 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]] for JDBC transaction
2023-12-18 18:32:32,877 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]] to manual commit
2023-12-18 18:32:32,891 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL query
2023-12-18 18:32:32,891 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement [select * from MEMBER where EMAIL = ?]
2023-12-18 18:32:32,934 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction rollback
2023-12-18 18:32:32,935 DEBUG o.s.j.d.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]]
2023-12-18 18:32:32,939 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@4bd2f0dc]]] after transaction
2023-12-18 18:32:32,939 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
코드 | 비고 | |
@Transcation과 프록시
스프링의 트랜잭션은 AOP(Aspect Oriented Programming)를 통해 처리됩니다. 다시 말해 스프링의 트랜잭션은 AOP 관점에서의 공통 기능에 해당합니다.
즉 AOP에 의해 트랜잭션을 수행하는 것은 프록시(Proxy)입니다.
스프링에서 @EnableTranscationManagement를 사용하면 @Transcation 어노테이션이 등록된 빈 객체를 찾아서 트랜잭션을 위한 프록시 객체를 생성합니다.
프록시 객체는 @Transactional 어노테이션이 등록된 메소드를 호출하면 PlatformTransactionManager를 사용해 트랜잭션을 시작합니다.
트랜잭션이 시작되면 실제 객체의 메소드를 호출하고, 메소드가 성공하면 트랜잭션을 커밋합니다.
롤백의 경우에도 마찬가지로 트랜잭션은 프록시 객체에 의해서 수행됩니다. 비밀번호 변경 코드를 살펴보면 RuntimeException을 발생시킵니다.
public void changePassword(String oldPassword,
String newPassword)
{
if (!this.password.equals(oldPassword))
throw new RuntimeException();
this.password = newPassword;
}
트랜잭션을 위한 프록시 객체는 원본 객체의 메소드에서 RuntimeException이 발생하면 트랜잭션을 롤백합니다.
별도 설정을 하지 않으면 RuntimeException에 의해서만 트랜잭션이 롤백됩니다.
JdbcTemplate 역시 DB 연동 과정에 문제가 있으면 DataAccessException을 발생시키는데 이 역시 RuntimeException을 상속받기 때문에 트랜잭션은 롤백됩니다.
반면 SQLException은 RuntimeException을 상속받지 않기 때문에 트랜잭션을 롤백하지 않습니다. 이 경우에도 트랜잭션을 롤백하려면 rollbackFor 속성을 사용합니다.
@Transactional(rollbackFor = SQLException.class)
public void changePassword(String email,
String oldPassword,
String newPassword)
{
Member member = memberDao.selectByEmail(email);
if (null == member)
throw new RuntimeException();
member.changePassword(oldPassword, newPassword);
memberDao.update(member);
}
이렇게 지정된 메소드는 RuntimeException을 포함해 다른 예외에 대해서도 트랜잭션을 롤백하도록 할 수 있습니다.
반대로 특정 예외에 대해서 롤백시키지 않고 커밋할려면 noRollbackFor 속성을 사용합니다.
그 외 @Transcation 어노테이션의 주요 속성은 다음과 같습니다.
속성 | 타입 | 비고 |
트랜잭션을 관리할 때 사용할 |
||
트랜잭션 |
||
트랜잭션 |
||
트랜잭션 |
정리 및 복습
- JdbcTemplate에서
InsertUpdateDelete쿼리는update()메소드를 사용합니다. - MySQL의
AUTO_INCREMENT컬럼은 행이 추가되면 자동으로 값이 할당됩니다. - JdbcTemplate에서 자동으로 생성된 키 값을 구하려면
KeyHolder를 사용합니다. - 스프링에서 트랜잭션을 사용하려면 메소드에
@Transactional어노테이션을 등록합니다. @Transactional을 사용하려면플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈을 등록하고@Transactional 어노테이션을 활성화합니다.- 스프링의 트랜잭션은
AOP(Aspect Oriented Programming)의프록시(Proxy)객체로써 동작합니다. - 트랜잭션을 위한 프록시 객체는 원본 메소드를 대신 호출하고 이 과정에서
RuntimeException이 발생하면롤백합니다. - 별도의 예외를 사용해 트랜잭션을 롤백하려면
rollbackFor속성을 사용합니다. - 반대로
특정 예외에 대해서 롤백시키지 않고 커밋할려면 noRollbackFor 속성을 사용합니다.