Spring 1-6. 스프링 DB 접근 기술 (1)

JG's Development Blog·2023년 1월 29일
0

Spring

목록 보기
8/10

  • 이전까지는 메모리에 저장해 자바를 종료하면 데이터도 날라가버렸다.
  • 이번 장에서 데이터베이스를 다루는 법을 배울 것이다.
  • 다음 5가지를 순차적으로 배우게 된다.
  1. 데이터베이스 설치
  2. 데이터베이스 SQL과 애플리케이션 서버를 연결하는데에 필요한 기술인 jdbc를 배운다.
    -> 순수 jdbc로 20년전 개발하기 힘들었던 환경을 경험한다.
  3. 스프링에서 jdbctemplate를 제공하여 보다 편한 jdbc 개발이 가능하다.
  4. SQL을 직접 짤 필요 없이 JPA를 이용하여 보다 편리하게 개발이 가능하다.
  5. JPA를 더욱 편리하게 개발하기 위해 감싼 기술인 스프링 데이터 JPA를 배운다.

H2 데이터베이스 설치

  • 실무로 자주 쓰이는 mysql 계열의 데이터베이스이다.
  • 교육용으로 사용하기에 좋다.
  • 위 화면에서 연결 버튼을 누르면 자동으로 데이터베이스가 생성되는데 나는 다음과 같은 오류가 발생하였다.

    Database "C:/Users/사용자명/test" not found, either pre-create it or allow remote database creation (not recommended in secure environments)

  • 이는 해당 폴더에 test 파일이 없어서 생긴 문제로 해당 경로에 빈 텍스트 파일을 생성하고 파일명을 확장자 포함 'test.mv.db' 로 바꿔주고 다시 연결을 시도한다.
  • 이후 성공적으로 데이터베이스가 접속되었다면 JDBC URL을 바꿔서 접근해주어야한다.

    jdbc:h2:tcp://localhost/~/test

  • 다음 코드를 넣고 실행시켜 왼쪽에 MEMBER 파일이 생성되었는지 확인한다.

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

  • 다음 코드를 넣고 or member파일을 클릭하고 한번 더 실행한다.

    SELECT * FROM MEMBER

  • 확인하기 위해 다음 코드를 넣고 member 파일을 확인해본다.

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

  • 데이터베이스 또한 intelliJ를 통해 관리할 수 있다.

순수 JDBC

  • 옛날에 사용하던 기술이므로 완전히 익힐 필요없이 인지 정도만 하면 된다.
  • build.gradle 파일에 다음 코드를 추가한다.
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
  • 이때 오류가 뜨는데 오른쪽에 코끼리 버튼을 누르거나 오른쪽 gradle탭에서 새로고침 모양을 눌러준다.

    ※ 스프링부트 2.4 버전 이상은 다음 코드를 추가로 넣어주어야 한다.
    spring.datasource.username=sa

  • 새로운 구현체인 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);
     }
    }
  
- 코드에 대한 자세한 설명은 강의를 참고하도록 하자..
  
> 강의 사이트
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard
  
---
  
- 이제 우리가 가상 시나리오를 설정하여 구현체를 따로 만들어준 이유를 알 수 있다.
- SpringConfig에서 MemberRepository에게 반환하는 값을 변경해준다.
  ![](https://velog.velcdn.com/images/jg-konkuk/post/feedce9a-5b87-4c0f-bd45-33251e136380/image.png)
  
- 또한 위쪽에 다음 코드를 추가해준다.
```java
    private final DataSource dataSource;

    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
  • 이렇게 @Configuration만 만져주면 데이터베이스와 연결된다.

  • 이렇게 구현체를 쉽게 바꾸는 객체지향적인 기술을 스프링이 지원해주기 때문에 스프링을 사용한다.


스프링 통합 테스트

  • 아래 코드 넣기
  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
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 중복_회원_예외() throws Exception {
        //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에 데이터가 남지 않으므로 다음 테스트에 영향을 주지
    않는다
    -> 따로 DB를 초기화하는 코드가 필요없게 된다.

  • 기존에 했던 MemberServiceTest와의 차이점
    스프링 서버를 동작하지 않으므로 빠르게 실행된다.
    웬만하면 스프링 서버 동작 없이(스프링 통합 테스트) 하는 것이 좋은 테스트일 가능성이 높다.

강의 사이트
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard

profile
왕왕왕초보

0개의 댓글