본문 바로가기
Study/Java Spring Boot

[Spring] 스프링 입문6 - DB 접근기술(JDBC, JdbcTemplate, JPA, SpringJpa)

많은 DBMS 중

가볍고 간단한 h2 DB를 이용해 Test해보자

 

* 생성 및 변경할 코드 목록

 

H2 데이터베이스 설치

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

http://localhost:8082/ 로 접속

테이블 생성 DDL

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

 

 

1. JDBC 로 연결

1) JDBC 연결설정

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

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

 

2) JDBC Repository 구현

기존 JDBC 방식의 구현은 코드가 길고 복잡하므로, save 메소드에 대해서만 살펴보자

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

dataSource라는 객체를 받아서 연결하고,

conn , pstmt , rs 객체 등을 이용해서 SQL를 수행시키고

결과를 반환하는 전형적인 JDBC Connection 코드다.

 

이제 기존에 MemoryMemberRepository Bean을 생성하던 Config 부분에서 

JdbcMemberRepository Bean객체를 생성하도록 바꿔주자!

service / SpringConfig.java

package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
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;
    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);
    }

DataSource 객체를 받아서 생성되도록 만들어 DI를 명확히 한다.

잘 들어간 것을 확인해 볼 수 있다.

 

이번에는 통합 테스트를 진행해보자.

MemberServiceIntegrationTest

package service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

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 com.example.intro.domain.Member;
import com.example.intro.repository.MemberRepository;
import com.example.intro.service.MemberService;

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring1");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,() -> memberService.join(member2)); //예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

@Transactional 을 명시하여 Test 해주면,

DataBase에 Auto Commit되지 않으므로, SQL이 Data를 적재하거나 변경하지 않고 테스트 하게 된다.

 

 

2. JdbcTemplate 으로 연결

이번에는 JDBC보다도 간결한 JdbcTemplate 방식을 알아보자

스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다.

하지만 SQL문은 그래도 우리가 직접 작성해야 한다.

JdbcTemplateMemberRepository

package com.example.intro.repository;

import com.example.intro.domain.Member;
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;
    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 member where id = ? ", memberRowMapper(), id);
        return result.stream().findAny();
    }
    @Override
    public List < Member > findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }
    @Override
    public Optional < Member > findByName(String name) {
        List < Member > result = jdbcTemplate.query("select * from member where name = ? ", memberRowMapper(), name);
            return result.stream().findAny();
        }
        private RowMapper < Member > memberRowMapper() {
            return (rs, rowNum) -> {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;
            };
        }
	}

 

모든 메소드를 구현했는데도 Jdbc방식에 save 메소드 하나구현한 코드보다 더 짧다.

rs, pstmt, conn 객체들을 import하지 않고 함축적으로 간결한 메소드들로 구현할 수 있어서 좋다.

일일이 객체를 닫아주지 않아도 되서 편한다.

 

Bean을 다시 연결해서 통합테스트를 해보자 

package com.example.intro.service;


import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.intro.repository.JdbcTemplateMemberRepository;
import com.example.intro.repository.MemberRepository;

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

 

 

3. JPA를 이용한 연결

JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.

JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.

JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

1) 설정

build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    //implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
   		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

jdbc가 아닌 jpa 라이브러리 추가후 새로고침하여 다운받는다

 

resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • show-sql : JPA가 생성하는 SQL을 출력한다.
  • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.
    • create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.

 

2) 작성

domain. Member 수정

package com.example.intro.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

// JPA 사용을 위해  Entity 어노테이션과 persistence 의 어노테이션을 명시함
@Entity
public class Member {    
	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	
	public Long getId() {
		return id; 
	}
	public void setId(Long id) {
		this.id = id; 
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	
	
}

사용하는 도메인 객체에 JPA 사용을 위해  Entity 어노테이션과 persistence 의 어노테이션을 명시함

 

repositoy . JpaMemberRepository 구현

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
    public Optional < Member > findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    public List < Member > findAll() {
        return em.createQuery("select m from Member m", Member.class)
            .getResultList();
    }
    public Optional < Member > findByName(String name) {
        List < Member > result = em.createQuery("select m from Member m where
            m.name =: name ", Member.class)
            .setParameter("name", name)
            .getResultList();
            return result.stream().findAny();
        }
    }

JdbcTemplateDataSource 클래스를 입력받아서 사용했지만,

JpaEntityManager 라는 클래스를 받아서 사용된다.

 

persist (지속하다) 간단한 메소드로 순식간에 insert 가 일어나며, 결과받는 메소드도 꽤 단순하다.

 

MemberService에서 @Transactional 명시 

 

간단한 추가이지만,

Jpa를 사용한다면, 서비스 컴포넌트 전체에 @Transactional 명시해주는것이 필수이다.

 

SpringConfig 수정

package com.example.intro.service;


import javax.persistence.EntityManager;
import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.intro.repository.*;
import com.example.intro.repository.MemberRepository;

@Configuration
public class SpringConfig {
	
	private EntityManager em;
	public SpringConfig(EntityManager em) { this.em= em;}
	/*
	// JdbcTemplate 파라미터를 위함
    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);
    	//return new JdbcTemplateMemberRepository(dataSource);
    	return new JpaMemberRepository(em);
    }
}

em을 주도록 설정하여 시작시 서비스Bean을 등록해준다. 

실행 및 테스트시 정상 동작함을 확인할 수 있다.

 

 

 

Jpa 가 근래 트렌드적으로 꽤나 필수적으로 알아야하는 기술로 부상중인것 같다.

획기적으로 코드를 줄여주고, 개발 비용을 절감시키기 때문이다.

 

4. Spring Data JPA

JPA 보다 더 간결한 Spring Data Jpa를 살펴보자

1) 구현

SpringDataJpaMemberRepository

package com.example.intro.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import com.example.intro.domain.Member;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository{
	@Override
	Optional<Member> findByName(String name);
}

위와같이 인터페이스를 새롭게 하나 만들어서

Spring Data Jpa 인터페이스인 JpaRepository기존에 MemberRepository를 상속시킨다 

그리고 딱 하나의 메소드만 오버라이드 해주면, 구현은 끝난다.

 

2) 연결

package com.example.intro.service;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.intro.repository.MemberRepository;


@Configuration
public class SpringConfig {
	
	/*
	// JdbcTemplate 연결
    private final DataSource dataSource;
     public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }*/
	/*
	// Jpa 연결
	private EntityManager em;
	public SpringConfig(EntityManager em) { this.em= em;}
	 */
	
	// SpringDataJpa 연결
    private final MemberRepository memberRepository;
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Bean
    public MemberService memberService() {
        //return new MemberService(memberRepository());
        return new MemberService(memberRepository);
    }
    
//    @Bean
//    public MemberRepository memberRepository() {
//        //return new MemoryMemberRepository();
//    	//return new JdbcMemberRepository(dataSource);
//    	//return new JdbcTemplateMemberRepository(dataSource);
//    	//return new JpaMemberRepository(em);
//    }
}

상속받았던 기존 인터페이스를 선언하고 생성자에 전달한다.

서비스에도 기존에 repository Bean을 생성하여 전달하는 형식이 아닌 해당 인터페이스를 받는 변수를 그대로 넣어준다. 

위와같이 설정해주면, Spring Data Jpa 구현이 된다. 훨씬 빠른 개발이 가능해진다.