[스프링 입문] Section 06 - 스프링 DB 접근 기술
카테고리: Spring-Tutorial
spring DB 접근 기술의 발전
application과 DB의 연결을 sql을 통해 한다.
자바 진영에서 DB에 sql을 통해 접근 하기 위한 기술들을 간략히 알아보자!
- 순수 Jdbc 기술
말 그대로 JDBC API
로 직접 코딩하는 것을 말한다. 직접 필요한 자원들을 가지고 connection을 얻고, db에 sql을 쏘고, 사용한 자원을 반환하는 등 필요한 과정을 하나하나 직접 만드는 기술이다.
- JDBC Template
순수 JDBC
기술에서 spring
이 편의를 제공해준 것으로, 반복되는 코드를 줄이고, application에서 DB로 sql을 편리하게 날려준다. 다만, 아직까지는 sql을 개발자가 직접 짜야한다.
- JPA
sql조차도 개발자들이 직접 짜는게 아니라 sql을 JPA
라는 기술이 DB에 직접 등록, 수정, 삭제, 조회 등의 query를 날려준다. 객체를 바로 DB에 query없이 저장이 가능하다.
- Spring Data JPA
spring
에 맞춰 더 사용하기 편리하도록 감싼 기술이다.
H2 데이터베이스 설치
먼저 h2 데이터베이스를 설치하고 member 테이블을 만들자.
-- ddl.sql
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity, -- id 필드를 추가 안 했을 시 자동으로 추가해줌
name varchar(255),
primary key (id)
);
spring & DB 사전 설정
// build.gradle
dependencies{
implementation 'org.springframework.boot:spring-boot-starter-jdbc' // // java는 DB랑 붙으려면 jdbc 드라이버가 꼭 있어야 함
runtimeOnly 'com.h2database:h2' // DB랑 붙을 때 DB가 제공하는 클라이언트가 필요해서 넣어줌
}
# application.properties
# 스프링 부트를 이용한 접속 정보 등록
spring.datasource.url=jdbc:h2:tcp://localhost/~/Study/spring-tutorial/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
springConfig와 OCP
- OCP란?
OCP
, 개방-폐쇄 원칙(OCP, Open-Closed Principle)이란, 확장에는 열려있고, 수정, 변경에는 닫혀있다는 내용!
스프링의 DI
(Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
/*
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체이다.
스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.
*/
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() { // MemberRepository의 코드 변경 없이 SpringConfig의 수정만으로 Repository의 종류를 바꿀 수 있다!
//return new MemoryMemberRepository();
//return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
}
순수 Jdbc
JDBC API로 직접 코딩하는 것은 옛날 이야기라고 한다. 참고만 하고 넘어가자.
// JdbcMemberRepository.java
package hello.hellospring.repository;
import hello.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; // DB에 붙으려면 DataSource가 필요
public JdbcMemberRepository(DataSource dataSource) { // 스프링이 DB 접속 정보를 통해 DataSource를 만들어 놓으면 spring을 통해 DataSource를 주입받으면 됨
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection(); // connection 가져오고
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS); // connection에 sql 쏘고
pstmt.setString(1, member.getName());
pstmt.executeUpdate(); // DB에 쿼리가 날아감
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); // 작업이 끝나고 사용한 자원들 release
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from 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 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 member where 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); // DataSource로부터 connection을 얻고, 닫아야 함
}
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);
}
}
스프링 JDBC Template
JDBC API의 반복 코드를 대부분 제거해준다. 다만 아직 SQL은 직접 작성해야 한다.
// JdbcTemplateMemberRepository
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate; // JdbcTemplate이나 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 다만, 여전히 sql은 직접 작성해야한다.
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.util.*;
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
// @Autowired // 생성자가 하나일 경우 Autowired 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id")); // query문에서 id랑
member.setName(rs.getString("name")); // name을 뽑아냄
return member;
};
}
}
JPA
JPA는 표준 인터페이스이다. hibernate, eclipse 등이 구현체로 있다.(여러 업체들이 구현한다고 보면 된다.)
JPA를 사용하면 ‘SQL과 데이터 중심의 설계’에서 ‘객체 중심의 설계’로 패러다임을 전환할 수 있다.
- build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc' // java는 DB랑 붙으려면 jdbc 드라이버가 꼭 있어야 함
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JPA 사용을 위한 구문 추가. 내부에 jdbc 관련 라이브러리를 포함하기 때문에 윗 줄은 제거해도 됨
testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'com.h2database:h2' // DB랑 붙을 때 DB가 제공하는 클라이언트가 필요해서 넣어줌
}
- 스프링 부트에 JPA 설정 추가
# application.properties
# 스프링 부트를 이용한 접속 정보 등록
spring.datasource.url=jdbc:h2:tcp://localhost/~/Study/spring-tutorial/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
# JPA 관련 설정 추가
# jpa가 날리는 sql을 볼 수 있음
spring.jpa.show-sql=true
# jpa를 쓰면 객체를 보고 테이블을 자동으로 생성해줌. 하지만 예제에서는 이미 만들어진 테이블을 사용할 것이기 때문에 none으로 설정함(create를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.)
spring.jpa.hibernate.ddl-auto=none
- JPA 엔티티 매핑
// Member.java
package hello.hellospring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity // JPA가 관리하는 엔티티라고 표시함
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // db에 추가하면 id를 자동으로 만들어주는 전략(strategy)을 Identity라고 함
private Long id; // 사용자 지정이 아닌 시스템이 지정해주는 id
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- JPA 회원 리포지토리
// JpaMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
/**
JPA는 Entity Manager를 이용하여 모든 동작을 함.
data-jpa로부터 spring boot가 현재 database랑 연결을 하여 자동으로 EntityManager를 생성해주니까 그냥 주입받아서 쓰면 됨
*/
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) { // jpql이라는 객체지향쿼리언어를 사용(테이블 대상 쿼리문이 아닌 객체를 대상으로 하는 쿼리문이다.)
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
}
-서비스 계층에 트랜잭션 추가
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고,
메서드가 정상 종료되면 트랜색션을 커밋한다.
만약 런타임 예외가 발생하면 롤백한다.
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
// MemberService.java
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class MemberService {}
-JPA를 사용하도록 스프링 설정 변경
// SpringConfig.java
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
private final EntityManager em;
@Autowired
public SpringConfig(DataSource dataSource, EntityManager em) {
this.dataSource = dataSource;
this.em = em;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() { // MemberRepository의 코드 변경 없이 SpringConfig의 수정만으로 Repository의 종류를 바꿀 수 있다!
return new JpaMemberRepository(em);
}
}
스프링 데이터 JPA
스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다.
스프링 데이터 JPA를 이용하면,
리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있고, 기본 CRUD 기능도 제공해준다.
- 스프링 데이터 JPA 회원 리포지토리
// SpringDataJpaMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
// spring data jpa가 jpaRepository를 갖고 있으면, 구현체를 자동으로 만들어주고 spring bean에 자동으로 등록해줌
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
- 스프링 설정 변경
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
댓글 남기기