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 검증
에 대해 알아보겠습니다.

참고 🔗