Hi yoahn 개발블로그

[Spring] #6 스프링 DB 접근 기술1 본문

Framework & Library/springboot

[Spring] #6 스프링 DB 접근 기술1

hi._.0seon 2021. 1. 25. 18:36
반응형

1. H2 데이터베이스 설치

https://www.h2database.com

 

H2 Database Engine (redirect)

H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo

www.h2database.com

download 후 터미널로 들어가서

cd h2/bin/

chmod 755 h2.sh

// DB 서버 실행

접속이 안되면 앞에 ip 주소만 localhost 로 변경

  • DB 파일 생성 방법
    • 최초: jdbc:h2:~/test    뒤에는 데이터베이스 파일 저장될 경로
    • ~/test.mv.db 파일 생성 확인
    • 이후 접속 시, jdbc:h2:tcp://localhost/~/test 로 접속

제대로 접근이 되지 않는 경우, 데이터베이스 파일을 삭제하고 다시 생성

 

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

- Long => DB : bigint 타입

- generated by default as identity

NULL이 들어왔을 때, 자동으로 id 값을 채워줌

INSERT INTO MEMBER(name) VALUES('spring');
INSERT INTO MEMBER(name) VALUES('spring2');

 

2. 순수 JDBC

환경설정

build.gradle

- java는 db랑 연결하기 위해서 JDBC Driver가 꼭 있어야 한다

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

- db 연결시 db 클라이언트 필요 => h2 (2번째줄)

 

스프링 부트 데이터 베이스 연결 설정 추가

resources/application.properties

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

>>>>>>  연결 완료 <<<<<<<<

 

repository/JdbcMemberRepository.java

public class JdbcMemberRepository implements MemberRepository {

    private final DataSource dataSource;

    public JdbcMemberRepository(DataSource dataSource) {
        // spring을 통해 db 주입받음
        this.dataSource = dataSource;

    }
    
    ...
}

db와 연결할 repository 생성

- pstmt.executeUpdate(); DB 업데이트 

    @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);
            // sql을 넣고, 자동으로 생성된 id 값이 반환됨
            
            pstmt.setString(1, member.getName());
            // sql의 첫번째 물음표에 name 삽입

            pstmt.executeUpdate();  // DB에 실제 쿼리 날라감
            rs = pstmt.getGeneratedKeys();
            // insert 후 반환된 id값을 꺼내줌
            
            if (rs.next()) { // 값이 있으면 값을 꺼내서 id 세팅
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
        // 사용한 자원들 리소스 반환
            close(conn, pstmt, rs);
        }
    }

- 조회

pstmt.executeQuery(); 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); // sql의 ? 에 id 값 세팅
        
            rs = pstmt.executeQuery(); // DB 쿼리 날림
            
            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 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);
        }
    }

    @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);
        }
    }
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }
    
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, 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 를 통해서 getConnection을 가져와야 한다. DB 트랙잭션에 걸릴수 있는데 이렇게 가져와야 정상적으로 처리

close 도 동일

 

스프링 config => 빈 수정

@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 JdbcMemberRepository(dataSource);
//        return new MemoryMemberRepository();
    }
}

- DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.

 

h2 데이터베이스 서버 실행중인 상태로 서버 실행

구현 클래스 추가 이미지

  • MemberService -> MemberRepository 의존
  • MemberRepository의 구현체

    - MemoryMemberRepository
    - JdbcMemberRepository

  • 개방-폐쇄 원칙 (OCP, Open-Closed Principle)
    - 확장에는 열려있고, 변경에는 닫혀있다.
  • 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
    (스프링 빈)
  • 회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자.
  • 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.

3. 스프링 통합 테스트

- 스프링을 데이터베이스와 연결하였기 때문에 자바 코드만으로 테스트 불가

- 스프링이랑 엮어서 테스트

test/service/MemberServiceIntegrationTest.java

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberRepository memberRepository;
    @Autowired MemberService memberService;
    // 구현체는 SpringConfig에서 올라옴

    @Test
    void join() {
        // 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
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        // 메세지 검증 위해 반환되는 데이터 저
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");

        /*try {
            memberService.join(member2);
            fail("예외가 발생해야 합니다");
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");
        }*/
        // then
    }

}

@SpringBootTest

- 스프링 컨테이너와 테스트를 함께 실행

 

@Transactional

- 테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작, 테스트 완료 후 항상 롤백

- DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다

 

@Autowired

- 테스트 케이스는 테스트를 다른 곳에서 가져다 쓰는 것이 아니므로, 필드 주입으로 편하게 사용

 

스프링 통합테스트도 필요할 때가 있지만, 느리다

순수한 단위 테스트가 더 좋은 테스트일 확률이 높음

반응형
Comments