티스토리 뷰
스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 김영한 인프런 강의 참고
[Goals]
- JDBC Template
- 통합 테스트
JDBC Template
순수 JDBC api를 사용하여 DB에 접근할 때 필수적으로 처리해줘야 하는 로직들이 있다.
Connection
객체를 생성하고, Statememt
객체를 생성하고 … 다시 해제하고 … 예외처리 하고…
(참고 : [Spring] 입문 - 순수 JDBC로 DB 연결하기(feat. mysql) - 1)
이전의 예제 코드만 보더라도, 핵심 서비스 로직을 제외 했는데도, 중복되는 부분이 적지 않다.
// ...
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
// DB의 연결 정보를 가지고 일정 시간 동안 DB와 연결할 수 있도록 통로 역할을 하는 객체
Connection conn = null;
// 다양한 SQL 구문들을 정의하고 바인딩 하는 방법들과 실제 DB로 전송하는 방법들이 정의된 객체
PreparedStatement pstmt = null;
// sql 쿼리 결과를 저장하는 데이터 집합 객체
ResultSet rs = null;
try {
conn = getConnction();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
// ...
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
// ...
// 할당한 리소스 해제
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();
}
}
// Connection 객체 리소스 해제
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
개발자라면 이런 중복 코드들을 없애고 싶은 마음이 드는 건 당연하다. 고맙게도 Jdbc api가 이러한 중복 코드를 최소화할 수 있도록 라이브러리를 제공한다.
JDBC Template은 JDBC 코어 패키지의 중심 클래스입니다. JDBC 사용을 단순화하고 일반적인 오류를 방지하는 데 도움이 됩니다. - spring 공식 문서
라이브러리 이름이 JDBC Template인 이유는 객체 지향 디자인 패턴 중 템플릿 메서드 패턴(Template Method Pattern)을 적용하여 중복을 제거했기 때문이다.
(참고 : https://coding-factory.tistory.com/712)
사용하는 방법은 매우 간단하다. 순수 JDBC와 동일한 환경설정을 하면 된다.
build.gradle
// ...
dependencies {
// ...
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// ...
}
이제 바로 JDBC Template api를 사용할 수 있다. 이를 사용하여 MemberRepository
를 다시 구현해 보겠다.
.../repository/JdbcTemplateMemberRepository.java
// ...
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
// ...
}
먼저 JdbcTemplate
타입의 인스턴스 변수를 선언한다. 이 객체는 스프링으로부터 DI를 받을 수 없다.
대신 먼저 클래스 생성자에서 이전처럼 DataSource
를 주입받는다.
그 다음 JdbcTemplate
생성자를 통해 JdbcTemplate
객체를 새로 생성해 주는데, 이때 주입받은 dataSouce
를 매개변수 값으로 넣어 준다.
그럼 이제 JdbcTemplate
객체로 api를 사용하여 구현하면 된다.
.../repository/JdbcTemplateMemberRepository.java
// ...
public class JdbcTemplateMemberRepository implements MemberRepository{
// ...
@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());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
// ...
}
먼저 save
메서드 구현이다.
SimpleJdbcInsert
는 테이블에 대한 간편한 삽입 기능을 제공하는 클래스로, 실제 삽입은 JdbcTemplate
을 사용한다. 때문에 객체를 생성할 때 JdbcTemplate
타입의 객체를 매개변수 값으로 넣어준다.
withTableName(String tableName)
: 삽입에 사용할 테이블 명usingGeneratedKeyColumns(String ... columnNames)
: 자동 생성 키가 있는 열의 이름을 지정executeAndReturnKey(SqlParameterSource parameterSource)
: 전달된 값을 사용하여 삽입을 실행하고 생성된 키를 반환
여기서 SqlParameterSource
는 간단히 말해서 sql에 들어갈 parameter Map 객체를 처리하는 인터페이스다. 구현체인 MapSqlParameterSource
를 사용하면 자바의 Map
객체를 테이블 속성 및 속성 값과 매핑시켜 삽입하는 sql문을 만들 수 있다.
// ...
public class JdbcTemplateMemberRepository implements MemberRepository{
// ...
@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());
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;
};
}
}
이 외의 메서드 구현은 거의 비슷하다.
JdbcTemplate
은 반복적인 로직을 거의 없앴지만 sql문은 직접 작성해 줘야 한다.
query(String sql, RowMapper<T> rowMapper, Object ... args)
: 주어진 SQL을 실행한다. 이때PreparedStatement
를 생성하고, 주어진 인수 목록을 바인딩 하여 실행한다. 그 결과 각 행을RowMapper
를 통해 매핑시킨다.
(참고로 반환 타입은 List<T>
로 매개변수 타입에 따라 정해진다)
query
메서드 안에 이전과 같이 String
타입으로 작성된 sql문을 넣어준다. 이때 두 번째 인자 값으로 RowMapper
타입의 객체가 필요한데. 이를 반환하는 메서드를 따로 구현해 준다.
RowMapper
는 행 단위로 ResultSet
의 행을 매핑하기 위해 JdbcTemplate
에서 사용하는 인터페이스다. 우리는 이 인터페이스의 mapRow
라는 메서드를 구현한 구현체를 매개변수 값으로 넘겨줘야 한다.
T mapRow(ResultSet rs, int rowNum) throws SQLException
rs
: 매핑할 ResultSet으로 쿼리 결과 값이 저장됨 (현재 행에 대해 미리 초기화됨)rowNum
: 현재 행의 번호
Member
객체를 생성해 rs
로부터 id
와 name
값을 가져와 대입해 준 뒤 반환한다.
또한 RowMapper
인터페이스는 추상 메서드가 mapRow
1개 밖에 없는 함수형 인터페이스이기 때문에 람다식으로 바꿀 수 있다.
아래의 코드를 람다식으로 바꾸면 위와 같은 코드가 된다.
// 함수형 인터페이스 객체를 생성과 동시에 구현
private RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
};
}
이제 마지막으로 spring 설정 파일에서 MemberRepository
인터페이스의 구현체를 변경해 준다.
…/SpringConfig.java
// ...
@Bean
MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
통합 테스트
이제 위 코드를 실제로 테스트 해봐야 한다. 이전의 테스트 코드는 오로지 spring의 도움 없이 java의 힘 만으로 테스트를 했다면, 지금은 DB와 연결해야 하므로 spring을 띄워야 한다. 이전의 테스트는 메서드 단위의 테스트로 단위 테스트라 한다면, 지금 작성하는 것은 어플리케이션을 띄워야 하므로 통합 테스트로 넘어간다.
Spring Boot는 어노테이션을 통해 이를 매우매우 간단하게 지원해 준다.
@SpringBootTest
: 스프링 컨테이너와 테스트를 함께 실행@Transactional
: 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백
기존의 MemberServiceTest
를 그대로 복사한 뒤, 아래와 같이 수정만 해주면 된다.
.../service/MemberServiceIntegrationTest.java
// ...
@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
// Assertions.assertThat(memberRepository.findById(saveId).get()).isEqualTo(member);
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
// ...
@Transactional
덕분에 각 테스트 메소드는 실행 중 생성되는 데이터가 DB에 Commit 되기 전에 Rollback을 하므로 테스트 DB는 항상 초기 상태를 유지된다.
@Autowired
를 속성에 사용한 이유는, 단지 테스트 코드를 더 직관적이고 작성하기 편리하게 하기 위함이다.
Assertions.assertThat(memberRepository.findById(saveId).get()).isEqualTo(member)
를 주석처리한 뒤 새로 검증 코드를 작성해야 한다.
이전에는 메모리 DB를 사용했기 때문에 생성한 객체를 다시 메모리에 저장하는 형태라서 사실상 같은 객체를 참조하고 있으므로 테스트를 통과한다.
하지만 외부 DB를 사용하게 되면, 외부 DB에서 가져온 객체는 새로운 메모리에 올라가기 때문에 기존에 코드로 생성한 객체와는 다른 메모리에 위치하게 된다.
따라서 검증 코드를 객체의 name
값을 비교하도록 약간 수정한다. 다른 테스트 메소드들도 동일하게 수정해 준다.
위와 같은 테스트 방식은 애플리케이션의 설정, 모든 Bean을 모두 로드하기 때문에 운영환경과 가장 유사한 테스트가 가능하다. 단, 테스트 속도는 느려질 수밖에 없다.
실제로 비교해 보면 Spring 컨테이너 및 설정들을 띄우는 MemberServiceIntegrationTest
는 전체 테스트가 완료되는데 2.4s지만, Java만을 사용하는 MemberServiceTest
는 139ms로 확연하게 차이가 난다.
실무에서도 어쩔 수 없는 상황을 제외하면, 통합 테스트보다는 단위 테스트가 더 좋은 테스트일 확률이 높다고 한다.
Reference
- 인프런 강의 - https://www.inflearn.com/course/스프링-입문-스프링부트
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html
- https://coding-factory.tistory.com/712
- https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html
'Spring&Spring Boot' 카테고리의 다른 글
[Spring Boot] 입문 - Spring Boot 프로젝트에 JPA 적용하기 (0) | 2023.02.24 |
---|---|
[Spring Boot] 입문 - JPA를 사용하는 이유 (0) | 2023.02.23 |
[Spring Boot] 입문 - 순수 JDBC로 DB 연결하기(feat. mysql) - 2 (1) | 2023.02.17 |
[Spring Boot] 입문 - 순수 JDBC로 DB 연결하기(feat. mysql) - 1 (0) | 2023.02.17 |
[Spring Boot] 입문 - 회원 관리 예제(웹 MVC 개발) (0) | 2023.02.11 |
- Total
- Today
- Yesterday
- 스프링
- 파이썬 for Beginner 솔루션
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- 생활코딩 javascript
- git branch
- jsp
- 쉽게 배우는 운영체제
- 스프링 mvc
- 선형 회귀
- 패킷 스위칭
- spring mvc
- 김영환
- Gradle
- 프로그래머스
- 지옥에서 온 git
- git merge
- Spring Data JPA
- 스프링 테스트
- Python Cookbook
- Spring
- Spring Boot
- 운영체제 반효경
- Thymeleaf
- 방명록 프로젝트
- 쉘 코드
- git
- 스프링 컨테이너
- Computer_Networking_A_Top-Down_Approach
- 파이썬 for Beginner 연습문제
- JPA
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |