본격적으로 JDBC를 사용해보려고 합니다.
package com.example.hellospring.repository;
import com.example.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into test_member (member_name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from test_member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from test_member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from test_member where member_name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
{
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
그리고 SpringConfig의 코드도 수정해 줍니다.
package com.example.hellospring.service;
import com.example.hellospring.repository.JdbcMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepositrory());
}
@Bean
public MemberRepository memberRepositrory() {
return new JdbcMemberRepository(dataSource);
}
}
DataSource 객체를 만들어주고 JdbcMemberRepository()도 객체를 만들어준 후 거기에 매개변수로 넘겨줍니다.
이것이 객체 지향의 장점이라고 할 수 있습니다.
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.
개방-폐쇄 원칙(OCP, Open-Closed Principle)
확장에는 열려있고 수정, 변경에는 닫혀있다.
방금 코드를 예시를 들자면, 다른 코드의 수정이나 변경없이 DataSource 객체를 만들고 매개변수로 넘겨주고 끝이다.
스프링의 DI을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
DB 연결할 때 커넥션을 미리 생성하고 사용하는 커넥션 풀
이라는 방법을 사용한다. 커넥션 풀은 커넥션을 관리하는 풀이라고 생각하면 된다.
애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다. 보통 얼마나 보관할지는 서비스의 특징과 서버 스펙에 따라 다르지만 기본값은 보통 10개이다.
커넥션 풀에 들어 있는 커넥션은 TCP/IP
로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있다.
적절한 커넥션 풀 숫자는 서비스의 특징과 애플리케이션 서버 스펙, DB 서버 스펙에 따라 다르기 때문에 성능 테스트를 통해서 정리해야 한다.
커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있다. 따라서 DB에 무한정 연결이 생성되는 것을 막아주어서 DB를 보호하는 효과가 있다.
커넥션 풀은 실무에서 항상 사용
사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많기 때문에 오픈소스를 사용
오픈 소스 : commons-dbcp2 , tomcat-jdbc pool , HikariCP 등이 있다.
성능과 사용의 편리함 측면에서 최근에는 hikariCP 를 주로 사용한다. 스프링 부트 2.0 부터는 기본 커넥션 풀로 hikariCP 를 제공한다. 성능, 사용의 편리함, 안전성 측면에서 이미 검증이 되었기 때문에 커넥션 풀을 사용할 때는 고민할 것 없이 hikariCP 를 사용하면 된다. 실무에서도 레거시 프로젝트가 아닌 이상 대부분 hikariCP 를 사용한다.
커넥션을 얻는 방법은 앞서 학습한 JDBC DriverManager 를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 존재한다.
DriveManager를 통해 커넥션 획등
커넥션을 획득하는 방법을 추상화
데이터를 저장할 때 단순히 파일에 저장해도 되는데 데이터베이스에 저장하는 이유는 무엇일까?
대표적인 이유는 데이터베이스에는 트랜잭션
이라는 개념을 지원하기 때문이다. 트랜잭션을 이름 그대로 번역하면 거래라는 뜻이다. 이것을 쉽게 풀어서 이야기하면, 데이터베이스에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다. 그런데 하나의 거래를 안전하게 처리하려면 생각보다 고려해야 할 점이 많다. 예를 들어서 A의 5000원을 B에게 계좌이체한다고 생각해보자. A의 잔고를 5000원 감소하고, B의 잔고를 5000원 증가해야한다.
계좌이체라는 거래는 이렇게 2가지 작업이 합쳐져서 하나의 작업처럼 동작해야 한다. 만약 1번은 성공했는데 2번에서 시스템에 문제가 발생하면 계좌이체는 실패하고, A의 잔고만 5000원 감소하는 심각한 문제가 발생한다.
데이터베이스가 제공하는 트랜잭션 기능을 사용하면 1,2 둘다 함께 성공해야 저장하고, 중간에 하나라도 실패하면 거래전의 상태로 돌아갈 수 있다. 만약 1번은 성공했는데 2번에서 시스템에 문제가 발생하면 계좌이체는 실패하고, 거래 전의 상태로 완전히 돌아갈 수 있다. 결과적으로 A의 잔고가 감소하지 않는다. 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋(Commit)이라 하고, 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.
트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다.
원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation
level)을 선택할 수 있다.
지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
여기에서 dependencies
에 추가를 해줘야 한다.
// 자바는 DB랑 붙으려고 하면 jdbc 드라이버가 필수다!
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'mysql:mysql-connector-java' // MySql
jdbc를 추가한 이유는 jdbc를 체험해보기 위해서 추가한것이다.
여기에도 설정을 추가해야 합니다.
# MySQL 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# DB Source URL
spring.datasource.url=jdbc:mysql://localhost:3306/study01
# DB username
spring.datasource.username=root
# DB userpassword
spring.datasource.password=1234
# true 설정시 JPA 쿼리문 확인 가능
# 처리시 발생하는 SQL을 보여줄 것인지 결정
spring.jpa.show-sql=true
# DDL(create, alter, drop) 정의시 DB의 고유 기능을 사용할 수 있다.
spring.jpa.hibernate.ddl-auto=update
# JPA의 구현체인 Hibernate가 동작하면서 발생한 SQL의 가독성을 높여준다.
spring.jpa.properties.hibernate.format_sql=true
[if]
<if test="조건식">
내용
</if>
[choose, when, otherwise]
<choose>
<when test="조건식">
내용
</when>
...
<otherwise>
내용
</otherwise>
</choose>
[where]
select * from spring_board
<where>
boardnum=#{boardNum}
</where>
→ select * from spring_board where boardNum=#{boardNum}
select * from spring_board where
<if test="boardNum>10">
boardNum=#{boardNum}
</if>
→ select * from spring_board where boardNum=11
11 이상이여야만 if문이 돌아간다.
select * from spring_board
<where>
</where>
where 태그안에 아무 것도 없으면 where절이 안만들어진다.
→ select * from spring_board
그렇기 때문에 다음과 같이 사용한다.
select * from spring_board where
<where>
<if test="boardNum>10">
boardNum=#{boardNum}
</if>
</where>
[trim]
1) prefix 속성 - <trim>
태그 내부 실행될 쿼리문 앞에 설정해둔 속성값을 삽입합니다.
2) suffix 속성 - <trim>
태그 내부 실행될 쿼리문 뒤에 설정해둔 속성값을 삽입합니다.
3) prefixOverrids 속성 - <trim>
태그 내부 실행될 쿼리문 가장 앞의 단어가 속성값에 설쟁해둔 문자와 동일할 경우 문자를 지웁니다.
4) suffixOverrids 속성 - <trim>
태그 내부 실행될 쿼리문 가장 뒤의 단어가 속성값에 설정해둔 문자와 동일한 경우 문자를 지웁니다.
<trim prefix="a " prefixOverrides="b">
b 내용
</trim>
→ (b 내용을 없에주고) a 내용
[foreach]
List, 배열, Map 등을 이용해서 루프를 처리한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.koreait.mapper.BoardMapper">
<sql id="cri">
<if test="keyword != null and type != null">
<trim prefixOverrides="or" prefix="(" suffix=") and">
<foreach collection="typeArr" item="type">
<trim prefix="or">
<choose>
<when test="type=='T'.toString()">
(boardtitle like('%${keyword}%'))
</when>
<when test="type=='C'.toString()">
(boardcontents like('%${keyword}%'))
</when>
<when test="type=='W'.toString()">
(userid like('%${keyword}%'))
</when>
</choose>
</trim>
</foreach>
</trim>
</if>
</sql>
<insert id="insert">
insert into spring_board (boardtitle,boardcontents,userid)
values(#{boardtitle},#{boardcontents},#{userid})
</insert>
<update id="update">
update spring_board set boardtitle=#{boardtitle}, boardcontents=#{boardcontents},
updatedate=now() where boardnum=#{boardnum}
</update>
<select id="getList" resultType="com.koreait.domain.BoardDTO">
select * from spring_board where
<include refid="cri"></include>
<![CDATA[
0<boardnum order by boardnum desc limit #{startrow},#{amount}
]]>
</select>
<select id="getMaxBoardnum" resultType="_int">
select max(boardnum) from spring_board where userid=#{userid}
</select>
<select id="getDetail" resultType="com.koreait.domain.BoardDTO">
select * from spring_board where boardnum=#{boardnum}
</select>
<select id="getTotal" resultType="_int">
select count(*) from spring_board where <include refid="cri"></include> boardnum>0
</select>
<delete id="delete">
delete from spring_board where boardnum=#{boardnum}
</delete>
</mapper>
이 예제는 동적태그와 mybatis를 xml로 사용하는 방식이다.