💡 JDBC란
JDBC(Java Database Connectivity)는 자바 프로그램과 데이터베이스를 연결하여 데이터베이스와의 상호 작용을 가능하게 하는 자바 API(응용 프로그래밍 인터페이스)입니다. JDBC는 데이터베이스에 접속하고 쿼리를 실행하며, 데이터를 검색하고 수정하는 등의 작업을 수행할 수 있습니다.
자바는 기본적으로 DB와 연결지어지려면 JDBC가 필요합니다.
애플리케이션에서 DB에 연동을 해서 데이터를 기존처럼 메모리에 저장하는 것이 아니라 DB에 저장될 수 있도록 하는 것입니다.
Spring과 DB를 연결하기 위해 H2 데이터 베이스를 사용할 예정인데, 해당 DB 설치관련내용은 아래의 링크에서 확인하여 설치를 진행해주시면 됩니다.
우선 JDBC와 h2 database를 사용하려면 build.grale → dependencies 항목에 라이브러리를 추가해주어야 합니다. https://memory-dev.tistory.com/entry/KimSprInt13
implementation 'org.springframework.boot:spring-boot-starter-jdbc'runtimeOnly 'com.h2database:h2'
그리고 repository 폴더에 JdbcMemberRepository를 생성하여 각각의 코드를 작성해 줍니다. Service폴더에는 SpringConfig파일에 DataSource를 생성합니다.
더보기 버튼을 누르시면 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);
}
}
@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.hellospring.service;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
//스프링이 실행될 때, Configuration을 읽고 하단의 코드를 스프링 빈에 등록하라고 인식하여 등록을 진행합니다.
//멤버 서비스와 멤버 리포지토리를 스프링 빈에 등록해주고, 스프링빈에 등록되어있는 멤버 리포지토리를 멤버 서비스에 넣어줍니다.
private DataSource dataSource;
@Autowired
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);
}
}
h2 DB에서 id와 name을 insert하여 3개의 데이터를 만들어 두었습니다. H2 DB와 스프링이 잘 연결되었는지 확인하기 위해 , 스프링을 실행하여 member url에서 회원 목록을 확인해보겠습니다.
서버를 실행한 후 회원 목록을 클릭하면 H2 서버 데이터에서 입력해두었던 id, name 데이터가 저장되어 있는 걸 확인할 수 있습니다. 다시 home 브라우저로 돌아간 후 이름을 입력한 후 회원 목록을 확인하면 추가가 되어있고, h2 database에서 table을 조회하면 브라우저에서 등록된 이름 데이터가 저장되어 있는 걸 확인할 수 있습니다.
🎈 DB와 연동된 MemberService 이용하여 Test코드 작성해보기
👉 통합 TEST 진행해보기
데이터 베이스는 기본적으로 트랜잭션이라는 개념이 있는데 DB에 데이터를 insert를 하게되면 커밋을 진행해야합니다. 테스터가 끝난 후 만약 롤백을 하게된다면 .join 후 검증까지 완료되었는데 롤백하면 DB에서 데이터가 전부 지워지게 됩니다. 여기서 @Transactional을 테스터케이스에 작성해주면 테스트를 실행할 때 트랜잭션을 먼저 실행하고 DB에 데이터를 인서트 한 후 테스터를 마친 후 데이터를 깔끔하게 지워줍니다.
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.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.*;
@SpringBootTest
//@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
//회원가입 서비스가 제대로 작동되는 지 확인해보기
//test는 한글로 적어도 괜찮다
@Test
void 회원가입() {
//given(어떤데이터) : 테스트 코드를 작성할 땐 어떠한 상황에 주어지는데
Member member = new Member();
member.setName("spring");
//when : 해당 상황을 실행했을 때 (memberService의 join 메서드를 검증)
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
}
}
먼저 h2 데이터 베이스에 저장된 기존 데이터들을 DELETE FROM MEMBER로 지워줍니다. 그리고 상단의 테스트 코드에서 회원가입 기능을 테스트할 수 있는 회원가입 메서드를 실행하게되면 테스트가 진행되며 다시 H2 데이터베이스에 join 메서드가 실행되며 setName(”spring”)의 값이 저장됩니다.
이렇게 “spring”이라는 name이 저장된 후 다시 회원가입 테스트 코드를 실행하게 되면, 아래처럼 같은 아이디를 존재하는 회원으로 인식하여 테스트가 진행되지 않는 경우가 발생합니다. 이 얘기는 하나의 name에 대해 테스트를 진행하고나면 다시 테스트를 진행했을 때 다음 테스트의 실행을 할 수 없다는 의미입니다. 이런 상황에서는 delete문을 DB에다가 또 작성하여 Test 메서드가 실행되고 나면 리포지토리에 저장된 데이터들을 지워줘야하는 추가 코드를 작성해주어야 하는데, 스프링 부트에서는 @SpringBootTest와 @Transactional 애노테이션을 사용하여 테스트가 전부 진행된 후 롤백 될 수 있도록 해주는 기능을 사용할 수 있습니다.
📄 @SpringBootTest와 @Transactional은 스프링 프레임워크에서 사용되는 어노테이션입니다.
@SpringBootTest : 스프링 부트 애플리케이션의 통합 테스트를 위해 사용되는 어노테이션입니다. 테스트 클래스에서 @SpringBootTest를 사용하면 스프링 애플리케이션 컨텍스트를 로드하고, 애플리케이션의 빈(Bean)들을 주입받을 수 있습니다. 이를 통해 실제 환경과 유사한 환경에서 테스트를 수행할 수 있습니다.
@Transactional : 데이터베이스 트랜잭션 처리를 위해 사용되는 어노테이션입니다. @Transactional을 메서드나 클래스에 적용하면 해당 메서드 또는 클래스의 실행이 트랜잭션 내에서 수행됩니다. 트랜잭션은 ACID(원자성, 일관성, 고립성, 지속성) 속성을 보장하며, 데이터베이스 조작을 일관성있게 처리할 수 있습니다. 메서드가 성공적으로 완료되면 트랜잭션은 커밋되고, 예외가 발생하면 롤백됩니다.
롤백(Rollback)은 트랜잭션에서 이전 상태로 되돌리는 작업을 의미합니다. 트랜잭션이란 하나의 논리적인 작업 단위를 의미하며, 여러 개의 데이터베이스 조작(쿼리 실행, 데이터 변경 등)을 포함할 수 있습니다.
트랜잭션은 ACID 원칙을 따르며, 롤백은 이 중 "원자성(Atomicity)" 속성을 보장하기 위한 메커니즘입니다. 원자성은 트랜잭션 내의 모든 데이터베이스 조작이 "전부 성공"하거나 "전부 실패"하는 것을 보장하는 속성입니다. 즉, 트랜잭션 내의 어떤 작업이라도 실패하면 트랜잭션 전체가 실패로 판단되어 이전 상태로 롤백됩니다.
롤백은 트랜잭션 내의 데이터 변경을 취소하고, 이전 상태로 복구하는 작업을 수행합니다. 데이터베이스 조작이 실패하거나 예외가 발생한 경우에 주로 롤백이 발생하며, 데이터 일관성을 유지하고 데이터베이스의 무결성을 보장하기 위해 사용됩니다.
예를 들어, 트랜잭션 내에서 A와 B라는 두 개의 데이터베이스 조작이 수행되는 경우를 생각해보겠습니다. 만약 B 조작이 실패하여 예외가 발생하면, 롤백이 실행되어 A 조작 역시 취소되고 이전 상태로 복구됩니다. 따라서 트랜잭션은 일관된 상태를 유지하게 됩니다.
롤백은 데이터베이스 관리 시스템(DBMS)에 의해 제공되는 기능으로, 트랜잭션의 시작과 끝을 명시적으로 정의하거나, 프레임워크에서 자동으로 처리될 수 있습니다. 롤백은 데이터 일관성과 안전한 데이터 조작을 위해 중요한 개념으로 사용됩니다.
'[ BACKEND] > Spring' 카테고리의 다른 글
[SPRING] 💡JPA (0) | 2023.07.13 |
---|---|
[SPRING] HTTP 요청과 응답 (1) | 2023.07.10 |
[SPRING] Spring Bean과 의존 관계 (0) | 2023.07.02 |
[SPRING] 스프링 웹 개발 기초 - 비즈니스 요구사항 정리 (0) | 2023.06.29 |
[Spring] view 환경설정 & 빌드와 실행해보기 (0) | 2023.06.23 |