JDBC 접근 방법
Table of Content
테스트용 H2 데이터베이스 설치 과정
H2 DB engine 설치
권한 설정
chmod 755 h2.sh
Shell
복사
실행
./h2.sh
Shell
복사
실행 후 다음과 같은 화면 이후 브라우저로 콘솔화면이 나타난다.
데이터베이스 파일 생성 방법과 콘솔 접근
아래와 같은 URL 입력 후 연결
jdbc:h2:~/test
Shell
복사
최초 생성시 다음처럼 홈 경로에 test.mv.db 생성 확인
이후 접속부터는 아래 URL로 접속
jdbc:h2:tcp://localhost/~/test
Shell
복사
기본 테이블 생성 확인
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
Shell
복사
1. 순수 JDBC
과거 JDBC의 커넥션 예시
기존 Java의 HashMap으로 임시로 구성했던 저장소를 H2 데이터베이스로 변경하지만 간편하게 config에서 Repository 구현체만 변경하고 나머지 코드는 그대로 사용함
설정 파일로 정의한 SpringConfig.java에서 Repository 빈의 명칭만 새로운 데이터베이스로 변경해도 된다.
SpringConfig.java
package com.example.demoapp.config;
import com.example.demoapp.repository.JdbcMemberRepository;
import com.example.demoapp.repository.MemberRepository;
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 {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberRepository memberRepository(){
//return new MemoryMemberRepository(); <- 기존 비활성화
return new JdbcMemberRepository(dataSource); // DB 변경
}
}
Java
복사
Repository JDBC 구현체 JdbcMemberRepository.java 의 코드
package com.example.demoapp.repository;
import com.example.demoapp.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;
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection connection) throws SQLException {
DataSourceUtils.releaseConnection(connection, 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();
}
}
@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());// 위의 values와 매칭이된다. parameter index 1은
pstmt.executeUpdate(); //실제 쿼리가 날라감
rs = pstmt.getGeneratedKeys();// 자동으로 key를 꺼내준다.
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 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);
} 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);
}
}
}
Java
복사
과거 코드로 상당히 복잡하고 많은 코드가 사용되고 있고, 쿼리문 자체도 직접 sql이라는 변수에 할당하여 작업되기 때문에 실수가 발생 할 수 있다.
하지만 현재 이 JDBC를 사용하면서 스프링의 장점은
•
Java의 객체지향 다형성을 활용
◦
개방-폐쇄 원칙(OCP, Open-Closed Principle)
▪
확장에는 열려있고, 수정, 변경에는 닫혀있다.
•
스프링의 DI (Dependencies Injection)를 통해 인터페이스를 두고 구현체만 교체 할 수 있음
•
이걸 스프링은 매우 편리하게 스프링 컨테이너가 지원
스프링 통합 테스트를 통한 확인
@SpringBootTest 를 통해 실제로 어플리케이션을 실행시키는 테스트를 진행한다. 실제 동작 여부를 테스트 할 수 있는 테스트 방식이다. 위 JDBC로 Repository를 변경한 부분이 @Autowired 를 통해서 주입되어 사용되므로 해당 부분을 확인 할 수 있다.
package com.example.demoapp.service;
import com.example.demoapp.domain.Member;
import com.example.demoapp.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.assertThrows;
@SpringBootTest // 스프링 통합테스트(실제 어플리케이션을 작동시킴)
@Transactional // 영속성을 활용하여 테스트 이후 롤백
class MemberServiceIntergrationTest {
//test라 필드인젝션으로 간단하게 해도 무방
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
//then
Member foundMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(foundMember.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("이미 존재하는 회원입니다.");
}
}
Java
복사
2. Spring JDBC template 활용
순수 JDBC 반복, 중복 코드를 제거해주는 라이브러리, 다만 쿼리문은 그대로 직접 작성하는 단점
SpringConfig.java 에서 기존 JdbcMemberRepository를 리포지토리 빈으로 사용하는 부분만 새로운 JdbcTemplateMemberRepository를 사용하도록 교체해준다.
package com.example.demoapp.config;
import com.example.demoapp.repository.JdbcMemberRepository;
import com.example.demoapp.repository.MemberRepository;
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 {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberRepository memberRepository(){
//return new MemoryMemberRepository();
//return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
}
Java
복사
새로운 JdbcTemplate.java에서는 기존 JDBC 커넥션과 클로징을 관리하는 중복코드보다 상당히 많은 코드를 줄일 수 있다.
package com.example.demoapp.repository;
import com.example.demoapp.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);
}
//순수 JDBC보다 템플릿을 통해 많은 코드를 줄일 수 있다.
@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());
// 위 SimpleJdbcInsert클래스를 통해 쿼리문 직접 작성 없이 쿼리가 작성됨.
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
Java
복사
통합 테스트를 통한 재확인
위 JDBC로 Repository를 변경하여 정상 작동하던 테스트 코드를 한번 더 JdbcTemplate으로 변경한 부분이 사용되므로 해당 부분을 확인 할 수 있다. Repository만 config에서 교체한 부분이 정상적으로 작동 되는 것으로 확인되고 있다.
Related Posts
Search