Skip to main content

A. ๋ถ€๋ก

About 6 minJavaSpringAWScrashcoursejavajdkjdk8streamspringspringframeworkspringbootawsaws-ec2

A. ๋ถ€๋ก ๊ด€๋ จ


A. ๋ถ€๋ก

์ ํ”„ ํˆฌ ์Šคํ”„๋ง๋ถ€ํŠธ - WikiDocs

01. ์ธํ…”๋ฆฌ์ œ์ด ์‚ฌ์šฉํ•˜๊ธฐ

STS ๋Œ€์‹  ์ธํ…”๋ฆฌ์ œ์ด ์ปค๋ฎค๋‹ˆํ‹ฐ ์—๋””์…˜์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ์˜ ์•ˆ๋‚ด์— ๋”ฐ๋ผ ์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ์„ค์น˜ํ•˜๊ณ  ์‚ฌ์šฉํ•˜์ž.

Spring Initializr

์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ์„ค์น˜ํ•˜๊ธฐ ์ „์— ์Šคํ”„๋ง๋ถ€ํŠธ ๊ฐœ๋ฐœ์„ ๋„์™€์ฃผ๋Š” Spring Initializr๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด์ž. ๊ณง ์šฐ๋ฆฌ๊ฐ€ ์„ค์น˜ํ•  ์ธํ…”๋ฆฌ์ œ์ด ๋ฌด๋ฃŒ๋ฒ„์ „์ธ CE(Comunity Edition)๋Š” ์Šคํ”„๋ง ๋„๊ตฌ ์ง€์›์ด ์•ˆ๋˜์ง€๋งŒ Spring Initializr๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์Šคํ”„๋ง๋ถ€ํŠธ ๊ฐœ๋ฐœ์„ ์‰ฝ๊ฒŒ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค. Spring Initializr๋ฅผ ํ†ตํ•ด ์Šคํ”„๋ง๋ถ€ํŠธ ํ”„๋กœ์ ํŠธ๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋‹ค์šด๋กœ๋“œํ• ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ URL์— ์ ‘์†ํ•˜์ž.

๐ŸŒhttps://start.spring.ioopen in new window

์ ‘์†ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚œ๋‹ค.
์ ‘์†ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚œ๋‹ค.

์œ„ ํ™”๋ฉด์—์„œ ๋นจ๊ฐ„ ์ƒ‰ ๋ฐ•์Šค์™€ ๋™์ผํ•˜๊ฒŒ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•œ๋‹ค.

  • Project: Gradle Project
  • Language: Java
  • Sprint Boot: 2.6.6 (SNAPSHOT ๋˜๋Š” M2, M3 ๊ฐ€ ๋ถ™์ง€ ์•Š์€ ์ตœ์‹  ๋ฒ„์ „์„ ์„ ํƒํ•œ๋‹ค.)
  • Project Meta Data
    • Group: com.mysite
    • Artifact: sbb
    • Name: sbb
    • Description: My project for Sprint Boot
    • Package name : com.mysite.sbb
    • Packaging: Jar
    • Java: 11

์œ„์™€ ๊ฐ™์ด ์„ค์ •ํ•˜๊ณ  ["ADD DEPENDENCIES"] ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด์ž. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŒ์—…์ฐฝ์ด ๋‚˜ํƒ€๋‚œ๋‹ค.

<FontIcon icon="iconfont icon-select"/>์„ ์„ ํƒํ•˜์ž. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด "Spring Web"์ด ์ถ”๊ฐ€๋œ๋‹ค.
["Spring Web"]์„ ์„ ํƒํ•˜์ž. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด "Spring Web"์ด ์ถ”๊ฐ€๋œ๋‹ค.
๋งˆ์ง€๋ง‰์œผ๋กœ <FontIcon icon="iconfont icon-select"/> ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๋‹ค.
๋งˆ์ง€๋ง‰์œผ๋กœ ["GENERATE"] ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด sbb.zip ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ ๋œ๋‹ค. sbb.zip ํŒŒ์ผ์„ "ํ”„๋กœ์ ํŠธ ํ™ˆ ๋””๋ ‰ํ„ฐ๋ฆฌ"์— ์••์ถ•ํ•ด์ œํ•˜์ž.

ํ”„๋กœ์ ํŠธ ํ™ˆ ๋””๋ ‰ํ„ฐ๋ฆฌ: ์œˆ๋„์šฐ๋Š” C:/Users/<์‚ฌ์šฉ์ž๋ช…>/projects ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋งฅ OS๋ผ๋ฉด /Users/<์‚ฌ์šฉ์ž๋ช…>/projects๋ฅผ ์‚ฌ์šฉํ•˜์ž.

๊ทธ๋Ÿฌ๋ฉด ํ”„๋กœ์ ํŠธ ํ™ˆ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ฐ‘์— sbb ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋  ๊ฒƒ์ด๋‹ค. ์ด์ œ ์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ์„ค์น˜ํ•˜๊ณ  sbb ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์ธํ…”๋ฆฌ์ œ์ด์—์„œ ["Open"] ํ•˜์—ฌ ์Šคํ”„๋ง๋ถ€ํŠธ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ• ์ˆ˜ ์žˆ๋‹ค.

์ธํ…”๋ฆฌ์ œ์ด ์„ค์น˜

๋‹ค์Œ์˜ URL์—์„œ ์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ๋‹ค์šด๋กœ๋“œ ํ•˜์ž.

์œ„ URL์— ์ ‘์†ํ•˜๋ฉด Ultimate์™€ Community ๋‘ ๊ฐ€์ง€ ๋ฒ„์ „์ด ์žˆ๋Š”๋ฐ ๋ฌด๋ฃŒ์ธ Community ๋ฒ„์ „์„ ๋‹ค์šด๋กœ๋“œํ•˜์—ฌ ์„ค์น˜ํ•˜์ž.

์„ค์น˜ ํ›„ ์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ์‹คํ–‰ํ•˜์ž.

์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ์ฒ˜์Œ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฐฝ์ด ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค.
์ธํ…”๋ฆฌ์ œ์ด๋ฅผ ์ฒ˜์Œ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฐฝ์ด ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค.

.["Open"] ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ณ  ์œ„์—์„œ ์••์ถ•ํ•ด์ œํ•œ sbb ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์„ ํƒํ•œ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด sbb ํ”„๋กœ์ ํŠธ๊ฐ€ ์ธํ…”๋ฆฌ์ œ์ด์—์„œ ์‹œ์ž‘๋œ๋‹ค.
๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด sbb ํ”„๋กœ์ ํŠธ๊ฐ€ ์ธํ…”๋ฆฌ์ œ์ด์—์„œ ์‹œ์ž‘๋œ๋‹ค.

ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ›„์—๋Š” Gradle ์ž‘์—…(๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‹ค์šด๋กœ๋“œ ๋“ฑ)์œผ๋กœ ์ธํ•œ ์‹œ๊ฐ„์ด 1~2๋ถ„ ์ •๋„ ์†Œ์š”๋œ๋‹ค.

SDK ์˜ค๋ฅ˜

.com/mysite/sbb/SbbApplication.java ํŒŒ์ผ์„ ์—ด์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด SDK๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์ด๋ฏ€๋กœ ์—๋””ํ„ฐ ์ฐฝ ์ƒ๋‹จ์— ํ‘œ์‹œ๋˜๋Š” "SDK" ์„ค์ •์„ ํ†ตํ•ด ์„ค์น˜๋œ ์ž๋ฐ” SDK๋ฅผ ์ง€์ •ํ•˜์ž.

๋กฌ๋ณต ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜

๋‹ค์Œ์ฒ˜๋Ÿผ [Preferences -> Plugins] ์—์„œ ๋กฌ๋ณต(Lombok)์„ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์„ค์น˜ํ•˜์ž.

๋กฌ๋ณต์ด ๋””ํดํŠธ๋กœ ์„ค์น˜๋˜์–ด ์žˆ์œผ๋ฉด enable ๋˜์—ˆ๋Š”์ง€๋งŒ ํ™•์ธํ•˜์ž.
๋กฌ๋ณต์ด ๋””ํดํŠธ๋กœ ์„ค์น˜๋˜์–ด ์žˆ์œผ๋ฉด enable ๋˜์—ˆ๋Š”์ง€๋งŒ ํ™•์ธํ•˜์ž.

Auto Reload ์„ค์ •

์ธํ…”๋ฆฌ์ œ์ด์—์„œ ์ž๋ฐ” ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ํ…œํ”Œ๋ฆฟ์„ ์ˆ˜์ •ํ•  ๊ฒฝ์šฐ ์ˆ˜์ž‘์—… ์—†์ด ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

Java

์ž๋ฐ” ํŒŒ์ผ์„ ๋ณ€๊ฒฝํ•œ ํ›„ ์ž๋™ ์ ์šฉ๋˜๊ฒŒ ํ•˜๋ ค๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ <FontIcon icon="iconfont icon-select"/> ์—์„œ ๋‹ค์Œ ํ•ญ๋ชฉ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•œ๋‹ค.
์ž๋ฐ” ํŒŒ์ผ์„ ๋ณ€๊ฒฝํ•œ ํ›„ ์ž๋™ ์ ์šฉ๋˜๊ฒŒ ํ•˜๋ ค๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ [Preferences -> Build, Execution, Deployment -> Compiler] ์—์„œ ๋‹ค์Œ ํ•ญ๋ชฉ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  <FontIcon icon="iconfont icon-select"/> ์—์„œ ๋‹ค์Œ ํ•ญ๋ชฉ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  [Preferences -> Advanced Settings] ์—์„œ ๋‹ค์Œ ํ•ญ๋ชฉ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•œ๋‹ค.

1-05์žฅ์˜ Spring Boot Devtools๋ฅผ ์„ค์น˜ํ•œ ํ›„์— ์ ์šฉํ•˜์ž.

ํƒ€์ž„๋ฆฌํ”„(thymeleaf)

ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์„ ๋ณ€๊ฒฝํ•œ ํ›„ ์ž๋™ ์ ์šฉ๋˜๊ฒŒ ํ•˜๋ ค๋ฉด application.properties ํŒŒ์ผ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•˜์ž.

ํŒŒ์ผ๋ช…: sbb/src/main/resources/application.properties

// (... ์ƒ๋žต ...)
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=file:src/main/resources/templates/

ํ…œํ”Œ๋ฆฟ์„ ์‚ฌ์šฉํ•˜๋Š” 2-07์žฅ ๋ถ€ํ„ฐ ํ•„์š”. jar ๋ฐฐํฌ์‹œ์—๋Š” spring.thymeleaf.prefix ํ•ญ๋ชฉ์„ ์ฃผ์„์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค. (์˜ค๋ฅ˜ ๋ณด๊ณ ๊ฐ€ ์žˆ์Œ)

Unused ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ๋„๊ธฐ

์ธํ…”๋ฆฌ์ œ์ด ์ปค๋ฎค๋‹ˆํ‹ฐ ๋ฒ„์ „์€ ์Šคํ”„๋ง์„ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์Šคํ”„๋ง์˜ ์ปจํŠธ๋กค๋Ÿฌ์™€ URL ๋งคํ•‘ ๋ฉ”์„œ๋“œ๋“ค์—์„œ Unused ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜ํƒ€๋‚œ๋‹ค. ํ•˜์ง€๋งŒ ๋ฌด์‹œํ• ์ˆ˜ ์—†์„ ๋งŒํผ ๋งŽ์€ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์ด ํ•ญ๋ชฉ์€ ์„ค์ •์—์„œ ๋„๋Š”๊ฒŒ ์ข‹๋‹ค.

๋‹ค์Œ์ฒ˜๋Ÿผ <FontIcon icon="iconfont icon-select"/> ๋ฉ”๋‰ด์—์„œ "Java -> Declaration redundancy" ํ•ญ๋ชฉ ์ค‘ <FontIcon icon="iconfont icon-select"/> ํ•ญ๋ชฉ์„ ์ฒดํฌํ•ด์ œํ•˜์ž.
๋‹ค์Œ์ฒ˜๋Ÿผ [Preferences -> Editor -> Inspections] ๋ฉ”๋‰ด์—์„œ "Java -> Declaration redundancy" ํ•ญ๋ชฉ ์ค‘ ["Unused declaration"] ํ•ญ๋ชฉ์„ ์ฒดํฌํ•ด์ œํ•˜์ž.

Gradle

๊ทธ๋ ˆ์ด๋“ค๋กœ ๋กœ์ปฌ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๋ฐฐํฌ ํŒŒ์ผ(jar)์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž.

๋กœ์ปฌ ์„œ๋ฒ„ ์‹คํ–‰ํ•˜๊ธฐ

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ทธ๋ ˆ์ด๋“ค ์ฐฝ์—์„œ <FontIcon icon="iconfont icon-select"/> ์„ ์„ ํƒํ•˜์ž.
๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ทธ๋ ˆ์ด๋“ค ์ฐฝ์—์„œ [sbb -> Tasks -> application -> bootRun] ์„ ์„ ํƒํ•˜์ž.
๊ทธ๋ฆฌ๊ณ  ์šฐ์ธก ๋งˆ์šฐ์Šค ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ <FontIcon icon="iconfont icon-select"/>์„ ์„ ํƒํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์šฐ์ธก ๋งˆ์šฐ์Šค ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ [Run sbb bootRun]์„ ์„ ํƒํ•œ๋‹ค.
๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋กœ์ปฌ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋ชจ์Šต์„ ์ธํ…”๋ฆฌ์ œ์ด ํ•˜๋‹จ์—์„œ ํ™•์ธํ• ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋กœ์ปฌ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋ชจ์Šต์„ ์ธํ…”๋ฆฌ์ œ์ด ํ•˜๋‹จ์—์„œ ํ™•์ธํ• ์ˆ˜ ์žˆ๋‹ค.

๋ฐฐํฌํŒŒ์ผ ์ƒ์„ฑํ•˜๊ธฐ

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ทธ๋ ˆ์ด๋“ค ์ฐฝ์—์„œ <FontIcon icon="iconfont icon-select"/> ์„ ์„ ํƒํ•˜์ž.
๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ทธ๋ ˆ์ด๋“ค ์ฐฝ์—์„œ [sbb -> Tasks -> build -> bootJar] ์„ ์„ ํƒํ•˜์ž.

๊ทธ๋ฆฌ๊ณ  ์šฐ์ธก ๋งˆ์šฐ์Šค ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ Run sbb [bootJar]๋ฅผ ์„ ํƒํ•œ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด build/libs/ ๋””๋ ‰ํ„ฐ๋ฆฌ์— sbb-0.0.1-SNAPSHOT.jar์™€ ๊ฐ™์€ ๋ฐฐํฌ ํŒŒ์ผ์ด ์ƒ์„ฑ๋œ๋‹ค.


02. AWS ๋ผ์ดํŠธ์„ธ์ผ ์‚ฌ์šฉ ์ทจ์†Œ

AWS ์„œ๋น„์Šค๋ฅผ ๋” ์ด์ƒ ์šด์˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ธ์Šคํ„ด์Šค์™€ ๊ณ ์ •IP ๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ญ์ œํ•˜์—ฌ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”๊ธˆ ๋ฐœ์ƒ์„ ๋ง‰์ž.

์ธ์Šคํ„ด์Šค์™€ ๊ณ ์ • IP ์‚ญ์ œ

AWS ๋ผ์ดํŠธ์„ธ์ผ ์ธ์Šคํ„ด์Šค๋Š” 3๋‹ฌ๊ฐ„ ๋ฌด๋ฃŒ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ  ์ดํ›„์—” ๋น„์šฉ์ด ๋ฐœ์ƒํ•œ๋‹ค. ์ด๋ฅผ ์›์น˜ ์•Š๋Š”๋‹ค๋ฉด ์ธ์Šคํ„ด์Šค์™€ ๊ณ ์ • IP๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•œ๋‹ค.

์ธ์Šคํ„ด์Šค๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ AWS ๋ผ์ดํŠธ์„ธ์ผ ํ™ˆํŽ˜์ด์ง€ ํ™”๋ฉด์˜ <FontIcon icon="iconfont icon-select"/> ํƒญ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค
์ธ์Šคํ„ด์Šค๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ AWS ๋ผ์ดํŠธ์„ธ์ผ ํ™ˆํŽ˜์ด์ง€ ํ™”๋ฉด์˜ [์ธ์Šคํ„ด์Šค] ํƒญ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค
๊ณ ์ • IP๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ <FontIcon icon="iconfont icon-select"/> ํƒญ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ณ ์ • IP๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ [๋„คํŠธ์›Œํ‚น] ํƒญ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ญ์ œ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ <FontIcon icon="iconfont icon-select"/> ํƒญ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ [๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] ํƒญ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

03. ์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๋ฒ„์ „ ์•ˆ๋‚ด

์ด ์ฑ…์€ ์Šคํ”„๋ง๋ถ€ํŠธ 3.x ๋ฒ„์ „์„ ๊ธฐ์ค€์œผ๋กœ ํ•œ๋‹ค. ๋งŒ์•ฝ ์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๋ฒ„์ „์œผ๋กœ ์ด ์ฑ…์˜ ๋‚ด์šฉ์„ ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ์˜ ์•ˆ๋‚ด์— ๋”ฐ๋ผ ์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๋ฒ„์ „์„ ์‚ฌ์šฉํ• ์ˆ˜ ์žˆ๋‹ค.

jakarta ํŒจํ‚ค์ง€ ๋ณ€๊ฒฝ

์ด ์ฑ…์— ์‚ฌ์šฉํ•œ import jakarta.* ํŒจํ‚ค์ง€๋ฅผ ๋ชจ๋‘ import javax.* ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค. ์ฆ‰, jakarta๋กœ ๋˜์–ด ์žˆ๋Š” import ๋ฌธ์€ ์ „๋ถ€ javax๋กœ ๊ต์ฒดํ•˜์—ฌ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

SecurityConfig.java

์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๋ฒ„์ „์˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์„ค์ •์€ ๋‹ค์Œ์˜ SecurityCofnig.java ํŒŒ์ผ๋กœ ๋Œ€์ฒดํ•ด์•ผ ํ•œ๋‹ค. antMatchers, ignoringAntMatchers ๋“ฑ์˜ URL ํŒจํ„ด ๋งค์น˜ํ•˜๋Š” ๋ถ€๋ถ„๋“ค์ด ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค.

package com.mysite.sbb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.mysite.sbb.user.UserSecurityService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserSecurityService userSecurityService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").permitAll()
            .and()
                .csrf().ignoringAntMatchers("/h2-console/**")
            .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
            .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/")
            .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
    }
}

์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๊นƒํ—ˆ๋ธŒ ์ฃผ์†Œ

์ด ์ฑ…์€ ์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๋ฒ„์ „๋ถ€ํ„ฐ ์ž‘์„ฑ๋œ ์ฑ…์ด๋‹ค. ์Šคํ”„๋ง๋ถ€ํŠธ 2.x ๋ฒ„์ „์œผ๋กœ ์ž‘์„ฑ๋œ ์†Œ์Šค์ฝ”๋“œ๋Š” ๋‹ค์Œ์˜ ๊นƒํ—ˆ๋ธŒ ์ฃผ์†Œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

pahkey/sbbopen in new window


04. ๋Œ“๊ธ€ (์‚ญ์ œ์˜ˆ์ •)

::: warn ์ฃผ์˜

์•„๋ž˜ ๋‚ด์šฉ์€ "์ ํ”„ ํˆฌ ์Šคํ”„๋ง๋ถ€ํŠธ" ์˜ˆ์ „ ๋ฒ„์ „์˜ ๋‚ด์šฉ์ด๋ฏ€๋กœ ํ˜„์žฌ๊นŒ์ง€ ์ง„ํ–‰ํ•œ ์†Œ์Šค์ฝ”๋“œ์— ์ ์šฉํ• ๋•Œ๋Š” ์ฃผ์˜ํ•ด์•ผ ํ•จ ์งˆ๋ฌธ ๋˜๋Š” ๋‹ต๋ณ€์— ๋Œ€ํ•˜์—ฌ ์งค๋ง‰ํ•˜๊ฒŒ ๋‹ตํ•ด์„œ ์˜ฌ๋ฆฌ๋Š” ๊ธ€์„ ๋Œ“๊ธ€์ด๋ผ๊ณ  ํ•œ๋‹ค. ์ด๋ฒˆ์—๋Š” ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€์— ๋Œ“๊ธ€(Comment) ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด ๋ณด์ž.

:::

๋Œ“๊ธ€ ๋„๋ฉ”์ธ

๋Œ“๊ธ€ ์—ญ์‹œ ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€์ฒ˜๋Ÿผ ํ•˜๋‚˜์˜ ๋„๋ฉ”์ธ์œผ๋กœ ์ทจ๊ธ‰ํ•˜์ž.

๋จผ์ € ๋‹ค์Œ์ฒ˜๋Ÿผ  ํŒจํ‚ค์ง€๋ฅผ ์ƒ์„ฑํ•˜์ž.
๋จผ์ € ๋‹ค์Œ์ฒ˜๋Ÿผ com.mysite.sbb.comment ํŒจํ‚ค์ง€๋ฅผ ์ƒ์„ฑํ•˜์ž.

๋Œ“๊ธ€ ๋ชจ๋ธ

Comment ๋ชจ๋ธ

๋Œ“๊ธ€ ์ž‘์„ฑ์„ ์œ„ํ•ด์„œ ๊ฐ€์žฅ ๋จผ์ € ์ค€๋น„ํ•ด์•ผ ํ•  ๊ฒƒ์€ ๋Œ“๊ธ€ ๋ชจ๋ธ์ด๋‹ค.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/comment/Comment.java

package com.mysite.sbb.comment;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

import com.mysite.sbb.answer.Answer;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @ManyToOne
    private SiteUser author;

    @Column(columnDefinition = "TEXT")
    private String content;

    private LocalDateTime createDate;

    private LocalDateTime modifyDate;

    @ManyToOne
    private Question question;

    @ManyToOne
    private Answer answer;
}

Comment ๋ชจ๋ธ์˜ ์†์„ฑ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

ํ•„๋“œ์„ค๋ช…
id๋Œ“๊ธ€์˜ ๊ณ ์œ ๋ฒˆํ˜ธ
author๋Œ“๊ธ€ ์ž‘์„ฑ์ž
content๋Œ“๊ธ€ ๋‚ด์šฉ
createDate๋Œ“๊ธ€ ์ž‘์„ฑ์ผ์‹œ
modifyDate๋Œ“๊ธ€ ์ˆ˜์ •์ผ์‹œ
question์ด ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆฐ ์งˆ๋ฌธ
answer์ด ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆฐ ๋‹ต๋ณ€

๋งŒ์•ฝ ์งˆ๋ฌธ์— ๋Œ“๊ธ€์ด ์ž‘์„ฑ๋  ๊ฒฝ์šฐ์—๋Š” question์— ๊ฐ’์ด ์ €์žฅ๋˜๊ณ  ๋‹ต๋ณ€์— ๋Œ“๊ธ€์ด ์ž‘์„ฑ๋  ๊ฒฝ์šฐ์—๋Š” answer์— ๊ฐ’์ด ์ €์žฅ๋  ๊ฒƒ์ด๋‹ค. ํ•œ ์‚ฌ๋žŒ์ด ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋Œ“๊ธ€์„ ๋‹ฌ์ˆ˜ ์žˆ๊ณ  ์งˆ๋ฌธ ๋˜๋Š” ๋‹ต๋ณ€ ํ•œ๊ฐœ์— ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋Œ“๊ธ€์„ ๋‹ฌ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— author, question, answer ์†์„ฑ์—๋Š” @ManyToOne ์• ๋„ˆํ…Œ์ด์…˜์„ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

์œ„์™€ ๊ฐ™์ด ๋Œ“๊ธ€ ๋ชจ๋ธ์„ ์ž‘์„ฑํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋  ๊ฒƒ์ด๋‹ค.
์œ„์™€ ๊ฐ™์ด ๋Œ“๊ธ€ ๋ชจ๋ธ์„ ์ž‘์„ฑํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋  ๊ฒƒ์ด๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋Œ“๊ธ€์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•œ ํ›„์— ์งˆ๋ฌธ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋Œ“๊ธ€์„ ํ†ตํ•ด ์งˆ๋ฌธ์˜ id๋ฅผ ์•Œ์•„๋‚ด๋Š” getQuestionId ๋ฉ”์„œ๋“œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ดํ›„ ์ง„ํ–‰ํ•  ๋Œ“๊ธ€ ์ˆ˜์ •, ์‚ญ์ œ์—์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์ด์ง€๋งŒ ํŽธ์˜๋ฅผ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ ๋จผ์ € ๋งŒ๋“ค๊ณ  ๊ฐ€๋„๋ก ํ•˜์ž.

๋‹ค์Œ๊ณผ ๊ฐ™์ด Comment ๋ชจ๋ธ์— getQuestionId ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/comment/Comment.java

package com.mysite.sbb.comment;

// (... ์ƒ๋žต ...)

@Entity
@Getter
@Setter
public class Comment {

    // (... ์ƒ๋žต ...)

    public Integer getQuestionId() {
        Integer result = null;
        if (this.question != null) {
            result = this.question.getId();
        } else if (this.answer != null) {
            result = this.answer.getQuestion().getId();
        }
        return result;
    }
}











ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

getQuestionId ๋ฉ”์„œ๋“œ๋Š” ๋Œ“๊ธ€์„ ํ†ตํ•ด ์งˆ๋ฌธ์˜ id ๊ฐ’์„ ๋ฆฌํ„ดํ•˜๋Š” ๋ฉ”์„œ๋“œ๋กœ question ์†์„ฑ์ด null์ด ์•„๋‹Œ ๊ฒฝ์šฐ๋Š” ์งˆ๋ฌธ์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€์ด๋ฏ€๋กœ this.question.getId() ๊ฐ’์„ ๋ฆฌํ„ดํ•˜๊ณ  ๋‹ต๋ณ€์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€์ธ ๊ฒฝ์šฐ this.answer.getQuestion().getId() ๊ฐ’์„ ๋ฆฌํ„ดํ•˜๋‹ค.

Question ๋ชจ๋ธ

๊ทธ๋ฆฌ๊ณ  ์งˆ๋ฌธ์—์„œ ๋Œ“๊ธ€์„ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์งˆ๋ฌธ ๋ชจ๋ธ์„ ์ˆ˜์ •ํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/questionQuestion.java

package com.mysite.sbb.question;

// (... ์ƒ๋žต ...)
import com.mysite.sbb.comment.Comment;
// (... ์ƒ๋žต ...)

@Getter
@Setter
@Entity
public class Question {
    // (... ์ƒ๋žต ...)

    @OneToMany(mappedBy = "question")
    private List<Comment> commentList;
}



ย 








ย 
ย 

์งˆ๋ฌธ์— ์ž‘์„ฑ๋œ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด commentList ์†์„ฑ์„ @OneToMany ์• ๋„ˆํ…Œ์ด์…˜์œผ๋กœ ์ƒ์„ฑํ–ˆ๋‹ค. Comment ๋ชจ๋ธ์—์„œ Question์„ ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์†์„ฑ๋ช…์ด question์ด๋ฏ€๋กœ mappedBy์˜ ๊ฐ’์œผ๋กœ "question"์ด ์ „๋‹ฌ๋˜์–ด์•ผ ํ•œ๋‹ค.

Answer ๋ชจ๋ธ

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋‹ต๋ณ€์—์„œ ๋Œ“๊ธ€์„ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‹ต๋ณ€ ๋ชจ๋ธ์„ ์ˆ˜์ •ํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/answer/ Answer.java

package com.mysite.sbb.answer;

// (... ์ƒ๋žต ...)
import com.mysite.sbb.comment.Comment;
// (... ์ƒ๋žต ...)

@Entity
@Getter
@Setter
public class Answer {
    // (... ์ƒ๋žต ...)

    @OneToMany(mappedBy = "answer")
    private List<Comment> commentList;
}



ย 








ย 
ย 

๋‹ต๋ณ€์— ์ž‘์„ฑ๋œ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด commentList ์†์„ฑ์„ @OneToMany ์• ๋„ˆํ…Œ์ด์…˜์œผ๋กœ ์ƒ์„ฑํ–ˆ๋‹ค.

์งˆ๋ฌธ ๋Œ“๊ธ€

์งˆ๋ฌธ์— ๋Œ“๊ธ€์„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด ๋ณด์ž. ์ด์ œ ์Šคํ”„๋ง๋ถ€ํŠธ์— ์ƒˆ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ํŒจํ„ด์— ์ต์ˆ™ํ•ด์กŒ์„ ๊ฒƒ์ด๋‹ค. ์งˆ๋ฌธ ๋Œ“๊ธ€์€ ์งˆ๋ฌธ ์ž‘์„ฑ๊ณผ ๊ฑฐ์˜ ์ฐจ์ด๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์ฝ”๋“œ์ž‘์„ฑ์„ ๋น ๋ฅด๊ฒŒ(ํ•œ๋ฒˆ์—) ์ง„ํ–‰ํ•ด ๋ณด์ž.

์งˆ๋ฌธ ๋Œ“๊ธ€ ๋งํฌ

์งˆ๋ฌธ ์ƒ์„ธ ํ…œํ”Œ๋ฆฟ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•˜์ž.

ํŒŒ์ผ๋ช…: C:\projects\mysite\templates\pyboquestion_detail.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
    <div class="card my-3">
        <div class="card-body">
            <!-- (... ์ƒ๋žต ...) -->
            <div class="my-3" sec:authorize="isAuthenticated()"
                th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}">
                <a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary">์ˆ˜์ •</a>
                <a href="javascript:void(0);" class="delete btn btn-sm btn-outline-secondary"
                    th:data-uri="@{|/question/delete/${question.id}|}">์‚ญ์ œ</a>
            </div>
            <!-- ์งˆ๋ฌธ ๋Œ“๊ธ€ Start -->
            <div class="mt-3" th:if="${not #lists.isEmpty(question.commentList)}">
                <div th:each="comment,index : ${question.commentList}" class="comment py-2 text-muted">
                    <span style="white-space: pre-line;" th:text="${comment.content}"></span>
                    <span th:if="${comment.modifyDate != null}"
                        th:text="| - ${comment.author.username}, ${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')} (์ˆ˜์ •: ${#temporals.format(comment.modifyDate, 'yyyy-MM-dd HH:mm')})|"></span>
                    <span th:if="${comment.modifyDate == null}"
                        th:text="| - ${comment.author.username}, ${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')}|"></span>
                    <a sec:authorize="isAuthenticated()"
                        th:if="${#authentication.getPrincipal().getUsername() == comment.author.username}"
                        th:href="@{|/comment/modify/${comment.id}|}" class="small">์ˆ˜์ •</a>,
                    <a sec:authorize="isAuthenticated()"
                        th:if="${#authentication.getPrincipal().getUsername() == comment.author.username}"
                        href="javascript:void(0);" class="small delete" th:data-uri="@{|/comment/delete/${comment.id}|}">์‚ญ์ œ</a>
                </div>
            </div>
            <div>
                <a th:href="@{|/comment/create/question/${question.id}|}" class="small"><small>๋Œ“๊ธ€ ์ถ”๊ฐ€ ..</small></a>
            </div>
            <!-- ์งˆ๋ฌธ ๋Œ“๊ธ€ End -->
        </div>
    </div>
    <h5 class="border-bottom my-3 py-2" th:text="|${#lists.size(question.answerList)}๊ฐœ์˜ ๋‹ต๋ณ€์ด ์žˆ์Šต๋‹ˆ๋‹ค.|"></h5>
<!-- (... ์ƒ๋žต ...) -->












ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 




๋‚ด์šฉ์ด ๋งŽ์ง€๋งŒ ์–ด๋ ต์ง€ ์•Š๋‹ค. ์ฐฌ์ฐฌํžˆ ์‚ดํŽด๋ณด์ž.

์งˆ๋ฌธ์— ๋“ฑ๋ก๋œ ๋Œ“๊ธ€ ์ „๋ถ€๋ฅผ ๋ณด์—ฌ ์ฃผ๊ธฐ์œ„ํ•ด question.commentList ๋ฃจํ”„๋ฅผ ๋Œ๋ฉฐ ๋Œ“๊ธ€ ๋‚ด์šฉ๊ณผ ๊ธ€์“ด์ด, ์ž‘์„ฑ์ผ์‹œ, ์ˆ˜์ •์ผ์‹œ๋ฅผ ์ถœ๋ ฅํ–ˆ๋‹ค. ๋˜ ๋Œ“๊ธ€ ๊ธ€์“ด์ด์™€ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์œผ๋ฉด '์ˆ˜์ •', '์‚ญ์ œ' ๋งํฌ๊ฐ€ ๋ณด์ด๋„๋ก ํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฃจํ”„ ๋ฐ”๊นฅ์ชฝ์—๋Š” ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” '๋Œ“๊ธ€ ์ถ”๊ฐ€ ..' ๋งํฌ๋„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

๋ฃจํ”„์— ์˜ํ•ด ๋ฐ˜๋ณต๋˜๋Š” ๋Œ“๊ธ€ ๊ฐ๊ฐ์€ comment๋ผ๋Š” ์Šคํƒ€์ผ ํด๋ž˜์Šค๋ฅผ ๋”ฐ๋กœ ์ง€์ •ํ–ˆ๋‹ค. ๋Œ“๊ธ€ ์˜์—ญ์€ ์ข€ ์ž‘์€ ๊ธ€์”จ๋กœ ๋ณด์—ฌ์งˆ ํ•„์š”๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ง€๊ธˆ๊นŒ์ง€ ๋นˆ ํŒŒ์ผ๋กœ ๋˜์–ด์žˆ๋˜ style.css์— comment ํด๋ž˜์Šค์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ๋‹ค์Œ์ฒ˜๋Ÿผ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/resources/staticstyle.css

.comment {
    border-top:dotted 1px #ddd;
    font-size:0.7em;
}

comment ํด๋ž˜์Šค๋Š” ๋Œ“๊ธ€ ๊ฐ๊ฐ์˜ ์ƒ๋‹จ์— ์ ์„ ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ธ€๊ผด ํฌ๊ธฐ๋ฅผ 0.7em์œผ๋กœ ์„ค์ •ํ•˜๋Š” ์Šคํƒ€์ผ์ด๋‹ค.

CommentRepository

๋Œ“๊ธ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด Comment ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/comment/CommentRepository.java

package com.mysite.sbb.comment;

import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Integer> {
}

CommentService

๊ทธ๋ฆฌ๊ณ  ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ“๊ธ€์„ ์กฐํšŒํ•˜๊ณ  ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/comment/CommentService.java

package com.mysite.sbb.comment;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;

@Service
public class CommentService {

    @Autowired
    private CommentRepository commentRepository;

    public Comment create(Question question, SiteUser author, String content) {
        Comment c = new Comment();
        c.setContent(content);
        c.setCreateDate(LocalDateTime.now());
        c.setQuestion(question);
        c.setAuthor(author);
        c = this.commentRepository.save(c);
        return c;
    }

    public Optional<Comment> getComment(Integer id) {
        return this.commentRepository.findById(id);
    }

    public Comment modify(Comment c, String content) {
        c.setContent(content);
        c.setModifyDate(LocalDateTime.now());
        c = this.commentRepository.save(c);
        return c;
    }

    public void delete(Comment c) {
        this.commentRepository.delete(c);
    }
}

CommentService ํด๋ž˜์Šค์— ์ƒ์„ฑ(create), ์กฐํšŒ(getComment), ์ˆ˜์ •(modify), ์‚ญ์ œ(delete) ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์„ฑํ–ˆ๋‹ค.

CommentForm

๊ทธ๋ฆฌ๊ณ  ๋Œ“๊ธ€ ์ž‘์„ฑ์‹œ ํ•„์š”ํ•œ CommentForm์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/comment/CommentForm.java

package com.mysite.sbb.comment;

import jakarta.validation.constraints.NotEmpty;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommentForm {
    @NotEmpty(message = "๋‚ด์šฉ์€ ํ•„์ˆ˜ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.")
    private String content;
}

CommentForm์— ํ•„์š”ํ•œ ์†์„ฑ์€ "๋‚ด์šฉ(content)" ํ•˜๋‚˜ ๋ฟ์ด๋‹ค.

CommentController

๊ทธ๋ฆฌ๊ณ  ์งˆ๋ฌธ ๋Œ“๊ธ€์„ ์ž‘์„ฑ, ์ˆ˜์ •, ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•œ ๋Œ“๊ธ€ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/java/com/mysite/sbb/comment/CommentController.java

package com.mysite.sbb.comment;

import java.security.Principal;
import java.util.Optional;

import jakarta.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;

import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;

@Controller
@RequestMapping("/comment")
public class CommentController {

    @Autowired
    private CommentService commentService;

    @Autowired
    private QuestionService questionService;

    @Autowired
    private UserService userService;

    @PreAuthorize("isAuthenticated()")
    @GetMapping(value = "/create/question/{id}")
    public String createQuestionComment(CommentForm commentForm) {
        return "comment_form";
    }

    @PreAuthorize("isAuthenticated()")
    @PostMapping(value = "/create/question/{id}")
    public String createQuestionComment(@PathVariable("id") Integer id, @Valid CommentForm commentForm,
            BindingResult bindingResult, Principal principal) {
        Optional<Question> question = this.questionService.getQuestion(id);
        Optional<SiteUser> user = this.userService.getUser(principal.getName());
        if (question.isPresent() && user.isPresent()) {
            if (bindingResult.hasErrors()) {
                return "comment_form";
            }
            Comment c = this.commentService.create(question.get(), user.get(), commentForm.getContent());
            return String.format("redirect:/question/detail/%s", c.getQuestionId());
        } else {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found");
        }
    }

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/modify/{id}")
    public String modifyComment(CommentForm commentForm, @PathVariable("id") Integer id, Principal principal) {
        Optional<Comment> comment = this.commentService.getComment(id);
        if (comment.isPresent()) {
            Comment c = comment.get();
            if (!c.getAuthor().getUsername().equals(principal.getName())) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์ˆ˜์ •๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
            }
            commentForm.setContent(c.getContent());
        }
        return "comment_form";
    }

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/modify/{id}")
    public String modifyComment(@Valid CommentForm commentForm, BindingResult bindingResult, Principal principal,
            @PathVariable("id") Integer id) {
        if (bindingResult.hasErrors()) {
            return "comment_form";
        }
        Optional<Comment> comment = this.commentService.getComment(id);
        if (comment.isPresent()) {
            Comment c = comment.get();
            if (!c.getAuthor().getUsername().equals(principal.getName())) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์ˆ˜์ •๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
            }
            c = this.commentService.modify(c, commentForm.getContent());
            return String.format("redirect:/question/detail/%s", c.getQuestionId());

        } else {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found");
        }
    }

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/delete/{id}")
    public String deleteComment(Principal principal, @PathVariable("id") Integer id) {
        Optional<Comment> comment = this.commentService.getComment(id);
        if (comment.isPresent()) {
            Comment c = comment.get();
            if (!c.getAuthor().getUsername().equals(principal.getName())) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์‚ญ์ œ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
            }
            this.commentService.delete(c);
            return String.format("redirect:/question/detail/%s", c.getQuestionId());
        } else {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found");
        }
    }
}

์งˆ๋ฌธ์„ ์ž‘์„ฑ, ์ˆ˜์ •, ์‚ญ์ œํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ๋ฐฉ๋ฒ•์ด๋ผ์„œ ๊ฐ๊ฐ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ๋”ฐ๋กœ ์„ค๋ช…ํ•˜์ง€๋Š” ์•Š๊ฒ ๋‹ค. ๋‹ค๋งŒ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด comment_form.html ํ…œํ”Œ๋ฆฟ์ด ํ•„์š”ํ•˜๊ณ  ๋Œ“๊ธ€์„ ์ž‘์„ฑ, ์ˆ˜์ •, ์‚ญ์ œํ•œ ํ›„์—๋Š” ํ•ด๋‹น ์งˆ๋ฌธ์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ธฐ ์œ„ํ•ด ์งˆ๋ฌธ์˜ id ๊ฐ’์ด ํ•„์š”ํ•˜์—ฌ c.getQuestionId()๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜์ž.

comment_form.html

๊ทธ๋ฆฌ๊ณ  ๋Œ“๊ธ€ ์ž‘์„ฑ๊ณผ ์ˆ˜์ •์‹œ ํ•„์š”ํ•œ comment_form ํ…œํ”Œ๋ฆฟ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜์ž.

ํŒŒ์ผ๋ช…: /sbb/src/main/resources/templates/comment_form.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
    <h5 class="my-3 border-bottom pb-2">๋Œ“๊ธ€ ๋“ฑ๋ก</h5>
    <form th:object="${commentForm}" method="post">
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        <nav th:replace="form_errors :: formErrorsFragment"></nav>
        <div class="mb-3">
            <label for="content" class="form-label">๋‚ด์šฉ</label>
            <textarea th:field="*{content}" class="form-control" rows="10"></textarea>
        </div>
        <input type="submit" value="์ €์žฅํ•˜๊ธฐ" class="btn btn-primary my-2">
    </form>
</div>

์งˆ๋ฌธ, ๋‹ต๋ณ€๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋Œ“๊ธ€ ๋“ฑ๋ก๊ณผ ์ˆ˜์ •์— ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด action ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  CSRF ํ•ญ๋ชฉ๋„ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

์งˆ๋ฌธ ๋Œ“๊ธ€ ๊ธฐ๋Šฅ ํ™•์ธ

์ด์™€ ๊ฐ™์ด ์ˆ˜์ • ํ›„ ์งˆ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ <FontIcon icon="iconfont icon-select"/>๋ฅผ ๋ˆŒ๋Ÿฌ ๋Œ“๊ธ€์„ ์ถ”๊ฐ€ํ•ด ๋ณด๊ณ  ์ˆ˜์ •๊ณผ ์‚ญ์ œ ๊ธฐ๋Šฅ๋„ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์ž.
์ด์™€ ๊ฐ™์ด ์ˆ˜์ • ํ›„ ์งˆ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ <๋Œ“๊ธ€ ์ถ”๊ฐ€ ..>๋ฅผ ๋ˆŒ๋Ÿฌ ๋Œ“๊ธ€์„ ์ถ”๊ฐ€ํ•ด ๋ณด๊ณ  ์ˆ˜์ •๊ณผ ์‚ญ์ œ ๊ธฐ๋Šฅ๋„ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์ž.

๋‹ต๋ณ€ ๋Œ“๊ธ€

์งˆ๋ฌธ ๋Œ“๊ธ€๊ณผ ๋™์ผํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ์ƒ๋žตํ•œ๋‹ค.


์ด์ฐฌํฌ (MarkiiimarK)
Never Stop Learning.