๋ฉ‹์‚ฌ Backend Plus 25์ผ์ฐจ ๐Ÿฆ

์‹ ์žฌ์›ยท2023๋…„ 11์›” 26์ผ

์˜ค๋Š˜์€ ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ˆ˜์ •, ๋…ผ๋ฆฌ (soft) ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ํ•˜์˜€๊ณ , ์˜คํ›„์—๋Š” mokito ๋ผ๋Š” ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€๋‹ค.
์ง„ํ–‰ํ•˜๋ฉด์„œ ๊ฒช์—ˆ๋˜ ์ ์„ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

(์ƒํ’ˆ์— ๋Œ€ํ•œ ์ˆ˜์ • ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ ๋ณ€๊ฒฝ๊ฐ์ง€์™€ ๋ณ‘ํ•ฉ์— ๋Œ€ํ•ด ์•Œ์ˆ˜ ์žˆ์—ˆ๋‹ค.)

๋ณ€๊ฒฝ๊ฐ์ง€ : ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์—์„œ ์ˆ˜์ • ๋ฉ”์†Œ๋“œ ์ƒ์„ฑ
์—”ํ‹ฐํ‹ฐ ํŒฉํ† ๋ฆฌ merge : ์œ„ํ—˜์„ฑ์ด ์žˆ์Œ

โžก ๋ณ€๊ฒฝ ๊ฐ์ง€์™€ merge์— ๋Œ€ํ•ด์„œ๋Š” ๋‹ค์Œ ๋ธ”๋กœ๊ทธ์—์„œ ๋‹ค๋ค„์•ผ๊ฒ ๋‹ค.

์˜ค์ „

๋…ผ๋ฆฌ (soft) ์‚ญ์ œ ๊ธฐ๋Šฅ ์ ์šฉ

์ฒ˜์Œ์œผ๋กœ ๋…ผ๋ฆฌ ์‚ญ์ œ๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์™œ ๋…ผ๋ฆฌ ์‚ญ์ œ๋ฅผ ํ•ด์•ผ๋˜๋Š” ๊ฒƒ ์ธ์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

  • ์‚ญ์ œ์—๋Š” ํฌ๊ฒŒ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.
    โžก ๋ฌผ๋ฆฌ ์‚ญ์ œ, ๋…ผ๋ฆฌ ์‚ญ์ œ

โžก Controller ํด๋ž˜์Šค

@DeleteMapping("/{id}")
    public RsData softDeleteProduct(
    	@PathVariable(name = "id") Long productId) {
        
        productService.softDeleteProduct(productId);
        return RsData.of(DELETE_SUCCESS_PRODUCT, "delete");
    }

๋ฌผ๋ฆฌ (Hard) ์‚ญ์ œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ row ์ž์ฒด๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

  • ๋“ฑ๋ก๋œ ์ƒํ’ˆ์„ select ํ•˜๊ณ , ๋“ฑ๋ก๋œ ์ƒํ’ˆ๊ณผ ์—ฐ๊ด€๋œ ์ด๋ฏธ์ง€๋ฅผ select ํ•˜์—ฌ ๋“ฑ๋ก๋œ ์ƒํ’ˆ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • @DeleteMapping ์˜ ์š”์ฒญ์œผ๋กœ delete ์ฟผ๋ฆฌ ๋ฌธ์ด ์‹คํ–‰ ๋˜๋ฉฐ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ row๊ฐ€ ์‹ค์ œ๋กœ ์‚ญ์ œ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋…ผ๋ฆฌ (Soft) ์‚ญ์ œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ row๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ ์‚ญ์ œ ์—ฌ๋ถ€์— ๋Œ€ํ•œ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จ ํ•ฉ๋‹ˆ๋‹ค. (์‚ญ์ œ ์‹œ๊ฐ„, ๋…ผ๋ฆฌ๊ฐ’ (false, true) ๋“ฑ์œผ๋กœ ํŒ๋‹จ)

update ์ฟผ๋ฆฌ ๋ฌธ์ด ์‹คํ–‰ ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • UPDATE ์ฟผ๋ฆฌ ๋ฌธ์ด ์‹คํ–‰ ๋˜๊ฒŒ ๋˜๋ฉฐ, deleted_at์˜ ํ•„๋“œ์— ์‚ญ์ œ ์š”์ฒญ์ด ๋“ค์–ด์˜จ ์‹œ๊ฐ„์„ ์ €์žฅํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
    (deleted_at ํ•„๋“œ๋Š” ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— ๋ฏธ๋ฆฌ ์ž‘์„ฑํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค์— ๋งคํ•‘ ํ•ด์•ผ ๋ฉ๋‹ˆ๋‹ค.)

โžก ๋…ผ๋ฆฌ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์—๋Š”
@SQLDelete(sql = "UPDATE ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ด๋ฆ„ SET ํ•„๋“œ ์ด๋ฆ„ = CURRENT_TIMESTAMP where id = ?")์˜
์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— @SQLDelete() ์–ด๋…ธํ…Œ์ด์…˜์„ ์ž‘์„ฑํ•˜์—ฌ, ์—”ํ‹ฐํ‹ฐ ์‚ญ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์„๋•Œ delete ์ฟผ๋ฆฌ๋ฌธ๋Œ€์‹  ์ปค์Šคํ…€ ํ•˜์—ฌ ๋งคํ•‘ ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

๊ทธ๋ž˜์„œ ์–ธ์ œ ๋…ผ๋ฆฌ ์‚ญ์ œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ๋ ๊นŒ ?
โžก ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜์ง€์•Š๊ณ  ์‚ญ์ œ ์ด๋ ฅ์„ ์ถ”์ ํ•ด์•ผ ๋˜๋Š”๊ฒฝ์šฐ, ๋ณต๊ตฌ ๊ฐ€๋Šฅ์„ฑ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋“ฑ๋“ฑ์˜ ์กฐ๊ฑด์—์„œ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณดํ˜ธ ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜คํ›„

Mokito ํ…Œ์ŠคํŠธ

  • ๊ฐ€์งœ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ• ์ˆ˜ ์žˆ๊ฒŒ ์ง€์›ํ•˜๋Š” ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ ์ด๋ฉฐ, build.gradle์˜ ์•„๋ž˜์™€ ๊ฐ™์ด ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ ํ•„์š” ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณดํ†ต Controller์™€ Service ๋‹จ์„ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    (์˜์กด์„ฑ์„ ์ฃผ์ž… ๋ฐ›์•„์•ผ ํ•˜๋Š”๊ฒฝ์šฐ, mokito๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€์งœ ๊ฐ์ฒด๋ฅผ ์ฃผ์ž…)
dependencies {
    testImplementation 'org.mockito:mockito-core:5.2.0'
}

service ๋‹จ ์—์„œ ์˜์กด์„ฑ ์ฃผ์ž…์„ ํ•˜๋Š” ๊ฐ์ฒด๋Š” @Mock ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฐ€์งœ ๊ฐ์ฒด๋กœ ๋งŒ๋“ค์–ด ์ค„์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@InjectMocks ์–ด๋…ธํ…Œ์ด์…˜์€ ์ฃผ์ž…์„ ๋ฐ›์„ ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์‚ฌ์šฉ ํ•ฉ๋‹ˆ๋‹ค.

@ExtendWith(MockitoExtension.class) // Mock ๊ฐ์ฒด ์ดˆ๊ธฐํ™”๋ฅผ ์œ„ํ•œ ์–ด๋…ธํ…Œ์ด์…˜
class ProductServiceImplTest {
 	@InjectMocks // ์ฃผ์ž…์„ ๋ฐ›์„ ํด๋ž˜์Šค
    private ProductServiceImpl productService;

    @Mock // ์ฃผ์ž…ํ•  ๊ฐ์ฒด
    private ProductRepository productRepository;

    @Mock // ์ฃผ์ž…ํ•  ๊ฐ์ฒด
    private ImageRepository imageRepository;


	@Test
    void test() {
    	// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ..
    }
    
}

Repository ํ…Œ์ŠคํŠธ

  • @DataJpaTest ์–ด๋…ธํ…Œ์ด์…˜์€ ์Šคํ”„๋ง ๋ถ€ํŠธ์˜ JPA ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ ์ž…๋‹ˆ๋‹ค.
  • ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ํ†ต์‹  ํ•˜์ง€์•Š์œผ๋ฉฐ, H2 ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค๋ฅผ default๋กœ ํ…Œ์ŠคํŠธ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

build.gradle ์˜ ์ถ”๊ฐ€ํ•˜์—ฌ ์ธ๋ฉ”๋ชจ๋ฆฌ DB๋ฅผ ๊ตฌ์ถ•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

dependencies {
    runtimeOnly 'com.h2database:h2'
    
    // ... ์ถ”๊ฐ€ ์˜์กด์„ฑ
}
@DataJpaTest
class ProductRepositoryTest {
    @Autowired
    private ProductRepository productRepository;

    @DisplayName("์ƒํ’ˆ ์ €์žฅ ํ™•์ธ")
    @Test
    void create_Product() {
        Product product = Product.builder().productName("๊ฐ•์•„์ง€ ์‚ฌ๋ฃŒ")
                .price("25_000")
                .content("์œ ํ†ต๊ธฐํ•œ 1๋…„ ๋‚จ์€ ์‚ฌ๋ฃŒ์ž…๋‹ˆ๋‹ค.")
                .build();

        Product saveProduct = productRepository.save(product);
        assertThat(saveProduct.getProductName())
                .isEqualTo(product.getProductName());
    }
}

๋งˆ๋ฌด๋ฆฌ

Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ฝ”๋“œ๋ฅผ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ ์ฒ˜์Œ ์‹œ๋„ ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์–ด์ƒ‰ํ•˜๊ณ , ์„œํˆฌ๋ฅด์ง€๋งŒ ์ถ”๊ฐ€๋กœ ๋งŒ๋“ค ๊ธฐ๋Šฅ๋“ค์— ๋Œ€ํ•ด ๊ณ„์† ์‚ฌ์šฉํ•ด๋ณด๋ฉฐ ์ต์ˆ™ํ•ด ์ง€๋Š”๊ฒŒ ๋ชฉํ‘œ ์ž…๋‹ˆ๋‹ค.

REFERENCE

https://velog.io/@sussa3007/Spring-JUnit-Mockito-%EA%B8%B0%EB%B0%98-Spring-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1 (mokito ํ…Œ์ŠคํŠธ ์ฐธ์กฐ ๋ธ”๋กœ๊ทธ)

0๊ฐœ์˜ ๋Œ“๊ธ€