개인 공부/인프런

[JavaSpring] Spring Integration Test & Spring JdbcTemplate

planting grass 2023. 4. 3. 22:16
728x90

해당 글은 김영환 자바스프링 입문을 요약 정리한 글입니다.

스프링 통합 테스트

이전에 진행한 테스트는 순수하게 코드로만 이뤄져 있기 때문에 DB가 사용된 지금은 이전과는 다르게 해야한다.

그렇기 때문에, 스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자.

이전에 MemberServiceTest를 만든 파일에 MemberServiceIntegrationTest이름으로 클래스를 생성해준다.

코드는 아래와 같다.

package study.studyspring.service;

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 study.studyspring.domain.Member;
import study.studyspring.repository.MemberRepository;
import study.studyspring.repository.MemoryMemberRepository;

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("Test");

        // 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("Test");

        Member member2 = new Member();
        member2.setName("Test");
        // when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

@SpringBootTest를 사용하면 스프링 컨테이너와 테스트를 함께 실행한다.

이후 회원가입 코드만 실행시키면 에러가 발생하는데 그 이유는 이미 DB상에 우리가 테스트할 이름인 Test가 들어가 있기 때문이다.

때문에 DB에 데이터를 먼저 지워야 한다.

H2 Consol에 들어가서 delete from member를 입력해준다.

이후 Member를 확인해보면 데이터가 지워진것이 확인된다.

  • 실무에서는 Test용 DB를 따로 구축하거나 로컬내에서 따로 만든다고 한다.

다시 Test를 돌려보면 잘 돌아가는게 확인된다.

여기서 H2 Consol에 들어가서 데이터를 확인하면 Test가 들어가 있는게 확인되면 Test가 잘 돌아간다는것이다.

하지만, 여기서 문제가 발생한다.

Test는 반복해서 실행이 가능해야 하는데 재 실행할 경우 DB에 데이터가 들어가 있기 때문에 오류가 발생한다.

우리가 과거에 Test에 만들었던 AfterEach를 사용해도 되지만, Spring에서 이를 대비해 지원해주는 기능이 있다.

DB는 기본적으로 Transactional개념이 있다.

  • Transactional이란?총 4가지 특성을 지닌다.
    1. 원자성: 트랜잭션의 모든 작업이 성공적으로 수행되거나 모두 취소되어야 한다. 즉, 작업 중 하나라도 실패하면 이전 상태로 롤백된다.
    2. 일관성: 트랜잭션을 수행하기 전과 후에 데이터베이스의 일관성이 유지되어야 한다.
    3. 격리성: 한 트랜잭션이 다른 트랜잭션에 영향을 미치지 않도록 격리되어야 한다.
    4. 지속성: 트랜잭션이 완료된 후에는 그 결과가 영구적으로 유지되어야 한다.
  • DB에 데이터를 일관성과 무결성을 보장하기 위해 사용된다.

이는 곧 오토 커밋과도 연관이 된다.

  • 오토 커밋(Auto Commit)이란?
    • 오토 커밋 활성화시: DBMS는 SQL 문이 실행될 때마다 해당 문을 하나의 트랜잭션으로 처리하고, 처리 결과를 DB에 바로 반영한다.
      개발자가 명시적으로 커밋 명령어를 사용하지 않아도 자동으로 이뤄진다.
    • 오토 커밋 비활성화시: 여러 개의 SQL 문을 하나의 트랜잭션으로 묶어서 처리해야 한다.
      개발자가 명시적으로 커밋 명령어를 사용해 트랜잭션을 수동으로 커밋해야 한다.
  • 각각의 SQL 문이 실행될 때마다 해당 문이 실행되는 트랜잭션을 자동으로 커밋하는 설정이다.

Spring은 @Transactional를 사용하면 DB를 사용 후 롤백 시켜준다.

코드에서 비활성화 시켜둔 트랜잭션을 //를 지워 활성화 시켜주고 DB에서 데이터를 날린 후, Test를 계속 돌려보면 정상 작동하는게 확인된다.

  • 단위 테스트
    • 우리가 사용한 MemberServiceTest가 이에 속한다.
    • 소프트웨어의 각각의 모듈 or 컴포넌트를 개별적으로 테스트한다.
    • 속도가 빠르고, 버그를 발견하고 수정하는데 효과적이다.
  • 통합 테스트
    • MemberServiceIntegerationTest가 이에 속한다.
    • 전체적인 시스템이 제대로 동작하는지 검증한다.
    • 속도가 느리고, 시스템이 정상적으로 동작하는지 검증한다.

스프링 JdbcTemplate

  • 순수 Jdbc와 동일한 환경설정을 하면 된다.
  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.

Repository에 JdbcTemplateMemberRepository클래스를 만들어준다.

코드는 아래와 같다.

package study.studyspring.repository;

import org.springframework.beans.factory.annotation.Autowired;
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 study.studyspring.domain.Member;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
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);
    }

    @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;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper());
        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @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;
        };
    }
}

여기서 Bean의 생성자는 1개라면 때문에 @Autowired는 생략해도 된다.

순수 Jdbc와 비교하면 코드가 매우 짧다는것을 알 수 있다.

이후 코드를 SpringConfig에 가서 memberRepository에 return을 아래와 같이 바꿔주면 된다.

        @Bean
    public MemberRepository memberRepository() {
        //return new MemoryMemberRepository();
        //return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }

이후 코드가 잘 되는지 확인하려면 Test 코드로 돌아가서 돌리면 된다.

728x90