웹 프로그래밍/공부, 연습

[강좌]스프링 입문 - 6. 스프링 DB 접근 기술(1)

이 글은 인프런에서 김영한 님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술"을 수강 후 개인적으로 공부한 내용을 정리한 게시글입니다. 잘못된 점이나 부족한 부분이 있다면 언제든 지적 부탁드립니다.
해당 강의는 이곳에서 수강할 수 있습니다.

 

이전 강의에서는 회원 관리 예제를 통해 웹 MVC 개발 원리를 학습하였습니다. 

이번 강의에서는 스프링 DB 접근 기술 중 첫 번째로 H2데이터베이스를 활용해 순수 Jdbc와 JdbcTemplate에 대해 학습합니다.

 

 

이전 강의에서까지 사용했던 메모리 리포지토리는 서버를 내리면 저장된 회원 데이터가 전부 사라진다는 문제가 있었습니다. 이번 강의에서는 스프링에서 데이터베이스를 통해 데이터에 액세스하는 방법에 대해 실습하고 학습해 봅니다.

 

1. H2 데이터베이스 설치

 

일반적으로 실무에서는 MySQL 또는 Oracle과 같은 데이터베이스를 주로 사용합니다. 

이번 강의에서는 교육용 데이터베이스인 H2를 사용합니다. H2는 가볍고 용량이 작다는 장점이 있어 이번 예제에서 사용하도록 합니다. H2는 웹 화면으로 콘솔 화면 또한 제공하여 사용하기 편리합니다.

 

링크를 통해 H2를 다운로드, 설치합니다.

윈도우 기준 설치 후 콘솔을 실행하면 다음과 같은 웹 화면을 볼 수 있습니다.

 

다른 것들을 변경하지 않고 연결을 하면 다음과 같은 화면을 확인할 수 있습니다.

 

 

왼쪽 맨 위 버튼을 통해 밖으로 나간 후, 홈 디렉토리(~)에 test.mv.db 파일이 생성되었는지 확인합니다.

 

 

위 파일이 생성된 것을 확인하면 다시 웹 콘솔창에서 다음과 같이 JDBC url을 변경합니다.

 

이전에는 파일을 통해 H2에 접근했다면 이제부터는 소켓을 통해 접근합니다. 파일로 접근하는 방식은 애플리케이션과 웹 콘솔이 동시에 동작시 오류가 생길 위험이 있지만, 소켓을 통해 H2에 접속하면 여러 군데서 접근이 가능합니다.

 

Member 테이블을 작성해 봅니다.

콘솔 창에 다음과 같이 sql을 작성, 실행합니다.

 

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

 

기존에 작성한 회원 객체 클래스입니다

 

회원 객체의 멤버 변수 중 id는 자바에서는 Long이지만 sql에서는 bigint로 표현되어있습니다.

generated by default as identity는 해당 속성값을 지정하지 않고 insert시 DB가 자동으로 값을 채워주는 코드입니다.

회원 리포지토리에서 squence의 값을 하나씩 증가시켜 id로 저장해준 것과 같은 것으로 이해할 수 있습니다.

 

위 sql코드를 실행 후 테이블이 생성된 것을 확인할 수 있습니다.

 

 

DB 작동 확인을 위해 

insert into member(name) values('spring') 을 입력, 실행하면 명령이 동작하고, 테이블에 값이 저장된 것을 확인할 수 있습니다.

 

 

 

2. 순수 Jdbc

 

지금부터 애플리케이션에서 DB를 접근, 데이터를 넣고 빼는 것을 순수 Jdbc 버전, 즉 옛날 방식(?)으로 구현해 봅니다.

이 장에서 사용하는 방식은 현재는 많이 쓰이지 않는 방식임을 미리 확인해두고 진행합니다.

 

먼저 자바와 데이터베이스를 연결하기 위해 환경 설정을 합니다.

 

build.gradle 파일에 다음과 같은 라이브러리를 추가해 줍니다.

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

 

위 라이브러리는 각각 jdbc, h2 데이터베이스와 관련된 라이브러리입니다. 참고로 위는 자바가 DB를 사용하기 위해 필요한 Jdbc 드라이버를 위한 라이브러리, 아래는 h2가 제공하는 DB클라이언트를 위한 라이브러리입니다.

 

다음은 스프링 부트가 데이터베이스를 연결할 수 있도록 연결 설정을 추가합니다.

source/application.properties 파일에 다음과 같은 코드를 작성합니다.

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

 

(이때 h2와 Driver에 error가 생긴다면 build.gradle의 sync를 맞춰줍니다.)

데이터베이스의 경로를 지정해 주었고 드라이버를 설정, 데이터베이스 회원 정보를 등록하였습니다. 위와 같이 코드 작성을 완료하였다면 데이터베이스에 접근할 준비가 되었습니다. 

 

이제는 Jdbc API를 가지고 Jdbc리포지토리를 구현합니다.

이제까지는 리포지토리 인터페이스를 MemoryMemberRepository로 구현하였다면, 이제는 실제 데이터베이스로 리포지토리를 구현합니다.

 

repository 폴더에 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);
    }
}

 

코드가 매우 길고 복잡해 보이지만 이러한 구현방식은 약 20년 전에 사용하던 방법(이라고 합니다 휴..)이라는 것에 주의하고 참고만 하도록 합니다.

 

구현한 클래스를 위에서부터 살펴보면 해당 리포지토리는 dataSource라는 DataSource객체를 갖습니다.

이 DataSource는 JdbcRepository가 생성될 때 스프링에 의해 주입됩니다.

 

DB에서 데이터를 가지고 오기 위해 연결을 하기 위해서는 Connection이 필요한데 이는 DataSourceUtils에 의해 dataSource를 넘겨 가져올 수 있습니다. (마찬가지로 DataSourceUtils를 통해 Connection을 release 합니다.)

 

구현한 save() 함수는 회원을 DB에 등록하는 함수입니다.

sql을 정의하고 Connection, PreparedStatement, ResultSet객체를 정의해 놓고 진행합니다. 

conn을 받고, sql과 RETURN_GENERATED_KEYS을 설정하고 pstmt에 받습니다. 이때 RETURN_GENERATED_KEYS은 DB에 insert 할 때 insert를 한 후에야 할 수 있는 key값인 1,2,3... 과 같은 값들을 가져올 수 있도록 설정합니다.

setString메서드는 1로 sql의 '?'와 매칭을 하고 그곳에 getName()으로 회원의 이름을 넣습니다.

executeUpdate() DB에 쿼리를 전송하고, 다음 줄에서 생성된 key를 받아 rs에 받습니다. 이때 위에서 설정한 RETURN_GENERATED_KEYS과 연관이 있습니다.

결과 값(생성된 key값)이 있으면 이를 꺼내 Long으로 바꾼 후 회원의 id로 설정해 준 후 member 객체를 반환합니다.

마지막으로 예외처리를 전부 한 후에는 받아온 자원들을 close로 해제해 주어야 합니다. 해제해 주지 않으면 db connection이 계속 쌓이고 이후 문제의 원인이 될 수 있습니다.

 

다음에 구현된 findById() 함수는 id로 특정 회원을 검색하는 함수입니다.

위 save() 함수와 다른 점은 executeUpdata()를 사용하지 않고 executeQuery()를 사용한다는 점입니다. DB에 저장(갱신)할 때는 executeUpdata()를 사용하고 DB를 조회할 때는 executeQuery()를 사용합니다.

결괏값을 rs로 받고 값이 있다면(검색되는 회원 객체가 있다면) 회원 객체를 생성, 반환된 회원 객체의 id와 name을 가져와 세팅하고 해당 객체를 반환합니다.

 

findAll() 함수는 등록된 모든 회원을 리스트 형태로 검색하는 함수입니다.

등록된 모든 회원을 검색하니 sql은 비교적 간단합니다. 쿼리를 전송해 받은 결과값 rs가 있다면 ArrayList를 하나 생성, rs를 돌며 회원 객체를 생성, id와 name을 세팅하고 ArrayList에 담아 리스트를 반환합니다.

 

findByName() 함수는 name변수로 특정 회원을 검색하는 함수입니다.

동작은 findById() 함수와 비슷합니다.

 

 

이렇게 Jdbc 회원 리포지토리 구현을 마쳤다면 Config파일을 수정합니다. 

SpringConfig 파일은 '4. 스프링 빈과 의존관계'에서 설명한 것처럼 @Bean을 사용해 스프링 빈을 등록하는 파일입니다.

기존에는 해당 파일에서 memberRepositoryMemberMemoryRepository 구현체를 사용하고 있었습니다.

해당 파일을 다음과 같이 수정합니다.

 

package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    
    //@Autowired
    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 DB구현체를 사용하도록 변경합니다. 이때 Config파일이 생성되면서 DataSource를 스프링으로부터 받아 멤버 변수로 갖고 있다가 리포지토리가 스프링 빈으로 등록될 때 이 dataSource를 넘겨주도록 설정합니다. 참고로 @Autowired는 생성자가 하나일 때 생략 가능하므로, 이때 @Autowired 또한 생략 가능합니다.

(DataSource는 DB Connection을 획득할 때 사용하는 객체로 스프링 부트는 DB Connection 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어 두어 DI를 받을 수 있습니다.)

 

중요한 것은 기존의 다른 어떤 코드를 변경하지 않고 단순히 인터페이스를 확장한 JdbcMemberRepository를 만들고 Config 파일만 변경하여 리포지토리 구현체를 변경했다는 것입니다. 인터페이스를 두고 손쉽게 구현체를 변경하였습니다. 이것을 우리는 '다형성'을 활용했다고 하며, 스프링은 이런 것을 편리하게 사용할 수 있도록 지원합니다. 

 

기존에는 서비스의 코드가 리포지토리를 의존하는 코드라면 리포지토리가 변경될 때 서비스 코드까지 변경해야 하는 경우가 생겼다면 이제는 스프링의 DI을 사용하여 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경할 수 있습니다.

 

이미지 출처는 해당 강의 자료입니다

 

이미지 출처는 해당 강의 자료입니다

위의 이미지를 통해 살펴보면 멤버 서비스는 리포지토리 인터페이스를 의존하고, 이 인터페이스는 MemoryMemberRepository와 JdbcMemberRepository 구현체로 각각 구현되어있습니다. 아래 이미지는 기존에 MemoryMemberRepository를 사용하던 멤버 서비스가 Config 변경을 통해 JdbcMemberRepository을 사용하는 것을 보여줍니다.

 

이러한 것을 개방-폐쇄 원칙(OCP; Open-Closed Principle)이라고 합니다. 이는 확장에는 열려있고, 수정과 변경에는 닫혀있는 개발 방식을 말합니다. 객체지향의 다형성 개념을 활용하여 기능을 완전히 변경하더라도 애플리케이션 전체 코드를 수정하지 않고 조립(Configuration)만을 수정, 변경하는 것을 의미합니다.

 

위의 Config파일 수정까지 마쳤다면 프로젝트를 실행해 봅니다.

 

홈 화면에서 회원 목록 링크로 이동하면 회원 가입으로 회원을 추가하지 않았음에도 회원 객체가 존재하는 것을 확인할 수 있습니다. 

이제는 데이터를 메모리 리포지토리가 아닌 DB에 저장하기 때문에 스프링 서버를 내렸다가 다시 시작하더라도 이전 데이터가 저장됩니다.

회원 가입 화면에서 새로운 회원을 등록하고 다시 한번 확인하면 새로운 회원이 추가된 회원 목록을 확인할 수 있습니다.

 

 

위와 같이 h2콘솔에서도 또한 데이터베이스에 저장된 member객체들을 확인할 수 있습니다.

 

 

3. 스프링 통합 테스트

 

'3. 회원 관리 예제 - 백엔드 개발'에서 진행했던 테스트는 스프링을 띄우지 않고 진행했던 순수한 자바 코드로 진행한 단위 테스트입니다.

DB 연결을 마쳤으니 이제 스프링 컨테이너DB를 연결하여 실행하는 통합 테스트를 진행해 봅니다.

 

test/java/프로젝트명/service에 MemberServiceIntegerationTest 파일을 생성하고 다음과 같은 코드를 작성합니다.

 

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.AssertionsForClassTypes.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
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("spring");

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

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertEquals(member.getName(),findMember.getName());
    }

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

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

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

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

 

회원가입()과 중복_회원_예외() 테스트 코드는 이전 테스트와 동일한 코드입니다.

 

@SpringBootTest를 사용하면 스프링 컨테이너와 테스트를 같이 실행합니다.

이전에는 @BeforeEeach를 사용해 각 테스트를 실행하기 전 회원 서비스와 리포지토리를 직접 객체 생성, 주입했지만 이제는 @SpringBootTest@Autowired를 사용해 컨테이너에서 스프링을 통해 서비스와 리포지토리를 주입할 수 있습니다. (회원 리포지토리는 이전의 MemoryMemberRepository 객체에서 JdbcMemberRepository를 사용하는 MemberRepository 객체로 변경되었습니다.)

 

일단 테스트를 실행하면 다음과 같은 error를 확인할 수 있습니다.

 

 

 

중복된 회원이 존재한다는 에러 메시지입니다. h2 콘솔을 확인하면 이전 실습을 진행하면서 DB에 저장된 데이터가 존재하여 중복이 발생합니다. 'delete from member'로 모든 데이터를 지우고 진행합니다.

 

테스트가 정상 작동합니다. (테스트 실행 시 @SpringBootTest로 스프링이 같이 올라오는 것을 확인할 수 있습니다.)

 

 

@Transational 애노테이션은 테스트 시작 전 트랜잭션을 실행, 실행 후 롤백을 수행하여 DB에 데이터가 반영되지 않도록 합니다. 따라서 @Transation이 없다면 테스트 후 DB에 데이터가 반영되어 데이터가 쌓입니다. 위 애노테이션을 주석 처리 후 테스트를 실행하면 다음과 같은 error를 확인할 수 있습니다.

 

 

h2콘솔을 확인하면 중복_회원_예외() 테스트 진행 후 데이터가 남아있어 회원가입() 테스트에서 error가 발생한 것임을 알 수 있습니다. 

 

 

다시 DB의 데이터를 모두 지우고 @Transational을 추가 후 테스트를 진행하면 테스트가 정상작동, h2 DB에도 데이터가 반영되지 않은 것을 확인할 수 있습니다.

 

 

 

위에서 설명했던 것 같이 @Transational을 사용하면 트랜잭션 수행 후 롤백을 수행하여 테스트를 수행해도 DB에는 데이터가 반영되지 않습니다. 그러므로 다음 테스트 또한 영향을 받지 않습니다.

 

이번 장에서 스프링과 DB을 모두 연결한 통합 테스트를 진행하였지만 그렇다고 이전에 수행한 순수 단위 테스트가 의미 없는 것은 아닙니다. 이전의 테스트를 진행하면 위의 통합 테스트보다 훨씬 속도가 빠름을 확인할 수 있습니다.

 

스프링과 DB를 연결할 필요가 없어 속도가 훨씬 빠릅니다

 

이렇게 상황에 따라 컨테이너와 DB를 올릴 필요 없이 테스트를 할 수 있는 단위 테스트가 더 좋은 경우일 수 있습니다.

어떻게 단위 테스트를 잘 작성하느냐 또한 고민해볼 만한 문제입니다.

 

 

4. 스프링 JdbcTemplate

 

스프링 JdbcTemplate, MyBatis와 같은 라이브러리는 순수 Jdbc API 코드에서의 반복되는 부분을 대부분 제거해 준다는 장점이 있습니다. (단 SQL문은 직접 작성해야 합니다)

위에서 Jdbc를 사용해서 구현한 JdbcMemberRepository를 JdbcTemplate 라이브러리를 사용해 더 간단하게 구현해 봅니다.

 

JdbcTemplate을 사용하기 위한 설정은 순수 Jdbc를 사용하기 위한 설정과 동일하므로 바로 리포지토리를 구현해 봅니다.

 

repository 폴더에 JdbcTemplateMemberRepository 파일을 생성, 다음과 같은 코드를 작성합니다.

 

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

public class JdbcTemplateMemberRepository implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource){
        jdbcTemplate = new JdbcTemplate(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());
        return member;
    }

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

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

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

    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

 

JdbcMemberRepository와 같은 코드를 훨씬 짧고 간단하게 작성하였습니다. 

 

jdbcTemplate을 사용하기 위해 생성자에서 dataSource를 받아 jdbcTemplate을 생성할 때 이를 인자로 넘겨주고 jdbcTemplate 객체를 생성합니다. 이때 생성자가 하나이기 때문에 @Autowired를 생략할 수 있습니다.

 

save() 함수에서는 위 코드와 같이 jdbcTemplate을 사용하여 sql 쿼리 작성을 생략할 수 있습니다. 설정을 통해 쿼리를 작성, 전송하여 생성된 key를 받고 반환된 id를 setting 하여 회원 가입된 회원 객체를 반환합니다.

 

나머지 함수에서는 중복되는 코드를 memberRowMapper() 함수로 작성하고 jdbcTemplate라이브러리를 사용하여 이를 sql과 함께 넘겨주어 sql의 결과를 List형태로 받았습니다.

memberRowMapper() 함수는 이전에 중복되었던 코드 부분으로 회원 객체를 생성하고 반환된 결과 값을 세팅해 반환합니다.

 

JdbcTemplate의 자세한 사용방법은 document를 통해 참고할 수 있습니다.

 

 

jdbcTemplate을 사용하는 리포지토리를 구현하였으니 이제 이 구현체를 사용하도록 설정합니다.

SpringConfig 파일의 memberRepository() 생성자 부분을 다음과 같이 수정합니다.

 

    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository();
        //return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }

 

기존에 JdbcMemberRepository 구현체를 사용하던 코드를 주석 처리하고 JdbcTemplateMemberRepository를 사용하도록 변경, 생성 시 dataSource를 넘겨줍니다. 

 

여기까지 수정을 마치고 프로젝트를 실행합니다.

위에서 DB까지 연동하여 test 하는 통합 테스트 코드를 작성하였기 때문에, 이제는 웹 애플리케이션을 직접 실행하여 회원가입, 회원 조회를 할 필요 없이 MemberServiceIntegrationTest 코드를 실행하여 테스트를 진행합니다.

 

 

JdbcTemplate 버전으로 DB까지 연동하여 작동하는 테스트가 성공적으로 실행된 것을 확인할 수 있습니다.

 

 

 

이번 강의에서는 H2데이터베이스를 활용하여 순수 Jdbc와 JdbcTemplate과 같은 스프링의 DB 접근 기술에 대해 학습하였습니다.

다음 강의에서는 스프링 DB 접근 기술 중 JPA에 대해 학습합니다.