스프링 MVC + 인프런 (6)

sein lee·2023년 8월 25일
0
post-thumbnail
post-custom-banner

섹션 6. 스프링 DB 접근 기술

H2 데이터베이스 설치

h2 데이터베이스 설치

\h2\bin 폴더에 들어가서 h2.bat 실행 (웹 콘솔로 접근하는 법)
jdbc:h2:~/test : ~/test - 파일경로
나가기 버튼
C:\Users\사용자 디렉토리에 test.mv.db 파일 여부 확인

localhost로 접속 후 Jdbc URl 바꾸기 jdbc:h2:tcp://localhost/~/test
=> 파일을 직접 접근하는게 아니라 소켓을 통해서 접근이 가능하여 여러군데에서 접근 가능

문제가 있을 시 삭제 방법
연결 해제 후 rm test.mv.db -> 다시 접속(h2.bat 부터..)

테이블 생성

drop table if exists member CASCADE;
create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

조회
생성

insert into member(name) values('spring')

  • sql 코드는 따로 관리해주는 것이 좋다

순수 Jdbc (예전 사용법, 참고용)

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa


spring.datasource.url = < jdbc URL >

Jdbc 리포지토리 구현

src/main/java/hello/hellospring/repository/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;
    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; //결과 받기
        //exception이 많기 때문에 try-catch를 잘 짜줘야한다!
        try {
            conn = getConnection(); //커넥션 가져오기
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); //sql : 21라인 쿼리문
            pstmt.setString(1, member.getName()); //parameterIndex : 1 -> 21라인 첫번째 ? 와 매칭

            pstmt.executeUpdate(); //실제로 Db에 쿼리가 날아가는 시점
            rs = pstmt.getGeneratedKeys(); //getGeneratedKeys : RETURN_GENERATED_KEYS와 매칭되어서 사용

            //값 꺼내고 세팅하기..
            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(); //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(); //없으면 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<>(); //List 형태 주의
            
            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);
        }
    }

    //Spring 프레임워크 사용 시 커넥션 가져오는 법
    //직접 datasource.getConnection() 해도 되지만 계속 새로운 커넥션이 주어지기 때문에 DataSourceUtils 사용 -> 이전의 트랜잭션에 걸릴 시 커넥션 유지
    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();
        }
    }

    //닫을 때도 DataSourceUtils 사용
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }

}

repository 작성하였다고 동작할 수 있는게 아니다. configuration 해야된다.

지금까지는 SpringConfig 에서MemoryMemberRepository를 bean 으로 등록하여 사용하고 있었다.
-> SpringConfig에 밑의 코드로 수정, 추가

실행(h2가 활성화 상태여야함!!)

결과
localhist:8080 -> 회원목록
DB에 저장했던 목록이 나오면 성공!
-> 회원 추가 후 회원목록 확인, h2 MEMBER Table도 확인해보기
데이터를 DB에 저장하기 때문에 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.

MemberService 는 MemberRepository를 의존하고 있다.
MemberRepository는 구현체로 MemoryMemberRepository, JdbcMemberRepository가 있다.

  • 개방-폐쇄 원칙(OCP, Open-Closed Principle) : 확장에는 열려있고, 수정, 변경에는 닫혀있다.
    스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현
    클래스를 변경
    할 수 있다.

스프링 통합 테스트

Spring과 DB까지 연결하여 동작하는 통합테스트를 진행해보자
src/test/java/hello/hellospring/service/MemberServiceTest.java 파일 복붙 후 MemberServiceIntegrationTest.java 로 이름 변경 후 코드 작성..

테스트 케이스 할 때는 필드 기반의 @Autowired 사용하는 것이 제일 편함

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 //Spring 테스트 시 사용하는 어노테이션
@Transactional
class MemberServiceIntegrationTest {

    //테스트 케이스 할 때는 필드 기반의 @Autowired 사용하는 것이 제일 편함
    @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 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("hello");
        Member member2 = new Member();
        member2.setName("hello");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

@SpringBootTest
스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional
테스트를 반복할 수 있어야 하는데 데이터가 남아있으면 오류가 발생한다.
데이터베이스는 기본적으로 트랜잭션이라는 개념이 있다 => insert 후 commit을 해야 DB에 반영된다.
@Transactional을 사용하면 테스트 끝나고 롤백해서 Db에서 테스트데이터를 없애주어 다음 테스트에 영향을 주지 않는다.


스프링 Jdbc Template

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

src/main/java/hello/hellospring/repository/JdbcTemplateMemberRepository.java

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.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

//implements 중요
public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;
//    @Autowired //생성자가 하나일 때는 Autowired 생략 가능
    //injection을 받을 수 없기 때문에 DataSource를 인젝션 받아야 함 !!
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource); //dataSource 중요
    }  //spring이 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());
        //=> 쿼리를 짤 필요가 없다 : SimpleJdbcInsert, withTableName, usingGeneratedKeyColumns...
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) { //Optional 중요
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id); //memberRowMapper: 결과가 나오는 것을 매핑
        return result.stream().findAny();
    } //JdbcMemberRepository - findById 와 비교하면 jdbcTemplate 사용하여 매우 간략하게 코딩할 수 있다는 것을 알 수 있음

    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }

    //memberRowMapper: 결과가 나오는 것을 매핑
//    private RowMapper<Member> memberRowMapper() {
//        return new RowMapper<Member>() {
//            @Override
//            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
//                Member member = new Member();
//                member.setId(rs.getLong("id"));
//                member.setName(rs.getString("name"));
//                return member;
//            }
//        };
//    }
    //위의 코드를 람다 스타일로 수정
    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

SpringConfig 에 repository 조립

실행
스프링 테스트 환경을 만들었기 때문에 Application을 재실행하지 않고 테스트만 실행해서 확인이 가능하다.
=> MemberServiceIntegrationTest.java 만 실행!


JPA

  • Jdbc, JdbcTemplate 에서 코드량은 줄긴했지만 sql은 개발자가 직접 작성해야한다.
    => JPA 가 기존의 반복 코드는 물론이고, sql 도 작성해준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

build.gradle 에 JPA, h2 데이터베이스 관련 라이브러리 추가
spring-boot-starter-data-jpa는 내부에 jdbc 관련 라이브러리를 포함한다. 따라서 jdbc는 제거해도된다.

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'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}

스프링부트에 JPA 추가
resources/application.properties

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

  • show-sql : JPA가 생성하는 SQL을 출력
  • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.
    create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다. 해보자.

gradle 빌드 후 External Libraries 에 JPA와 hiebernate 확인

JPA 앤티티 매핑
src/main/java/hello/hellospring/domain/Member.java
@Entity : JPA가 관리하는 Entity

package hello.hellospring.domain;

import javax.persistence.*;

@Entity //Member 는 JPA가 관리하는 Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) //PK 매핑
    private Long id;

//    @Column(name = "username") //DB의 Column명이 username과 매핑
    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 회원 리포지토리
pk 기반이 아닌 것들은 jpql을 작성해야한다.
src/main/java/hello/hellospring/repository/JpaMemberRepository.java

package hello.hellospring.repository;

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

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;
    //생성자
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member); //persist : 저장
        return member;
    }
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id); //find(조회할 type, PK) : 조회
        return Optional.ofNullable(member);
    }
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    //jpql 객체 지향 언어를 사용 하여야 한다.
    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();
    }
}

서비스 계층에 트랜잭션 추가
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
src/main/java/hello/hellospring/service/MemberService.java

  • org.springframework.transaction.annotation.Transactional 를 사용
  • 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.

JPA를 사용하도록 스프링 설정 변경
src/main/java/hello/hellospring/SpringConfig.java
기존 코드 주석 후 작성

실행
MemberServiceIntegrationTest.java
회원가입만 Run하면 성공 & 로그에 해당 문장 확인전체 Run -> 성공


스프링 데이터 JPA

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

앞의 JPA 설정 그대로 사용한다.

스프링 데이터 JPA 회원 리포지토리
인터페이스로 생성
src/main/java/hello/hellospring/repository/SpringDataJpaMemberRepository.java
interface 는 implements가 아니라 extends !!, 다중상속 가능

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

스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
JpaRepository 를 사용하면 SpringDataJpaMemberRepository가 자동으로 구현체를 만들어서 스프링 Bean자동으로 등록해준다. => 우리는 그것을 가져다 쓰기만 하면 된다.
src/main/java/hello/hellospring/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;

@Configuration
public class SpringConfig {

//    //MemberRepository를 Jdbc...로 바꾸기 위한 작업
//    private final DataSource dataSource;
//    @Autowired
//    public SpringConfig(DataSource dataSource) {
//        this.dataSource = dataSource;
//    }
//    //

//    //JPA 사용할 때
//    private final DataSource dataSource;
//    private final EntityManager em;
//    public SpringConfig(DataSource dataSource, EntityManager em) {
//        this.dataSource = dataSource;
//        this.em = em;
//    }
//    //

    //Spring-JPA 사용할 때
    private final MemberRepository memberRepository;

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

    //스프링 빈 등록
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }


    //Spring-JPA 사용 할 때 전체 주석 걸기
//    @Bean
//    public MemberRepository memberRepository() {
////        return new MemoryMemberRepository();
////        return new JdbcMemberRepository(dataSource);
////        return new JdbcTemplateMemberRepository(dataSource);
//        return new JpaMemberRepository(em);
//    }
}

테스트 실행 -> 성공

스프링 데이터 JPA 제공 클래스
JpaRepository에 가면 자동으로 스프링데이터 JPA(findAll, save...)를 제공해주는 것을 확인할 수 있다.

스프링 데이터 JPA 제공 기능

  • 인터페이스를 통한 기본적인 CRUD
  • findByName() , findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공
  • 페이징 기능 자동 제공
실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는
라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적
쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를
사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.
profile
개발감자
post-custom-banner

0개의 댓글