๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Spring/Test Driven Development

Practical Testing: ์‹ค์šฉ์ ์ธ ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ

๐Ÿ’ก ๋ณธ ๊ฒŒ์‹œ๊ธ€์€ ๋ฐ•์šฐ๋นˆ๋‹˜์˜ '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 ์‚ฌ์ดํด

  1. RED: ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ๊ฐ€ ์—†๋Š” ์ƒํ™ฉ์—์„œ ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•œ๋‹ค.
  2. GREEN: ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ต๊ณผํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  3. 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๋ฅผ ํ†ตํ•ด ์ˆ˜๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์˜ํ–ฅ์„ ์ตœ์†Œํ™”ํ•œ๋‹ค.

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๋ฅผ ๊ตฌ๋ถ„ํ•ด ์˜์กด์„ฑ๊ณผ ์ฑ…์ž„์„ ๋ถ„๋ฆฌ.
  • ๊ณ„์ธต๋ณ„ ํ…Œ์ŠคํŠธ: ๊ฐ ๊ณ„์ธต์— ๋งž๋Š” ํ…Œ์ŠคํŠธ ์ „๋žต์„ ์ ์šฉํ•ด ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ๋ณ‘ํ–‰.