PromleeBlog
sitemap
aboutMe

posting thumbnail
Repository 계층 테스트 전략 - Spring Boot로 시작하는 TDD 7편
Repository Layer Testing Strategy - TDD with Spring Boot Part 7

📅

들어가기 전에 🔗

지난 6편까지 우리는 핵심 비즈니스 로직인
도메인(Domain)
서비스(Service)
영역을 튼튼하게 구축했습니다.
하지만 한 가지 아쉬운 점이 있었습니다.
바로 데이터 저장소로 HashMap을 사용했다는 점입니다.

실무에서는 데이터를 영구적으로 저장하기 위해
데이터베이스(DB)
를 사용합니다.
오늘은 우리의 코드를 실제 Spring Data JPA를 사용하는 형태로 업그레이드하고, 이 DB 계층(Repository)을 어떻게 테스트해야 하는지 알아보겠습니다.

필수: JPA 의존성 추가 🔗

가장 먼저 build.gradle 파일에 JPA와 H2 데이터베이스 라이브러리를 추가해야 합니다.
이것들이 없으면 @EntityJpaRepository 같은 기능을 사용할 수 없습니다.

build.gradle 수정 🔗

dependencies 블록에 아래 두 줄을 추가하고, 꼭
Gradle Refresh(코끼리 아이콘)
버튼을 눌러주세요.
build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
 
    // [추가] JPA와 H2 데이터베이스
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
}

Repository 테스트 목적 🔗

Repository 계층을 테스트하는 이유는 단순합니다.
"내가 작성한 코드가 DB와 제대로 대화하고 있는가?"를 확인하기 위해서입니다.

자바 객체와 DB 테이블 사이를 연결하는 다리(Repository)
자바 객체와 DB 테이블 사이를 연결하는 다리(Repository)

JPA 적용을 위한 리팩토링 🔗

이제 기존 코드를 JPA 코드로 변경하겠습니다.
이 과정에서 기존의 HashMap이나 persistence 변수 등은 모두
삭제
됩니다.

1. Entity 설정 🔗

먼저 Product 클래스를 DB 테이블과 매핑되도록 변경합니다.
@Entity, @Id, @GeneratedValue 어노테이션을 추가합니다.
(빨간 줄이 뜬다면 import가 제대로 되었는지 확인해 주세요. jakarta.persistence.* 패키지입니다.)
src/main/java/com/example/tdd_study/product/Product.java
package com.example.tdd_study.product;
 
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;
 
@Entity
@Table(name = "products")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int price;
 
    public Product(String name, int price) {
        Assert.hasText(name, "상품명은 필수입니다.");
        Assert.isTrue(price > 0, "상품 가격은 0보다 커야 합니다.");
        Assert.isTrue(price % 1000 == 0, "상품 가격은 1000원 단위여야 합니다."); 
        this.name = name;
        this.price = price;
    }
    // (중요) 기존의 assignId 메서드는 삭제합니다. DB가 ID를 자동 생성해주기 때문입니다.
}

2. Repository 인터페이스 전환 🔗

가장 큰 변화가 일어나는 곳입니다.
기존의 ProductRepository 클래스 내용을
모두 지우고
, 아래와 같이 interface로 변경합니다.
기존에 있던 persistence 변수나 save 메서드 구현 코드는 더 이상 필요하지 않습니다.
src/main/java/com/example/tdd_study/product/ProductRepository.java
package com.example.tdd_study.product;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // 코드가 비어있어도 괜찮습니다. JpaRepository가 save, findById 등을 대신 해줍니다.
}
이 시점에서 ProductService에서 컴파일 에러가 날 수 있습니다.
JPA의 save 메서드는 저장된 엔티티를 반환하지만, 기존 코드는 void였기 때문일 수 있습니다.
하지만 보통은 메서드 이름이 같아 큰 수정 없이 넘어갑니다.

깨진 테스트 복구하기 🔗

여기까지 수정하고 나면 컴파일 에러가 발생합니다.
바로 ProductServiceTest.java 파일입니다.
Error: ProductRepository is abstract; cannot be instantiated
이전에는 ProductRepository가 클래스여서 new ProductRepository()가 가능했지만, 이제는
인터페이스
라서 new를 사용할 수 없기 때문입니다.
이 문제를 해결하기 위해
Mockito
를 사용하여 가짜 객체(Mock)를 만들어 주입해야 합니다.

ProductServiceTest 수정 🔗

테스트 코드를 아래와 같이 수정하여 에러를 잡습니다.
src/test/java/com/example/tdd_study/product/ProductServiceTest.java
package com.example.tdd_study.product;
 
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.verify;
import static org.mockito.ArgumentMatchers.any;
 
@ExtendWith(MockitoExtension.class) // Mockito 기능을 쓰겠다고 선언
class ProductServiceTest {
 
    @Mock // 가짜 Repository 생성
    private ProductRepository productRepository;
 
    @InjectMocks // 가짜 Repository를 Service에 주입
    private ProductService productService;
 
    @Test
    void 상품_등록_성공() {
        // given
        String name = "아메리카노";
        int price = 4000;
 
        // when
        productService.register(name, price);
 
        // then
        // productRepository.save()가 호출되었는지 검증
        verify(productRepository).save(any(Product.class));
    }
}
이제 new 키워드 대신 @Mock을 사용하여 인터페이스의 가짜 구현체를 만들었기 때문에 테스트가 다시 정상 작동합니다.

@DataJpaTest 활용 🔗

이제 진짜 DB(H2)와 통신하는 Repository 전용 테스트를 작성해 보겠습니다.
src/test/java/com/example/tdd_study/product/ProductRepositoryTest.java
package com.example.tdd_study.product;
 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.assertThat;
 
@DataJpaTest
class ProductRepositoryTest {
 
    @Autowired
    private ProductRepository productRepository;
 
    @Test
    void 상품_저장() {
        // given
        Product product = new Product("아메리카노", 4000);
 
        // when
        Product savedProduct = productRepository.save(product);
 
        // then
        assertThat(savedProduct.getId()).isNotNull();
        assertThat(savedProduct.getName()).isEqualTo("아메리카노");
    }
}

테스트 실행 및 결과 분석 🔗

이제 작성한 ProductRepositoryTest를 실행해 봅니다.
IntelliJ에서는 메서드 옆의 초록색 화살표(Run)를 클릭하면 됩니다.

1. 결과 확인 (Green) 🔗

콘솔 창에 녹색 체크 표시와 함께 Tests passed 메시지가 뜬다면 성공입니다.
하지만 우리는 단순히 성공 여부뿐만 아니라
로그
를 볼 줄 알아야 합니다.

2. SQL 로그 분석 🔗

콘솔 로그를 자세히 살펴보면 아래와 같은 SQL 문장을 찾을 수 있습니다.
Hibernate: insert into products (name, price, id) values (?, ?, default)
이 로그가 보인다면 다음을 의미합니다.
만약 @DataJpaTest가 없었다면, 우리는 DB를 켜고 테이블을 만드는 복잡한 과정을 매번 직접 해야 했을 것입니다.

결론 🔗

오늘은 Repository 계층 테스트 작업을 진행했습니다.
이제 Service는 Mock으로 빠르고 가볍게, Repository는 DB와 연결하여 확실하게 테스트하는
이원화된 테스트 전략
이 완성되었습니다.
다음 시간에는 사용자가 만나는 최종 관문,
Controller 테스트와 API 검증
에 대해 알아보겠습니다.

참고 🔗