JDBC API로 직접 코딩하는 것은 옛날 방식이라 요즘에는 안쓴다고 함. 하지만 알아두는게 좋다고 함.
//resources/application.properties 에 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
//build.gradle 에 추가. implementtion 아래에 넣어야지 잘 됨.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
package hello.hello_spring.repository;
import hello.hello_spring.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);
}
}
package hello.hello_spring;
import hello.hello_spring.repository.JdbcMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import hello.hello_spring.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 JdbcMemberRepository(dataSource); //변경
}
}
테스트 코드는 단위테스트했을 때의 코드와 별로 추가된게 없다.
@SpringBootTest,@Transactional를 넣으면 통합테스트가 되고, @Transactional를 넣으면 기존에 단위테스트했을때 @AfterEach, @BeforeEach를 넣어서 메모리를 지우거나 의존성을 주입해주지않아도 된다. 왜냐면 자동 롤백이 되니까. 만약 롤백이 되지않도록 하기 위해서는 @Commit을 넣어주면 됨.package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest //추가
@Transactional //추가
class MemberServiceIntegrationTest {
@Autowired MemberService memberService; //추가
@Autowired
MemberRepository memberRepository; //추가
// @BeforeEach
// public void beforeEach() { // 각 테스트 전에 실행됨. DI 의존성 주입하기
// memberRepository = new MemoryMemberRepository();
// memberService = new MemberService(memberRepository);
// }
//
// @AfterEach
// public void afterEach() { // 각 테스트 후에 실행. -> repository 비우기
// memberRepository.clearStore();
// }
@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("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
//then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
"Jdbc -> JdbcTemplate으로 변경"
repository에 jdbcTemplateMemberRepository 클래스 파일을 하나 추가해준다.
jdbcTemplate사용시 기본적으로 이렇게 사용.
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
}
public RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
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;
}
}
}
// 위와 같은 메소드를 아래와 같이 람다 식으로 변경할 수 있음 (return 부에 커서 올리고 Alt + Enter 하면 됨)
public RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
}
위의 코드를 참고해서 아래와 같은 코드가 나옴.
package hello.hello_spring.repository;
import hello.hello_spring.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) { // 쿼리를 날려서 결과를 rowMapper로 맵핑을 하고 list 로 받아서 옵셔널로 반환
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper());
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper());
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
public RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
}
}
잊지않고 config파일도 수정해준다.
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository()
// return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
"JdbcTemplate -> JPA 으로 변경"
객체 + ORM
JdbcTemplate는 쿼리를 직접작성했어야했음. 이에 반해 jpa는 자동으로 처리를 해서 개발 생산성을 높일 수 있음. sql과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있음. 더나아가 SQL Injection 공격을 대비할 수 있겠음.
//build.gradle 에 추가.
// data-jpa에 jdbc를 포함함.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//resources/application.properties 에 추가
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
이제 데이터베이스에 entity를 만들고, 각 컬럼이 맵핑이 될 수 있도록 어노테이션을 붙여주면 된다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import jakarta.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;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
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();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
}
// service/MemberService 수정
// jpa는 조인들어올때 모든 데이터변경이 transation안에서 실행되어야함
@Transactional
public class MemberService {
}
package hello.hello_spring;
import hello.hello_spring.repository.JpaMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.service.MemberService;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
EntityManager em; //추가
@Autowired
public SpringConfig(EntityManager em) { //추가
this.em = em;
}
// 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); //추가
}
}
인터페이스 spring data jpa가 jpaRepository를 상속을 받고 있으면 구현체를 알아서 만들어주면서 자동으로 스프링 빈에 등록해줌. 우리는 그걸 가져다가 인젝션 받으면 됨.
나머지 메소드들은 스프링 데이터에서 기본적으로 공통적으로 사용되는 메소드는 만들어둔게 있어서 그걸로 사용이 가능하다. ( jpaRepository에 들어가보면 알수있음)
공통적으로 사용되지않는 메소드는 일정한 규칙아래 나름 커스텀해서 사용할 수 있다. 완전히 커스텀은 아니고, findById를 findByName, findByNameAndId(String name, Long id)와 같이 사용할 수 있음.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
package hello.hello_spring;
import hello.hello_spring.repository.*;
import hello.hello_spring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository); //멤버서비스에 의존관계를 넣어준다.
}
}
복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면됨. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있음. 이 조합으로 해결하기 어려운 쿼리는 jpa가 제공하는 네이티브 쿼리를 사용하거나, 스프링 jdbcTemplate를 사용하면 됨.