Java - Spring &&n SpringBoot

Spring - DB 연동(1) : H2 DB, 순수 JDBC, JdbcTemplate

TerianP 2021. 12. 21.
728x90

이전까지는 회원가입 데이터가 자바 스프링 메모리에 저장되는 방식이었다. 이 때문에 스프링을 종료시키면 당연하게도 저장된 정보가 모두 날아가버렸다. 이번에는 이런 문제가 없도록 그리고 보통 실무에서 기본적으로 사용하는 것처럼 DB 접근을 통해서 회원 가입과 조회가 이루어지도록 해보겠다. 

 

이번 글에서는 H2 DB 설치와 순수하게 JDBC만을 사용해서 spring 과 DB 를 연동하고 회원 가입이 잘 되는지 확ㅇ니해보겠다.

1. H2 데이터 베이스 설치하기

  • H2 DB는 특히나 공부하기 좋은 DB로 웹으로 admin 환경을 제공(GUI) 하며, 설치도 따로 필요 없고 용량도 작다는 장점이 있다.
  • 내가 공부하기 위해 사용할때는 MySQL로 바꿀 예정이다.

 

1) H2 DB 설치하기

  • 아래 사이트에서 1.4.200 버전의 Platform-Independent Zip 를 다운로드한다. 이쪽이 설치 없이 진행할 수 있어 개인적으로 더 편했다.

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

 

Downloads

Downloads Version 2.0.202 (2021-11-25) Windows Installer (SHA1 checksum: f6f6f91c67075a41ce05bdfc4499ee987dacb02e) Platform-Independent Zip (SHA1 checksum: e4a6c2e54332304cb4acbe48b55f9421c7f4b870) Version 1.4.200 (2019-10-14), Last Stable Windows Installe

www.h2database.com

Platform-Independent Zip!!

 

  • 다운로드 후 압축을 풀고, h2 폴더 안 bin 디렉토리 안에 있는 h2-1.4.200.jar 을 실행한다.
  • 그 후 인터넷창에서 http://localhost:8082/ 로 들어간다. 그러면 아래와 같은 화면이 나올때 일단 연결을 한번 눌러준다.

  • 연결을 누른 후 내 컴퓨터 > 로컬 C > 윈도우 사용자 명 디렉토리 안에서 test.mv.db 가 있는지 확인한다.

확인!!

  • 이후 다시 H2 DB 화면으로 와서 URL 부분을 아래와 같이 고친다. 그 후 연결을 눌러 제대로 DB 가 보이는지 확인하면 끝!!

수정!!

 


2. JDBC 로 DB 다루기

  • JDBC 란 Java Database Connectivity 의 약자로 자바에서 DB 프로그래밍을 하기 위해 사용되는 API이다.
  • 각 DBMS 회사에서 제공하는 JDBC 드라이버를 받아서 사용하면 DB 종류에 상관없이 사용 가능하다.
  • 스프링에서 DB로 연결되는 과정은 다음과 같다.
    • spring 에서 DataSource 즉 DB에 접속할 수 있는 접속 정보를 생성해둠 -> 스프링을 통해 DataSource 에 DB 접속 정보를 주입 -> sql 문을 통해서 DB에 쿼리를 실행 -> DB 커넥션 정보를 모두 종료

 

0) DB 에 테이블 생성하기

DB 에 접속해서 요렇게 입력한다.

1) application.properties 파일 수정

spring.datasource.url=jdbc:h2:tcp://localhost/~/test // DB URL
spring.datasource.driver-class-name=org.h2.Driver // DB 드라이버
spring.datasource.username=sa // DB 접근을 위한 ID

 

2) build.gradle 중 dependencies 부분에 H2 DB와 jdbc 사용을 위해 아래 내용 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2' // DB가 제공하는 클라이언트

 

이후 오른쪽 위에 아래 사진과 같은 코끼리 아이콘이 하나 보이는데 이를 눌러서 관련 정보를 설치, 업데이트 해준다.

요렇게 코끼리가 보여요!

 

3) JdbcMemberRepository 생성

  • H2 DB로 접근하여 정보를 저장하기 위한 repository 자바 파일 생성하는데...이거는 JPA 방식이 아니고 완전 순수한 JDBC 를 이용한 방식이여서 코드가 엄청나게 복잡하다. 때문에 다 옮겨적진 않을꺼고 필요한 부분과 내가 수정한 부분만 적어두겠다.
  • 여기서 코드 모두를 알아야 할 필요는 없고, 다음 부분만 주의해서 공부하면 될 듯 하다.
    • 첫번째는 이 JdbcMemberRepository 는 앞서 이전에 만들어두었던 MemberRepository 인터페이스의 구현체 중 하나라는 점이다. 자바 객체 지향의 가장 큰 장점 중 하나는 이렇게 interface 로 만들어두었던 것을 구현체로 만들고 그것을 갈아 끼움으로써 보다 쉽게, 간결하고 편하게 수정이 가능하다. 참고로 다른 하나의 구현체는 MemoryMemberRepository
    • 순수하게 아래처럼 JDBC 를 이용해서만 코드를 짜면 엄청 아주아주 복잡해진다. 왜냐하면 위에 잠깐 설명한 DB connection 정보를 가져오고 PrepareStatement 사용해서 쿼리문을 넣어주고, 제대로 넣어지면 DB 커넥션 정보를 닫아주고 를 모두 코드로 짜주어야 하기 때문이다. 심지어 이와중에 try ~ catch ~ 를 넣어 exception 에 대비해야한다.
    • PrepareStatement 를 사용하면 insert into member(name) values(?) 식의 쿼리문에서 ? 로 되어진 첫번째 파라미터를 pstmt.setString(1, member.getName()) 요런식으로 지정해서 넣을 수 있다.
    • 자세한 내용은 아래 주석문으로 적어두었다. 참고로 강의에서는 ID 로 지정해둔 부분을 나는 code 로 넣어두고 ID 는 따로 만들었기 때문에 아래 내용은 이런 부분을 수정한 내용이다.
      • 사실 간단하게 id 를 넣거나 확인하는 부분을 code 로 바꿔주었다.
import HJproject.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(?)"; //  sql문
        Connection conn = null; // 접속 정보
        PreparedStatement pstmt = null;
        ResultSet rs = null; // 결과를 받기 위한 변수
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            // DB에서 회원가입 정보가 들어갈때 자동으로 들어가는 Code 정보 가져오기
            pstmt.setString(1, member.getName()); // 파라미터 정보 1번, 즉  values(?) 부분
            pstmt.executeUpdate(); // 실제 쿼리가 DB로 전달
            rs = pstmt.getGeneratedKeys(); // code 값 가져옴
            if (rs.next()) {
                member.setCode(rs.getLong(1));
            } else {
                throw new SQLException("code 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }

    @Override
    public Optional<Member> findByCode(Long code) {
        String sql = "select * from member where code = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, code);
            rs = pstmt.executeQuery(); // 조회하기 위한 쿼리문
            if(rs.next()) {
                Member member = new Member();
                member.setCode(rs.getLong("CODE"));
                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.setCode(rs.getLong("CODE"));
                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.setCode(rs.getLong("CODE"));
                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() {
        // 스프링을 통해서 DB Connection 를 얻을 때는 반드시 DataSourceUtils 를 통해 얻어야한다.
        // 이는 DB 커넥션 정보 유지를 위해서
        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);
    }
}

 

4) SpringConfig 수정

  • 사실 JDBC 를 공부하면서 가장 아주아주 매우매우 중요한 부분이다. 바로 의존성 주입 DI 가 확실하게 나타는 대목이기도 하며 객체지향의 엄청난 장점을 보여주는 부분이다.
  • 이전에 MemoryRepository 에서 JdbcMemberRepository 로 바꿀 때, 즉 미리 만들어져서 사용되던 구현체에서 다른 구현체로 바꾸려 할 때 spring 에서는 다른 부분의 코드를 수정하고 의존성을 확인하고 하는 번거로움 없이 기존에 만들어두었던 SpringConfiguration 파일을 수정하면 된다.
  • 정확히는 MemberRepository 스프링빈에서 MemoryMemberRepository -> JdbcMemberRepository 로 바꿔주면 된다. 물론 이때 사용되는 DataSource 에 대해서도 지정해주어야 하는데 이는 간단하게 생성자를 만든 후 Autowired 로 엮어주면 된다.
@Configuration // 스프링 빈에 등록하기 위한 설정파일이라는 Annotation
public class SpringConfig {

    private DataSource dataSource;
	
    // 아래 JdbcMemberRepository 에서 사용되는 dataSource 는 아래처럼 Aitowired 로 엮어주면 끝!
    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean // Bean 에 등록되어야하는 객체라는 의미의 Annotation
    public memberService memberService(){
        return new memberService(memberRepository());
        // 아래에서 생성된 memberRepository 객체를 넣어줌. 의존성 주입 DI
    }

    @Bean
    // 여기서 MemberRepository 는 인터페이스, 기존과는 다르게 JdbcMemberRepository 가 구현체
    // 따라서 구현체를 객체로 가져와야함
    public MemberRepository memberRepository(){
        return new JdbcMemberRepository(dataSource); // 구현체를 객체로 가져옴
    }
    
}

클래스간 의존 관계는 아래 사진으로 한번 더 확인하자. 이렇게 구현체를 바꿔끼면서 확장하되 수정을 최소화 하는 프로그래밍 개발 원칙을 개방 - 폐쇄 원칙이라고 한다.

개방 - 폐쇄 원칙(OCP, Open-Closed Principle) : 위 처럼 모듈(구현체)을 바꿔끼면서 확장은 허용하되 수정을 최소화 한다는 원칙이 바로 개방 - 폐쇄의 원칙이다. 정확히는  '소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다'는 프로그래밍 원칙 라고 하는데 사실상 객체 지향 언어에서 가장 중요한 원칙 중 하나이다.


5) 실제 구동 확인

요렇게 회원가입을 하면
# : code, 이름 : name // 요렇게 저장이 완료된 모습을 볼 수 있다.
당연히 DB에도 저장이 잘 된다


3. JdbcTemplate

여기는 실제 동작되는 스샷은 생략하고 뒤에 5. 내 맘대로 구현하기 부분에서 추가, 수정 후 올리겠습니다.

  • 순수 Jdbc 를 사용할때와 동일한 설정을 해준다. 
  • 순수 JDBC API에서 본 반복 코드를 대부분 제거해준다. 예를 들면 conn 이나 resultset 으로 반복되던 중복 코드를 제거해준다.
  • 단, SQL 은 직접 작성해야한다.
  • 추가로 JdbcTemplate 의 경우에는 실무에서도 자주 쓰이는 편이라고

1) JdbcTemplateMemberRepository

  • 위에서 보았던 JdbcMemberRepository 랑 비교하자면 엄청나게 코드가 줄어든게 보인다. 추가로 중복 코드도 많이 줄어들었다.
  • 이전에는 conn으로 일일히 연결하고, resultset 으로 결과를 받고 prepareStatememt 로 sql을 넣고를 매번 써주었다면 이번에는 RowMapper 로 객체를 SQL에 넣기위한 객체를 미리 생성해두고 , jdbctemplate 를 사용해 sql 을 보내는 방식을 사용한다. 
import HJproject.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);
    }

    // RowMapper
    private RowMapper<Member> memberRowMapper(){
        // 아래에서 사용되는 RowMapper 객체 내용은 여기서 생성되서 넣어짐

        // 기본 코드
//        return new RowMapper<Member>() {
//            @Override
//            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
//
//                Member member = new Member();
//                member.setCode(rs.getLong("code"));
//                member.setName(rs.getString("name"));
//                return member;
//            }

        // 람다식으로 바꾼 코드
        return (rs, rowNum) -> {

            Member member = new Member();
            member.setCode(rs.getLong("code"));
            member.setName(rs.getString("name"));
            return member;
        };
    }


    @Override // jdbcTemplate 이용해서 DB 에 정보 저장
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("code");
        // usingGeneratedKeyColumns : code 가 자동으로 입력되도록 함 -> 보통 DB에서 auto_increment 해서 자동으로 생성되고, 여기에 그 값이 자동으로 입력됨


        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setCode(key.longValue());
        return member;
    }

    @Override // SQL 쿼리를 사욯하기 위해서는 RowMapper 를 이용한다.
    public Optional<Member> findByCode(Long code) {  // code 로 회원 찾기
        List<Member> result = jdbcTemplate.query("select * from member where code = ?", memberRowMapper(), code);

        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) { // 이름으로 회원 찾기
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);


        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findById(String ID) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findByPasswd(String passwd) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.empty();
    }

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


}

 

2) SpringConfig

import HJproject.Hellospring.repository.JdbcMemberRepository;
import HJproject.Hellospring.repository.JdbcTemplateMemberRepository;
import HJproject.Hellospring.repository.MemberRepository;
import HJproject.Hellospring.repository.MemoryMemberRepository;
import HJproject.Hellospring.service.memberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration // 스프링 빈에 등록하기 위한 설정파일이라는 Annotation
public class SpringConfig {

    private DataSource dataSource;

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

    @Bean // Bean 에 등록되어야하는 객체라는 의미의 Annotation
    public memberService memberService(){
        return new memberService(memberRepository());
        // 아래에서 생성된 memberRepository 객체를 넣어줌. 의존성 주입 DI
    }

    @Bean
    // 여기서 MemberRepository 는 인터페이스, 이번에는 JdbcTemplateMemberRepository 가 구현체
    // 따라서 구현체를 객체로 가져와야함
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }
}

4. Spring 통합 테스트

  • 위에서처럼 spring - DB 연동후에 제대로 동작하는지 확인하기 위해서는 물론 직접 spring을 올리고 회원가입을 해보고 DB 를 확인하면서 테스트 할 수도 있지만 통합 테스트 코드를 만들어 테스트를 실행해볼수도 있다.
  • 실무에서는 이렇게 테스트하는것은 시간, 비용이 많이 들기 때문에 '통합 테스트' 코드를 작성하고 이를 통해 테스트하는 것이 바람직하다.
  • 이번 테스트는 이전 테스트와는 다르게 스프링이 실행되고, DB와 연동되면서 테스트 코드가 동작한다.

 

1) memberServiceIntegrationTest : 멤버 통합 테스트

  • @springBootTest : 이 어노테이션은 실제 spring을 실행해서 테스트가 진행되도록한다
  • @Transactional : 이 어노테이션은 DB 트랜잭션 수행 시 테스트를 완료한 후 테스트한 내용을 Rollback 한다. 따라서 이 어노테이션이 없다면 테스트한 내용이 그대로 DB에 남고, 사용하면 테스트한 내용이 DB에서 지워진다.
    • 추가 설명을 하자면 기본적으로 DB에 SQL 이 적용되는 과정은 1. 트랜잭션 실행 -> 2. select, insert, delete , update 에 해당하는 SQL 동작 -> 2. 테스트 케이스 종료 후 commit(SQL적용)  이다.
    • 이때 Transactional 어노테이션을 사용하면 1. 트랜잭션 실행 -> 2. select, insert, delete , update 에 해당하는 SQL 동작 -> 3. 테스트 케이스 종료 후 Rollback(DB에서 테스트를 위해 사용된 내용 삭제) 로 바뀌게 된다.
    • 나머지는 코드의 주석을 확인.
import HJproject.Hellospring.domain.Member;
import HJproject.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.assertThrows;


@SpringBootTest // 실제 spring boot가 실행되며 동작하는 테스트
@Transactional // DB 트랜잭션 시 테스트 코드가 동작한 후 다시 Rollback 되도록 함
class memberServiceIntegrationTest {


    // 아래는 DI 의 또다른 방식인 Field 선언 방식
    // 필드에서 생성자 필요없이 Autowired 로 엮어서 DI 하는 방식
    @Autowired memberService memberService;
    @Autowired MemberRepository memberRepository;




    @Test
    void 회원가입() { // 테스트 코드에서는 한글로 메서드 잡아도 무방 -> 실제코드에 포함 X

        // given : 주어지는 데이터
        Member member = new Member();
        member.setName("통합테스트");
        member.setId("둘");

        // when : 검증하고자 하는 코드드
       Long saveCode = memberService.join(member);


        // then : 예상 결과(기댓값)
        Member findCode = memberService.findOne(member.getCode()).get();
        // findOne 메서드를 사용해 memberCode 가져오기

        System.out.println(findCode.getCode()+ " " +findCode.getName() + " "+ findCode.getId()+ " "+findCode.getPasswd());

    }

    @Test
    public void 중복_아이디_체크(){
        //given
        Member member1 = new Member();
        member1.setName("spring");

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

        // when : 예외가 발생하는 상황은 동일한 아이디로 회원가입을 요청하는 경우


        /* 예외상황 확인 방법 2 */
        memberService.join(member1);

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

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

}

2) 테스트코드 동작확인

통합테스트 완료!!

  • 이때 29 는 code 통합테스트 는 이름이다. 그렇다면 null , null 에 해당하는 부분은 각각 id, passwd 이다. 우리의 테스트 코드에서는 code 와 name 만 받아서 테스트 케이스가 동작하기 때문에 당연히 null, null 로 나오게 된다.
  • 심지어 중복 아이디 체크 부분은...'중복 이름 체크' 가 동작한다ㅋㅋ 이 부분은 내 맘대로 구현하기 부분에서 고쳐볼 예정이다.

5. 내 마음대로 구현하기 -  JdbcTemplate으로 유저 ID, PASSWD 저장되게 만들기

  • 강의에서 나온 코드는 단순히 이름만 저장할 수 있는 코드이다. 그런데 내가 만든 회원가입 페이지를 활용하려면 최소한 ID 와 패스워드가 저장이 되어야한다.
    • 첫번째로 회원가입과 관련된 코드를 수정하고, DB에 컬럼을 추가해서 유저 ID와 Passwd 가 저장되도록 만들어보겠다.
    • 두번째로 통합 테스트 부분을 수정해서 ID 중복 검사와 passwd 까지 넣은 통합 테스트를 실행해보겠다.
    • 각 코드의 설명은 5. 내 맘대로 구현하기 주석 부분을 참고한다.

 

0) DB 에 ID, Passwd 컬럼 추가하기

alter table member add id varchar(255) not null; // id 컬럼 추가
alter table member add passwd varchar(255) not null; // passwd 컬럼추가

1) jdbcTemplateMemberRepository

=
import HJproject.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);
    }

    // RowMapper
    private RowMapper<Member> memberRowMapper(){
        // 아래에서 사용되는 RowMapper 객체 내용은 여기서 생성되서 넣어짐

        // 기본 코드
//        return new RowMapper<Member>() {
//            @Override
//            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
//
//                Member member = new Member();
//                member.setCode(rs.getLong("code"));
//                member.setName(rs.getString("name"));
//                return member;
//            }

        // 람다식으로 바꾼 코드
        return (rs, rowNum) -> {

            Member member = new Member();
            member.setCode(rs.getLong("code"));
            member.setName(rs.getString("name"));

            // 5. 내 맘대로 구현하기
            member.setId(rs.getString("id"));
            member.setPasswd(rs.getString("passwd"));

            return member;
        };
    }


    @Override // jdbcTemplate 이용해서 DB 에 정보 저장
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("code");
        // usingGeneratedKeyColumns : code 가 자동으로 입력되도록 함 -> 보통 DB에서 auto_increment 해서 자동으로 생성되고, 여기에 그 값이 자동으로 입력됨


        Map<String, Object> parameters = new HashMap<>();

        parameters.put("name", member.getName()); // name 컬럼에 member.getName 넣기

        // 5. 내 맘대로 구현하기
        parameters.put("id", member.getId()); // id 컬럼에 member.getId 넣기
        parameters.put("passwd", member.getPasswd());


        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setCode(key.longValue());
        return member;
    }

    @Override // SQL 쿼리를 사욯하기 위해서는 RowMapper 를 이용한다.
    public Optional<Member> findByCode(Long code) {  // code 로 회원 찾기 : memberRowMapper() 객체의 code 부분
        List<Member> result = jdbcTemplate.query("select * from member where code = ?", memberRowMapper(), code);

        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) { // 이름으로 회원 찾기
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);

        return result.stream().findAny();
    }
	
    
    // 5. 내 맘대로 구현하기
    @Override
    public Optional<Member> findById(String ID) { // id 로 회원 찾기 : memberRowMapper() 객체의 id 부분
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), ID);

        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByPasswd(String passwd) {
        List<Member> result = jdbcTemplate.query("select * from member where passwd = ?", memberRowMapper(), passwd);

        return result.stream().findAny();
    }

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


}

 

2) memberServiceIntegartionTest : member 에 Id 와 Passwd 를 저장하는 기능 테스트하는 부분 추가

@Test
void 회원가입() { // 테스트 코드에서는 한글로 메서드 잡아도 무방 -> 실제코드에 포함 X

    // given : 주어지는 데이터
    Member member = new Member();
    member.setName("통합테스트");
    
    /*  내 맘대로 구현하기   */
    member.setId("통합테스트ID");
    member.setPasswd("통합테스트PW");

    // when : 검증하고자 하는 코드드
   Long saveCode = memberService.join(member);


    // then : 예상 결과(기댓값)
    Member findCode = memberService.findOne(member.getCode()).get();
    // findOne 메서드를 사용해 memberCode 가져오기

    assertThat(member.getId()).isEqualTo(findCode.getId());
    // 내가 작성한 값과 실제 저장된 값 비교 -> member.getId : findCode.getId

    System.out.println(findCode.getCode()+ " " +findCode.getName() + " "+ findCode.getId()+ " "+findCode.getPasswd());

}

 

 

 

3) memberService 부분 수정 : 중복 체크를 ID 로 하기 위해 변경

    private void checkDuplicateID(Member member) { // 메서드 추출 단축키 컨트롤 + 쉬프트 + M

//            Optional<Member> result = memberRepository.findById(member.getId());
//         이렇게 Optional 로 감싸면 Optional 과 관련된 다양한 메서드 사용가능 대표적으로 아래와 같은 ifPresent(값이 있다면)
//
//        result.ifPresent(m -> { // result 값이 이미 존재한다면, 아래 문구 출력력
//           throw new IllegalStateException("이미 존재하는 ID 입니다");
//        });
//         위 코드에서 Optional 부분을 쉽게 정리하면 이는 memberRepository.findById(member.getId()) 부분을 따로 result 로 저장하고
//         다시 그걸로 검사하는 것이 아닌 memberRepository.findById(member.getId()) 그 자체로 검사하는 방법이다.

        memberRepository.findById(member.getId()) // findByname -> findById , member.getname -> member.getId
                        .ifPresent(m -> {
                            throw new IllegalStateException("이미 존재하는 ID 입니다");
                        });
    }

4) 결과 확인하기

회원 가입 및 회원 조회

1. 회원 가입

 

2. 회원 조회

 

DB 조회 결과


중복 ID 회원 가입 시 : 여기서 조금 더 발전시켜 중복 ID 인 경우 웹에서 체크해서 '회원 가입' 버튼이 안 눌려지거나 중복 ID 체크하는 부분을 회원가입 화면에 넣으면 좋을 것 같다.

중복 ID로 회원가입시 화면 : 중복 ID 는 허용하지 않아 에러 발생
인텔리J 에서의 화면


통합 테스트 확인

회원 가입 테스트 사진을 자세히 보면 Began transaction(트랜잭션 시작) -> 테스트 코드 실행(테스트 SQL) -> Rolled back transaction(트랜잭션 롤백) 을 확인할 수 있다.

회원 가입 테스트
중복 ID 체크 테스트

댓글