[Spring] 스프링 DB 접근기술 - 1편

Gerry·2022년 4월 29일
0

스프링 입문

목록 보기
7/9
post-thumbnail

H2 데이터베이스

지금까지는 메모리를 이용하는 임시DB를 만들어서 회원들을 저장해왔지만 이 방식은 서버가 종료되면 데이터도 같이 날라가는 치명적인 단점이 존재했습니다.
이를 극복하기 위해 H2 데이터베이스를 활용해, 서버가 종료되어도 데이터를 저장할 수 있도록 설계를 바꿔보겠습니다.


💾 H2 데이터베이스 설치하기

우선 아래의 H2 데이터베이스 사이트에 접속해서 1.4.200 버전을 다운로드합니다.

https://www.h2database.com/html/main.html


다운로드가 완료되면 첫번째로 h2/bin 디렉토리에 들어가서 h2.sh 파일의 권한을 변경해주는 작업이 필요합니다. (원도우 사용자는 필요 X)
아래와 같이 chmod 755 h2.sh 명령어를 통해 권한을 변경해줍니다.

⚠️ chmod 755
소유자는 모든 권한 (읽기, 쓰기, 실행) 을 가지고 그룹 및 그 외 사용자는 읽기와 실행만 가능하도록 설정해주는 리눅스 명령어입니다.


권한을 변경해준 이후에 ./h2.sh 명령어로 파일을 실행시키면 아래와 같이 H2 데이터베이스 콘솔에 접속할 수 있는 사이트가 켜집니다. (원도우 사용자는 ./h2.bat)

맨 처음 접속할 때는 JDBC URL을 jdbc:h2:~/test 로 설정하고 연결을 클릭해 자신의 홈 디렉토리에 /test.mv.db 파일을 만들어주고, 그 다음부터는 JDBC URL을 jdbc:h2:tcp://localhost/~/test 로 설정해 접속해주시면 됩니다.


💾 테이블 생성하기

회원 데이터를 저장할 회원 테이블을 생성하기 위해 아래의 SQL문을 입력하고 Ctrl + 엔터 를 눌러 테이블을 생성해줍니다.

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문을 입력하고 SELECT * FROM MEMBER 로 확인해보면 아래와 같이 'Spring' 데이터가 입력된 것을 확인할 수 있습니다.


순수 JDBC

Jdbc는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API입니다. 순수 Jdbc만 이용해서 데이터베이스와 연결시키는 방식은 20년전 고대(?)의 방식이지만 '20년전에는 이렇게 구성을 했구나' 하고 알아보는 차원에서 순수 Jdbc만을 이용해 데이터베이스와 연동을 시켜보겠습니다.

💻 JDBC, H2 라이브러리 추가

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
spring.datasource.username=sa

💻 JDBC 회원 리포지토리 구현

Jdbc를 이용한 회원 리포지토리를 구현하기 위해 /hello.hellospring/repository 하위에 JdbcMemberRepository 를 생성하고 아래의 코드를 작성해줍니다.

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 파일을 아래와 같이 작성해 데이터베이스 연결을 기존의 MemoryMemberRepository 에서 JdbcMemberRepository 로 변경하면 설정이 마무리됩니다.

 @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);
      }
  • 기존의 MemoryMemberRepository 와 연결된 부분을 주석처리하고 JdbcMemberRepository(dataSource) 를 넣어줍니다.
  • DataSource 는 데이터베이스 커넥션을 획득할 때 사용하는 객체입니다. 스프링부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource 를 생성하고 스프링 빈을 만들어둠으로써 DI, 즉 의존성을 주입받을 수 있습니다.


👉 데이터베이스 연결 확인

이렇게 스프링의 의존성 주입을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있으며 프로젝트를 실행해 확인해보면 위에서 H2 데이터베이스에 저장해 놓았던 'Spring' 을 확인할 수 있습니다.


스프링 통합 테스트

💻 스프링 통합 테스트 코드작성

실제 프로젝트를 실행시켜봄으로써 H2 데이터베이스와 프로젝트가 연결된 것을 확인하였지만 테스트 코드로도 이를 확인해보기 위해 /test/java/hello.spring/service 하위에 MemberServiceIntegrationTest 를 생성하고 아래와 같이 테스트 코드를 작성해보겠습니다.

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest2 {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

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

        //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("이미 존재하는 회원입니다.");

    }
}
  • 이전의 테스트들은 순수한 자바코드를 가지고만 테스트를 진행했었지만, 이번 테스트는 스프링부트가 직접 데이터를 가져와야 하기 때문에 @SpringBootTest 어노테이션을 작성해 스프링 컨테이너와 테스트를 함께 실행하도록 합니다.
  • 실시된 테스트로 DB에 데이터가 남게되면 안되기 때문에 테스트 시작전에 트랜잭션을 시작하고, 테스트 완료 후에 롤백을 하도록 하는 @Transactional 어노테이션을 작성해줍니다.
  • @Autowired 어노테이션으로 memberServicememberRepository 가 필요한 의존 객체의 타입에 해당하는 빈을 스프링 IoC 컨테이너가 찾아서 주입해주도록 합니다.

⚠️ 트랜잭션이란?

  • 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용하고, 어떤 작업에서 오류가 발생했을 때는 이전에 있던 모든 작업들이 성공적이었더라도 없었던 일처럼 완전히 되돌리는 것이 트랜잭션의 개념입니다.
  • 데이터베이스를 다룰 때 트랜잭션을 적용하면 데이터 추가, 갱신, 삭제 등으로 이루어진 작업을 처리하던 중 오류가 발생했을 때 모든 작업들을 원상태로 되돌릴 수 있습니다.
    -출처: Tecoble

👉 스프링 통합 테스트

작성된 테스트 코드를 실행시키면 스프링이 띄워지면서 함께 테스트를 진행하게 됩니다.
아래와 같이 테스트가 정상적으로 통과된 모습을 확인할 수 있습니다.

🙏 이 포스트는 김영한 개발자님의 <스프링 입문 강의> 를 듣고 공부한 내용을 바탕으로 작성되었습니다.

1개의 댓글

comment-user-thumbnail
2022년 9월 2일

nICE👌

답글 달기