PromleeBlog
sitemap
aboutMe

posting thumbnail
테스트를 유지한 상태에서 리팩토링하기 - Spring Boot로 시작하는 TDD 5편
Refactoring while Maintaining Tests - TDD with Spring Boot Part 5

📅

🚀

들어가기 전에 🔗

지난 4편에서 우리는
최소 구현
을 통해 초록 불(테스트 통과)을 확인했습니다.
하지만 그 코드는 임시방편으로 작성된 것이라 꽤 지저분했습니다.
Service 클래스가 데이터 저장까지 직접 하고 있었으니까요.

이제 마지막 단계로 코드를 올바르게 정리하는 시간이 왔습니다.
TDD에서는 이 단계를
리팩토링(Refactoring)
이라고 부릅니다.

🚀

리팩토링이란? 🔗

리팩토링은
결과(동작)는 그대로 유지하면서, 내부 구조만 개선하는 작업
을 말합니다.
사용자 입장에서는 바뀐 게 없지만, 개발자 입장에서는 코드가 훨씬 읽기 좋고 유지보수하기 좋게 변하는 것입니다.

여기서 중요한 점은
테스트 코드
입니다.
우리가 앞서 작성한 테스트 코드가 있다면, 내부 구조를 마음껏 뜯어고쳐도 두렵지 않습니다.
실수해서 기능을 망가뜨리면 테스트가 바로 빨간 불로 알려줄 테니까요.
테스트 코드 기반 내부 구조 변경
테스트 코드 기반 내부 구조 변경

🚀

코드 중복 제거와 책임 분리 🔗

현재 우리의 ProductService는 두 가지 일을 하고 있습니다.
  1. 비즈니스 로직 처리 (상품 등록)
  2. 데이터 저장 (메모리에 변수로 저장)

객체지향 설계 원칙에 따르면, 클래스는
하나의 책임
만 가지는 것이 좋습니다.
데이터를 저장하고 관리하는 역할은 보통
Repository
라는 친구에게 맡깁니다.
이제 Service가 쥐고 있던 저장소 역할을 Repository로 분리해 보겠습니다.

1. Product 클래스 수정 🔗

먼저 데이터를 식별하기 위해 Product 클래스에 ID 필드를 추가합니다.
이때, 코드를 깔끔하게 유지하기 위해 우리가 설치했던
Lombok
을 적극 활용하겠습니다.
Getter 메서드를 직접 칠 필요 없이, 어노테이션 하나로 해결할 수 있습니다.
src/main/java/com/example/tdd_study/product/Product.java
package com.example.tdd_study.product;
 
import org.springframework.util.Assert;
import lombok.Getter;
 
@Getter
public class Product {
    private Long id;
    private final String name;
    private final int price;
 
    public Product(String name, int price) {
        Assert.hasText(name, "상품명은 필수입니다.");
        Assert.isTrue(price > 0, "상품 가격은 0보다 커야 합니다.");
        this.name = name;
        this.price = price;
    }
 
    public void assignId(Long id) {
        this.id = id;
    }
}

2. Repository 클래스 생성 🔗

이제 데이터를 저장할 전용 클래스를 만듭니다.
아직 DB 연결은 하지 않고, 기존처럼 메모리(Map)를 사용하겠습니다.
패키지 경로가 com.example.tdd_study.product임을 주의해 주세요.
src/main/java/com/example/tdd_study/product/ProductRepository.java
package com.example.tdd_study.product;
 
import java.util.HashMap;
import java.util.Map;
 
public class ProductRepository {
    private Map<Long, Product> persistence = new HashMap<>();
    private Long sequence = 0L;
 
    public void save(Product product) {
        product.assignId(++sequence);
        persistence.put(product.getId(), product);
    }
}

3. Service 코드 수정 🔗

이제 ProductService가 직접 데이터를 저장하지 않고, 방금 만든 Repository를 사용하도록 수정합니다.
src/main/java/com/example/tdd_study/product/ProductService.java
package com.example.tdd_study.product;
 
public class ProductService {
    // Repository를 의존하게 변경
    private final ProductRepository productRepository;
 
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
 
    public void register(String name, int price) {
        Product product = new Product(name, price);
        productRepository.save(product);
    }
}

🚀

테스트 안정성 유지 전략 🔗

자, 코드를 꽤 많이 바꿨습니다.
Service 클래스의 구조가 완전히 달라졌습니다.
이제 가장 떨리는 순간입니다.
테스트를 돌렸을 때 여전히 초록 불이 나올까요?

테스트 코드 수정 🔗

Service의 구조가 바뀌었으므로(생성자가 생김), 테스트 코드의 설정 부분도 약간의 수정이 필요합니다.
우리는 POJO(Plain Old Java Object) 방식으로 테스트하고 있으므로, 직접 객체를 생성해서 넣어줍니다.
src/test/java/com/example/tdd_study/product/ProductServiceTest.java
package com.example.tdd_study.product;
 
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
 
class ProductServiceTest {
 
    private ProductService productService;
    private ProductRepository productRepository;
 
    @BeforeEach
    void setUp() {
        // Service가 Repository를 필요로 하므로 주입해줍니다.
        productRepository = new ProductRepository();
        productService = new ProductService(productRepository);
    }
 
    @Test
    void 상품_등록_성공() {
        // given
        String name = "아메리카노";
        int price = 4500;
 
        // when
        productService.register(name, price);
        
        // then: Repository를 통해서 검증하는 것도 좋은 방법입니다.
        // 하지만 지금은 에러 없이 실행되는지 확인하는 것에 집중합니다.
    }
}
수정을 마치고 테스트를 실행해 봅니다.
여전히
Green(성공)
이라면 리팩토링은 완벽하게 성공한 것입니다.

이 과정에서 우리가 얻은 것은 무엇일까요?
기능(상품 등록)은 똑같이 동작하지만, 코드의 구조는
역할과 책임
에 따라 깔끔하게 분리되었습니다.
나중에 DB를 MySQL로 바꾸고 싶다면, Service는 건드리지 않고 Repository만 바꾸면 됩니다.

🚀

리팩토링 시점 판단 🔗

그렇다면 언제 리팩토링을 해야 할까요?
보통 다음과 같은 기준을 따릅니다.


🚀

결론 🔗

오늘은 TDD의 마지막 단계인 리팩토링에 대해 알아보았습니다.


지금까지 우리는 **Red(실패) -> Green(성공) -> Refactor(개선)**의 한 사이클을 돌았습니다.
이 사이클을 계속 반복하면서 건물을 한 층씩 쌓아 올리는 것이 바로 TDD입니다.

다음 시간에는 이제 단순한 자바 클래스가 아닌, 실제 스프링 빈(Spring Bean)을 활용하여 도메인 중심의 설계를 적용해 보겠습니다.

참고 🔗