[Clean Coding] 9. Unit Test

๋ผ์„ยท2024๋…„ 12์›” 9์ผ

Clean Coding

๋ชฉ๋ก ๋ณด๊ธฐ
8/10

๐Ÿ–‹๏ธ ๊ฐœ์š”

Testing์˜ 4๋‹จ๊ณ„

  1. Unit Test : ์œ ๋‹›๋ณ„ ํ…Œ์ŠคํŠธ
  2. Integration Test : ์œ ๋‹› ํ…Œ์ŠคํŠธ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ
  3. System Test : ๊ฐœ๋ฐœ ํšŒ์‚ฌ ๋‚ด์—์„œ์˜ ํ…Œ์ŠคํŠธ
  4. Acceptance Test : ๊ณ ๊ฐ์˜ ํ…Œ์ŠคํŠธ

์ด๋ฒˆ ์žฅ์—์„œ๋Š” Test์˜ ๊ธฐ๋ณธ ๋‹จ์œ„๊ฐ€ ๋˜๋Š” Unit Test์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณผ ๊ฒƒ์ด๋‹ค.

๐Ÿ“Œ Unit Test

  • Unit : ํ”Œ๊ณ ๊ทธ๋žจ์˜ ๊ฐ€์žฅ ์ž‘์€ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•œ ์ž‘์—… ๋‹จ์œ„
    • Java : class
    • Java : method
    • C : function
  • Unit Test : ๊ฐœ๋ณ„ ๋‹จ์œ„๊ฐ€ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•
    • ๊ฐ๊ฐ์˜ Unit์€ ์ตœ์†Œํ•œ ํ•˜๋‚˜์˜ test๊ฐ€ ์žˆ์–ด์•ผํ•˜๋ฉฐ
    • ์กฐํ•ฉ์ด ์žˆ์„์ˆ˜๋ก Good!
    • Production Code : ์ œํ’ˆ์œผ๋กœ ๋‚ด๋ณด๋‚ผ ์ฝ”๋“œ

๊ฐ„๋‹จํ•œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์‚ดํŽด๋ณด์ž
์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•ด๋ณด์ž

public class Calc {
	public int add(int a, int b) {
    	return a + b;
    } //Unit
    
    public int subtract(int a, int b){
    	return a - b;
    } //U kt
}

Unit Test์—์„œ๋Š” add(), subtract()๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์„œ ์ž˜ ์ž‘๋™์„ ํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค

์ด์— ๋”ฐ๋ผ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค

public class TestCalc {
	public void testAdd() {
    	Calc calc = new Calc();
        if (calc.add(1, 2) == 3)
        	System.out.println("Success");
        else
        	System.out.println("Failure");
    }
    
    public void testSubtract() {
    	Calc calc = new Calc();
        if (calc.subtract(2,3) == -1)
        	System.out.println("Success");
        else 
        	System.out.println("Failure");
    }
}

์—ฌ๊ธฐ์„œ Test case์˜ 3๊ฐ€์ง€ ํ•„์ˆ˜ ์š”์†Œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค

  1. ์ž…๋ ฅ์ด ๋ฌด์—‡์ธ๊ฐ€
  2. ์ž…๋ ฅ์œผ๋กœ ๋Œ๋ ค๋ณผ ์ฝ”๋“œ๊ฐ€ ๋ฌด์—‡์ด๊ณ 
  3. ๊ทธ ๊ฒฐ๊ณผ๊ฐ€ ๋ฌด์—‡์ด์–ด์•ผ ํ•˜๋Š”๊ฐ€

๐Ÿ–‹๏ธ JUnit

  • Java ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋ฅผ ์œ„ํ•œ ์œ ๋‹› ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ
  • ์ด๋Ÿฌํ•œ ์œ ๋‹› ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ xUnit์ด๋ผ ํ•œ๋‹ค
    • Java : JUnit
    • C++ : CPPUnit
    • C : CUnit
    • Javascript : JSUnit
    • Python : PyUnit
  • ์ง€์›
    • IDE : IntelliJ IDEA, Eclipse, NetBeans, Visual Studio Code (VS Code)
    • Build tools : Gradle, Maven, Ant
    • Console Launcher
  • JUnit 5 = JUnit Jupiter + JUnit Platform
    • JUnit Jupiter : JUnit 5์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ
    • JUnit Platform : JVM์—์„œ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ฐ˜

์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณด์ž

//static์„ ์ž‘์„ฑํ•˜์ง€ ์•Š์œผ๋ฉด ์“ธ๋•Œ๋งˆ๋‹ค org.junit.~์„ ์ž‘์„ฑํ•ด์ค˜์•ผ ํ•œ๋‹ค
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;

class MyTest {
	@BeforeAll
    static void initAll() { //๋ชจ๋“  ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ์‹คํ–‰
    }
    
    @BeforeEach
    void init() { //๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „ ์‹คํ–‰
   	}
    
    @Test
    void test() { //ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์ฝ”๋“œ
    }
    
    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() { //์‹คํ–‰ x
    }
    
    @AfterEach
    void tearDown() { //๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ ํ›„ ์‹คํ–‰
    }
    
    @AfterAll
    static void tearDownAll() { // ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚œ ํ›„ ํ•œ ๋ฒˆ ์ˆ˜ํ–‰
    }
  • BeforeEach์™€ AfterEach๋Š” optional์ด๋‹ค
    • ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ์ˆ˜ํ–‰๋˜๊ธฐ ์ „์— ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์žˆ๊ฑฐ๋‚˜, ํ…Œ์ŠคํŠธ ์ดํ›„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ
  • AfterAll์€ TearDown ๊ธฐ๋ฒ•์— ํ•ด๋‹นํ•˜๋Š” ๊ฒƒ์œผ๋กœ, ํ…Œ์ŠคํŠธ ํ›„ ์›๋ž˜ ์ƒํƒœ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ ์œ„ํ•ด ์ž„์‹œํŒŒ์ผ์ด๋‚˜ record ์ž‘์—…์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค

org.junit.jupiter.api.*๋ฅผ importํ•ด์•ผ๋งŒ ์•„๋ž˜ annotation์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

  • Annotations
    • @Test
    • @BeforeAll, @BeforeEach, @AfterEach, @AfterAll
    • @Disabled
    • @DisplayName
      • ํ™”๋ฉด์— ์ฐ์–ด๋‚ด๊ณ  ์‹ถ์€ ๊ฒƒ์ด ์žˆ์„ ๊ฒฝ์šฐ
    • @Tag
      • ํ…Œ์ŠคํŠธ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ธฐ ์œ„ํ•ด ํƒœ๊ทธ ์„ ์–ธ
    • @Order
      • ์ˆœ์„œ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•˜์ง€๋งŒ ๋ณ„๋กœ ๋ฐ”๋žŒ์งํ•œ ํƒœ๊ทธ๋Š” ์•„๋‹˜. ์ถ”์ฒœํ•˜์ง€ ์•Š์Œ

๋งจ ์ฒ˜์Œ์— ์‚ดํŽด๋ดค๋˜ add, subtract ๋ฉ”์„œ๋“œ์˜ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ JUnit์„ ํ™œ์šฉํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์„๊นŒ?

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;

class CalcTest { // test class ์„ ์–ธ
	@Test //์ด๋ ‡๊ฒŒ annotation์„ ์ž‘์„ฑํ•ด์ค˜์•ผ ํ•จ
    void testAdd() {
    	Calc calc = new Calc();
        assertEquals(3, calc.add(1,2));
    }
    
    @Test
    void testSubtract() {
    	Calc calc = new Calc();
        assertEquals(2, calc.subtract(5,3));
    }
}
  • org.junit.jupiter.api.Assertions ๋Š” ๋งŽ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๊ณตํ•œ๋‹ค

Assertion methods

  • assertEquals()
  • assertTrue()
  • assertFalse()
  • assertNotNull()
  • assertNull()
  • assertArrayEquals()
  • fail() : "์ด ์ž๋ฆฌ์— ์˜ค๋ฉด ์•ˆ๋จ.."์ฒ˜๋Ÿผ ๊ทธ ์ž์ฒด๋กœ fail. (ex. ๋ฃจํ”„ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด ์•ˆ๋˜๋Š”๋ฐ ๋ฃจํ”„ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ„ ๊ฒฝ์šฐ)

compiling and executing your test

  1. compile your program
    c:\javac Calc.java
  2. compile yout test class
    C:>javac CalcTest.java
  3. run the console laucher to execute JUnit tests
    C:>java -jar .\junit-platform-console-standalone-1.11.3jar -cp. -c CalcTest
    - cp: class path๋ฅผ ์˜๋ฏธ. .์€ ๊ฐ™์€ ์œ„์น˜์— ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธ
    - ๊ตฌ๋™์„ ์‹œ์ž‘ํ•˜๋Š” ๋†ˆ์ด๋‹ค. ์ง€์ •๋œ ์•„์ด๋ฅผ ์ˆ˜ํ–‰์‹œํ‚ค๋„๋ก ํ•˜๋Š” ๊ฒƒ (-c)
  • JUnit์€ failํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ๋ณด๊ณ ํ•œ๋‹ค
  • ๊ทธ๋Ÿผ Calc ํด๋ž˜์Šค๋ฅผ ๊ณ ์น˜๊ณ  ๋‹ค์‹œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค

์ด๋Ÿฌํ•œ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ๋ชจ๋‘ ์ˆ˜๊ธฐ๋กœ ์ž‘์„ฑํ•ด์•ผ ํ• ๊นŒ?
์ž๋™์œผ๋กœ ์ž‘์„ฑํ•ด์ฃผ๋Š” ํ”Œ๋žซํผ์ด ์กด์žฌํ•œ๋‹ค
์˜ˆ์‹œ๋กœ Agile methods, XP, test-driven development(TDD) ๋“ฑ์ด ์žˆ๋‹ค

TDD์— ๋” ์‚ดํŽด๋ณด์ž

๐Ÿ–‹๏ธ TDD (Test-Driven Development)

  • ๊ต‰์žฅํžˆ ์งง์€ ๊ฐœ๋ฐœ ์ฃผ๊ธฐ์˜ ๋ฐ˜๋ณต์„ ๊ฑฐ์น˜๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค์ด๋‹ค
    • ๊ต‰์žฅํžˆ ๊ตฌ์ฒด์ ์ธ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์— ๋Œ€ํ•˜์—ฌ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ œ์‹œ
    • ์ด๋Ÿฌํ•œ ํ…Œ์ŠคํŠธ๋“ค์ด ํŒจ์Šค๋˜๋„๋ก ์ฝ”๋“œ๊ฐ€ ๊ฐœ์„ ๋œ๋‹ค

Test-driven development cycle

  1. ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค
  2. ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ์ƒˆ๋กœ์šด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š”์ง€ ๋ณธ๋‹ค
  3. ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค
  4. ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค
  5. ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋ง ํ•œ๋‹ค

์ด ๋ฐฉ๋ฒ•์„ ๋ฐ˜๋ณตํ•œ๋‹ค
๊ธฐ์กด์˜ ์ฝ”๋“œ๋ฅผ ๋ง์น  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ณ ์ณ์ง„ ๊ฒƒ์ด ์žˆ์œผ๋ฉด ๊ธฐ์กด์˜ ๊ฒƒ๋“ค์ด ๋‹ค ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๋Š”์ง€ ๋ด์•ผํ•œ๋‹ค

Three Laws of TDD

  • First Law
    • ์‹ค์ œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ํ•ด๋‹น ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค
    • ์ฝ”๋“œ๋ถ€ํ„ฐ ๋ง๊ณ  ํ…Œ์ŠคํŠธ๋ถ€ํ„ฐ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค!

  • Second Law
    • ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ, ์‹คํŒจ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ๋งŒ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค
      • ์˜ˆ: ๋‹จ์ˆœํžˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์ปดํŒŒ์ผ๋˜์ง€ ์•Š๋Š” ๊ฒƒ๋„ ์‹คํŒจ๋กœ ๊ฐ„์ฃผ๋œ๋‹ค
    • ์ด ๋ฒ•์น™์€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋ฐฉ๋Œ€ํ•ด์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ํ•˜๋‚˜์”ฉ ์ ์ง„์ ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ๋ฌธ์ œ๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•˜๋„๋ก ๋•๋Š”๋‹ค
    • ์กฐ๊ธˆ์”ฉ ์Œ“์•„์˜ฌ๋ ค์•ผ ํ•˜๋ฉฐ, ํ•˜๋‚˜์”ฉ ์•„๋ž˜์„œ๋ถ€ํ„ฐ ํ…Œ์ŠคํŠธํ•ด์˜จ ๊ฒƒ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๊ฐ€ ๋‚œ ์ง€์ ์„ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋‹ค

  • Third Law
    • ํ˜„์žฌ ์ž‘์„ฑํ•œ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๊ธฐ์— ์ถฉ๋ถ„ํ•œ ์ตœ์†Œํ•œ์˜ production code(๊ธฐ๋Šฅ ์ฝ”๋“œ)๋งŒ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค
    • ํ•„์š” ์ด์ƒ์˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ๋ง๊ณ , ํ…Œ์ŠคํŠธ ํ†ต๊ณผ์— ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ๊ตฌํ˜„ํ•˜๋ผ๋Š” ๋œป์ด๋‹ค
    • ์ด๋Š” ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ ์œ ์ง€ํ•œ๋‹ค

๐Ÿ–‹๏ธ Test ์ž‘์„ฑ ๋ฐฉ๋ฒ•

๐Ÿ“Œ Good Test

๊ทธ๋ƒฅ ๋ณด์—ฌ์ฃผ๊ธฐ์‹์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ, ์ข‹์€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ค„ ์•Œ์•„์•ผ ํ•œ๋‹ค

๐Ÿ“ Clean Test

ํ…Œ์ŠคํŠธ๊ฐ€ ์—†๋Š” ๊ฒƒ๋ณด๋‹ค ๋”๋Ÿฌ์šด ํ…Œ์ŠคํŠธ๋ผ๋„ ์žˆ๋Š”๊ฒŒ ๋” ๋‚ซ๋‹ค๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๊ฒƒ์€ ํ‹€๋ฆฐ ์ƒ๊ฐ์ด๋‹ค

why?

  • production code๊ฐ€ ๋ฐœ์ „ํ•˜๋ฉด์„œ ํ…Œ์ŠคํŠธ๋„ ํ•จ๊ป˜ ๋ณ€ํ™”ํ•ด์•ผ ํ•œ๋‹ค
  • ์˜ค๋ž˜๋œ ๋”๋Ÿฌ์šด ํ…Œ์ŠคํŠธ๋Š” ๋ณ€๊ฒฝ์ด ์–ด๋ ค์›Œ์ง€๊ณ , ์ ์  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ๋ณต์žกํ•ด์ง„๋‹ค

Test Suite์˜ ๊ตฌ์„ฑ
1. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค
2. ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค
3. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ
4. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ

์ด๋Ÿฌํ•œ test suite์˜ ์ค‘์š”์„ฑ

  • test suite๊ฐ€ ์—†๋‹ค๋ฉด ์ฝ”๋“œ ๋ณ€๊ฒฝ์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ๋ณด์žฅํ•  ์ˆ˜ ์—†์Œ
  • test suite๊ฐ€ ์—†์œผ๋ฉด ์ฝ”๋“œ ๋ณ€๊ฒฝ์ด ๋‘๋ ค์›Œ์ง€๊ณ , ๊ฐœ๋ฐœ ์†๋„์™€ ํ’ˆ์งˆ์— ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์น˜

๊ฒฐ๋ก  : ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ๋งŒํผ ์ค‘์š”ํ•˜๋‹ค. ๊นจ๋—ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ๋†’์—ฌ์ค€๋‹ค

๐Ÿ”ป clean test๋ฅผ ๊ฐ€๋Šฅ์ผ€ ํ•˜๋Š” ๊ฒƒ์€

READABILITY ๊ฐ€๋…์„ฑ์ด๋‹ค!

what makes tests readable?

  • clarity
  • simplicity
  • density of expression
    • ์ด๊ฒƒ์€ test์—๋งŒ ๋ฃฐ๋กœ, ์ฝ”๋“œ๋ฅผ ๋ฐ€๋„์žˆ๊ฒŒ ์ž‘์„ฑํ•ด์•ผํ•˜๋ฉฐ formatting์„ ๊ผญ ์•ˆ ๋”ฐ๋ผ๋„ ๋œ๋‹ค. ๋ฐ˜๋ณต์ ์ธ ์ฝ”๋“œ๊ฐ€ ๋งŽ์ด ์ƒ๊ธฐ๊ธฐ ๋•Œ๋ฌธ

๐Ÿ“ Test๋Š” -ilities๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค

ํ…Œ์ŠคํŠธ๋Š” ์ฝ”๋“œ์˜ ์ž์‹ ๊ฐ์„ ์ œ๊ณตํ•œ๋‹ค

  1. ํ…Œ์ŠคํŠธ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
  • ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์ž ์žฌ์  ๋ฒ„๊ทธ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค
  • ๊ฒฐ๊ณผ์ ์œผ๋กœ ์ฝ”๋“œ ๋ณ€๊ฒฝ์ด ๋‘๋ ค์›Œ์ง€๊ณ , ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ๋А๋ ค์ง„๋‹ค
  1. ์œ ๋‹› ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
  • ์ฝ”๋“œ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๋‘๋ ค์›€์„ ์—†์• ์ค€๋‹ค
  • ํ…Œ์ŠคํŠธ coverage๊ฐ€ ๋†’์„์ˆ˜๋ก, ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๋‘๋ ค์›€์ด ์ ์–ด์ง„๋‹ค
  • ์œ ์—ฐ์„ฑ, ์ง€์† ๊ฐ€๋Šฅ์„ฑ, ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์—ฌ์ค€๋‹ค!

๐Ÿ”ป Refactoring Procedure

โœช Dirty Test

๋ฌธ์ œ

  • ์ค‘๋ณต๋œ ์ฝ”๋“œ๊ฐ€ ๋งค์šฐ ๋งŽ๋‹ค : ex. addPage9), assertSubString()
  • ์˜๋„๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ–ˆ์–ด์•ผ ํ–ˆ๋Š”๋ฐ ๋””ํ…Œ์ผ์„ ์Ÿ์•„๋ถ€์—ˆ๋‹ค - StepDown rule ์ค€์ˆ˜ x

โœช Refactored Test

UnitTest๋ฅผ ์งค ๋•Œ๋Š” 3๋‹จ๊ณ„๋กœ ๋‚˜๋ˆ„์–ด์„œ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค

  1. ํ…Œ์ŠคํŠธ๋ฅผ ์ค€๋น„ํ•˜๋Š” ๋‹จ๊ณ„
  2. ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋‹จ๊ณ„
  3. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋Š” ๋‹จ๊ณ„

๊ฐœ์„ ๋œ ์ฝ”๋“œ์—๋Š” ๋””ํ…Œ์ผ๋„ ์‚ฌ๋ผ์ง€๊ณ , ์ค‘๋ณต๋„ ์ œ๊ฑฐ๊ฐ€ ๋˜์—ˆ๋‹ค


๐Ÿ”ป Simple and Expressive Tests

ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋‹ค๋ณด๋ฉด ๋™์ผํ•œ ๊ณผ์ •์„ ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค

๋‹ค์Œ ์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณด์ž

@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
	hw.setTemp(WAY_TOO_COLD);
    controller.tic();
    assertTrue(hw.heaterState());
    assertTrue(hw.blowerState());
    assertFalse(hw.coolerState());
    assertFalse(hw.hiTempAlarm());
    assertTrue(hw.loTempAlarm());
}

์ด ์žฅ๋น„๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ๋ณด๊ณ ์‹ถ์€ ๊ฒƒ์ด๊ณ , ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” 5๊ฐœ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๋‹ค

hardware=hw๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ, ์›๋ž˜๋Š” mental mapping์„ ํ”ผํ•˜๋ผ๊ณ  ํ–ˆ์—ˆ๋Š”๋ฐ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” ๋ฐ˜๋ณต์ด ์›Œ๋‚™ ๋งŽ์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ—ˆ์šฉํ•œ๋‹ค

์œ„ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์–ด๋– ํ•œ๊ฐ€?
true, false๋งŒ ๊ณ„์† ๋ฐ”๋€Œ๊ณ  ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์•„์ ธ ๋ˆˆ์— ์ž˜ ๋“ค์–ด์˜ค์ง€ ์•Š๋Š”๋‹ค

๋ˆˆ์— ์ž˜ ๋“ค์–ด์˜ค๊ฒŒ ๊ฐœ์„ ํ•ด๋ณด์ž

@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
	wayTooCold();
    assertEquals("HBchL", hw.getState());
}
  • H : Heater
  • B : Blower
  • C : Cooler
  • H : HighTemp
  • L : LowTemp

๋กœ ์•ฝ์–ด๋ฅผ ์•ฝ์†ํ•ด์„œ ์‚ฌ์šฉํ–ˆ์œผ๋ฉฐ, true์ผ ๋•Œ๋งŒ ๋Œ€๋ฌธ์ž๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋‹ค์„ฏ๊ฐœ์˜ ๊ฒฐ๊ณผ๋ฅผ ํ•˜๋‚˜์˜ string์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ•ด์„œ ์‚ฌ์šฉํ–ˆ๋‹ค

  • ์™ธ์›Œ์•ผ ํ•˜์ง€๋งŒ, ์ค‘๋ณต๋œ ํ‘œํ˜„์ด ๋‚˜์˜ค๋‹ˆ๊นŒ ๊ฐ„๋žตํ•˜๊ฒŒ ํ‘œํ˜„ํ•˜๊ณ ์ž ํ•œ ๊ฒƒ์ด๋‹ค
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class TemperatureControlTest {

    // ๊ฐ€์ •: hw๋Š” ์‹œ์Šคํ…œ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ์ฒด
    private Hardware hw = new Hardware();

    @Test
    public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
        tooHot(); // ๋„ˆ๋ฌด ๋œจ๊ฑฐ์šด ์ƒํƒœ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        assertEquals("hBChl", hw.getState());
    }

    @Test
    public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
        tooCold(); // ๋„ˆ๋ฌด ์ถ”์šด ์ƒํƒœ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        assertEquals("HBchl", hw.getState());
    }

    @Test
    public void turnOnHiTempAlarmAtThreshold() throws Exception {
        wayTooHot(); // ๋„ˆ๋ฌด ๋œจ๊ฑฐ์šด ๊ฒฝ๊ณ„๊ฐ’ ์ƒํƒœ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        assertEquals("hBCHl", hw.getState());
    }

    @Test
    public void turnOnLoTempAlarmAtThreshold() throws Exception {
        wayTooCold(); // ๋„ˆ๋ฌด ์ถ”์šด ๊ฒฝ๊ณ„๊ฐ’ ์ƒํƒœ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        assertEquals("HBChl", hw.getState());
    }
    // ํ•˜๋“œ์›จ์–ด ํด๋ž˜์Šค (ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ํด๋ž˜์Šค์˜ ๋”๋ฏธ ๊ตฌํ˜„)
    class Hardware {
        private String state;

        public void setState(String state) {
            this.state = state;
        }

        public String getState() {
            return state;
        }
    }
}

๐Ÿ“Œ One Assert Per Test

ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜์— ํ•˜๋‚˜์˜ assert๋งŒ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ์ง€ํ–ฅํ•˜๋ผ๊ณ  ํ•œ๋‹ค

@Test
public void testBankAccount() {
    BankAccount account = new BankAccount();
    
    // ์ž”์•ก ์ดˆ๊ธฐ๊ฐ’ ํ™•์ธ
    assertEquals(0, account.getBalance());
    
    // ์ž…๊ธˆ ํ›„ ์ž”์•ก ํ™•์ธ
    account.deposit(100);
    assertEquals(100, account.getBalance());
    
    // ์ถœ๊ธˆ ํ›„ ์ž”์•ก ํ™•์ธ
    account.withdraw(50);
    assertEquals(50, account.getBalance());
}

ํ•˜์ง€๋งŒ ์ด ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ, ์—ฌ๋Ÿฌ assertEquals๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด ์ฝ”๋“œ๊ฐ€ ๊ธด ๊ฒฝ์šฐ, ์–ด๋–ค assert๊ฐ€ ์‹คํŒจํ–ˆ๋Š”์ง€ ์ฐพ๊ธฐ๊ฐ€ ์–ด๋ ต๊ณ , ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค

์žฅ์ 
ํ…Œ์ŠคํŠธ๊ฐ€ ๋‹จ์ผ ๊ฒฐ๋ก ์„ ๋‚ด๋ฆด ์ˆ˜ ์žˆ์–ด ์ดํ•ดํ•˜๊ธฐ๊ฐ€ ์‰ฝ๊ณ , ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์–ด๋А ๋ถ€๋ถ„์ด ์‹คํŒจํ–ˆ๋Š”์ง€ ๋ช…ํ™•ํžˆ ํŒŒ์•…์ด ๊ฐ€๋Šฅํ•˜๋‹ค

๋‹จ์ 
ํ…Œ์ŠคํŠธ๋ฅผ ๋„ˆ๋ฌด ๋งŽ์ด ๋ถ„๋ฆฌํ•˜๋ฉด ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋งŽ์•„์งˆ ์ˆ˜ ์žˆ๋‹ค

๋”ฐ๋ผ์„œ ๋‘ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  given-when-then ํŒจํ„ด์„ ํ™œ์šฉํ•œ๋‹ค!

@Test
public void testGetPageHierarchyAsXml() throws Exception {
    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
    
    whenRequestIsIssued("root", "type:pages");
    
    thenResponseShouldBeXML();
}

@Test
public void testGetPageHierarchyHasRightTags() throws Exception {
    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
    
    whenRequestIsIssued("root", "type:pages");
    
    thenResponseShouldContain(
        "<name>PageOne</name>", 
        "<name>ChildOne</name>", 
        "<name>PageTwo</name>"
    );
}
  • ๋‹จ์ผ assert ๊ทœ์น™์€ ์ง€์ผœ์•ผ ํ•  "์ข‹์€ ์ง€์นจ"์ด์ง€๋งŒ, ์ƒํ™ฉ์— ๋”ฐ๋ผ ์œ ์—ฐํ•˜๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅ.
  • ํ…Œ์ŠคํŠธ ๋‚ด assert ๊ฐœ์ˆ˜๋ฅผ ์ตœ์†Œํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜์ง€๋งŒ, ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ assert๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๊ดœ์ฐฎ๋‹ค.

๐Ÿ”ป Single Concept per Test ์˜ˆ์‹œ

๊ฐ€์žฅ ์ข‹์€ ๋ฃฐ์€, ํ…Œ์ŠคํŠธ๋‹น ํ•˜๋‚˜๋ฅผ ๊ฒ€์ฆํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด assert๋ฌธ์ด ์—ฌ๋Ÿฌ๋ฒˆ ๋‚˜์™€๋„ ok!

์œ„ ์ฝ”๋“œ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค
์˜๋„๊ฐ€ ์ž˜ ๋“œ๋Ÿฌ๋‚˜๋Š” ์ด๋ฆ„์œผ๋กœ ์ˆ˜์ •ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ๋ฅผ task๋งˆ๋‹ค ๊ฐ๊ฐ ๋งŒ๋“ค์–ด์ค€๋‹ค

  • ๋ฌธ์ œ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ assert๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ง„์งœ ๋ฌธ์ œ๋Š” ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ๋…์„ ๋™์‹œ์— ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒํ•œ๋‹ค
  • ํ•˜๋‚˜์˜ ๊ฐœ๋…๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผ ํ…Œ์ŠคํŠธ๊ฐ€ ๋‹จ์ˆœํ•˜๊ณ  ๋ช…ํ™•ํ•˜๋ฉฐ, ์‹คํŒจ ์‹œ ์›์ธ์„ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๊ฐ ๊ฐœ๋…์˜ ๋Œ€ํ•ด ์ตœ์†Œํ•œ์˜ assert๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜ ํ•˜๋‚˜๋‹น ํ•˜๋‚˜์˜ ๊ฐœ๋…๋งŒ ํฌํ•จํ•˜๋„๋ก ์„ค๊ณ„ํ•œ๋‹ค
  • ๊ทธ๋Ÿฌ๋ฉด ํ…Œ์ŠคํŠธ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋  ๊ฒƒ์ด๋‹ค!

๐Ÿ–‹๏ธ [F.I.R.S.T] ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋ฉด ์ง€์ผœ์•ผํ•  ๊ทœ์น™

๐Ÿ“ Fast

ํ…Œ์ŠคํŠธ๋Š” ๋น ๋ฅด๊ฒŒ ์ž‘๋™๋˜์–ด์•ผ ํ•œ๋‹ค
๋А๋ฆฌ๋ฉด ์—ฌ๋Ÿฌ ๋ฒˆ ๋Œ๋ ค๋ณผ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ!

๐Ÿ“ Independent

๋…๋ฆฝ์„ฑ์„ ๋ณด์žฅํ•ด์•ผํ•œ๋‹ค. ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ์ˆ˜ํ–‰๋  ์ˆ˜ ์žˆ๋„๋ก!
ํ…Œ์ŠคํŠธ๊ฐ€ ์™„๋ฃŒ๊ฐ€ ๋˜๋ฉด ์›์ƒํƒœ๋กœ ๋˜๋Œ๋ฆฌ๋Š” ์ž‘์—…์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•˜๋‹ค! (TearDown)

๐Ÿ“ Repeatable

ํ…Œ์ŠคํŠธ๋Š” ํŠน์ˆ˜ํ•œ ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ผ, ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์„ค์น˜ํ•  ๋ชจ๋“  ํ™˜๊ฒฝ์—์„œ ์ž‘๋™์ด ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค. ๋ฒ”์šฉ์„ฑ ์ค‘์š”!

๐Ÿ“ Self-Validating

์œ ๋‹› ํ…Œ์ŠคํŠธ ์—”ํ„ฐ๋ฅผ ๋ˆ„๋ฅด๊ณ  ๋‚˜์„œ๋ถ€ํ„ฐ๋Š” ์ค‘๊ฐ„์— ์‚ฌ์šฉ์ž์—๊ฒŒ input์„ ๋ฐ›๊ฑฐ๋‚˜ ๊ฒฐ๊ณผ๋ฅผ ํŒŒ์ผ๋กœ ์ €์žฅํ•ด์„œ ์‚ฌ์šฉ์ž๋ณด๊ณ  ํ™•์ธํ•ด๋ณด๋ผ๋Š” ์•ก์…˜์ด ๋‚˜์˜ค๋ฉด ์•ˆ๋œ๋‹ค

๋ชจ๋“ ๊ฒŒ ๋‹ค ์ž๋™ํ™” ๋˜์–ด์•ผ ํ•œ๋‹ค

๐Ÿ“ Timely

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์‹œ์ ์ด ์ค‘์š”ํ•˜๋‹ค!
TDD! test lag๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ์•ž์„œ๊ฐ€๊ฒŒ ๋œ๋‹ค

์ฝ”๋”ฉ์„ ํ•˜๊ธฐ ์ „์— ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์ž‘์„ฑ์„ ํ•˜๋˜๊ฐ€, ํ…Œ์ŠคํŠธ๊ฐ€ ์ฝ”๋”ฉ์„ ๋”ฐ๋ผ๊ฐ€๋„๋ก ๋ฐ˜๋“œ์‹œ ์„ค์ •์„ ํ•ด์•ผํ•œ๋‹ค


๐Ÿ”ป ๋‚˜์œ Unit Test ์ผ€์ด์Šค์˜ 7๊ฐœ ์ฆ์ƒ

  1. ํ…Œ์ŠคํŠธ๋Š” ํŒจ์Šคํ•˜์ง€๋งŒ ์‹ค์ œ featrue๋Š” ํ…Œ์ŠคํŒ…ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ
  2. ๊ด€๋ จ์—†๋Š” ๊ฒƒ๋“ค์„ ํ…Œ์ŠคํŒ… ํ•˜๋Š” ๊ฒƒ
  3. assertion์—์„œ ๋‹ค์ˆ˜์˜ ๊ฒƒ์„ ํ…Œ์ŠคํŒ…ํ•˜๋Š” ๊ฒƒ
  4. ์—๋Ÿฌ๋ฅผ ์‚ผ์ผœ๋ฒ„๋ฆฌ๋Š” ํ…Œ์ŠคํŠธ
  5. ๋น ๋ฅด๊ฒŒ ์ง„ํ–‰ํ•˜์ง€ ์•Š๊ณ , setup์ด ๋А๋ ค์„œ ์‹œ๊ฐ„์ด ๋งŽ์ด ์†Œ์š”๋˜๋Š” ํ…Œ์ŠคํŠธ
  6. ๊ฐœ๋ฐœ์ž ์ž์‹ ์˜ pc์—์„œ๋งŒ ์ž‘๋™ํ•˜๋Š” ํ…Œ์ŠคํŠธ
  7. ๊ฒฐ๊ณผ๋ฅผ ํŒŒ์ผ๋กœ ๋‚ด๋ฟœ์–ด์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ํด๋ฆญํ•ด์„œ ๋ณด๊ฒŒ ํ•˜๋Š” ํ…Œ์ŠคํŠธ

๊ฒฐ๋ก 

๊ฐ€๋…์„ฑ ์ค‘์š”!
์œ ์—ฐ์„ฑ, ์ง€์†๊ฐ€๋Šฅ์„ฑ, ์žฌ์‚ฌ์šฉ์„ฑ!

profile
์š•์‹ฌ ๋งŽ์€ ๊ณต๋Œ€์ƒ

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