[스프링 입문] Section 06 - 스프링 DB 접근 기술

Update:     Updated:

카테고리:

태그:

spring DB 접근 기술의 발전

Image

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);
    }
}

Spring-Tutorial 카테고리 내 다른 글 보러가기

댓글 남기기