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

Hyeonjun·2022년 8월 17일
0

스프링입문

목록 보기
6/8
post-thumbnail
post-custom-banner

H2 데이터베이스 설치

스프링 DB 접근 기술

  1. H2 데이터베이스를 통해 데이터 저장
  2. 서버와 DB연결(JDBC)
  3. 순수 JDBC로 연결
  4. 스프링 JdbcTemplate으로 sql을 편리하게 날리기
  5. JPA로 sql 없이 사용하기
  6. 스프링 데이터 JPA로 JPA를 더 쉽게 사용할 수 있도록

H2 데이터베이스

  • h2/bin/h2.bat으로 실행
  • 최초에 database file을 만들게 됨.

순수 JDBC

20년전 방식으로. 고대의 선배님들은 이렇게 했구나…

JDBCMemberRepository

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;

    public JdbcMemberRepository(DataSource 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);
            pstmt.setString(1, member.getName()); // ?에 값을 넣음
            pstmt.executeUpdate(); // db에 query가 날라감.
            rs = pstmt.getGeneratedKeys(); // key를 꺼내줌.
            if (rs.next()) { // 값이 있으면
                member.setId(rs.getLong(1)); // 해당 값을 꺼내줌.
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) { // 오류를 정말 많이 발생시키기 때문에 try-catch를 잘 해줘야함.
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs); // 외부 리소스를 모두 끊어줘야 함. db 커넥션을 계속 쌓다가 정말 큰일이 날 수 있음.
        }
    }

    @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(); // 조회는 excetueQuery를 사용
            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를 list에 담음
                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); // DataSourceUtils를 통해서 connection을 가져와야함. database transaction과 관련한 문자가 발생하지 않도록.
    }

    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); // DataSourceUtils를 통해 커넥션을 끊어야함.
    }
}

Config를 통한 빠른 Bean 설정

@Configuration
public class SpringConfig {

    private DataSource dataSource;

    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}
  • 다른 코드에서 변경 없이 Config만을 통해 설정할 수 있음.
  • 기존에 MemoryMemberRepository()에서 JdbcMemberRepository로 변경함.

스프링을 사용하는 이유

  • 객체 지향적인 설계에서 이점을 가질 수 있음. (다형성을 활용)
  • 인터페이스를 두고 구현체만 수정해 편리하게 사용할 수 있음.
  • Config만을 어셈블리(수정)하여 편리하게 사용할 수 있다.
  • 개방-폐쇠 원칙 (OCP, Open-Closed Principle)
    • 확장에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI(Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

구현 클래스 추가 이미지

스프링 설정 이미지

스프링 컨테이너와 DB를 연결해 통합 테스트를 진행해보자.

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired // 테스트 할 때엔 가장 편한 방법으로 하는 것이 좋음.
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;

		...
}
  • @SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
  • @Transactional: 테스트 케이스에 이 Annotation이 있으면 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

테스트를 반복적으로 할 수 있도록

  • @Transaction을 설정해서 테스트가 끝나면 테스트 중에 생성한 데이터를 자동으로 지워줌.
  • 테스트가 끝날 때 rollback으로 db에 실제 데이터를 반영하지 않는 것.

테스트와 관련해서

  • 순수하게 자바 코드를 활용해서 진행하는 테스트를 단위 테스트라고 함.
  • 스프링 컨테이너와 DB까지 연동하는 테스트를 통합 테스트라고 함.
  • 순수한 단위 테스트가 훨씬 좋은 테스트일 가능성이 크다.
  • 스프링 컨테이너와 관련 없이 테스트를 진행할 수 있도록 하는 것이 좋음.

스프링 JdbcTemplate

  • 순수 JDBC와 동일한 환경설정을 하면 된다.
  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.

JdbcTemplateMemberRepository

public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    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"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}
  • 생성자가 하나만 있으면 @Autowired를 생략할 수 있음.
  • 디자인 패턴 중 템플릿 메서드 패턴을 사용해 코드를 많이 줄이게 됨.
  • memberRowMapper()로 데이터를 객체로 매핑해줌.
  • 현업에서는 테스트 코드 잘 짜는 것이 중요함.

JPA

  • 자바 퍼시스턴스 API
  • ORM 기술
    • Object(객체)와 Relational DB의 테이블을 Mapping하는 것
  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
    • 개발 생산성을 크게 높일 수 있음.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

JPA로 사용하기 위해서

  • Entity 매핑(@Entity)
  • PK 설정
    • @Id
    • @GeneratedValue(strategy = GenerationType.IDENTITY)
      • Identity 전략: DB에 값을 넣으면, Pk 값을 자동으로 올려주는 것.
  • DB의 Column과 객체의 값을 매핑할 수도 있음.
    • @Column(name = “{db의 컬럼 이름}”)

JpaMemberRepository

public class JpaMemberRepository implements MemberRepository{

    private final EntityManager em;

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

EntityManager

  • JPA는 EntityManager로 모든 것이 동작함.
  • spring-start-data-jpa를 받으면 Spring boot가 EntityManager를 만들어줌.
    • application.yml파일 등 확인해서 만들어줌.
  • 우리는 Injection 받으면 됨.
  • EntityManager가 DB와 통신하는 등 모든 역할을 제어해준다.
  • JPA를 쓰기 위해 EntityManager를 주입받아야 한다.

@Transactional

  • JPA를 사용하기 위해 반드시 필요
    • 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
  • Service에 입력함.
    @Transactional
    public class MemberService {
    	...
    }

JPQL

  • 객체지향 쿼리
  • 데이터 지향, SQL과는 다르게 객체를 중심으로 개발할 수 있다.

스프링 데이터 JPA

  • 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다.
  • 반복 개발 했던 기본 CRUD 기능을 스프링 데이터 JPA가 모두 제공한다.
  • 개발자는 이를 통해 핵심 비즈니스 로직을 개발하는 데 집중할 수 있다.
  • 실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 선택이 아닌 필수.

주의 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술입니다. 따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 합니다.

SpringDataJpaMemberRepository

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    @Override
    Optional<Member> findByName(String name);
}
  • Bean에 등록할 필요 없이 Spring Data JPA가 JpaRepository를 상속받는 인터페이스에 대한 구현체를 Bean에 등록해줌.

SpringConfig

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

		@Bean
    public MemberService memberService(){
        return new MemberService(memberRepository);
    }
}

스프링 데이터 JPA 제공 클래스

  • JpaRepository에서 기본적인 find, save를 제공함.
  • CrudRepository에서 기본적인 CRUD를 제공함.

스프링 데이터 JPA 제공 기능

  • 인터페이스를 통한 기본적인 CRUD
  • findByName(), findByEmail()처럼 메서드 이름 만으로 조회 기능 제공
    • 해당 내용들은 도메인마다 달라질 수 있는 부분이기 때문에 공통화 할 수 없음.
    • 따라서 규칙에 따라 메서드를 자동으로 만들어줄 수 있다.
      • reflection 기술로 만들어줌.
      • 이 때문에 Entity에 기본 생성자(@NoArgConstructor)가 들어가야 함.
  • 페이징 기능 자동 제공

실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 JdbcTemplate을 사용하면 된다.

profile
더 나은 성취
post-custom-banner

0개의 댓글