여니의 프로그래밍 study/Spring & Spring Boot

[Spring] DB 연동 - 1(Chapter 8)

여니's 2023. 9. 16. 21:43

참고 자료 : 초보 개발자를 위한 스프링5 프로그래밍 입문


<Chapter8. DB 연동>

1. JDBC란?

: Java Database Connectivity, 

자바 프로그램이 데이터베이스와 연결되어 데이터를 주고받을 수 있게 해주는 프로그래밍 인터페이스

 

- 응용프로그램 <-> JDBC <-> DBMS 

중간에서 번역해주는 통역자 역할을 진행한다. 

 

- JDBC 드라이버는 각 DBMS 회사에서 제공하는 라이브러리 압축파일이다. 

 

- JDBC를 사용하면 여러 종류의 데이터베이스에 접속할 수 있으며

데이터베이스에 SQL 쿼리를 실행하고 결과를 처리할 수 있다. 

JDBC를 사용하여 자바 코드에서 데이터베이스 연결 및 쿼리를 수행할 수 있다. 

(JDBC를 사용하여 SELECT , DELETE, UPDATE 등의 SQL문을 실행한다)

 

 

- 스프링의 장점은 트랜잭션 관리가 쉽다는 것이다. 

트랜잭션을 적용하고 싶은 메서드에 @Transactional 애노테이션을 붙이기만 하면 된다. 

 


 

2. 트랜잭션

: 데이터베이스에서 한 번에 수행되어야 하는 하나 이상의 작업을 묶어서 실행하는 논리적인 작업 단위이다.

예를 들어 A계좌에서 B계좌로 인출을 한다고 하자. 

A계좌에서 B계좌로 인출을 하는 도중에 문제가 발생했다.

A계좌에서는 이미 돈이 빠져나갔지만, B계좌로 도착하지 못한 상황이 발생했을 때

트랜잭션 처리가 되어있으면 롤백을 하면 된다.

트랜잭션의 일부가 실패하면 롤백을 하면 문제가 없다. 

이는 데이터 일관성을 유지하기 위해 매우 중요한 개념이다!

 

트랜잭션 처리가 되어 있지 않다면 데이터의 불일치와 함께 심각한 문제가 일어날 수 있다. 

 

 


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
		http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>sp5</groupId>
	<artifactId>sp5-chap08</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>5.0.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>5.0.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-jdbc</artifactId>
			<version>8.5.27</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.45</version>
		</dependency>

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.25</version>
		</dependency>

		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.2.3</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.7.0</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>utf-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

- pom.xml

spring-jdbc : JDBC연동에 필요한 기능을 제공함

tomcat-jdbc : DB 커넥션풀 기능을 제공한다.

mysql-connector-java : MySQL 연결에 필요한 JDBC 드라이버를 제공함

 

 

실제 스프링이 제공하는 트랜잭션 기능을 사용하려면 spring-tx 모듈이 필요한데

spring-jdbc 모듈에 포함되어 있다. 


 

3. 커넥션 풀

 

자바 프로그램에서 DBMS로 커넥션을 생성하는 시간은 매우 길다.

따라서 전체 성능에 영향을 줄 수 있다. 

또한 동시에 접속하는 사용자수가 많으면, DBMS에 부하를 주게 된다. 

 

최초 연결에 따른 응답 속도와 동시 접속사가 많을 때 발생하는 부하를 줄이기 위해 

커넥션 풀을 사용한다.

 

커넥션 풀은 일정 개수의 DB 커넥션을 미리 만들어두는 기법이다. 

DB 커넥션이 필요한 프로그램은 커넥션 풀에서 커넥션을 가져와 사용한 후

커넥션을 다시 풀에 반납하는 구조이다. 

 

커넥션 풀을 제공하는 모듈은 Tomcat JDBC가 있다. 

 

 

프로그램 <- 커넥션 풀, 커넥션 전달 시, 해당 커넥션은 활성상태가 된다.

프로그램 -> 커넥션 풀, 커넥션 반납시, 해당 커넥션은 유휴상태가 된다. 

 

package dbquery;

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

import javax.sql.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) {
				}
		}
	}

}

 


4. JDBC 템플릿을 이용한 쿼리 실행

(1) JDBC 템플릿 생성하기

public class MemberDao {
	private JdbcTemplate jdbcTemplate;
    
    public MemberDao(DataSource datasSource) {
    	this.jdbcTemplate=new JdbcTemplate(dataSource)'
    }
}

 

JDBC template 객체를 생성하려면

DataSource를 주입받는 생성자를 구현하면 된다. (ex. MemberDao)

 

@Configuration
public class AppCtx {
	...
    @Bean
    public MemberDao memberDao(){
    	return new MemberDao(dataSource());
    }
}

 

(2) JdbcTemplat을 이용한 조회쿼리 실행

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)

 

query() 메서드는 sql 파라미터로 전달받은 쿼리를 실행하고

RowMapper 를 이용해서 ResultSe의 결과를 자바 객체로 변환한다.

 

RowMapper 의 mapRow() 메서드는

SQL 실행 결과로 구한 ResultSet 에서 한 행의 데이터를 읽어와

자바 객체로 변환하는 매퍼 기능을 구현한다. 

 

public Member selectByEmail(String email) {
	# query 메서드를 통해 쿼리 실행
    List<Member> results = jdbcTemplate.query(
            "select * from MEMBER where EMAIL = ?",
            # 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;
                }
            # ?에 들어갈 파라미터 (가변인자)
            }, email);

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

 

query 메서드는 쿼리를 실행한 결과가 존재하지 않으면

길이가 0인 리스트를 리턴한다.

리스트가 비어있는지 여부로 결과가 존재하지 않는지 확인할 수 있다. 

 

 

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

public int count(){
	List<Integer> results = jdbcTemplate.query(
    	"select count(*) from MEMBER",
        new RowMapper<Integer>(){
        	@Override
            public Integer mapRow(ResultSet rs, int rowNum) throws SQLException {
            	return rs.getInt(1);
            }
        });
    return results.get(0);
}

 

-> queryForObject() 메서드 사용

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

결과 행이 정확히 한 개가 아니면

queryForObject 메서드 대신 query 메서드를 사용해야 한다.

 

 

(4) JdbcTemplat을 이용한 변경 쿼리 실행

INSERT, UPDATE, DELETE 쿼리는 UPDATE() 메서드를 사용한다.

해당 메서드는 쿼리 실행 결과로 변경된 행의 개수를 리턴한다. 

 

 

(5) PreparedStatementCreator를 이용한 쿼리 실행

지금까지 위에서 작성한 코드는 쿼리에서 사용할 값을 인자로 전달했다.

PreparedStatement의 set 메서드를 사용해서

직접 인덱스 파라미터의 값을 설정해야할 때도 있다. 

 

PreparedStatementCreator를 구현한 클래스는

createPreparedStatement() 메서드의 파라미터로 전달받는 커넥션을 이용해서

PreparedStatement 객체를 생성하고

인덱스 파라미터를 알맞게 설정한 뒤에 리턴하면 된다. 

 

 

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

insert 쿼리에 자동 증가 컬럼에 해당하는 값은 지정하지 않는다. 

자동으로 생성된 키값을 구할 수 있는 방법은 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());
	}

 

    KeyHolder keyHolder = new GeneratedKeyHolder();

해당 클래스는 자동 생성된 키값을 구해주는 KeyHolder 구현 클래스이다. 

 

 


5. MemberDao 테스트하기

package main;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import config.AppCtx;
import spring.Member;
import spring.MemberDao;

public class MainForMemberDao {
	private static MemberDao memberDao;

	public static void main(String[] args) {
    	# 스프링 컨테이너 생성
		AnnotationConfigApplicationContext ctx = 
				new AnnotationConfigApplicationContext(AppCtx.class);
		# 스프링 컨테이너로부터 MemberDao 빈을 구해 정적 필드인 memberDao에 할당
		memberDao = ctx.getBean(MemberDao.class);

		selectAll();
		updateMember();
		insertMember();

		ctx.close();
	}
	
    # 전체 행의 개수를 구하고, 전체 Member 데이터를 구한뒤 출력한다. 
	private static void selectAll() {
		System.out.println("----- selectAll");
		int total = memberDao.count();
		System.out.println("전체 데이터: " + total);
		List<Member> members = memberDao.selectAll();
		for (Member m : members) {
			System.out.println(m.getId() + ":" + m.getEmail() + ":" + m.getName());
		}
	}

	private static void updateMember() {
		System.out.println("----- updateMember");
		Member member = memberDao.selectByEmail("madvirus@madvirus.net");
		String oldPw = member.getPassword();
		String newPw = Double.toHexString(Math.random());
		member.changePassword(oldPw, newPw);

		memberDao.update(member);
		System.out.println("암호 변경: " + oldPw + " > " + newPw);
	}

	private static DateTimeFormatter formatter = 
			DateTimeFormatter.ofPattern("MMddHHmmss");

	private static void insertMember() {
		System.out.println("----- insertMember");

		String prefix = formatter.format(LocalDateTime.now());
		Member member = new Member(prefix + "@test.com", 
				prefix, prefix, LocalDateTime.now());
		memberDao.insert(member);
		System.out.println(member.getId() + " 데이터 추가");
	}

}

 

DB 연동 과정에서 발생 가능한 익셉션 (207p)

> DB 연결 정보가 올바르지 않으면 Access denied for user ~~ 에러가 뜰 수 있음

> MySQL 서버에 연결할 권한이 없어도 익셉션 발생