본문 바로가기

Java/Spring

Spring 5 입문: Chapter 08A. JdbcTemplate을 사용한 DB 연동

더보기

이 프로젝트의 개발 환경

  • 개발 언어 및 개발 환경
    • 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

자바의 JDBC와 스프링의 JdbcTemplate

자바에서 JDBC 프로그래밍을 사용하면 DB 연동에 필요한 Connection을 구한 다음 PreparedStatement를 생성하고, 쿼리 결과로 ResultSet을 처리합니다.

이 코드는 사실상 데이터 처리와는 무관하지만 JDBC 사용을 위해서는 반복해서 작성해야합니다.

Member member;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSets rs = null;
try {
	conn = DriverManager.getConnection("jdbc:mysql://localhost/spring5fs", "spring5", "spring5");
	...
} catch (SQLException e) {
	throw e;
} finally {
	if (null != rs) {
		try {
    		rs.close();
		} catch (SQLException e) {
    		e.printStackTrace();
	    }
    }
    
    if (null != pstmt) {
    	try {
        	pstmt.close();
		} catch (SQLException e) {
    		e.printStackTrace();
        }
    }
    
    if (null != conn) {
    	try {
        	conn.close();
		} catch (SQLException e) {
    		e.printStackTrace();
        }
    }
}

구조적으로 반복되는 코드를 줄이기 위해 템플릿 메소드 패턴전력 패턴을 함께 사용 할 수 있습니다.

스프링이 제공하는 JdbcTemplate에서는 이 두 가지 패턴을 모두 제공합니다.

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.setId(rs.getLong("ID"));
            return member;
		}
	}, email);
return results.isEmpty() ? null : results.get(0);

JDBC에서 필요로했던 SQL을 위한 준비 코드가 상당히 줄어든 것을 볼 수 있습니다.

여기서 자바 8부터 지원하는 람다 구문을 사용하면 추가적으로 코드를 줄일 수 있습니다.

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.setId(rs.getLong("ID"));
		return member;
	}, email);
return results.isEmpty() ? null : results.get(0);

스프링의 JdbcTemplate이 제공하는 또 다른 장점은 트랜잭션 관리가 쉽다는 것입니다.

JDBC를 사용할 때는 트랜잭션 관리를 위해 Connection을 setAutoCommit(false)로 지정하고 commit() 또는 rollback()을 사용해서 트랜잭션을 종료해야합니다.

Member member;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSets rs = null;
try {
	conn = DriverManager.getConnection("jdbc:mysql://localhost/spring5fs", "spring5", "spring5");
	conn.setAutoCommit(false);
	...
	conn.commit();
} catch (SQLException e) {
	throw e;
} finally {
	if (null != rs) {
		try {
    		rs.close();
		} catch (SQLException e) {
    		e.printStackTrace();
	    }
    }
    
    if (null != pstmt) {
    	try {
        	pstmt.close();
		} catch (SQLException e) {
    		e.printStackTrace();
        }
    }
    
    if (null != conn) {
    	try {
			conn.rollback();
		} catch (SQLException e) {
    		e.printStackTrace();
        }
    }
}

반면 스프링에서는 트랜잭션을 적용하고 싶은 함수에서 @Transcational 어노테이션을 등록하면 됩니다.

커밋과 롤백은 스프링에서 자동으로 처리됩니다.

@Transcational
public void insert(Member member)
{

}

예제 프로젝트 작성

sp5-chap08 프로젝트를 생성하고 chap08 패키지를 추가합니다. 이 예제에서는 chap03 패키지의 소스 코드를 복사하여 사용합니다.

build.gradle 파일에서는 spring-jdbc tomcat-jdbc mysql-connect-java 의존성을 추가합니다.

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-jcbc', version: '8.5.27'
    implementation group: 'mysql', name: 'mysql-connector-java', version: '5.1.45'
}
코드 비고
Line 3 spring-jdbc JdbcTemplate 등 JDBC 연동에 필요한 스프링 API를 제공합니다.
Line 4 tomcat-jdbc DB Connection pool을 제공합니다.
Line 5 mysql-connector-java MySQL 연결에 필요한 JDBC 드라이버를 제공합니다.

예제에서는 DBMS로 MySQL을 사용합니다. MySQL 설치와 관련해서는 이 블로그의 문서: Docker에서 MySQL 서버 실행하기를 참고합니다.

DB 테이블 생성

로컬 호스트에서 실행 중인 MySQL 서버에 접속하고 다음 SQL을 모두 실행합니다.

create user 'spring5'@'localhost' identified by 'spring5';
create database spring5fs character set=utf8;
grant all privileges on sspring5fs.* to 'spring5'@'localhost';

use spring5fs;
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;
코드 비고
Line 1 create user localhost에서 접근 가능한 spring5 유저를 생성합니다.
엑세스 비밀번호는 spring5입니다.
Line 2 create database spring5fs 데이터베이스를 생성합니다. 기본 캐릭터셋은 utf8입니다.
Line 3 grant all privileges spring5 유저에게 spring5fs 데이터베이스의 모든 엑세스 권한을 부여합니다.
Line 5:13 create table MEMBER spring5fs 데이터베이스에 MEMBER 테이블을 생성합니다.

Datasource 설정

스프링이 제공하는 DB 연동은 DataSource를 사용해서 DB Connection을 구합니다.

예제에서는 Datasource를 Bean으로 등록하고 DB 연동 기능을 구현한 Bean 객체는 DataSource를 주입 받아서 사용합니다.

chap08 패키지에서 DbConifg 클래스를 생성합니다.

package chap08;

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

@Configuration
public class DbConfig
{
	@Bean(destroyMethod = "close")
	public DataSource dataSource()
	{
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost/spring5ffs?characterEncoding=utf8");
		ds.setUsername("spring5");
		ds.setPassword("spring5");
		ds.setInitialSize(2);
		ds.setMaxActive(10);
		return ds;
	}
}
코드 비고
Line 13 new DataSource() tomcat의 DataSource는 javax.sql.DataSource를 구현합니다.
Line 14 setDriverClassName() MySQL 연동을 위해 MySQL 드라이버 클래스를 사용합니다.

chap08 패키지에서 DbQuery 클래스를 생성합니다.

package chap08;

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 ds;
	
	public DbQuery(DataSource ds)
	{
		this.ds = ds;
	}
	
	public int count()
	{
		Connection conn = null;
		try {
			conn = ds.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 (null != conn) {
				try {
					conn.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
코드 비고
Line 23 conn = ds.getConnection() Connection pool에서 Connection을 구합니다.
Line 34 conn.close() Connection을 Connection pool에 반환합니다.

JdbcTemplate을 사용한 쿼리 실행

스프링의 JdbcTemplate을 사용하면 DataSource Connection Statement ResultSet을 직접 사용하지 않더라도 쿼리를 실행 할 수 있습니다.

chap08 패키지에서 MemberDao 클래스의 함수를 JdbcTemplate으로 구현합니다.

package chap08;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

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

public class MemberDao
{
	private JdbcTemplate jdbcTemplate;
	
	public MemberDao(DataSource ds)
	{
		this.jdbcTemplate = new JdbcTemplate(ds);
	}
	
	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);
	}
	
	public void insert(Member member)
	{
    
	}
	
	public void update(Member member)
	{
    
	}
	
	public List<Member> selectAll()
	{
		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.setId(rs.getLong("ID"));
					return member;
				}
			});
		return results;
	}
}
코드 비고
Line 15:18 MemberDao(DataSource ds) JdbcTemplate을 사용하기 위해 DataSource를 의존 주입합니다.
Line 20:39 selectByEmail() JdbcTemplate을 사용해 쿼리 구문을 코드로 구현합니다.
Line 51:70 selectAll()

DataSource를 의존 주입하기 위해 chap08 패키지에서 AppConfig 클래스를 수정합니다.

package chap08;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(DbConfig.class)
public class AppConfig
{
	@Autowired
	public DataSource dataSource;
	
	@Bean
	public MemberDao memberDao()
	{
		return new MemberDao(dataSource);
	}
}

예제 프로젝트 실행 및 테스트

예제 프로젝트를 실행하고 멤버 추가를 위해 list 명령문을 사용합니다. list를 실행하면 MemberDao의 selectAll() 함수가 실행됩니다.

public List<Member> selectAll()
{
	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.setId(rs.getLong("ID"));
				return member;
			}
		});
	return results;
}

아직 데이터베이스에 멤버를 추가하는 코드는 작성하지 않았으므로 명령문의 실행 결과로 아무것도 표시되지 않습니다.

명령문이 오류 없이 실행된다면 JdbcTemplate이 MySQL에 연결하고 SQL을 실행하고 있는 상태입니다.

정리 및 복습

  • 스프링의 JdbcTemplate을 사용해 MySQL에 연결하고 SQL을 실행합니다.
  • JdbcTemplate에서 MySQL에 연결하기 위해 tomcat의 DataSource를 생성하고 연결 정보를 입력합니다.
  • 스프링에서 트랜잭션을 적용하려면 @Transcational 어노테이션을 등록합니다.