๐ก ๋ณธ ๊ฒ์๊ธ์ ๋ฐ์ฐ๋น๋์ 'Practical Testing: ์ค์ฉ์ ์ธ ํ ์คํธ ๊ฐ์ด๋' ๊ฐ์๋ฅผ ๊ณต๋ถํ๊ณ , ์ด์ ๋ํด ์ ๋ฆฌํ ๋ด์ฉ์ ๋๋ค.
๋ค์ด๊ฐ๋ฉฐ
์ํํธ์จ์ด ๊ฐ๋ฐ์๋ก์ ๊ฒฝ๋ ฅ์ ์์๊ฐ๋ ๊ณผ์ ์์, ์ฝ๋์ ํ์ง๊ณผ ์ ์ง๋ณด์์ฑ์ ๋ํด ๋ง์ ๊ณ ๋ฏผ์ ํ๊ฒ ๋์์ต๋๋ค. ๊ฐ๋ฐ ์ด๊ธฐ์๋ ์ฃผ์ด์ง ์๊ฐ ์์ ๊ธฐ๋ฅ์ ์์ฑํ๋ ๊ฒ์ด ์ค์ํ๊ธฐ ๋๋ฌธ์ ํ ์คํธ์ ์ค์์ฑ์ ๊ฐ๊ณผํ๊ณ , ๊ธฐ๋ฅ ๊ตฌํ์๋ง ์ง์คํ์์ต๋๋ค. ํ์ง๋ง ํ๋ก์ ํธ๊ฐ ๋ณต์กํด์ง๊ณ ๊ท๋ชจ๊ฐ ์ปค์ง๋ฉด์, ๊ธฐ๋ฅ ๊ตฌํ ํ์ ๋ฐ์ํ๋ ์๊ธฐ์น ์์ ๋ฒ๊ทธ์ ๋ฌธ์ ๋ค์ด ์ ์ ๋ ๋น๋ฒํด์ก์ต๋๋ค. ์ด๋ฌํ ๊ฒฝํ์ ํตํด ํ ์คํธ ์ฝ๋์ ํ์์ฑ์ ์ ๊ฐํ๊ฒ ๋์์ต๋๋ค. ๊ทธ๋์ ์ ๋ ์ข ๋ ์ฒด๊ณ์ ์ธ ํ ์คํธ ์ฝ๋ ์์ฑ๊ณผ TDD(Test Driven Development)์ ๋ํด ๊ตฌ์ฒด์ ์ผ๋ก ํ์ตํ๊ณ ์ถ์์ต๋๋ค.
Section 01. ํ ์คํธ ์ฝ๋์ ํ์์ฑ
1. ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ง ์๋๋ค๋ฉด?
- ๊ท์ฐฎ์ ์์ : ํ ์คํธ ์ฝ๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ท์ฐฎ์ ์์ ์ด๋ค. ์ค๋ฌด์์๋ ์งง์ ์๊ฐ ์์ ๊ธฐ๋ฅ ๊ตฌํ๋ง ํ๊ธฐ์๋ ๋ฒ ์ฐฌ๋ฐ, ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ๊ฐ ์ฝ์ง ์๋ค.
- ํ ์คํธ ์ํ: ํ ์คํธ์ ํ์์ฑ์ ๋ช ํํ ์ดํดํ์ง ๋ชปํ๋ฉด, ์ด ์์ ์ ์ํํํ๊ณ ๋ฌด์ํ๋ ๊ฒฝํฅ์ด ์๊ธธ ์ ์๋ค.
- ์๋ ํ ์คํธ์ ํ๊ณ: ํ ์คํธ ์ฝ๋๊ฐ ์๋ ํ๊ฒฝ์์๋ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ์ฝ๋๋ฅผ ์์ ํ ๋ ๊ธฐ์กด ์ฝ๋๊ฐ ์ฌ์ ํ ์ ์ ์๋ํ๋์ง ํ์ธํ๊ธฐ ์ํด ์ฌ๋์ด ์ง์ ์๋์ผ๋ก ํ ์คํธํด์ผ ํ๋ค.
- ํ์ฅ์ฑ๊ณผ ์ธ๋ ฅ ๋ฌธ์ : ํ๋ก๋์ ์ฝ๋๋ ์๊ฐ์ด ์ง๋ ์๋ก ์ ์ ๋ ํ์ฅํ๊ฒ ๋๋๋ฐ, ๊ทธ๋๋ง๋ค ํ ์คํธ ์ธ๋ ฅ์ ๋ฌดํ์ ๋๋ฆด ์๋ ์๋ค.
- ๋๋ฝ๋ ํ ์คํธ ์ผ์ด์ค: ์ฌ๋์ด ์๋์ผ๋ก ํ ์คํธ๋ฅผ ํ๋ค ๋ณด๋ฉด ๋๋ฝ๋๋ ์ผ์ด์ค๊ฐ ๋ฐ์ํ ์ ์๊ณ , ์ด๋ ์น๋ช ์ ์ธ ๊ฒฐํจ์ผ๋ก ์ด์ด์ง ์ ์๋ค.
- ์ปค๋ฒ๋ฆฌ์ง ๋ถ์กฑ: ์ํํธ์จ์ด๊ฐ ์ปค์ง๋ ์๋๋ฅผ ๋ฐ๋ผ์ก์ง ๋ชปํ๊ฒ ๋๊ณ , ๊ธฐ๋ฅ๋ค๋ ์๋ก ๊ฒน์น๋ฉด์ ๊ธฐ์กด์ ํ ์คํธํ๋ ์์ญ์ ๋ ํ ์คํธํ๊ฒ ๋๋ฉด์ ์ปค๋ฒํ ์ ์๋ ์์ญ์ด ๋ฐ์ํ๊ฒ ๋๋ค.
- ๊ฒฝํ๊ณผ ๊ฐ์ ์์กด: ํ๋ก์ ํธ๋ฅผ ์ค๋ ํ๋ ์ฌ๋๋ค ๋๋ ๊ฐ๋ฐ ๋ฐ ํ ์คํธ๋ฅผ ์ค๋ ํ๋ ์ฌ๋๋ค์ ๊ฒฝํ๊ณผ ๊ฐ์ ์์กดํ๊ฒ ๋๋ค.
- ํผ๋๋ฐฑ ์ง์ฐ: ์ฌ๋์ด ํ ์คํธ๋ฅผ ํ๋ค ๋ณด๋ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ ค ํผ๋๋ฐฑ์ด ๋ฆ์ด์ง๊ณ , ๋ฒ๊ทธ ์์ ๊ณผ์ ์ด ๋๋ ค์ง๋ค. ์ด๋ ์ ์ง๋ณด์๋ฅผ ์ด๋ ต๊ฒ ํ๊ณ , ์ํํธ์จ์ด์ ์ ๋ขฐ๋๋ฅผ ๋ฎ์ถ๋ค.
2. ํ ์คํธ ์ฝ๋๊ฐ ํ์ํ ์ด์
- ๋น ๋ฅธ ํผ๋๋ฐฑ: ํ ์คํธ ์ฝ๋๋ฅผ ํตํด ๋ด๊ฐ ๊ฐ๋ฐํ ๊ธฐ๋ฅ์ด ์๋ํ ๋๋ก ๋์ํ๋์ง ๋น ๋ฅธ ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์๋ค.
- ์๋ํ ๋ฐ ์ ๋ขฐ์ฑ: ๊ธฐ๊ณ๊ฐ ๊ฒ์ฆํ ์ ์๋๋ก ์๋ํ๋ฅผ ํตํด ์ํํธ์จ์ด์ ๋ํ ์์ ๊ฐ๊ณผ ์ ๋ขฐ๊ฐ์ ์ป์ ์ ์๋ค.
- ์ํํธ์จ์ด ์ปค๋ฒ๋ฆฌ์ง: ํ ์คํธ ์ฝ๋๋ฅผ ์ ์ถ๊ฐํ๋ฉด, ์ปค์ง๋ ์ํํธ์จ์ด์ ํ๋ก๋์ ์ฝ๋๋ฅผ ํ ์คํธ ์ฝ๋๊ฐ ๊ณ์ ์ปค๋ฒํ ์ ์๋ค.
- ์ ์ง๋ณด์ ์ฉ์ด์ฑ: ์ฌ๋ฐ๋ฅธ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ฉด ํ๋ก๋์ ์ฝ๋์ ์์ ์ฑ์ ์ ๊ณตํ๋ฉฐ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์์ง๋ค.
- ๋น์ฉ ์ ์ฝ: ์๋ํ๋ฅผ ํตํด ๋น ๋ฅธ ์๊ฐ ์์ ๋ฒ๊ทธ๋ฅผ ๋ฐ๊ฒฌํ ์ ์๊ณ , ์๋ ํ ์คํธ์ ๋๋ ๋น์ฉ์ ์ ์ฝํ ์ ์๋ค.
- ๋น ๋ฅธ ๋ณํ ์ง์: ์ํํธ์จ์ด์ ๋น ๋ฅธ ๋ณํ๋ฅผ ์ง์ํ ์ ์๋ค.
- ํ ๋ด ๊ณต์ ์ง์: ๊ณ ๋ฏผํ๋ ๊ฒ๋ค์ ์ฝ๋๋ก ๋ น์ฌ๋ด๋ฉด, ํ ๋ด ๊ณต์ ์ง์์ด ๋์ด ํ์๋ค์ ์ง๋จ ์ง์ฑ์ ํ ์ฐจ์์ ์ด์ต์ผ๋ก ์น๊ฒฉ์ํฌ ์ ์๋ค. ์ด๋ ‘๊ฐ๊น์ด ๋ณด๋ฉด ๋๋ฆฌ์ง๋ง, ๋ฉ๋ฆฌ ๋ณด๋ฉด ๊ฐ์ฅ ๋น ๋ฅด๋ค’๋ก ํํ๋ ์ ์๋ค
3. ๊ฒฐ๋ก
1) ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ง ์๋๋ค๋ฉด?
- ๋ณํ๊ฐ ์๊ธฐ๋ ๋งค์๊ฐ๋ง๋ค ๋ฐ์ํ ์ ์๋ ๋ชจ๋ Case๋ฅผ ๊ณ ๋ คํด์ผ ํ๋ค.
- ๋ณํ๊ฐ ์๊ธฐ๋ ๋งค์๊ฐ๋ง๋ค ๋ชจ๋ ํ์์ด ๋์ผํ ๊ณ ๋ฏผ์ ํด์ผ ํ๋ค.
- ๋น ๋ฅด๊ฒ ๋ณํํ๋ ์ํํธ์จ์ด์ ์์ ์ฑ์ ๋ณด์ฅํ ์ ์๋ค.
2) ํ ์คํธ ์ฝ๋๊ฐ ๋ณ๋ชฉ์ด ๋๋ค๋ฉด?
- ํ๋ก๋์ ์ฝ๋์ ์์ ์ฑ์ ์ ๊ณตํ๊ธฐ ํ๋ค์ด์ง๋ค.
- ํ ์คํธ ์ฝ๋ ์์ฒด๊ฐ ์ ์ง๋ณด์ํ๊ธฐ ์ด๋ ค์ด, ์๋ก์ด ์ง์ด ๋๋ค.
- ์๋ชป๋ ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง ๊ฐ๋ฅ์ฑ์ด ์๊ธด๋ค.
3) ์ฌ๋ฐ๋ฅธ ํ ์คํธ ์ฝ๋๋?
- ์๋ํ ํ ์คํธ๋ก ๋น๊ต์ ๋น ๋ฅธ ์๊ฐ ์์ ๋ฒ๊ทธ๋ฅผ ๋ฐ๊ฒฌํ ์ ์๊ณ , ์๋ ํ ์คํธ์ ๋๋ ๋น์ฉ์ ํฌ๊ฒ ์ ์ฝํ ์ ์๋ค.
- ์ํํธ์จ์ด์ ๋น ๋ฅธ ๋ณํ๋ฅผ ์ง์ํ๋ค.
- ํ์๋ค์ ์ง๋จ ์ง์ฑ์ ํ ์ฐจ์์ ์ด์ต์ผ๋ก ์น๊ฒฉ์ํจ๋ค.
- ๊ฐ๊น์ด ๋ณด๋ฉด ๋๋ฆฌ์ง๋ง, ๋ฉ๋ฆฌ ๋ณด๋ฉด ๊ฐ์ฅ ๋น ๋ฅด๋ค.
Section 02. ๋จ์ ํ ์คํธ
โป ์ด๊ฐ๋จ ์นดํ ํค์ค์คํฌ ์์คํ ๊ฐ๋ฐ๊ณผ ํ ์คํธ ์ฝ๋ ์์ฑ
1. ๊ฐ๋ฐ ํ๊ฒฝ
- IDE: IntelliJ Ultimate
- Java: 11
- Framework: Spring Boot 2.7.7
- Build Tool: Gradle & Groovy
- Dependencies: Spring Web, Thymeleaf, Spring Data JPA, H2 Database, Lombok, Validation
- Testing: Junit5, AssertJ
2. ๋จ์ ํ ์คํธ๋?
- ์ ์: ๋จ์ ํ ์คํธ๋ ์์ ์ฝ๋ ๋จ์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฒ์ฆํ๋ ํ ์คํธ๋ฅผ ์๋ฏธํ๋ค. ์ฌ๊ธฐ์ ์์ ์ฝ๋๋ ํด๋์ค ํน์ ๋ฉ์๋๋ฅผ ์๋ฏธํ๋ค.
- ํน์ง: ๋จ์ ํ ์คํธ๋ ๊ฒ์ฆ ์๋๊ฐ ๋น ๋ฅด๊ณ ์์ ์ ์ด๋ค.
3. ์๋ ํ ์คํธ์ ์๋ํ ํ ์คํธ
1) ์๋ ํ ์คํธ
- ์ ์: ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ธฐ๋ฅ์ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ฆํ๋ ํ ์คํธ ๋ฐฉ์.
- ์ฅ์ :
- ๊ฐ๋จํ ๊ธฐ๋ฅ์ ๋ํด์ ๋น ๋ฅด๊ฒ ๊ฒ์ฆํ ์ ์๋ค.
- ์ง๊ด์ ์ด๊ณ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ป์ ์ ์๋ค.
- ๋จ์ :
- ๋ฐ๋ณต์ ์ด๊ณ ์๊ฐ์ด ๋ง์ด ์์๋๋ค.
- ํ ์คํธ ๊ณผ์ ์์ ์ค์๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋๋ค.
- ๋๊ท๋ชจ ํ๋ก์ ํธ์์๋ ๋นํจ์จ์ ์ด๋ค.
2) ์๋ํ ํ
์คํธ
- ์ ์: ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ฌ ์๋์ผ๋ก ํ ์คํธ๋ฅผ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ฆํ๋ ํ ์คํธ ๋ฐฉ์.
- ์ฅ์ :
- ๋ฐ๋ณต์ ์ธ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์ํํ์ฌ ์๊ฐ์ ์ ์ฝํ ์ ์๋ค.
- ์ผ๊ด๋ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํ๋ฉฐ, ์ธ๊ฐ ์ค๋ฅ๋ฅผ ์ค์ผ ์ ์๋ค.
- ๋๊ท๋ชจ ํ๋ก์ ํธ์์ ํจ์จ์ ์ด๋ฉฐ ์ ์ง๋ณด์๊ฐ ์ฉ์ดํ๋ค.
- ๋จ์ :
- ์ด๊ธฐ ์ค์ ๊ณผ ํ ์คํธ ์ฝ๋ ์์ฑ์ ์๊ฐ์ด ์์๋๋ค.
- ํ ์คํธ ์ฝ๋ ์ ์ง๋ณด์๊ฐ ํ์ํ๋ค.
4. Junit5์ AssertJ
- Junit5: ๋จ์ ํ ์คํธ๋ฅผ ์ํ ํ ์คํธ ํ๋ ์์ํฌ์ด๋ค. (์ฐธ๊ณ - ๊ณต์๋ฌธ์)
- AssertJ: ํ ์คํธ ์ฝ๋ ์์ฑ์ ์ํํ๊ฒ ๋๋ ํ ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ํ๋ถํ API์ ๋ฉ์๋ ์ฒด์ด๋์ ์ง์ํ๋ค. (์ฐธ๊ณ - ๊ณต์๋ฌธ์)
5. ํ ์คํธ ์ผ์ด์ค ์ธ๋ถํํ๊ธฐ
1) ์๊ตฌ ์ฌํญ: ํ ์ข ๋ฅ์ ์๋ฃ ์ฌ๋ฌ ์์ ํ ๋ฒ์ ๋ด๋ ๊ธฐ๋ฅ
- ์ง๋ฌธํ๊ธฐ: ์๋ฌต์ ์ด๊ฑฐ๋ ์์ง ๋๋ฌ๋์ง ์์ ์๊ตฌ ์ฌํญ์ด ์๋์ง ํญ์ ์ผ๋ํ๊ณ ๊ณ ๋ฏผํด๋ด์ผ ํ๋ค.
- ํดํผ ์ผ์ด์ค์ ์์ธ ์ผ์ด์ค ๋์ถ: ๊ฒฝ๊ณ๊ฐ ํ ์คํธ๋ฅผ ํตํด ํดํผ ์ผ์ด์ค์ ์์ธ ์ผ์ด์ค๋ฅผ ์์ฑํ ์ ์๋ค.
6. ์์ ํ ์คํธ ์ผ์ด์ค
- ํดํผ ์ผ์ด์ค:
addSeveralBeverages
๋ฉ์๋๋ ํ๋์ ์ข ๋ฅ์ธ ์๋ฉ๋ฆฌ์นด๋ ธ์ 2๊ฐ๋ฅผ ๋ด๋ ํ ์คํธ - ์์ธ ์ผ์ด์ค:
addZeroBeverages
๋ฉ์๋๋ ํ๋์ ์ข ๋ฅ์ธ ์๋ฉ๋ฆฌ์นด๋ ธ์ 0๊ฐ๋ฅผ ๋ด์์ ๋ ์์ธ๊ฐ ๋ฐ์ํ๋ ํ ์คํธ
7. ๊ฒฝ๊ณ๊ฐ ํ ์คํธ
- ์ ์: ์ ๋ ฅ ๊ฐ์ ๊ฒฝ๊ณ์์ ๋ฐ์ํ ์ ์๋ ์ค๋ฅ๋ฅผ ์ฐพ๊ธฐ ์ํด ๊ฒฝ๊ณ ์กฐ๊ฑด์ ํ ์คํธํ๋ ๋ฐฉ์.
- ๋ชฉ์ : ์ ๋ ฅ ๊ฐ์ ์ต์๊ฐ, ์ต๋๊ฐ, ๊ฒฝ๊ณ ๊ทผ์ฒ์ ๊ฐ๋ค์ ํ ์คํธํ์ฌ ์์คํ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ๋์ง ๊ฒ์ฆํ๋ค.
- ์์:
- ์ด๋ค ์ ์ ๊ฐ์ด 3 ์ด์์ผ ๋ ํน์ ์กฐ๊ฑด์ ๋ง์กฑํ๋ค๊ณ ๊ฐ์ ํ๋ฉด, 3 ์ด์์ ๋ํ ํดํผ ์ผ์ด์ค์ 3 ๋ฏธ๋ง์ ๋ํ ์์ธ ์ผ์ด์ค๋ฅผ ์์ฑํ๋ค.
- ๊ฒฝ๊ณ๊ฐ ํ ์คํธ๋ ๋ฒ์(์ด์, ์ดํ, ๋ฏธ๋ง, ์ด๊ณผ), ๊ตฌ๊ฐ, ๋ ์ง ๋ฑ์ ํ ์คํธํ ๋ ์ ์ฉํ๋ค.
8. ํ ์คํธํ๊ธฐ ์ด๋ ค์ด ์์ญ
1) ์๊ตฌ์ฌํญ: ๊ฐ๊ฒ ์ด์ ์๊ฐ(10:00 ~ 22:00) ์ธ์๋ ์ฃผ๋ฌธ์ ์์ฑํ ์ ์๋ค.
- ์๊ฐ ์์กด์ฑ ๋ฌธ์ ํด๊ฒฐ: ์ฃผ๋ฌธ ์์ฑ ๋ก์ง์ ์๊ฐ ๊ด๋ จ ๋ถ๋ถ์ ์ธ๋ถ์์ ๋ฐ์์ฌ ์ ์๋๋ก ๋ณ๊ฒฝํ๋ค.
- ์ธ๋ถ ์๊ฐ ๋ฐ์ดํฐ ์์ : ํ ์คํธ ์์๋ ์ํ๋ ์๊ฐ์ ํตํด ๊ฒ์ฆํ๊ณ , ํ๋ก๋์ ์ฝ๋์์๋ ํ์ฌ ์๊ฐ์ ์ธ์๋ก ์ฃผ์ด ๋์ํ ์ ์๋ค.
2) ์ฝ๋ ์์
@Getter
public class CafeKiosk {
public static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10, 0);
public static final LocalTime SHOP_CLOSE_TIME = LocalTime.of(22, 0);
// ๋ณ๊ฒฝ ์
public Order createOrder() {
LocalDateTime currentDateTime = LocalDateTime.now();
LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("์ฃผ๋ฌธ ์๊ฐ์ด ์๋๋๋ค. ๊ด๋ฆฌ์์๊ฒ ๋ฌธ์ํ์ธ์.");
}
return new Order(currentDateTime, beverages);
}
// ๋ณ๊ฒฝ ํ: ์ธ๋ถ์์ ์๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ์ ์๋๋ก ๋ณ๊ฒฝ
public Order createOrder(LocalDateTime currentDateTime) {
LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("์ฃผ๋ฌธ ์๊ฐ์ด ์๋๋๋ค. ๊ด๋ฆฌ์์๊ฒ ๋ฌธ์ํ์ธ์.");
}
return new Order(currentDateTime, beverages);
}
}
3) ํ ์คํธ ์ฝ๋ ์์
class CafeKioskTest {
@Test
void createOrder() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
Order order = cafeKiosk.createOrder();
assertThat(order.getBeverages()).hasSize(1);
assertThat(order.getBeverages().get(0).getName()).isEqualTo("์๋ฉ๋ฆฌ์นด๋
ธ");
}
@Test
void createOrderWithCurrentTime() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
Order order = cafeKiosk.createOrder(LocalDateTime.of(2023, 11, 30, 10, 0));
assertThat(order.getBeverages()).hasSize(1);
assertThat(order.getBeverages().get(0).getName()).isEqualTo("์๋ฉ๋ฆฌ์นด๋
ธ");
}
@Test
void createOrderOutsideOpenTime() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
assertThatThrownBy(() -> cafeKiosk.createOrder(LocalDateTime.of(2023, 11, 30, 9, 59)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("์ฃผ๋ฌธ ์๊ฐ์ด ์๋๋๋ค. ๊ด๋ฆฌ์์๊ฒ ๋ฌธ์ํ์ธ์.");
}
}
9. ํ ์คํธํ๊ธฐ ์ด๋ ค์ด ์์ญ
- IN: ๊ด์ธกํ ๋๋ง๋ค ๋ค๋ฅธ ๊ฐ์ ์์กดํ๋ ์ฝ๋ - ํ์ฌ ๋ ์ง/์๊ฐ, ๋๋ค ๊ฐ, ์ ์ญ๋ณ์/ํจ์, ์ฌ์ฉ์ ์ ๋ ฅ
- OUT: ์ธ๋ถ ์ธ๊ณ์ ์ํฅ์ ์ฃผ๋ ์ฝ๋ - ํ์ค ์ถ๋ ฅ, ๋ฉ์์ง ๋ฐ์ก, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ธฐ๋กํ๊ธฐ
10. ํ ์คํธํ๊ธฐ ์ฌ์ด ์ฝ๋
- ํน์ง: ์ธ๋ถ ์ธ๊ณ์ ๋จ์ ๋ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ์์ ํจ์(pure functions)
- ๊ฐ์ ์ ๋ ฅ์๋ ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ
- ์ธ๋ถ ์ธ์๊ณผ ๋จ์ ๋ ํํ
11. Lombok ์ฌ์ฉ ๊ฐ์ด๋
1) Lombok์ด๋?
- ์ ์: Lombok์ ์๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฐ๋ณต๋๋ ์ฝ๋ ์์ฑ์ ์ค์ฌ์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
- ์ฅ์ :
- Getter, Setter, Constructor, ToString ๋ฑ์ ๋ฉ์๋๋ฅผ ์๋์ผ๋ก ์์ฑํด์ค๋ค.
- ์ฝ๋ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ ์ ์ง๋ณด์๋ฅผ ์ฉ์ดํ๊ฒ ํ๋ค.
2) ์ฃผ์ ์ด๋ ธํ ์ด์
@Getter
: ๋ชจ๋ ํ๋์ getter ๋ฉ์๋๋ฅผ ์์ฑํ๋ค.@Setter
: ๋ชจ๋ ํ๋์ setter ๋ฉ์๋๋ฅผ ์์ฑํ๋ค.- ์ฃผ์: ๋ฌด๋ถ๋ณํ setter ์ฌ์ฉ์ ์บก์ํ ์์น์ ์๋ฐฐํ ์ ์์ผ๋ฏ๋ก ํ์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํด์ผ ํ๋ค.
@NoArgsConstructor
: ๊ธฐ๋ณธ ์์ฑ์๋ฅผ ์์ฑํ๋ค.@AllArgsConstructor
: ๋ชจ๋ ํ๋ ๊ฐ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋ ์์ฑ์๋ฅผ ์์ฑํ๋ค.- ์ฃผ์: ๋ชจ๋ ํ๋์ ๋ํ ์์ฑ์๋ฅผ ๋ง๋๋ ๊ฒ์ ๊ฐ์ฒด์ ์ผ๊ด์ฑ์ ํด์น ์ ์์ผ๋ฏ๋ก ์ง์ํ๋ ๊ฒ์ด ์ข๋ค.
@ToString
: ๊ฐ์ฒด์ toString ๋ฉ์๋๋ฅผ ์์ฑํ๋ค.- ์ฃผ์: ์๋ฐฉํฅ ์ฐ๊ด๊ด๊ณ์์ ์ํ ์ฐธ์กฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ์ฐ๊ด๊ด๊ณ์ ํ๋๋ฅผ ์ ์ธํ๋ ๊ฒ์ด ์ข๋ค.
@Data
:@Getter
,@Setter
,@RequiredArgsConstructor
,@ToString
,@EqualsAndHashCode
๋ฅผ ๋ชจ๋ ํฌํจํ ์ข ํฉ ์ด๋ ธํ ์ด์ .- ์ฃผ์: @Data๋ ๋ฌด๋ถ๋ณํ๊ฒ ์ฌ์ฉํ๋ฉด ์บก์ํ์ ์ผ๊ด์ฑ์ ํด์น ์ ์์ผ๋ฏ๋ก ์ ์คํ๊ฒ ์ฌ์ฉํด์ผ ํ๋ค.
Section 03. TDD: Test Driven Development
1. TDD๋?
- ์ ์: TDD(Test Driven Development)๋ ํ๋ก๋์ ์ฝ๋๋ณด๋ค ํ ์คํธ ์ฝ๋๋ฅผ ๋จผ์ ์์ฑํ์ฌ ํ ์คํธ๊ฐ ๊ตฌํ ๊ณผ์ ์ ์ฃผ๋ํ๋ ๋ฐฉ๋ฒ๋ก ์ด๋ค.
- ์ฌ์ดํด: TDD๋ RED -> GREEN -> REFACTOR ์ธ ๊ฐ์ง ์ฌ์ดํด์ ๋ฐ๋ณตํ๋ ์ผ์ ํ ๋ฆฌ๋ฌ ์์์ ์งํ๋๋ค.
2. TDD ์ฌ์ดํด
- RED: ํ๋ก๋์ ์ฝ๋๊ฐ ์๋ ์ํฉ์์ ์คํจํ๋ ํ ์คํธ ์ฝ๋๋ฅผ ๋จผ์ ์์ฑํ๋ค.
- GREEN: ์คํจํ๋ ํ ์คํธ ์ฝ๋๋ฅผ ํต๊ณผํ๊ธฐ ์ํด ํ๋ก๋์ ์ฝ๋์์ ์ต์ํ์ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
- REFACTOR: ํ๋ก๋์ ์ฝ๋๋ฅผ ์ข์ ์ฝ๋๋ก ๊ฐ์ ํ๋ค.
3. TDD์ ํต์ฌ ๊ฐ์น
- ํผ๋๋ฐฑ: ๋ด๊ฐ ์์ฑํ ์ฝ๋, ํ๋ก๋์ ์ฝ๋์ ๋ํด ์์ฃผ, ๋น ๋ฅด๊ฒ ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์๋ค.
4. ์ ๊ธฐ๋ฅ ๊ตฌํ, ํ ํ ์คํธ ์์ฑ์ ๋ฌธ์ ์
- ํ ์คํธ ์์ฒด์ ๋๋ฝ ๊ฐ๋ฅ์ฑ
- ํน์ ํ ์คํธ ์ผ์ด์ค(ํดํผ ์ผ์ด์ค)๋ง ๊ฒ์ฆํ ๊ฐ๋ฅ์ฑ
- ์๋ชป๋ ๊ตฌํ์ ๋ค์ ๋ฆ๊ฒ ๋ฐ๊ฒฌํ ๊ฐ๋ฅ์ฑ
5. ์ ํ ์คํธ ์์ฑ, ํ ๊ธฐ๋ฅ ๊ตฌํ์ ์ฅ์
- ๋ณต์ก๋๊ฐ ๋ฎ์(์ ์ฐํ๋ฉฐ ์ ์ง๋ณด์๊ฐ ์ฌ์ด), ํ ์คํธ ๊ฐ๋ฅํ ์ฝ๋๋ก ๊ตฌํํ ์ ์๋ค.
- ์ฝ๊ฒ ๋ฐ๊ฒฌํ๊ธฐ ์ด๋ ค์ด ์ฃ์ง(Edge) ์ผ์ด์ค๋ฅผ ๋์น์ง ์๊ฒ ํด์ค๋ค.
- ๊ตฌํ์ ๋ํ ๋น ๋ฅธ ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์๋ค.
- ๊ณผ๊ฐํ ๋ฆฌํฉํ ๋ง์ด ๊ฐ๋ฅํด์ง๋ค.
6. TDD์ ํจ๊ณผ
- TDD๋ ์ฐ๋ฆฌ์ ์ฌ๊ณ ์ ๊ด์ ์ ๋ณํ๋ฅผ ์ผ์ผํค๋ ๋๊ตฌ์ด๋ค.
- TDD ๋์ ์ ์ ํ ์คํธ๋ ๋จ์ํ ๊ตฌํ๋ถ ๊ฒ์ฆ์ ์ํ ๋ณด์กฐ ์๋จ์ด์์ง๋ง, TDD ๋์ ํ์๋ ๊ตฌํ๋ถ ์ฝ๋์ ํ ์คํธ ์ฝ๋๊ฐ ์ํธ ์์ฉํ์ฌ ๊ฒฌ๊ณ ํ ํ๋ก๋์ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๊ฒ ํด์ค๋ค.
7. ๊ฒฐ๋ก
- TDD๋ ํด๋ผ์ด์ธํธ ๊ด์ ์์์ ํผ๋๋ฐฑ์ ์ฃผ๋ Test Driven ๋ฐฉ๋ฒ๋ก ์ด๋ค.
- TDD๊ฐ ์ต์ํ์ง ์๋ค๋ฉด ์ต์ํด์ง ๋๊น์ง ์๋ํด๋ณด์.
Section 04. ํ ์คํธ๋ ๋ฌธ์๋ค
1. ํ ์คํธ๋ฅผ ๋ฌธ์๋ผ๊ณ ํ ์ด์
- ์ค๋ช ์ญํ : ํ๋ก๋์ ๊ธฐ๋ฅ์ ์ค๋ช ํ๋ ํ ์คํธ ์ฝ๋๋ ๋ฌธ์๊ฐ ๋ ์ ์๋ค.
- ์ดํด ๋ณด์: ๋ค์ํ ํ ์คํธ ์ผ์ด์ค๋ฅผ ํตํด ํ๋ก๋์ ์ฝ๋๋ฅผ ์ดํดํ๋ ์๊ฐ๊ณผ ๊ด์ ์ ๋ณด์ํ ์ ์๋ค.
- ์ง์ ๊ณต์ : ์ด๋ ํ ์ฌ๋์ด ๊ณผ๊ฑฐ์ ๊ฒฝํํ๋ ๊ณ ๋ฏผ์ ๊ฒฐ๊ณผ๋ฌผ์ ํ ์ฐจ์์ผ๋ก ์น๊ฒฉ์์ผ์, ๋ชจ๋์ ์์ฐ์ ๊ณต์ ํ ์ ์๋ค.
- ํ์ํฌ: ์ค๋ฌด์์๋ ํญ์ ํ์ผ๋ก ์ผํ๊ธฐ ๋๋ฌธ์ ๋ ๋๋ ๋ค๋ฅธ ๋๊ตฐ๊ฐ๊ฐ ์์ฑํ ๋ฌธ์๊ฐ ํ ์ ์ฒด์ ํฐ ๋์์ ์ค ์ ์๋ค.
2. DisplayName์ ์ฌ์ธํ๊ฒ
- DisplayName ์ฌ์ฉ: Junit5์ @DisplayName ์ด๋ ธํ ์ด์ ์ ํ์ฉํ์ฌ ํ ์คํธ์ ๋ํ ์ค๋ช ์ ํ๊ธ๋ก ์์ฑํ๋ฉด ์ด๋ค ํ ์คํธ๋ฅผ ์๋ฏธํ๋์ง ์ฝ๊ฒ ์ ์ ์๋ค.
- ํ
์คํธ ์ด๋ฆ ์์ฑ ๋ฐฉ๋ฒ:
- “~ํ ์คํธ”๋ผ๊ณ ์์ฑํ๋ ๊ฒ์ ์ง์ํ๊ณ ๋ฌธ์ฅ์ผ๋ก ์์ฑํ๋ค. ์: “์๋ฃ๋ฅผ 1๊ฐ ์ถ๊ฐํ ์ ์๋ค.”
- ํ ์คํธ ํ์์ ๋ํ ๊ฒฐ๊ณผ๊น์ง ๊ธฐ์ ํ๋ค. ์: “์๋ฃ๋ฅผ 1๊ฐ ์ถ๊ฐํ๋ฉด ์ฃผ๋ฌธ ๋ชฉ๋ก์ ๋ด๊ธด๋ค.”
- ๋๋ฉ์ธ ์ฉ์ด๋ฅผ ์ฌ์ฉํ์ฌ ํ์ธต ์ถ์ํ๋ ๋ด์ฉ์ ๋ด๋๋ค. ์: “์์ ์์ ์ด์ ์๋ ์ฃผ๋ฌธ์ ์์ฑํ ์ ์๋ค.”
3. BDD ์คํ์ผ๋ก ์์ฑํ๊ธฐ
- BDD(Behavior Driven Development): TDD์์ ํ์๋ ๊ฐ๋ฐ ๋ฐฉ๋ฒ์ผ๋ก ํจ์ ๋จ์์ ํ ์คํธ์ ์ง์คํ๊ธฐ๋ณด๋ค, ์๋๋ฆฌ์ค์ ๊ธฐ๋ฐํ ํ ์คํธ์ผ์ด์ค(TC) ์์ฒด์ ์ง์คํ์ฌ ํ ์คํธ๋ฅผ ํ๋ค.
- ์ถ์ํ ์์ค: ๊ฐ๋ฐ์๊ฐ ์๋ ์ฌ๋์ด ๋ด๋ ์ดํดํ ์ ์์ ์ ๋์ ์ถ์ํ ์์ค์ ๊ถ์ฅํ๋ค.
- Given / When / Then:
- Given: ์๋๋ฆฌ์ค ์งํ์ ํ์ํ ๋ชจ๋ ์ค๋น ๊ณผ์ (๊ฐ์ฒด, ๊ฐ, ์ํ)
- When: ์๋๋ฆฌ์ค ํ๋ ์งํ
- Then: ์๋๋ฆฌ์ค ์งํ์ ๋ํ ๊ฒฐ๊ณผ ๋ช ์, ๊ฒ์ฆ(AssertJ)
4. BDD ์คํ์ผ ์์
class CafeKioskTest {
@DisplayName("์ฃผ๋ฌธ ๋ชฉ๋ก์ ๋ด๊ธด ์ํ๋ค์ ์ด ๊ธ์ก์ ๊ณ์ฐํ ์ ์๋ค.")
@Test
void calculateTotalPrice() {
// given
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
Latte latte = new Latte();
cafeKiosk.add(americano);
cafeKiosk.add(latte);
// when
int totalPrice = cafeKiosk.calculateTotalPrice();
// then
assertThat(totalPrice).isEqualTo(8500);
}
}
5. [Tip] IntelliJ Live Template ์ค์ ๋ฐฉ๋ฒ
(1) ์ค์ ๊ฒฝ๋ก: IntelliJ IDEA - Preferences - Live Templates - Java - test
(2) Template ์ค์ : ์๋์ ๊ฐ์ด ์
๋ ฅํ๊ณ Apply ํ Ok ๋ฒํผ์ ํด๋ฆญํ๋ค.
@Test
void $TEST_NAME$() {
// given
// when
// then
}
(3) ์ฌ์ฉ ๋ฐฉ๋ฒ: ๋ค์๋ถํฐ test
๋ฅผ ์
๋ ฅํ๋ฉด ์ค์ ํ ํ
ํ๋ฆฟ์ด ์๋์ผ๋ก ์
๋ ฅ๋๋ค.
Section 05. Spring & JPA ๊ธฐ๋ฐ ํ ์คํธ
1. ๋ ์ด์ด๋ ์ํคํ ์ฒ์ ํตํฉ ํ ์คํธ
1) ๋ ์ด์ด๋ ์ํคํ ์ฒ
- ๊ตฌ์กฐ: ์ฌ์ฉ์์ ์์ฒญ์ Presentation Layer, Business Layer, Persistence Layer๋ก ๊ตฌ๋ถํ์ฌ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐ.
- ๋ชฉ์ : ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํด ์ฑ ์์ ๋๋๊ณ ์ ์ง๋ณด์๋ฅผ ์ฉ์ดํ๊ฒ ๋ง๋ ๋ค.
- ํ ์คํธ ์ ๊ทผ ๋ฐฉ๋ฒ: ๊ด์ฌ์ฌ๊ฐ ๋ถ๋ฆฌ๋์ด ์์ผ๋ฏ๋ก ๊ฐ ๋ ์ด์ด๋ณ๋ก ํ ์คํธ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ์ ๊ทผํ ์ ์๋ค.
2) ํตํฉ ํ ์คํธ
- ์ ์: ์ฌ๋ฌ ๋ชจ๋์ด ํ๋ ฅํ๋ ๊ธฐ๋ฅ์ ํตํฉ์ ์ผ๋ก ๊ฒ์ฆํ๋ ํ ์คํธ.
- ํ์์ฑ: ๋จ์ ํ ์คํธ๋ง์ผ๋ก๋ ๊ธฐ๋ฅ ์ ์ฒด์ ์ ๋ขฐ์ฑ์ ๋ณด์ฅํ ์ ์๋ค. ํ๋ถํ ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ๋ฅผ ๋ณํํด์ผ ํ๋ค.
2. Spring / JPA & ๊ธฐ๋ณธ ์ํฐํฐ ์ค๊ณ
1) Spring Framework
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ vs ํ๋ ์์ํฌ: ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ์ํ ๊ธฐ๋ฅ๋ง ๋์ด์์ ์ฌ์ฉํ๋ ๋ฐ๋ฉด, ํ๋ ์์ํฌ๋ ์ด๋ฏธ ๊ฐ์ถฐ์ง ํ๊ฒฝ ์์ ์ฝ๋๋ฅผ ๋ผ์ ๋ฃ์ด ๋์ํ๊ฒ ๋๋ค.
- Spring์ ์ฃผ์ ๊ฐ๋
:
- IoC(Inversion of Control) : ๊ฐ์ฒด์ ์ ์ด ๊ถํ์ ํ๋ ์์ํฌ๊ฐ ๊ด๋ฆฌํ๋ ์ค๊ณ ์์น.
- DI(Dependency Injection) : ๊ฐ์ฒด ๊ฐ์ ์์กด์ฑ์ ์ธ๋ถ์์ ์ฃผ์ ํ๋ ์ค๊ณ ํจํด.
- AOP (Aspect Oriented Programming) : ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ํก๋จ ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํ์ฌ ๋ชจ๋ํํ๋ ํ๋ก๊ทธ๋๋ฐ ๊ธฐ๋ฒ.
2) ORM๊ณผ JPA
- ORM: ๊ฐ์ฒด์ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ๋งคํ.
- JPA: ์๋ฐ์์ ์ฌ์ฉ๋๋ ORM์ ํ์ค. ๊ตฌํ์ฒด๋ก๋ Hibernate ๋ฑ์ด ์๋ค.
- Spring Data JPA: JPA๋ฅผ ์ถ์ํํ์ฌ ์ฌ์ฉํ๊ธฐ ์ฝ๊ฒ ๋ง๋ ์คํ๋ง ์ง์์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ.
3) Entity ์ค๊ณ
- Order(์ฃผ๋ฌธ)์ Product(์ํ) ์ํฐํฐ ์ค๊ณ.
- ๋ค๋๋ค ๊ด๊ณ๋ฅผ ์ผ๋๋ค, ๋ค๋์ผ ๊ด๊ณ๋ก ํ์ด์ ์ ๊ทผํ๊ธฐ ์ํด ์ค๊ฐ ๋งคํ ํ ์ด๋ธ(OrderProduct)์ ์ฌ์ฉ.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "orders")
@Entity
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
private int totalPrice;
private LocalDateTime registeredDateTime;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderProduct> orderProducts = new ArrayList<>();
public Order(List<Product> products, LocalDateTime registeredDateTime) {
this.orderStatus = OrderStatus.INIT;
this.totalPrice = calculateTotalPrice(products);
this.registeredDateTime = registeredDateTime;
this.orderProducts = products.stream()
.map(product -> new OrderProduct(this, product))
.collect(Collectors.toList());
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class OrderProduct extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
private Product product;
public OrderProduct(Order order, Product product) {
this.order = order;
this.product = product;
}
}
3. Persistence Layer
- ์ญํ : Data Access์ ์ญํ ๋ก, ๋น์ฆ๋์ค ๋ก์ง์ด ํฌํจ๋์ง ์์ผ๋ฉฐ, CRUD ์์ ์๋ง ์ง์คํ๋ค.
- ProductRepository ํด๋์ค:
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
/**
* select *
* from product
* where selling_type in ('SELLING', 'HOLD')
*/
List<Product> findAllBySellingStatusIn(List<ProductSellingStatus> sellingStatuses);
}
1) Repository ํ ์คํธ
- ๋จ์ ํ ์คํธ: Data Accessํ๋ ๋ก์ง๋ง ํฌํจ๋์ด ๋จ์ ํ ์คํธ์ ์ฑ๊ฒฉ์ ๊ฐ์ง๋ค.
- ํ ์คํธ ์์ :
@ActiveProfiles("test")
@DataJpaTest
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@DisplayName("์ํ๋ ํ๋งค์ํ๋ฅผ ๊ฐ์ง ์ํ๋ค์ ์กฐํํ๋ค.")
@Test
void findAllBySellingStatusIn() {
// given
Product product1 = createProduct("001", HANDMADE, SELLING, "์๋ฉ๋ฆฌ์นด๋
ธ", 4000);
Product product2 = createProduct("002", HANDMADE, HOLD, "์นดํ๋ผ๋ผ", 4500);
Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "ํฅ๋น์", 7000);
productRepository.saveAll(List.of(product1, product2, product3));
// when
List<Product> products = productRepository.findAllBySellingStatusIn(List.of(SELLING, HOLD));
// then
assertThat(products).hasSize(2)
.extracting("productNumber", "name", "sellingStatus")
.containsExactlyInAnyOrder(
tuple("001", "์๋ฉ๋ฆฌ์นด๋
ธ", SELLING),
tuple("002", "์นดํ๋ผ๋ผ", HOLD)
);
}
private Product createProduct(String productNumber, ProductType type, ProductSellingStatus sellingStatus, String name, int price) {
return Product.builder()
.productNumber(productNumber)
.type(type)
.sellingStatus(sellingStatus)
.name(name)
.price(price)
.build();
}
}
4. Business Layer
- ์ญํ : ๋น์ฆ๋์ค ๋ก์ง์ ๊ตฌํํ๊ณ , Persistence Layer์ ์ํธ์์ฉ์ ํตํด ๋น์ฆ๋์ค ๋ก์ง์ ์ ๊ฐํ๋ค.
- ํธ๋์ญ์ ๋ณด์ฅ: ๋น์ฆ๋์ค ๋ก์ง ์ํ ์ ํธ๋์ญ์ ์ ๋ณด์ฅํด์ผ ํ๋ค.
1) OrderService ํ ์คํธ
- ์์ :
@ActiveProfiles("test")
@SpringBootTest
class OrderServiceTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderProductRepository orderProductRepository;
@Autowired
private StockRepository stockRepository;
@Autowired
private OrderService orderService;
@AfterEach
void tearDown() {
orderProductRepository.deleteAllInBatch();
productRepository.deleteAllInBatch();
orderRepository.deleteAllInBatch();
stockRepository.deleteAllInBatch();
}
@DisplayName("์ฃผ๋ฌธ๋ฒํธ ๋ฆฌ์คํธ๋ฅผ ๋ฐ์ ์ฃผ๋ฌธ์ ์์ฑํ๋ค.")
@Test
void createOrder() {
LocalDateTime registeredDateTime = LocalDateTime.now();
// given
Product product1 = createProduct(HANDMADE, "001", 1000);
Product product2 = createProduct(HANDMADE, "002", 3000);
Product product3 = createProduct(HANDMADE, "003", 5000);
productRepository.saveAll(List.of(product1, product2, product3));
OrderCreateServiceRequest request = OrderCreateServiceRequest.builder()
.productNumbers(List.of("001", "002"))
.build();
// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);
// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime", "totalPrice")
.contains(registeredDateTime, 4000);
assertThat(orderResponse.getProducts()).hasSize(2)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("002", 3000)
);
}
@DisplayName("์ค๋ณต๋๋ ์ํ๋ฒํธ ๋ฆฌ์คํธ๋ก ์ฃผ๋ฌธ์ ์์ฑํ ์ ์๋ค.")
@Test
void createOrderWithDuplicateProductNumbers() {
// given
LocalDateTime registeredDateTime = LocalDateTime.now();
Product product1 = createProduct(HANDMADE, "001", 1000);
Product product2 = createProduct(HANDMADE, "002", 3000);
Product product3 = createProduct(HANDMADE, "003", 5000);
productRepository.saveAll(List.of(product1, product2, product3));
OrderCreateServiceRequest request = OrderCreateServiceRequest.builder()
.productNumbers(List.of("001", "001"))
.build();
// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);
// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime", "totalPrice")
.contains(registeredDateTime, 2000);
assertThat(orderResponse.getProducts()).hasSize(2)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("001", 1000)
);
}
}
- ํ
์คํธ ํด๋์ค์ @Transactional:
- OrderServiceTest์์ @Transactional์ ์ ๊ฑฐํ๊ณ @AfterEach๋ฅผ ์ฌ์ฉํ๋ ์ด์ :
- @Transactional์ ์ฌ์ฉํ๋ฉด update ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ์ง ์์ ํ ์คํธ๊ฐ ์คํจํ ์ ์๋ค.
- @AfterEach๋ฅผ ํตํด ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ์ฌ ํ ์คํธ ๊ฐ์ ๋ฐ์ดํฐ ์ํฅ์ ์ต์ํํ๋ค.
- OrderServiceTest์์ @Transactional์ ์ ๊ฑฐํ๊ณ @AfterEach๋ฅผ ์ฌ์ฉํ๋ ์ด์ :
4. API ํ ์คํธ
- ์ค์ :
- ์ต์๋จ์
http
ํด๋๋ฅผ ๋ง๋ค๊ณ ๊ทธ ์์order.http
,product.http
ํ์ผ์ ์์ฑํ๋ค. - ์์ :
- ์ต์๋จ์
POST localhost:8080/api/v1/orders/new
Content-Type: application/json
{
"productNumbers": [
"001",
"002"
]
}
GET localhost:8080/api/v1/products/selling
5. Presentation Layer
- ์ญํ : ์ธ๋ถ ์ธ๊ณ์ ์์ฒญ์ ๊ฐ์ฅ ๋จผ์ ๋ฐ๋ ๊ณ์ธต์ผ๋ก, ํ๋ผ๋ฏธํฐ์ ๋ํ ์ต์ํ์ ๊ฒ์ฆ์ ์ํํ๋ค.
- ๋ชฉ์ : ๋น์ฆ๋์ค ๋ก์ง๋ณด๋ค๋ ๋๊ฒจ์จ ๊ฐ์ด ์ ํจํ์ง ๊ฒ์ฆํ๋ ๊ฒ์ด ์ต์ฐ์ ์ด๋ค.
- Mocking: ํ์ 2๊ฐ์ Layer๋ฅผ Mocking(๊ฐ์ง ๊ฐ์ฒด๋ก ๋์ฒด)ํ๊ณ Presentation Layer๋ฅผ ํ ์คํธํ๋ค.
1) Mock ์ฌ์ฉ ์ด์
- ์์กด๊ด๊ณ ์ฐจ๋จ: ํ๋์ ๊ฐ์ฒด ๋๋ ํ๋์ ๋ ์ด์ด๋ฅผ ํ ์คํธํ ๋, ์์กด๊ด๊ณ๊ฐ ์๋ ๊ฒ๋ค์ด ๋ฐฉํด๊ฐ ๋๋ค. ์ด๋ฅผ ๊ฐ์ง ๊ฐ์ฒด๋ก ๋์ฒดํด ์ฒ๋ฆฌํ๊ณ ์ ํ๋ค.
- MockMvc: Mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ์คํ๋ง MVC ๋์์ ์ฌํํ ์ ์๋ ํ ์คํธ ํ๋ ์์ํฌ.
2) ์๊ตฌ์ฌํญ: ๊ด๋ฆฌ์ ํ์ด์ง์์ ์ ๊ท ์ํ์ ๋ฑ๋ก
- ์ํ๋ช , ์ํ ํ์ , ํ๋งค ์ํ, ๊ฐ๊ฒฉ ๋ฑ์ ์ ๋ ฅ๋ฐ๋๋ค.
3) ProductService ์์
- ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ: ์ํ๋ฒํธ ํ๋์ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด์ ๊ฑธ๊ณ ์ฌ์๋ ๋ก์ง์ ์ถ๊ฐํ๊ฑฐ๋ UUID๋ฅผ ์ฌ์ฉํ์ฌ ํด๊ฒฐํ ์ ์๋ค.
- Transactional ์ค์ :
@Transactional(readOnly = true)
๋ฅผ ์ฌ์ฉํด ์ฑ๋ฅ ํฅ์์ ๋๋ชจํ๊ณ , CQRS ์์น์ ์ ์ฉํด Command์ Query๋ฅผ ๋ถ๋ฆฌํ๋ค.
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class ProductService {
private final ProductRepository productRepository;
// ๋์์ฑ ์ด์ ํด๊ฒฐ์ ์ํ ์ฌ์๋ ๋ก์ง ์ถ๊ฐ
@Transactional
public ProductResponse createProduct(ProductCreateServiceRequest request) {
String nextProductNumber = createNextProductNumber();
Product product = request.toEntity(nextProductNumber);
Product savedProduct = productRepository.save(product);
return ProductResponse.of(savedProduct);
}
@Transactional
private String createNextProductNumber() {
String latestProductNumber = productRepository.findLatestProductNumber();
if (latestProductNumber == null) {
return "001";
}
int latestProductNumberInt = Integer.parseInt(latestProductNumber);
int nextProductNumberInt = latestProductNumberInt + 1;
return String.format("%03d", nextProductNumberInt);
}
public List<ProductResponse> getSellingProducts() {
List<Product> products = productRepository.findAllBySellingStatusIn(ProductSellingStatus.forDisplay());
return products.stream()
.map(ProductResponse::of)
.collect(Collectors.toList());
}
}
4) ControllerTest - @WebMvcTest
- ์ฌ์ฉ ๋ฐฉ๋ฒ: @WebMvcTest ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด ์ปจํธ๋กค๋ฌ ๊ด๋ จ ํ ์คํธ๋ฅผ ์งํํ๊ณ , MockMvc๋ฅผ ํ์ฉํด Mocking ์ฒ๋ฆฌ.
@WebMvcTest(controllers = ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private ProductService productService;
@DisplayName("์ ๊ท ์ํ์ ๋ฑ๋กํ๋ค.")
@Test
void createProduct() throws Exception {
// given
ProductCreateRequest request = ProductCreateRequest.builder()
.type(ProductType.HANDMADE)
.sellingStatus(ProductSellingStatus.SELLING)
.name("์๋ฉ๋ฆฌ์นด๋
ธ")
.price(4000)
.build();
// when & then
mockMvc.perform(post("/api/v1/products/new")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print()) // log ํ์ธ
.andExpect(status().isOk());
}
}
@RequiredArgsConstructor
@RestController
public class ProductController {
private final ProductService productService;
@PostMapping("/api/v1/products/new")
public ProductResponse createProduct(@RequestBody ProductCreateRequest request) {
return productService.createProduct(request);
}
}
5) Controller์์ ์ ํจ์ฑ ๊ฒ์ฌ
- ์์กด์ฑ ์ถ๊ฐ: ์คํ๋ง ๋น ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํด ์์กด์ฑ์ ์ถ๊ฐํ๋ค.
- DTO์ ์ ํจ์ฑ ์ด๋
ธํ
์ด์
์ถ๊ฐ:
@NotNull
,@NotBlank
,@Positive
๋ฑ.
@Getter
@NoArgsConstructor
public class ProductCreateRequest {
@NotNull
private ProductType type;
@NotNull
private ProductSellingStatus sellingStatus;
@NotBlank
private String name;
@Positive
private int price;
}
@RequiredArgsConstructor
@RestController
public class ProductController {
private final ProductService productService;
@PostMapping("/api/v1/products/new")
public ProductResponse createProduct(@Valid @RequestBody ProductCreateRequest request) {
return productService.createProduct(request);
}
}
6) DTO๋ฅผ ํตํ ๊ณ์ธต์ ๊ตฌ์กฐ์์ ์์กด์ฑ ๊ด๊ณ
- ๋ชฉ์ : ํ์ ๊ณ์ธต์ธ Service๊ฐ ์์ ๊ณ์ธต์ธ Controller๋ฅผ ๋ชจ๋ฅด๊ฒ ๊ตฌํํ์ฌ, ์ฑ ์ ๋ถ๋ฆฌ๋ฅผ ๋ช ํํ ํ๋ค.
- ๋ฐฉ๋ฒ: Controller์ DTO์ Service์ DTO๋ฅผ ๋ถ๋ฆฌํ๋ค.
@RequiredArgsConstructor
@RestController
public class OrderController {
private final OrderService orderService;
@PostMapping("/api/v1/orders/new")
public ApiResponse<OrderResponse> createOrder(@Valid @RequestBody OrderCreateRequest request) {
LocalDateTime registeredDateTime = LocalDateTime.now();
return ApiResponse.ok(orderService.createOrder(request.toServiceRequest(), registeredDateTime));
}
}
@Getter
@NoArgsConstructor
public class OrderCreateRequest {
@NotEmpty(message = "์ํ ๋ฒํธ ๋ฆฌ์คํธ๋ ํ์์
๋๋ค.")
private List<String> productNumbers;
@Builder
public OrderCreateRequest(List<String> productNumbers) {
this.productNumbers = productNumbers;
}
public OrderCreateServiceRequest toServiceRequest() {
return OrderCreateServiceRequest.builder()
.productNumbers(productNumbers)
.build();
}
}
@Getter
@NoArgsConstructor
public class OrderCreateServiceRequest {
private List<String> productNumbers;
@Builder
public OrderCreateServiceRequest(List<String> productNumbers) {
this.productNumbers = productNumbers;
}
}
7) Spring & JPA ๊ธฐ๋ฐ ํ ์คํธ ๋ฆฌ๋ทฐ
- ๊ณ์ธต๋ณ ํ
์คํธ ์์ฑ:
- Presentation Layer(Controller): Business Layer, Persistence Layer๋ฅผ Mocking ์ฒ๋ฆฌํ์ฌ ๋จ์ ํ ์คํธ.
- Business Layer(Service): Persistence Layer๋ฅผ ์ฃผ์ ๋ฐ์ ํตํฉ ํ ์คํธ. @SpringBootTest๋ฅผ ์ฌ์ฉํ๊ณ , ์๋ ์ญ์ ๋ฉ์๋(tearDown) ํ์ฉ.
- Persistence Layer(Repository): Data Access ๋ก์ง๋ง์ ํ ์คํธํ๋ ๋จ์ ํ ์คํธ. @DataJpaTest ์ฌ์ฉ.
- DTO ๊ตฌ๋ถ: ๊ฐ Layer๋ง๋ค DTO๋ฅผ ๊ตฌ๋ถํด ์์กด์ฑ๊ณผ ์ฑ ์ ๋ถ๋ฆฌ.
- ์ ํจ์ฑ ๊ฒ์ฌ: Presentation Layer์์ ๊ธฐ๋ณธ์ ์ธ ์ ํจ์ฑ ๊ฒ์ฌ, ๋๋ฉ์ธ ์ ํจ์ฑ ๊ฒ์ฆ์ ๋๋ฉ์ธ ๊ฐ์ฒด์์ ์ฒ๋ฆฌ.
8) ๊ฒฐ๋ก
- Mocking: Mocking์ ํตํด ์์กด๊ด๊ณ๋ฅผ ์ฐจ๋จํ๊ณ ๋จ์ ํ ์คํธ๋ฅผ ํจ์จ์ ์ผ๋ก ์ํ.
- ์ ํจ์ฑ ๊ฒ์ฌ: Controller์์ ๊ธฐ๋ณธ์ ์ธ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํํ๊ณ , ๋๋ฉ์ธ ์ ํจ์ฑ ๊ฒ์ฆ์ ๋๋ฉ์ธ ๊ฐ์ฒด์์ ์ฒ๋ฆฌ.
- DTO ๋ถ๋ฆฌ: ๊ฐ Layer๋ง๋ค DTO๋ฅผ ๊ตฌ๋ถํด ์์กด์ฑ๊ณผ ์ฑ ์์ ๋ถ๋ฆฌ.
- ๊ณ์ธต๋ณ ํ ์คํธ: ๊ฐ ๊ณ์ธต์ ๋ง๋ ํ ์คํธ ์ ๋ต์ ์ ์ฉํด ํตํฉ ํ ์คํธ์ ๋จ์ ํ ์คํธ๋ฅผ ๋ณํ.