1 H2 데이터베이스 설치
(1) h2 데이터베이스 1.4.200 버전 설치
- h2.bat으로 콘솔 실행 > 연결
- C:\Users\사용자명에 test.mv.db가 생성
- 이후부터는 URL 변경하여 소켓으로 연결
- member 테이블 생성
> generated by default as identity로 null 값이 들어왔을 때 자동으로 채워줌
- insertinto를 사용하여 spring, spring2 두 명 추가
2 순수 JDBC
(1) 환경설정
- build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
}
- 스프링 부트 데이터베이스 연결 설정 추가 > application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
(2) Jdbc 리포지토리 구현
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource; }
@Override
public Member save(Member member) {
/* insert 쿼리 */
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
/* 커넥션 > prepareStatement > executeUpdate */
conn = getConnection();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
/* 여기서 DB에 실제 쿼리가 날아감 */
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
/* name 조회 */
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)
{
/* 리소스 close */
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();
}
}
/* Spring Framwork는 반드시 이렇게 close! */
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource); }
}
(3) SpringConfig 수정
- 기존의 코드는 하나도 손대지 않고 이 코드만 수정하여 구현체 변경 가능
- 개방-폐쇄 원칙(OCP, Open-Closed Principle) : 확장에는 열려있고, 수정, 변경에는 닫혀있다.
- 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 저장됨
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
/* MemberService를 SpringBin에 등록 */
@Bean
public MemberService memberService() {
/* SpringBin에 등록된 memberRepository를 요기 넣어줌 */
return new MemberService(memberRepository());
}
/* MemberRepository를 SpringBin에 등록 */
@Bean
public MemberRepository memberRepository() {
return new JdbcMemberRepository(dataSource);
}
}
3 스프링 통합 테스트
(1) 스프링 통합 테스트
- 지금까지의 테스트는 스프링과 무관하게 동작했던 테스트
- @Transactional : 테스트 케이스 달 테스트 시작 전에 트랜잭션을 시작 > 테스트 완료 후에 항상 롤백 > 반복 가능
- @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행
@SpringBootTest
@Transactional
public 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 {
/* name 중복 회원 저장을 시도 */
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
memberService.join(member1);
/* 오류 발생 */
/* memberService.join(member2) 로직을 수행할 때 > IllegalStateException.class 오류가 발생 */
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}