챕터 8: DB 연동

binary_j·2023년 11월 13일
0

스프링은 JDBC에서 반복되는 코드들을 JdbcTemplate 클래스로 제공해준다. 이 클래스를 사용하면 중복되는 코드를 효과적으로 줄일 수 있다.

또한 스프링은 트랜잭션 관리 기능을 애노테이션으로 제공한다.(@Transactional) 커밋과 롤백은 스프링이 알아서 처리해준다.

DB 설치 후 테스트용 기본 테이블을 생성하는 과정은 넘어가도록 하겠다.

참고) pom.xml


책이 조금 오래전에 나왔기 때문에 최신 버전과 호환되지 않을 수 있다.
난 의존성을 내 MySQL 버전에 맞게 다음과 같이 설정하였다.

<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>5.1.6.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-jdbc</artifactId>
			<version>8.5.65</version>
		</dependency>
		<dependency>
    		<groupId>com.mysql</groupId>
    		<artifactId>mysql-connector-j</artifactId>
    		<version>8.0.33</version>
		</dependency>

DataSource 설정


스프링이 제공하는 DB 연동 기능은 DataSource를 사용하여 DB Connection을 구한다.

DB 연동에 사용할 DataSource를 스프링 빈으로 등록하고 DB 연동 기능을 구현한 빈 객체는 DataSource를 주입받아 사용한다.

package config;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppCtx {

	@Bean(destroyMethod = "close")
	public DataSource dataSource() {
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
		ds.setUsername("spring5");
		ds.setPassword("spring5");
		ds.setInitialSize(2);
		ds.setMaxActive(10);
		return ds;
	}
	
}

DataSource 객체를 생성하고 MySQL DB 관련 설정을 하는 코드이다.

package dbquery;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.apache.tomcat.jdbc.pool.DataSource;

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) {
				}
		}
	}

}

커넥션을 구하고 종료하는 코드이다.

커넥션을 구할 때는 커넥션 풀에서 커넥션을 가져와서 사용한다. 사용이 완료된 후에는 커넥션을 다시 반환한다. 반환된 커넥션은 유휴 상태로 대기하게 된다.

  • maxActive: 활성 상태가 가능한 최대 커넥션 수
  • maxWait: 이미 커넥션이 최대로 활성화 되어있을 때 새 커넥션을 요청하면 다른 커넥션이 반환될 때까지 대기하는 최대 대기 시간(반환 X면 익셉션 발생)

커넥션 풀을 사용하면 미리 커넥션을 생성해놓고 꺼내와서 쓸 수 있기 때문에 성능적으로 더 유리하다.

일정 시간 이상 쿼리가 실행되지 않으면 커넥션을 끊어버리는 DBMS도 있기 때문에 때때로 지속적으로 커넥션을 테스트 해야되는 경우도 존재한다. 이런 경우 다음과 같은 속성을 사용하면 된다.

  • minEvictableTimeMillis: 최소 유휴 시간
  • timeBetweenEvictionRunsMillis: 검사 주기
  • testWhileIdle: 유휴 커넥션 검사

JDBC Template 생성하기


스프링을 사용하면 JDBC Template을 사용하여 편리하게 쿼리를 실행할 수 있다.

JDBC Template 생성

private JdbcTemplate jdbcTemplate;

	public MemberDao(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

JDBCTemplate 객체 생성.

DataSource를 주입받아 JDBCTemplate 객체를 생성하도록 했다.

JDBC Template을 이용한 조회 쿼리 실행

JDBCTemplate의 query() 메서드는 SELECT 쿼리를 실행한 후 실행 결과를 RowMapper 인터페이스로 자바 객체로 변환해준다.

public Member selectByEmail(String email) {
		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").toLocalDateTime());
						member.setId(rs.getLong("ID"));
						return member;
					}
				}, email);

		return results.isEmpty() ? null : results.get(0);
	}

임의 클래스로 이메일을 조회하는 코드이다.

결과가 한 행밖에 없을 경우에는 List보다는 Integer 같은 타입으로 결과를 받는 것이 간편할 것이다. 이럴 때는 queryForObject()를 사용하면 된다.

public int count() {
		Integer count = jdbcTemplate.queryForObject(
				"select count(*) from MEMBER", Integer.class);
		return count;
	}

queryForObject()는 실행 결과가 0개 거나 두 개 이상이면 익셉션을 발생기키기 때문에 반드시 실행 결과가 한 행이어야 한다.

JDBC Template을 이용한 변경 쿼리 실행

INSERT, UPDATE, DELETE 쿼리는 모두 update()를 사용한다.
update()의 리턴값은 변경된 행의 개수이다.

public void update(Member member) {
		jdbcTemplate.update(
				"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
				member.getName(), member.getPassword(), member.getEmail());
	}

PreparedStatementCreator를 이용한 쿼리 실행

PreparedStatement의 set 메서드를 사용해서 직접 인덱스 파라미터의 값을 설정해야 하는 경우에는 PreparedStatementCreator를 인자로 받는 메서드를 이용해서 직접 PreparedStatement를 생성하고 설정해야 한다.

INSERT 쿼리 실행 시 KeyHolder를 이용해서 자동 생성 키값 구하기

MySQL의 AUTO_INCREMENT 컬럼은 행이 추가되면 자동으로 값이 할당되며 주요키 컬럼에 사용된다.

쿼리 실행 후에 해당 키값을 알고 싶다면 JdbcTemplate의 KeyHolder를 사용할 수 있다.

public void insert(Member member) {
		KeyHolder keyHolder = new GeneratedKeyHolder();
		jdbcTemplate.update(new PreparedStatementCreator() {
			@Override
			public PreparedStatement createPreparedStatement(Connection con)
					throws SQLException {
				// 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
				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()));
				// 생성한 PreparedStatement 객체 리턴
				return pstmt;
			}
		}, keyHolder);
		Number keyValue = keyHolder.getKey();
		member.setId(keyValue.longValue());
	}

update() 메서드는 PreparedStatement를 실행한 후 자동 생성된 키 값을 KeyHolder에 보관한다. 이를 getKey() 메서드를 이용하여 구할 수 있다.

스프링의 익셉션 변환 처리


스프링은 호환성을 위해 SQLException을 그대로 전파하지 않고 변환 처리해서 전달한다. 이렇게 하면 연동 기술에 상관없이 동일하게 익셉션을 처리할 수 있기 때문에 개발할 때 편리하다는 장점이 있다.

트랜잭션 처리


여러 작업을 하나의 작업으로 묶어서 처리해야 하는 경우 트랜잭션을 사용할 수 있다.

@Transactional을 이용한 트랜잭션 처리

트랜잭션 범위 내에서 실행하고 싶은 메서드에 @Transactional 애노테이션만 붙이면 간단하게 트랜잭션 처리를 할 수 있다.

@Transactional 애노테이션을 사용하려면 스프링 설정에 다음 두 가지를 추가해야 한다.

  • 플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈 설정:
    스프링이 제공하는 트랜잭션 매니저 인터페이스, 구현기술에 상관 없이 동일한 방식으로 트랜잭션 처리 가능
  • @Transactional 애노테이션 활성화 설정
@Configuration
@EnableTransactionManagement
@Bean
	public PlatformTransactionManager transactionManager() {
		DataSourceTransactionManager tm = new DataSourceTransactionManager();
		tm.setDataSource(dataSource());
		return tm;
	}

제대로 실행되는지 로그를 확인하기 위해 Logback 기능을 사용해 보도록 하겠다.

<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>2.0.9</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.4.11</version>
		</dependency>

pom.xml을 변경한 후 resources에 logback.xml을 설정해주면 로그 기능을 사용할 수 있다.

트랜잭션과 프록시

트랜잭션도 공통 기능 중 하나이기 때문에 내부적으로 AOP를 사용한다.
@Transactional 애노테이션이 적용되어 있는 경우 스프링은 트랜잭션 기능을 적용한 프록시 객체를 생성한다. 이 프록시 객체를 @Transactional이 붙은 메서드를 호출하면 PlatformTransactionManager를 사용해서 트랜잭션을 시작하고 성공적으로 실행된 경우 트랜잭션을 커밋한다.

@Transactional 적용 메서드의 롤백 처리

별다른 설정이 존재하지 않는 경우 RuntimeException이 발생했을 때 트랜잭션을 롤백한다. SQLException은 RuntimeException을 상속하고 있지 않으므로 이 경우에는 트랜잭션을 롤백하지 않는다. SQLException이 발생했을 때에도 트랜잭션을 롤백하고 싶다면 @Transactional의 rollbackFor 설정을 사용해야 한다.

Transactional(rollbackFor = SQLException.class)

이와 반대의 기능을 제공하는 noRollbackFor 속성도 존재한다.
여러 익셉션 타입을 지정하고 싶다면 배열 형태로 지정하면 된다.

@Transactional의 주요 속성

  • value(String): 트랜잭션을 관리할 때 사용할 PlatformTransactionManager의 빈의 이름 지정. default ""
  • propagation(Propagation): 트랜잭션 전파 타입 지정. default Propagation.REQUIRED
  • isolation(Isolation): 트랜잭션 격리 레벨 지정. default Propagation.DEFAULT
  • timeout(int): 트랜잭션 제한 시간 지정. default -1

@EnableTransactionManagement 애노테이션의 주요 속성

  • proxyTargetClass: 클래스를 이용해서 프록시를 생성할지 여부 지정. default는 false로 인터페이슬르 이용해서 프록시를 생성한다.
  • order: AOP 적용 순서 지정.

트랜잭션 전파

JDBCTemplate은 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 범위 내에서 쿼리를 실행한다.
만약 새로운 트랜잭션이 시작되게 하고싶다면 propagation 속성값이 REQUIRED_NEW가 되게 하면 된다.

0개의 댓글