이 글은 김영한 님의 Infrean 강의를 학습한 내용을 정리하여 작성합니다.
스프링 데이터 엑세스
- H2 데이터베이스 설치
- 순수 Jdbc
- 스프링 JdbcTemplate
- JPA
- 스프링 데이터 JPA
H2 데이터베이스 설치
개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공
H2를 다운받고 실행시키면 최초의 데이터베이스 파일을 생성해야 한다.
- 데이터베이스 생성 방법
- Jdbc:h2:~/test(최초 한번)
- ~/test.mv.db 파일 생성 확인
- 이후부터는 jdbc:h2:tcp://localhost/~/test 이렇게 접속
테이블 생성하기
테이블 관리를 위해 프로젝트 루트에 sql/ddl.sql 파일을 생성
위 내용을 실행하면 다음과 같이 MEMBER가 생성된다.
생성된 이후로는 다음과 같은 명령을 통해 MEMBER를 조회할 수 있다.
생성한 파일을 확인해보자.
우리가 Member 클래스에 생성한 id와 name이 존재한다.
bigint는 Long에 해당하며 이때, 중요하게 보아야 할 것은 "generated by default as identity"다.
이는 id에 값을 세팅하지 않고 있어 DB가 자동으로 id값을 채워줬음을 의미한다.
순수 Jdbc
예전에 사용하던 방식이지만 어떻게 발전되어 왔는지 알아보고자 정리한다.
환경설정
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
자바는 기본적으로 DB와 연결되려면 jdbc 드라이버가 필요하다.
또한, 데이터베이스가 제공하는 클라이언트가 필요하므로 두 번째 줄을 추가해준다.
스프링 부트 데이터베이스 연결 설정 추가
데이터베이스와 연결되려면 접속 정보를 기입해야한다.
이전에는 개발자가 모두 설정했으나, 요즘에는 경로와 driver-class-name만 기입해주면 스프링 부트가 알아서 수행해준다.
주의!
스프링부트 2.4부터는 spring.datasource.username=sa를 꼭 추가해주어야 한다.
그렇지 않으면 Wrong user name or password 오류가 발생한다.
참고로 다음고 ㅏ같이 마지막에 공백이 들어가면 같은 오류가 발생한다.
spring.datasource.username=sa <- 공백 주의, 공백은 모두 제거해야 한다.
이제 데이터베이스에 접근하기 위한 준비는 모두 마쳤다.
Jdbc 리포지토리 구현
주의! 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기다. 따라서 고대 개발자들이 이렇게 고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자!!
Jdbc 회원 리포지토리
이전에 repository 폴더에 interface로 생성한 MemberRepository이 존재할 것이다.
지금까지는 MemberRepository를 구현한 MemoryMemberRepository를 사용해 메모리에 데이터를 저장해 왔다.
이제는 데이터베이스와 연동해 데이터를 저장하고 사용해 본다.
이때, 데이터베이스와 연동하기 위해 DataSource라는 것이 필요하다.
또한, DataSource를 스프링으로부터 주입(DI)받아야 한다.
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);
}
}
@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); }
}
스프링 설정 변경
지금까지 SpringConfig 파일에 스피링 빈으로 MemberRepository를 등록해 메모리에 저장하고 사용하였다.
이 내용을 다음과 같이 변경해준다.
MemoryMemberRepository를 방금 생성한 JdbcMemberRepository로 변경해 주었다.
이때, JdbcMemberRepository를 생성하기 위해 DataSource가 매개변수로 필요하다.
@Configuration 또한 스프링 빈으로 관리된다.
스프링 부트가 미리 생성해 둔 DataSource를 위와 같은 방법으로 주입(DI)해준다.
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다.
스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다.
그래서 DI를 받을 수 있다.
현재 중요한 것은 어떠한 코드도 건드리지 않았다는 것이다.
그저, MemberRepository interface를 확장해 JdbcMemberRepository 클래스를 구현하고 @Configuration(조립하는 코드)만 약간 변경하였다.
이렇듯, 인터페이스 구현체를 기존 코드는 건드리지 않고 변경할 수 있는 것이 객체 지향의 장점이다.
이제 실제로 데이터베이스를 사용해 보도록 하자!
보다시피 데이터베이스에 존재하던 회원 목록이 그대로 출력된다.
회원 가입을 하여 데이터베이스에 새로운 회원을 추가하였다.
주의!
데이터베이스가 다음과 같이 실행중인 상태여야 한다.
구현 클래스 추가 이미지
스프링 설정 이미지
- 개방-폐쇄 원칙(OCP, Open-Closed Principle)
- 확장에는 열려있고, 수정, 변경에는 닫혀있다.
- 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
- 회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자.
- 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
객체 지향 언어의 장점에 대해 더욱 자세히 공부하고 싶다면 다음 강의를 들어보자!
매우 도움이 된다!!
블로그 정리
스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해본다.
이전까지 스프링과 관련 없는 순수 자바 코드만을 테스트 하였다.
하지만, DB까지 연결한 통합 테스트를 진행하기 위해서는 스프링이 필요하다.
회원 서비스 스프링 통합 테스트
- @SpringBootTest
- 스프링 컨테이너와 테스트를 함께 실행한다.
- @Transactional
- 테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
- @BeforeEach, @AfterEach 어노테이션을 사용하지 않아도 된다.
참고
테스트에서는 이전처럼 생성자를 통해 주입받지 않아도 된다.
이전에는 해당 클래스를 다른 클래스에서도 생성해 사용하므로 생성자를 통해 주입을 하였다.
하지만, 테스트의 경우 그럴 일이 없으므로, 생성자를 사용하지 않고 간단히 필드 주입 방식을 사용해 빈을 주입한다.
<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.assertThat;
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("이미 존재하는 회원입니다.");
}
}
실행
현재 회원가입 메서드를 실행하면 다음과 같이 오류가 발생할 것이다.
이러한 오류가 발생하는 이유는 현재 DB에 이전에 저장한 내용들이 남아있기 때문이다.
기존 존재하던 DB 내용들을 모두 삭제해준다.
실제로는 테스트 전용 DB를 따로 구축해 사용한다.
재실행
재실행 결과 실제 spring이 실행되며 회원가입 메서드가 정상적으로 수행되는 것을 확인할 수 있다.
이번에는 @Transactional 어노테이션을 주석 처리한 뒤 실행시켜 보자.
테스트를 수행한 뒤 DB에 저장한 내용이 삭제되지 않고 그대로 저장된다.
모든 테스트는 반복 실행할 수 있어야 한다.
하지만, 현재 DB에 테스트한 내용이 그대로 남아 있으므로 다시 실행하면 오류가 발생할 것이다.
이를 해결하기 위해서는 이전 메모리를 사용하던 예제에서 테스트하던 것처럼 @BeforeEach와 @AfterEach 어노테이션을 작성해 주어야 하는데 이는 매우 번잡하고 귀찮은 일이다.
데이터베이스는 기본적으로 Transaction이라는 개념을 가지는데
쿼리를 insert하고 commit을 수행해야 해당 정보가 데이터베이스에 반영된다.
즉, commit을 수행하지 않으면 DB에 반영되지 않는다.
이때, 테스트를 마친 후 롤백을 수행하면 DB에 데이터가 반영되지 않고 모두 사라지게 된다.
이러한 개념을 사용하는 방법이 @Transactional 어노테이션이다.
이전 순수 자바 코드만을 최소한으로 사용해 테스트 하던 방식을 단위 테스트
현재, 스프링 컨테이너와 DB를 연동해 수행한 테스트 방식을 통합 테스트라고 한다.
대게, 순수한 단위 테스트가 더 좋은 테스트일 확률이 높다.
그래서 스프링 컨테이너 없이 단위 단위로 쪼개 테스트를 수행할 수 있도록 연습하는 것이 중요하다.
'스프링 > 스프링 입문' 카테고리의 다른 글
AOP (0) | 2022.07.24 |
---|---|
스프링 DB 접근 기술 #2 (0) | 2022.03.06 |
회원 관리 예제 - 웹 MVC 개발 (0) | 2022.03.01 |
스프링 빈과 의존관계 (0) | 2022.03.01 |
회원 관리 예제 - 백엔드 개발 (0) | 2022.02.28 |