[Spring] 스프링 입문(코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술) - 스프링 DB 접근 기술

밀크야살빼자·2023년 4월 19일
0

순수 JDBC

Jdbc 리포지토리 구현
❗❗ 주의 이렇게 JDBC API로 직접 코딩하는 것은 15년전 이야기이기 때문에 참고만하는 것이 좋다.

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

@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    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);
    }
}
  • MemberService는 MemberRepository를 의존하고 있고, MemberRepository는 구현체로MemoryMemberRepository와 JdbcMemberRepository를 가지고 있다.
  • 기존에는 memory버전의 memberRepository를 등록했다면, 지금은 jdbc의 memberRepository를 등록하면 구현체가 바껴서 수정할게 없다.
  • 개방-폐쇄 원칙(OCP, Open-Closed Principle)
    • 확장(=코드 확장)에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링 DI를 사용하면, 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

스프링 통합 테스트

test/service/MemberServiceIntegrationTest

//회원 서비스 스프링 통합 테스트
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
   @Autowired
   MemberService memberService;
   @Autowired
   MemberRepository memberRepository;



    @Test
    public void 회원가입() throws Exception{
        //given
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2)); //예외가 발생해야 한다.

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

    }
}
  • @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.
  • @Transactional : 테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
  • 단위 테스트(스프링 없이 순수 자바로 이루어진 코드)로 테스트 하는 것이 좋은 테스트일 확률이 높다.(통합 테스트 : 스프링이 포함된 테스트)

스프링 JdbcTemplate

  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거 해 주지만 SQL은 직접 작성해야 한다.
  • JdbcTemplate는 실무에서 많이 쓰인다.
//스프링 JdbcTemplate 회원 리포지토리
package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

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"));
            member.setName(rs.getString("name"));
            return member;
        };
    }

}

JPA

  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

  • spring-boot-starter-data-jpa는 내부에 jdbc 관련 라이브러리를 포함하기 때문에 jdbc는 제거해도 된다.

application.properties

spring.jpa.show-sql=true // JPA가 생성하는 SQL을 출력
spring.jpa.hibernate.ddl-auto=none // JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none을 사용하면 해당 기능을 끈다.

Member

package hello.hellospring.domain;

import javax.persistence.*;

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;

    @Column(name = "username")
    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;
    }
}

JpaMemberRepository

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 {
	//스프링 부트가 자동으로 DB와 다 연결해서 EntityManager를 생성해준다.
    //JPA를 쓰려면 EntityManager를 주입 받아야 한다.
    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로 파라미터를 바인딩 시킨다.
                .setParameter("name",name)
               .getResultList(); //결과가 없으면 빈 리스트 반환, 빈 collection이 반환되기 때문에, nullPoiontException에 대한 걱정은 안 해도 된다.
       return result.stream().findAny();
    }

    @Override
    public List<Member> findAll(){
        List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
        return result;
    }

}
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository;


    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     * @param member
     * @return
     */
    public Long join(Member member){

           validateDuplicateMember(member); //중복 회원 검증
           memberRepository.save(member);
           return member.getId();
 
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
            .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }
    /*
    *전체 회원 조회
     */
    public List<Member> findMembers(){
            return memberRepository.findAll();

    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}
  • JPA는 데이터 변경이 항상 @Transactional안에서 실행이 되어야 한다.
  • 메소드를 실행할 때 트랜잭션을 시작하고 메소드가 정상종료되면 트랜잭션을 커밋한다. 예외가 발생하면 롤백한다.

SpringConfig

@Configuration
public class SpringConfig {
	
    //@PersistenceContext
    private EntityManager em;

    public SpringConfig(EntityManager em){
        this.em = em;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        // return new MemoryMemberRepository();
        // return new JdbcMemberRepository(dataSource);
       // return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

스프링 데이터 JPA

repository/SpringDataJpaMemberRepository

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    @Override
    Optional<Member> findByName(String name);

}

SpringConfig

package hello.hellospring;

import hello.hellospring.app.TimeTraceAop;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    /*@Autowired
    DataSource dataSource;*/

//    private DataSource dataSource;
//
//    @Autowired
//    public SpringConfig(DataSource dataSource){
//        this.dataSource=dataSource;
//    }
    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository){
        this.memberRepository=memberRepository;
    }
//    @PersistenceContext
//    private EntityManager em;

//    @Autowired
//    public SpringConfig(EntityManager em){
//        this.em=em;
//    }

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

    @Bean
    public TimeTraceAop timeTraceAop(){
        return new TimeTraceAop();
    }

//    @Bean
//    public MemberRepository memberRepository(){
//        //return new MemoryMemberRepository();
//        //return new JdbcMemberRepository(dataSource);
//        //return new JdbcTemplateMemberRepository(dataSource);
//        //return new JpaMemberRepository(em);
//    }
}
  • 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해준다.
  • JpaRepository 인터페이스를 통해 쿼리문 작성 없이 기본적인 CRUD가 가능하다.
  • 페이징 기능 자동 제공해준다.
  • findByName(), findByEmail()로 메서드 이름 만으로 조회 기능 제공해준다.
profile
기록기록기록기록기록

0개의 댓글