통합 테스트
- 한 클래스나 메서드만 테스트하는 단위 테스트가 아니라, 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);
}