관리 메뉴

나구리의 개발공부기록

스프링 DB 접근 기술 1 - H2데이터베이스 설치, 순수JDBC, 스프링 통합 테스트 본문

인프런 - 스프링 완전정복 코스 로드맵/코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술(무료)

스프링 DB 접근 기술 1 - H2데이터베이스 설치, 순수JDBC, 스프링 통합 테스트

소소한나구리 2024. 1. 24. 15:39

H2데이터베이스 설치

 

  • https://www.h2database.com 접속 후 다운로드 후 압축해제
  • (Mac ver.)터미널 접속 -> 압축해제폴더/bin 으로 경로 이동
    • cd 폴더의경로/bin
  • 권한 부여 : chmod 755 h2.sh
  • 실행 : ./h2.sh -> 잠시후 웹사이트 이동 -> 연결 클릭
    • 만약 홈페이지가 안뜬 다면 아래 이미지 url 중 localhost 부분이 ip로 작성 되어있을 텐데 localhost로 변경
    • 왼쪽 상단 빨간 N 모양을 클릭하면 나갈 수 있음
 

좌) 실행 창 / 우) 데이터베이스 생성

  • 아래의 터미널 창을 끄면 DB가 종료됨

  • 새로운 터미널을 열어 Home(터미널을 키면 초기 위치인 Home 디렉토리)에 test.mv.db 파일이 있는지 확인

  • H2데이터베이스 설치 이후 접근 방법 -> JDBC URL을 아래 이미지처럼 변경 후 연결

 

H2 데이터 베이스 테이블 생성 및 활용

 

  • SQL문 입력 후 실행 -> 왼쪽 카테고리에 MEMBER 생성 완료
drop table if exists member CASCADE;
create table member
(
	-- bigint = 자바에서의 long
    -- 값을 세팅하지않고 insert하면 db에 데이터가 들어왔을 때 자동으로 id값을 채워 줌
     id   bigint generated by default as identity,
     name varchar(255), -- varchar: 문자열
     primary key (id)
);

  • SELECT * FROM MEMBER 구문으로 조회하거나 왼쪽 카테고리의 MEMBER를 클릭해서 조회

  • 데이터 입력 -> 아래의 SQL구문 입력
insert into member(name) values('spring')
insert into member(name) values('spring2')

좌) 구문입력 / 우) 조회 - 결과

참고사항

 

  • DDL관리
    • DDL ( Data Definition Language)은 데이터베이스 스키마를 정의하는 일련의 SQL 명령
    • 별도의 디렉토리를 만들고 파일을 만들어서 관리(src 밖에)

 


순수JDBC 

 

  • 지금은 안쓰는 과거의 방식 - 흐름을 파악하는데에 중점
  • build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 입력 - 입력 후 우측 상단 코끼리버튼 클릭(새로고침)

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

  • ~/resources/application.properties 파일에 경로 입력 (스프링부트에 설정)
// url = h2 db의 url
spring.datasource.url=jdbc:h2:tcp://localhost/~/test

// h2db에 접근하는 드라이버
spring.datasource.driver-class-name=org.h2.Driver

// 스프링부트 2.4부터는 `spring.datasource.username=sa` 를 꼭 추가해주어야 함
// 그렇지 않으면 `Wrong user name or password` 오류가 발생 (공백은 꼭 제거)
spring.datasource.username=sa

jdbc 리포지토리 구현

 

  • JdbcMemberRepository class작성
package start.startspring.repository;

import org.springframework.jdbc.datasource.DataSourceUtils;
import start.startspring.domain.Member;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {

    //DB에 붙으려면 DataSource가 필요
    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 class 수정
    • 실제 애플리케이션에 관련된 코드는 손을 대지않고 db를 바꿈 (다형성을 활용)
@Configuration
public class SpringConfig {

    // DataSource 주입
    private DataSource dataSource;

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

    // Bean을 등록
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository(); // 메모리리포지토리
        return new JdbcMemberRepository(dataSource); // h2db리포지토리
    }
}
  • 실행 후 localhost 및 db에서 결과 확인

객체지향설계가 좋은 이유(다형성을 활용)

 

  • assembly 코드(조립하는 코드)만 수정하면 원래의 애플리케이션을 구동하는 코드는 수정이 없어도 됨
    • 다형성 개념을(인터페이스, 상속 등) 활용하면 이렇게 기능을 완전히 변경하는데에 유리함
  • 개방-폐쇄 원칙(OCP, Open-Closed Principle)이 지켜짐.
    • 확장(기능을 추가 하는 것 등)에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI (Dependency Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경 가능
  • DB에 Data가 저장 되므로 서버를 내렸다 올려도 데이터가 유지 됨

스프링 통합 테스트

 

  • MemberServiceIntegrationTest Test class를 새로 작성
  • @SpringBootTest
    • 스프링 컨테이너와 테스트를 함께 실행(스프링을 띄움)
  • @Transactional
    • 테스트가 끝나면 롤백이 됨(db에 데이터가 남지않음 - 반복해서 테스트가 가능)
    • @Transactional이 없으면 db에 데이터가 남음
    • 테스트케이스에 붙었을 때만 롤백하고 일반적인 서비스 등에 적용하면 정상적으로 데이터가 붙음
// 스프링 컨테이너와 테스트를 함께 실행(스프링을 띄움)
@SpringBootTest
// 테스트가 끝나면 롤백이 됨(db에 데이터가 남지않음 - 반복해서 테스트가 가능)
// @Transactional이 없으면 db에 데이터가 남음
@Transactional
class MemberServiceIntegrationTest {

    // 테스트케이스는 Autowired를 바로 작성해도 됨
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        //given - 준비
        Member member = new Member();
        member.setName("spring");

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

        //then - 결과
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

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

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

        //when
        // assertThrows 활용
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

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

 

그렇다면 스프링없이 memory에서 하는 테스트는 필요 없는 것인가?

 

  • 순수하게 java코드로 최소한의 단위로 테스트 = 단위테스트
    • 통합 테스트도 필요하지만 단위테스트를 잘 만드는 것이 더 좋은 테스트를 하는 것이라고 볼 수 있다
  • 스프링 컨테이너와 DB까지 연동해서 하는 테스트 =  통합테스트

출처 : 인프런 - 스프링 입문(무료) / 김영한님

https://inf.run/hivx6