JDBC란 무엇인가? https://ittrue.tistory.com/250
JDBC(Java Database Connectivity)는 Java 기반 애플리케이션의 데이터를 데이터베이스에 저장 및 업데이트하거나, 데이터베이스에 저장된 데이터를 Java에서 사용할 수 있도록 하는 자바 API이다.
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
→ 자바는 기본적으로 DB랑 붙으려면 jdbc 드라이버가 꼭 있어야 한다.
→ 2번째 줄은 db랑 붙을 때 데이터베이스가 제공하는 클라이언트가 필요하므로 h2 데이터베이스 클라이언트로 설정.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
스프링 부트 데이터베이스 연결 설정 추가
db에 붙으려면 접속정보 같은 거를 넣어야겠지?
resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
주의! : 스프링부트 2.4부터는
spring.datasource.username=sa를 꼭 추가해주어야 한다. 그렇지 않으면Wrong user name or password오류가 발생한다. 참고로 다음과 같이 마지막에 공백이 들어가면 같은 오류가 발생한다.spring.datasource.username=sa←공백 주의, 공백은 모두 제거해야 한다.
참고 : 인텔리J 커뮤니티(무료) 버전의 경우 application.properties 파일의 왼쪽이 다음 그림고 같이 회색으로 나온다. 엔터프라이즈(유료) 버전에서 제공하는 스프링의 소스 코드를 연결해주는 편의 기능이 빠진 것인데, 실제 동작하는데는 아무런 문제가 없다.
주의! 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게 고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자.
기존에 MemberRepository를 인터페이스로 만들었었다. 이를 활용하여 Jdbc 구현체를 만들자!!
Jdbc 회원 리포지토리
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; // DB에 붙으려면 데이터소스가 필요.
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(); // db와의 커넥션을 가지고옴.
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); // sql문을 넘기고, DB상에 AUTO_INCREMENT로 인해 자동으로 생성되어진 key(=id)를 가져오는 쿼리
pstmt.setString(1, member.getName()); //parameterIndex를 1로 하면 위 sql 문의 물음표랑 매칭. 거기에 member.getName 값을 넣기
pstmt.executeUpdate(); // db에 실제 쿼리 날리기
rs = pstmt.getGeneratedKeys(); // key 꺼내와서 저장.
if (rs.next()) {
member.setId(rs.getLong(1)); // DB가 생성해준 값을 읽어서 Member 클래스에 넣어주는 과정
} 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(); // 조회시 날리는 쿼리 함수는 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() {
// 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를 통해서 릴리즈 해줘야한다.
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
스프링 설정 변경
지금까지는 db가 아닌 메모리에서 하고 있었다 맞지? 즉, MemoryMemberRepository를 쓰고 있었다.
스프링 빈에 등록한 MemoryMemberRepository를 JdbcMemberRepository로 바꿔주자.
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) {
// application.properties 에서 설정해놓은 것을 보도 데이터소스를 생성하고 스프링 빈으로 등록해둔다.
// 그래서 이렇게 DI가 가능한것이다.
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다.구현 클래스 추가 이미지

MemberService는 MemberRepository를 의존하고 있다.
그리고 MemberRepository는 구현체로 MemoryMemberRepository와 JdbcMemberRepository가 있다.
그런데 위에 스프링 컨테이너에서 설정을 우리가 어떻게 바꿧지?
스프링 설정 이미지

기존에는 memory 버전의 memberRepository를 스프링 빈으로 등록을 했다면, 이제는 jdbc 버전의 memberRepository를 등록했다.
즉, 구현체만 바꿔서 기능을 완전히 변경해도 애플리케이션 전체를 수정할 필요가 없다.
이전까지는 전혀 스프링과 관련이 없는 순수한 자바코드를 가지고 테스트를 진행해왔다.
그런데 순수한 자바코드를 가지고 지금 테스트를 진행할 수 없다.
왜냐하면 데이터베이스 커넥션 정보도 스프링부트가 들고 있으니 말이다.
그러면 스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자.!!!!
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@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("spring");
//When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));//예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
@SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.@Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다. 당연히 서비스 이런데에 붙으면 항상 롤백하지 않는다.💡
@Autowired로 필드 주입을 해주는 이유?
→ 스프링 빈으로 등록한다는 것은 껍데기에 어떤 알맹이가 채워지는지 알려주는 행위.
그리고, 주입이라는 것은 껍데기에 알맹이를 채워넣는 행위
따라서@Autowired로 필드 주입을 하지 않는다면 껍데기만 만들어놓고 알맹이를 채워넣지 않아 오류가 발생하는 것으로 이해하시면 될 것 같습니다 :)
💡
@Autowired MemberService memberService는 무슨 의미일까?
앱이 실행될 떄@Configuration에서@Bean된MemberService메서드를 찾아, 변수(memberservice)에 해당 객체 레퍼런스가 들어간다.
즉, 스프링 빈 객체 등록은 그 이전에 등록되고@Autowired를 통해 해당 타입의 빈을 찾아 연결해준다고 보시는게 더 정확합니다
✅ 그렇다면 순수한 자바 코드로 이뤄진 테스트(MemberServiceTest)는 필요 없지 않나?
→ 아니다
지금은 테스트 케이스가 별로 없지만, 테스트 케이스가 많아지면 스프링 통합 테스트는 시간이 많이 걸린다. 왜냐하면 스프링 컨테이너를 같이 띄워야 하기 때문이다.
하지만 순수한 자바 코드로 이뤄진 테스트(=단위 테스트)는 스프링 컨테이너를 띄울 필요가 없기 때문에 시간이 매우 짧다.
진짜 좋은 테스트는 이러한 단위 테스트를 잘 만드는 것이다.