본문 바로가기
Java/[스프링 5 프로그래밍 입문]

[Spring] DB 연동(2) - JdbcTemplate과 쿼리

2021. 9. 13.

이전 포스트에서는 스프링에서 JDBC를 이용해 DB 연동을 처리하는 방법을 알아보았다. 이번에는 JdbcTemplate 클래스를 이용해 스프링에서 편리하게 쿼리를 실행하는 방법을 알아볼 것이다. 

 

JdbcTemplate을 이용한 쿼리 실행

스프링에서는 DataSource나 Connection, Statement, ResultSet을 직접 사용하지 않고 JdbcTemplate을 이용해서  편하게 쿼리를 실행할 수 있다. DB와 상호작용할 클래스에서 JdbcTemplate 객체를 생성해서 사용한다. 또한 해당 클래스를 설정에서 빈으로 등록한다.

public class MemberDao {
	private JdbcTemplate jdbcTemplate;
	public MemberDao(DataSource dataSource) {
	// DataSource를 생성자에 전달하여 JdbcTemplate 객체를 생성
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}
}
@Configuration
public class AppCtx {
	...
	
	@Bean
	public MemberDao memberDao() {
		return new MemberDao(dataSource());
	}
}

 

조회 (SELECT) 쿼리 실행

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)

sql 파라미터로 전달받은 쿼리를 실행하고 RowMapper를 이용해서 ResultSet의 결과를 자바 객체로 변환한다. sql이 인덱스 기반 파라미터를 가진 쿼리라면 args 파라미터를 이용해서 각 인덱스의 값을 지정한다.

 

RowMapper 인터페이스의 mapRow() 메서드는 SQL 실행 결과로 구한 ResultSet에서 한 행의 데이터를 읽어와 자바 객체로 변환하는 매퍼 기능을 구현한다. RowMapper 인터페이스를 구현한 클래스를 작성할 수도 있지만 임의 클래스나 람다식으로 RowMapper 객체를 생성해서 query() 메서드에 전달할 수도 있다.

public class MemberDao {
	
	private JdbcTemplate jdbcTemplate;
	
	public MemberDao(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}
	
	public Member selectByEmail(String email) {
		List<Member> results = jdbcTemplate.query(
				"select * from MEMBER where EMAIL = ?", 
				// 1. 임의 클래스를 이용해서 RowMapper 객체 전달
				// ResultSet에서 데이터를 읽어와 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;
//					}
//				},
				
				// 2. 람다 이용
//				(ResultSet rs, int rowNum)->
//				{
//					Member member = new Member(
//							rs.getString("EMAIL"),
//							rs.getString("PASSWORD"),
//							rs.getString("NAME"),
//							rs.getTimestamp("REGDATE").toLocalDateTime()
//							);
//					member.setId(rs.getLong("ID"));
//					return member;
//				},
				
				// 3. 직접 구현한 클래스 이용 (재사용성 증가)
				new MemberRowMapper(),
				
				// sql 쿼리에 입력될 파라미터
				email);
		
		// query() 메서드는 실행한 결과가 존재하지 않으면 길이가 0인 List를 리턴함
		return results.isEmpty()? null: results.get(0);
	}

	public List<Member> selectAll(){
		List<Member> results = jdbcTemplate.query("select * from MEMBER", new MemberRowMapper());
		return results;
	}
	
	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번 코드에서는 임의 클래스를 이용해서 RowMapper의 객체를 전달한다. 이 RowMapper는 ResultSet에서 데이터를 읽어와 Member 객체로 변환해주므로 Member를 RowMapper의 타입 파라미터로 사용했다. mapRow() 메서드는 전달받은 ResultSet에서 데이터를 읽어와서 Member 객체를 생성해서 리턴하도록 구현했다.

 

2번 코드는 람다를 사용해서 임의 클래스를 사용하는 것보다 간결하게 구현했다.

 

3번 코드는 아래 구현한 MemberRowMapper 클래스 객체를 생성하여 selectAll() 메서드와의 코드 중복을 막을 수 있었다.

 

query() 메서드는 실행 결과가 존재하지 않으면 길이가 0인 List를 리턴한다. 따라서 결과로 반환된 리스트의 길이를 통해 쿼리 실행 결과가 존재하는 경우와 존재하지 않는 경우를 구분해서 처리할 수 있다.

 

결과가 딱 1행인 경우 사용할 수 있는 queryForObject() 메서드

public int count() {
	// queryForObject() 메서드는 쿼리 실행 결과 행이 한 개인 경우에 사용할 수 있음
	// 실행 결과 칼럼이 두개 이상이면 query() 메서드를 쓸 때와 마찬가지로 RowMapper를 사용하면 됨
	Integer count = jdbcTemplate.queryForObject("select count(*) from MEMBER", Integer.class);
	return count;
}

count(*) 쿼리는 결과가 한 행뿐이기 때문에 결과를 List보다는 Integer와 같은 정수 타입으로 받으면 편리할 것이다. queryForObject() 메서드를 이용하면 위 코드처럼 구현할 수 있다. queryForObject() 메서드는 쿼리 실행 결과 행이 한 개인 경우에 사용할 수 있는 메서드이다. 실행 결과 칼럼이 두 개 이상이면 아래 코드처럼 RowMapper를 파라미터로 전달해서 결과를 생성할 수 있다.

Member member = jdbcTemplate.queryForObject(
	"select * from MEMBER where ID = ?",
	new RowMapper<Member>() {
		@Override
		public Member mapRow(ResultSet rs, int rowNum) throws SQLException{
			Member member = new Member( ... );
			return member;
		}
	},
100);

위 코드에서는 query() 메서드와는 달리 리턴 타입이 List가 아니라 RowMapper로 변환해주는 타입(Member)이다. queryForObject() 메서드를 사용하려면 쿼리 실행 결과가 0개도, 2개 이상도 아닌 오직 1행이어야 한다. 그렇지 않으면 익셉션이 발생하므로 query() 메서드를 사용해야 한다.

 

변경 쿼리 실행 (INSERT, UPDATE, DELETE)

데이터를 변경하는 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를 이용한 쿼리 실행

지금까지는 아래와 같이 쿼리에서 사용할 값을 인자로 전달했다. 인덱스 파라미터에 순서대로 값을 전달한다.

jdbcTemplate.update("update MEMBER set NAME = ?, PASSWORD = ? where EAMIL = ?",
	"wjy", "1234", "wjy@gmail.com");

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

 

PreparedStatementCreator 인터페이스의 createPreparedStatement() 메서드는 Connection 타입의 파라미터를 받아 PreparedStatement 객체를 생성한다. PreparedStatementCreator를 구현한 클래스는 createPreparedStatement() 메서드의 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 객체를 생성하고, 인덱스 파라미터를 알맞게 설정한 뒤 리턴하면 된다.

public void insert(final Member member) {
	KeyHolder keyHolder = new GeneratedKeyHolder();
	jdbcTemplate.update((Connection con) -> {
			PreparedStatement preparedStatement = con.prepareStatement(
					"insert into MEMBER(EMAIL, PASSWORD, NAME, REGDATE) values (?, ?, ?, ?)",
					// KeyHolder를 이용해서 Auto-increment된 컬럼 값 구하기
					new String[] {"ID"});
			preparedStatement.setString(1, member.getEmail());
			preparedStatement.setString(2, member.getPassword());
			preparedStatement.setString(3, member.getName());
			preparedStatement.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
			return preparedStatement;
		}
	// KeyHolder를 이용해서 Auto-increment된 컬럼 값 구하기
	, keyHolder);
	Number keyValue = keyHolder.getKey();
	member.setId(keyValue.longValue());
}

 

KeyHolder를 이용해서 INSERT시 Auto Increment 키값 구하기

MariaDB(MySQL)에서 AUTO_INCREMENT 칼럼은 행이 추가되면 자동으로 값이 할당되는 칼럼으로 주로 primary key 칼럼에 사용된다. 앞선 예시에서 MEMBER 테이블은 ID 칼럼을 AUTO_INCREMENT 칼럼으로 지정했다.

 

자동 증가 칼럼을 가진 테이블에 INSERT를 할 때는 해당 칼럼에 대한 값을 지정하지 않는다. 그런데 update() 메서드는 변경된 행의 개수를 리턴할 뿐, 쿼리 실행 후 생성된 키값을 알려주지 않는다. 이때 사용되는 것이 KeyHolder이다.

 

위의 insert() 메서드 코드에서도 KeyHolder를 사용하여 자동 생성된 키값을 구하였다.

  • 2행에서는 GeneratedKeyHolder 객체를 생성한다. 이 클래스는 자동 생성된 키 값을 구해주는 KeyHolder 구현 클래스이다.
  • 3행의 update() 메서드에서는 PreparedStatementCreator 객체와 KeyHolder 객체를 파라미터로 갖는다.
  • 7행에서는 prepareStatement 메서드의 두 번째 인자로 String의 배열인 "ID"를 주었다. 이 두 번째 인자는 자동 생성되는 키 칼럼 목록을 지정할 때 사용한다.
  • 16~17행에서는 keyHolder에서 키값을 구하여 원하는 타입으로 변환하여 사용하였다.

 

 

 

 

 

참고 서적: <초보 웹 개발자를 위한 스프링 5 프로그래밍 입문>

 

초보 웹 개발자를 위한 스프링5 프로그래밍 입문

COUPANG

www.coupang.com

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.


Spring Framework 시리즈

  1. 스프링 프로젝트 시작하기 (Maven)
  2. 스프링 컨테이너(Container) 의미
  3. Dependency, DI, Assembler (의존, 의존 주입, 주입기) 개념 정리
  4. 스프링에서의 의존 주입(DI)의 의미와 사용법
  5. 스프링 애노테이션을 사용한 의존 주입(DI)
  6. 의존 자동 주입(1) - @Autowired 애노테이션
  7. 의존 자동 주입(2) - 빈 이름과 한정사
  8. 의존 자동 주입(3) - @Autowired의 필수 여부, 자동 주입과 명시적 의존 주입
  9. 컴포넌트 스캔 - @Component, @ComponentScan 사용하기
  10. 빈 객체의 라이프사이클과 범위 (Life Cycle & Scope of Bean)
  11. AOP 프로그래밍(1) - 프록시와 AOP
  12. AOP 프로그래밍(2) - 스프링에서의 AOP
  13. AOP 프로그래밍(3) - 스프링에서의 프록시 생성 방식
  14. Tomcat JDBC DataSource 클래스의 주요 설정(프로퍼티)
  15. DB 연동(1) - 스프링 프로젝트에 DB 연동하기
728x90

댓글