통합 테스트

  • 한 클래스나 메서드만 테스트하는 단위 테스트가 아니라, Bean과 db까지 포함하여 테스트 하는 것.
  • 의존성이 있는 객체(MemberService, MemberRepository 등)가 모두 주입된 상태에서 테스트가 실행된다. 
  • 실제 DB와 연동해서 테스트하고 @Transactional을 통해 테스트 후 롤백한다.
@SpringBootTest // 스프링 컨테이너와 함께 통합 테스트 실행. 스프링 컨테이너가 함께 실행되면서, 빈(Bean)이 자동으로 주입됨.
@Transactional // 테스트 코드에 달려있으면, 테스트 후에 db가 롤백됨
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService ;
    @Autowired MemberRepository memberRepository;

    @Test
    void join() {
        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember  = memberService.findOne(member.getId()).get();

        // 실제 값과 기대 값을 비교
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void duplicateMemberException() {
        // 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("이미 존재하는 회원입니다.");

        // fail
        //assertThrows(NullPointerException.class, () -> memberService.join(member2));

//        try {
//            memberService.join(member2);
//        } catch (IllegalStateException e) {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원 아이입니다.");
//        }


        // then

    }
}

 

 

단위 테스트(Unit Test) vs 통합 테스트(Integration Test)

  • 단위 테스트는 빠르고 단일 메서드만 테스트 가능해서 유지보수에 유리하고 잘 작동하는지 빠르게 확인 가능.
  • 통합 테스트는 각 컴퍼넌트가 잘 연결되는지 확인할 때 필요.
  • 단위 테스트로 모든 메서드의 정상 동작 확인 후 통합 테스트 하는 것이 이상적.
테스트 유형 장점 단점
단위 테스트 (Unit Test) 한 개의 클래스나 메서드를 독립적으로 테스트 빠른 실행 속도, 특정 로직 검증 용이 DB, 스프링 컨테이너가 필요 없는 환경
통합 테스트 (Integration Test) 여러 클래스가 협력하여 동작하는 전체 흐름 테스트 실제 환경과 유사한 테스트 가능 느림, 설정이 복잡함 

 

 

 

스프링 JdbcTemplate

  • 기존 JDBC의 반복 코드를 제거 해줌.(PreparedStatement, Connection, ResultSet etc..)
  • JdbcTemplate에 스프링이 관리하는 DataSource를 주입해서 사용.
  • JdbcTemplate이 자동으로 DB 연결을 관리해서, 개발자가 직접 Connection을 열고 닫을 필요가 없다.
// 기존 자바의 Connection 생성 -> SQL 실행 -> ResultSet 처리 -> 연결 닫기 과정의
// 반복적 작업을 줄이기 위해 JdbcTemplate 사용
public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    // 생성자가 하나면, 자동으로 Autowired 생성돼서 생략 가능
    // @Autowired 
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // 테이블 키 정보만 설정하면 알아서 INSERT SQL 쿼리를 생성하고 실행해 줌
    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

        // INSERT할 데이터 값을 Map으로 
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        // INSERT 실행 후 자동 생성된 키(id) 반환
        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());

        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        // SQL 쿼리 실행해서 결과 값을 String으로 받아옴
        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());
    }

    // ResultSet을 Member 객체로 변환하는 역할
    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));

            return member;
        };
    }
}



// --------------------------------------------------------



// 스피링 빈에 직접 등록
// 구조가 정형화 되지 않아서, 구현 클래스가 변경되는 경우가 잦을 경우에 사용하면 좋다
@Configuration
public class SpringConfig {

    private final 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);
        return new JdbcTemplateMemberRepository(dataSource);
    }
}

 

 

 

JPA

  • 기본적인 SQL도 JAP가 직접 만들어서 실행해줌. SQL과 데이터 중심의 설계 -> 객체 중심 설계로 전환.
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다. 그래서 서비스 계층에 트랜잭션을 추가해주어야 한다.
  • build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리를 추가해준다.
  • 그리고 스프링 부트에 JPA 설정을 추가해준다.

 

JPA 엔티티 매핑

  • Entity는 데이터베이스의 테이블과 매핑되는 객체.
  • 즉, SQL을 작성하지 않고, 객체로 데이터베이스를 다룰 수 있다.
// JPA가 관리하는 엔티티
@Entity
public class Member {

    @Id // 기본 키(Primary Key) 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // db에 데이터를 생성하면 생성되는 ID. 자동 증가 (Auto Increment)
    private Long id;

	@Column(name = "username", nullable = false) // 테이블 컬럼과 매핑 (생략 가능)
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

 

 

 

JPA 저장소

public class JpaMemberRepository implements MemberRepository {

    // JPA를 사용하려면, 엔티티매니저를 생성하고 주입 받아야 함
    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    // JPA가 쿼리 만들고, id 만들고 다 해줌
    @Override
    public Member save(Member member) {
        em.persist(member);

        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);

        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();

        return result.stream().findAny();
    }

    // 객체를 대상으로 쿼리를 날리는 것
    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}



// -------------------------------------------------------------------



// 스피링 빈에 직접 등록
// 구조가 정형화 되지 않아서, 구현 클래스가 변경되는 경우가 잦을 경우에 사용하면 좋다
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntitiyManager em;

    @Autowired
	public SpringConfig(DataSource dataSource, EntitiyManager em) {
 		this.dataSource = dataSource;
        this.em = em;
	}	

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }

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

 

 

 

Spring Data JPA

  • JPA를 기반으로 한 CRUD 기능을 자동으로 제공하고, 반복적인 코드 작성을 줄여주는 강력한 라이브러리.
  • JpaRepository를 상속받으면, 기본 CRUD 메서드가 자동 제공된다.
  • 즉, 메서드 이름만으로 자동으로 SQL을 실행한다.
JPA Spring Data JPA
EntityManager 필요 JpaRepository 자동 제공
기본 CRUD 메서드 직접 구현 필요 save(), findById() 등 자동 제공
JPQL 직접 작성 필요 메서드 이름만으로 SQL 자동 생성

 

// 스프링 데이터 Jpa가 자동으로 구현체를 만들어서 등록해줌
// 즉, 인터페이스 이름 만으로도 쿼리 작성, db 접근 등의 기능을 모두 알아서 작성해줌
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    // select m from Member m where m.name = ? 이렇게 쿼리를 짜줌
    @Override
    Optional<Member> findByName(String name);
}