mojo's Blog

순수 JDBC & 스프링 JdbcTemplate 본문

Spring

순수 JDBC & 스프링 JdbcTemplate

_mojo_ 2022. 1. 13. 20:41

H2 데이터베이스

 

 

1. H2 콘솔에 접속한다.

 

2. 다음과 같이 member 테이블을 create 해준다.

create table member
(
  id  bigint  generated by default as identity,
  name varchar(255),
  primary key(id)
)
  • generated by default as identity : 만약 id 값이 null 일 경우 db가 자동으로 id 값을 채워주도록 한다.

 

3. 실행 버튼을 누르면 다음과 같이 member 테이블이 만들어진다.

 

4. 다음과 같이 member 테이블에 값을 insert 해보도록 한다.

insert into member(name) values('spring')

 

 

5. 여러 값들을 삽입한 후 member 테이블의 값들을 select 해보도록 한다.

select * from member

 

 

 

순수 JDBC

 

 

1. jdbd 연동을 위해 build.gradle 의 dependencies 부분에 다음과 같이 코드를 추가해준다.

 

2. [src] -> [main] -> [resources] 폴더 아래의 application.properties 에 다음과 같이 코드를 추가한다.

  • spring.datasource.url=jdbc:h2:tcp://localhost/~/test : h2 console에 접속한 url을 넣어줘야 한다.

 

3. repository 폴더 아래에 JdbcMemberRepository 클래스를 형성한다.

그 후에 MemberRepository를 구현하며 MemberRepository 의 메서드에 대한 오버라이딩을 해준다.

public class JdbcMemberRepository implements MemberRepository {

    @Override
    public Member save(Member member) {
        return null;
    }

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

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

    @Override
    public List<Member> findAll() {
        return null;
    }
}

 

4. Datasource 타입의 필드 dataSource를 private final 형으로 선언한다.

new 를 통해 할당하지 않고 생성자를 통해 자동으로 할당하는 방식으로 구현한다.

public class JdbcMemberRepository implements MemberRepository {

    private final DataSource dataSource;

    public JdbcMemberRepository(DataSource dataSource){
        this.dataSource = dataSource;
    }
    ...
}
  • Datasource datasource : 자동으로 DataSource 의 필드 dataSource가 파라미터로 넘어온 DataSource의 객체를 통해 할당하게 된다.

 

5. save 메서드 코드는 다음과 같이 작성한다. (getConnection(), close() 메서드도 포함)

    @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();
        } 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);
    }
  • close() : conn은 conn.close() 가 아니라 따로 추상 클래스 DataSourceUtils의 releaseConnection() 메서드를 통해 진행된다.

 

6. findById 메서드는 다음과 같이 코드를 작성한다.

    @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();
        } finally{
            close(conn, pstmt, rs);
        }
    }

 

7. findByName 메서드는 다음과 같이 구현한다.

    @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);
        }
    }

 

8. findAll() 메서드는 다음과 같이 구현한다.

    @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);
        }
    }

 

9. hello.hellospring 폴더 아래에 SpringConfig 클래스의 코드를 수정을 해야한다.

현재 실행시키려고 하는 JdbcMemberRepository는 인터페이스 MemberRepository를 구현하였기 때문에 빈 설정에서 다음과 같이 DataSource의 필드값을 자동으로 할당한 후에 해당 필드를 인자로 하여 JdbcMemberRepository를 new 하여 리턴해줘야 한다.

@Configuration
public class SpringConfig {

    private DataSource dataSource; // New!

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

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

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

}

 

10. 실행하기 전에 h2 console을 다음과 같이 접속해야 하며 application.properties 에 코드 하나를 추가해줘야 한다. (에러 처리를 위함)

먼저 h2 console은 다음과 같이 접속해야 한다.

 

(1) :8082/login.jsp?~~~ 의 앞부분을 localhost으로 변경해줘야 한다.

 

(2) 변경을 완료하면 연결 버튼을 누른다.

 

(3) 정상적으로 연결되었다.

 

 

application.properties 에 다음과 같이 코드를 추가해줘야 한다.

 

11. h2 데이터베이스에 존재하는 member 테이블의 데이터가 정상적으로 연결되었는지 실행하여 확인해본다.

 

 

※ 스프링의 장점 : 다형성(polymorphism)

 

 

  • MemberService : MemberRepository 인터페이스를 의존하고 있는 상태이다.
  • MemberRepository : 인터페이스며 구현체로 MemoryMemberRepository, JdbcMemberRepository 가 있다.
  • MemoryMemberRepository : MemberRepository 인터페이스를 의존하고 있는 상태이며 h2 database 를 이용하지 않고 프로그램이 실행되면 새로 데이터를 저장해야 하며 종료되면 데이터가 사라진다.
  • JdbcMemberRepository : MemberRepository 인터페이스를 의존하고 있는 상태이며 h2 database 를 이용하고 있어서 프로그램이 종료되도 데이터가 사라지지 않고 유지한다.

 

※ 스프링 설정

 

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

 

스프링 통합 테스트

 

1. [test] -> [service] 폴더 아래에 MemberServiceIntegrationTest 클래스를 형성한다.

코드는 다음과 같이 작성한다.

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

        //then

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}
  • @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다. (진짜 스프링을 띄워서 실행)
  • @Transactional : 테스트를 실행할 때 애너테이션 transactional 을 통해 트랜잭션을 실행하고 DB에 데이터가 삽입된 후에 테스트가 종료되면 DB에 저장되어 있는 데이터가 rollback 된다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다. (중요한 개념)

 

2. Transactional 애너테이션이 제대로 동작하는지 확인해본다.

우선 회원가입 부분을 실행해보도록 한다.

 

잘 실행되었다면 h2 데이터베이스에 "spring" 이름이 삽입되었는지 확인해본다.

 

정상적으로 rollback 된 것을 확인할 수 있다.

이를 통해서 알 수 있는 점은 @afterEach 애너테이션을 이용하여 delete를 하지 않아도 되며 @Transactional 애너테이션을 통해 rollback 기능을 통해 손쉽게 테스트가 가능하다.

 

3. 이번엔 @Transactional 애너테이션을 제거하고 실행해보도록 한다.

 

정상적으로 실행되었다면 h2 데이터베이스에 "spring" 이름이 삽입되었는지 확인해본다.

 

@Transactional 애너테이션을 제거했더니 rollback이 일어나지 않고 그대로 데이터가 삽입된 모습이다.

굉장히 편리한 기능이므로 잘 알아두도록 해야겠다.

 

4. 이번엔 h2 데이터베이스에 데이터를 여러개 삽입한 후에 @Transactional 애너테이션을 달고 실행해보도록 한다.

(1) 먼저 {"spring1", "spring2", "spring3"} 를 member 테이블에 삽입한다.

 

(2) "spring" 을 join 하도록 하는 회원가입을 실행해보도록 한다.

 

(3) h2 데이터베이스에 정상적으로 "spring"이 롤백되었는지, 다른 데이터들은 사라졌는지 아니면 그대로 남아있는지 확인해본다.

 

(4) 정상적으로 회원가입에 삽입하려고 했던 "spring"이 rollback되고 나머지 데이터들은 보존되어 있음을 알 수 있다.

 

 

스프링 JdbcTemplate

 

※ JdbcTemplate는 실무에서 많이 쓰인다고 한다.

 

1. repository 폴더 아래에 JdbcTemplateMemberRepository 클래스를 형성한다.

그리고 MemberRepository를 구현하여 다음과 같이 코드를 형성한다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository{

    @Override
    public Member save(Member member) {
        return null;
    }

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

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

    @Override
    public List<Member> findAll() {
        return null;
    }
}

 

2. private final 형의 JdbcTemplate의 필드 jdbcTemplate를 선언한 후에 생성자를 통해 DataSource 의 객체 dataSource를 파라미터로 받아서 할당해주는 방식으로 한다.

이때 @Autowired 애너테이션을 달아줘서 자동으로 할당되도록 한다.

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource){
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
  • 생성자가 하나인 경우 : @Autowired가 없어도 무방하다.
  • 생성자가 두개 이상인 경우 : @Autowired를 달아줘야 한다.

 

3. memberRowMapper() 메서드를 다음과 같이 작성한다.

♠ lamda를 사용하지 않고 작성하는 방법

    private RowMapper<Member> memberRowMapper(){
        return new RowMapper<Member>(){
            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;
            }
        }; 
    }

 

♠ lamda를 사용하고 작성하는 방법 (위에 작성했던걸 alt + enter를 통해 lamda 변환 가능)

    private RowMapper<Member> memberRowMapper(){
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
  • jdbcTemplate 필드의 메서드 query에 사용되는 부분이다. 아래의 메서드를 통해 어떻게 사용되는지 확인할 수 있다.

 

4. save 메서드를 다음과 같이 작성한다.

    @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;
    }
  • jdbcInsert : SimpleJdbcInsert의 객체를 형성한다. 이때 jdbcTemplate의 필드를 인자로 넘겨준다.
  • parameters : Map<String, Object> 형태이다. 파라미터로 받아온 Member 의 객체 member를 통해 member의 name값을 넣어준다.
  • key : jdbcInsert 객체를 executeAndReturnKey 를 함으로써 해당 이름을 DB에 삽입함과 동시에 id 값을 반환한다.

 

4. findById 메서드를 다음과 같이 작성한다.

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
        return result.stream().findAny();
    }
  • memberRowMapper() 가 jdbcTemplate의 query 메서드의 인자로 쓰이는 것을 확인할 수 있다.
  • 단 2줄에 구현이 가능하다. (이전에 했던 JDBC와는 차원이 다름)

 

5. findByName 메서드를 다음과 같이 작성한다.

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

 

6. findAll() 메서드를 다음과 같이 작성한다.

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

 

7. SpringConfig 의 빈 설정에서 JdbcTemplateMemberRepository 가 사용될 수 있도록 다음과 같이 코드를 수정해야 한다.

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

 

8. 이전에 작성했던 MemberServiceIntegrationTest 통합 테스트를 실행하여 정상적으로 돌아가는지 확인해본다.

 

정상적으로 실행되었다.

 

'Spring' 카테고리의 다른 글

AOP  (0) 2022.01.15
JPA  (0) 2022.01.14
회원 관리 예제 - 웹 MVC 개발  (0) 2022.01.13
스프링 빈과 의존관계  (0) 2022.01.12
회원 관리 - 백엔드 개발  (0) 2022.01.12
Comments