ํ๋ก์ ํธ ๊ฐ์
ChatGPT๋ก ์์ด๋ฅผ ๊ณต๋ถํ๋ฉฐ ์๊ธฐ๋ ๋ถํธํจ์ ํด์ํ๊ณ , ํ์ต ํจ์จ์ ๋์ฌ์ฃผ๊ธฐ ์ํ ์น ์ดํ๋ฆฌ์ผ์ด์
์ ๋ง๋๋ ํ๋ก์ ํธ์
๋๋ค. ๋ค์ํ ์์ฐ์ด ์ฒ๋ฆฌ API๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์์ด๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ตํ ์ ์๋๋ก ๋์์ค๋๋ค (๋ณธ ํ๋ก์ ํธ๋ ๊ธฐ์กด ์งํผํฐ์ฒ ํ๋ก์ ํธ๋ฅผ ๋ฆฌํฉํฐ๋งํ์์ต๋๋ค).
โข
ํ๋ก์ ํธ๋ช
โข
์งํ ๊ธฐ๊ฐ
โข
์ฐธ์ฌ ์ธ์
โข
URL
: ์ํฐ (engT, ์๊ธ๋ฆฌ์ ํฐ์ฒ)
: 2023.05.24 ~ 2023.06.27 (1๊ฐ์ 4์ผ)
: ๊ฐ์ธ
ํ๋ก์ ํธ ์ ์ ๋๊ธฐ
์ต๊ทผ ์์ฐ์ด AI ๋ชจ๋ธ์ธ ChatGPT๋ฅผ ์ด์ฉํด ์์ด๊ณต๋ถํ๋ ๋ฐฉ๋ฒ์ด ๋งค์ฐ ๊ฐ๊ด๋ฐ๊ณ ์์ต๋๋ค. ํ์ง๋ง ChatGPT๋ฅผ ์ด์ฉํด ์์ด๊ณต๋ถ๋ฅผ ํ ๊ฒฝ์ฐ ์๋์ฒ๋ผ ์ฌ๋ฌ ๋ฌธ์ ์ ๋ค์ด ๋ฐ์ํฉ๋๋ค.
โข
์ํฉ๊ทน์ ํ์๊ณ ํ๋๋ฐ GPT ํผ์ ๋ชจ๋ ๋๋ณธ์ ์ค์ค ์ธ์๋ฒ๋ฆผ
โข
GPT์ ๋ํ๋ ์์ฐ์ค๋ฝ๊ฒ ์ด์ด๊ฐ๋ฉด์๋ ๋ด ์์ด ๋ฌธ์ฅ์ ๋ํ ํผ๋๋ฐฑ์ ๋ ๋ฐ๋ก ๋ฐ๊ณ ์ถ์ ๋, GPT์๊ฒ ์์ฒญํ๋ ๊ฒ์ด ๋งค์ฐ ๋ฒ์กํ๊ณ ์ฃผ์๋ ฅ์ ๋จ์ด๋จ๋ฆผ
โข
ํผ๋๋ฐฑ์ ๋ฌ๋ผ๊ณ ์ ํํ ์์ฒญํ๊ธฐ๋ ์ฝ์ง ์์
โข
์์ฑ์ผ๋ก ๋ํ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์์
โข
์ด์ ํ์ต ๋ด์ญ๋ค์ ์ ์ฅํ๊ณ ๋ณต์ตํ๊ธฐ ์ด๋ ค์
์ด๋ฐ ๋ฌธ์ ์ ์ ํด๊ฒฐํ๊ณ ์ ์์ฐ์ด ์ฒ๋ฆฌ API๋ฅผ ํตํด ํ์ต์ ๋ณด์กฐํด์ฃผ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์ ์ํ์์ต๋๋ค.
์ฌ์ฉ ๊ธฐ์
โข
๋ฐฑ์๋
Spring Boot 3
Java 17
Apache Tomcat
Thymeleaf
Gradle
AWS EC2, ELB, RDS
โข
ํ๋ก ํธ์๋
HTML5 / CSS
Javascript ES6+
Bootstrap 5.3
โข
DB
MySQL
Spring Data JPA
Spring Data Redis
QueryDSL
AWS RDS
โข
ํ์๊ด๋ฆฌ
Git, Github
Sourcetree
โข
IDE
IntelliJ IDEA Ultimate
โข
์ธ๋ถ API
Google OAuth 2.0 API
OpenAI ChatGPT 3.5
OpenAI Whisper API(Speech To Text)
Microsoft Azure Speech TTS(Text To Speech) API
Microsoft Azure Speech Pronunciation Assessment API
โข
์ฌ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
opusRecorder.js
opusMediaRecorder.js
sweetalert2.js
์ฃผ์ ๊ธฐ๋ฅ
ํ์ต ๊ธฐ๋ฅ - ๊ธ์ฐ๊ธฐ ์ฐ์ต
โข
์ฌ์ฉ์๊ฐ ์ฃผ์ ๋ฅผ ์ง์ ์
๋ ฅํ ์๋ ์๊ณ , ์นดํ
๊ณ ๋ฆฌ๋ฅผ ์ ํํ๋ฉด AI๊ฐ ๋๋ค ์ฃผ์ ๋ฅผ ์ถ์ฒํด์ค๋๋ค.
โข
์ฌ์ฉ์ ์๋ฌธ์ ๋ํด ๋ฌธ์ฅ๋ณ๋ก ์์ธํ ํผ๋๋ฐฑ์ ํด์ค๋๋ค.
ํ์ต ๊ธฐ๋ฅ - ๋งํ๊ธฐ ์ฐ์ต
โข
์ฌ์ฉ์๊ฐ ์ฃผ์ ๋ฅผ ์ง์ ์
๋ ฅํ ์๋ ์๊ณ , ๋งํ๊ธฐ ์ํ์ ์ ํํ๋ฉด AI๊ฐ ๋๋ค ์ฃผ์ ๋ฅผ ์ถ์ฒํด์ค๋๋ค.
โข
์ฌ์ฉ์ ๋ฐํ์ ๋ํด ๋ฌธ์ฅ๋ณ๋ก ์์ธํ ํผ๋๋ฐฑ์ ํด์ค๋๋ค.
ํ์ต ๊ธฐ๋ฅ - ํํ ์ฐ์ต
โข
์ฌ์ฉ์๊ฐ ์ฃผ์ ๋ฅผ ์ง์ ์
๋ ฅํ ์๋ ์๊ณ , ์นดํ
๊ณ ๋ฆฌ๋ฅผ ์ ํํ๋ฉด AI๊ฐ ๋๋ค ์ฃผ์ ๋ฅผ ์ถ์ฒํด์ค๋๋ค.
โข
AI๊ฐ ๋จผ์ ์ด์ผ๊ธฐ๋ฅผ ์์ํ ์ชฝ์ ํ๋จํ์ฌ ๋จผ์ ๋ง์ ๊ฑธ๊ฑฐ๋ ์ฌ์ฉ์์๊ฒ ๋ํ๋ฅผ ์์ํ ๋ฌธ์ฅ์ ์ถ์ฒํด์ค๋๋ค.
โข
๋ํ ์งํ๊ณผ ๋์์ ์ค์๊ฐ์ผ๋ก ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์์ต๋๋ค.
โข
ํ๊ตญ์ด๋ก ์ด์ผ๊ธฐ๋ฅผ ํด๋ ์๋์ผ๋ก ๋ฒ์ญ์ด ๋์ด ๋ํ๋ฅผ ์ด์ด๋๊ฐ ์ ์์ต๋๋ค.
๋ฐ์ ํ๊ฐ
โข
๋ฌธ์ฅ๋ณ๋ก ๋ฐ์์ ํ๊ฐ๋ฐ์ ์ ์์ต๋๋ค.
๋ณต์ตํ๊ธฐ
โข
ํ์ต ์ด๋ ฅ๋ค์ ์กฐํํ ์ ์์ต๋๋ค.
๋ฒ์ญ ๊ธฐ๋ฅ
โข
ChatGPT๋ฅผ ํตํด ๋ฒ์ญ์๋น์ค๋ฅผ ๋ฐ์ ์ ์์ต๋๋ค.
๋ฆฌํฉํฐ๋ง
๋ณธ ํ๋ก์ ํธ๋ ์งํผํฐ์ฒ ํ๋ก์ ํธ๋ฅผ ๋ฐฐํฌํ๊ธฐ ์ํด ํต์ฌ ๊ธฐ๋ฅ๋ง ์ถ๋ ค ๋ฆฌํฉํฐ๋ง ๊ณผ์ ์ ๊ฑฐ์ณค์ต๋๋ค. ๋ฆฌํฉํฐ๋ง์ ํตํด ๊ธฐ์กด ํ๋ก์ ํธ ๋๋น ์๋์ ๊ฐ์ ๋ถ๋ถ์ ๊ฐ์ ํ์์ต๋๋ค.
AWS ๋ฐฐํฌ
โข
๊ธฐ์กด ๋ก์ปฌ ํ๊ฒฝ์์ ์ฌ์ฉํ๋ ์ ํ๋ฆฌ์ผ์ด์
์ AWS์ ๋ฐฐํฌํ์์ต๋๋ค.
โฆ
EC2 ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ์ต๋๋ค(Linux Ubuntu).
โฆ
HTTPS ์ค์ , ์๋ฒ ๋ถ์ฐ ๋ฑ์ ์ํด Application Load Balancer๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
โฆ
RDS(MySQL)๋ฅผ ์ฌ์ฉํด DB ์๋ฒ๋ฅผ ๊ตฌ์ถํ์ต๋๋ค.
์๋ฐ์คํฌ๋ฆฝํธ ๋ชจ๋ํ
โข
๊ธฐ์กด ํ์ผ ๋ถ๋ฆฌ์ ๋๋ง ์ํํ๋ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ๊ธฐ๋ฅ๋ณ๋ก ๋ชจ๋ํํ์ฌ ์ฌ์ฌ์ฉ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ์ ๋์์ต๋๋ค.
โข
์ฌ์ฉ์ด ๋น๋ฒํ ์ ํธ๋ฆฌํฐ ํ์ผ์ ๊ธ๋ก๋ฒ ๋ค์์คํ์ด์ค๋ก ๋ฑ๋กํด ๊ฐ๋ฐ ์์ฐ์ฑ์ ๋์์ต๋๋ค.
โข
์๋ฆผ์ฐฝ, ajax ๋ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ API๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, ์๋์ฒ๋ผ ์ถ์ํ๋ฅผ ํตํ์ฌ ์ถํ ๊ตฌํ์ฒด๊ฐ ๋ณ๊ฒฝ๋๋๋ผ๋ ๋ณ๊ฒฝ ์ง์ ์ด ํ ํ์ผ๋ก ํ์ ๋๋๋ก ํ์์ต๋๋ค.
const Alertor = {
alert: (message) => {
Swal.fire(message);
},
confirm: (message, callback) => {
Swal.fire({
title: message,
showCancelButton: true,
confirmButtonText: '๋ค',
cancelButtonText: '์๋์',
}).then(function (result) {
if (result.isConfirmed) {
callback();
}
});
},
notice: (message, callback) => {
Swal.fire({
title: message,
showCancelButton: false,
confirmButtonText: '๋ค',
}).then(function (result) {
if (result.isConfirmed) {
callback();
}
});
},
wait: (title, text) => {
Swal.fire({
title: title,
text: text,
allowOutsideClick: false
});
Swal.showLoading();
},
...
}
JavaScript
๋ณต์ฌ
์๋ฆผ์ฐฝ์ ์ถ์ํ์ํจ ๊ธ๋ก๋ฒ ๋ค์์คํ์ด์ค Alertor ๊ฐ์ฒด
API ํธ์ถ ๋ณ๋ ฌ ์ฒ๋ฆฌ
โข
ํํ ์ฐ์ต์ ๊ฒฝ์ฐ ๋ํ ์๋ต๊ณผ ์ ์ ์ ๋ฌธ์ฅ์ ๋ํ ํผ๋๋ฐฑ์ ๋ณ๋๋ก ์งํํ๋๋ฐ, ์ด๋ ๋ณ๋ ฌ์ฒ๋ฆฌ๋ฅผ ์ํ์ฌ ExecutorService๋ฅผ ์ฌ์ฉํ์ฌ ์๋ต ์๋๋ฅผ ๋์์ต๋๋ค.
ExecutorService executor = Executors.newFixedThreadPool(2);
List<Callable<String>> apiCalls = new ArrayList<>();
apiCalls.add(() -> lmc.chat(correctionRequestMessages, 33));
apiCalls.add(() -> lmc.chat(chatRequestMessages));
List<Future<String>> futureResponses;
try {
futureResponses = executor.invokeAll(apiCalls);
} catch (InterruptedException e) {
log.error("An error occurred while invoking methods with ExecutorService.", e);
throw new IllegalStateException(e);
}
String correctionResult;
try {
correctionResult = futureResponses.get(0).get();
} catch (Exception e) {
log.error("Parsing the correction result for a dialogue sentence failed.", e);
throw new ApiFailException(e);
}
String aiSentence;
try {
aiSentence = JsonPath.parse(futureResponses.get(1).get()).read("response", String.class);
} catch (Exception e) {
log.error("Parsing the response result for a dialogue sentence failed.", e);
throw new ApiFailException(e);
}
Java
๋ณต์ฌ
API ์ธํฐํ์ด์ค ์ถ์ํ
โข
API ํด๋์ค๋ฅผ ์ธํฐํ์ด์ค๋ก ์ถ์ํํ์ฌ ์ถํ ์ฌ์ฉ ๊ธฐ์ ์ด ๋ณ๊ฒฝ๋๋๋ผ๋ ๋ณ๊ฒฝ์ ์ ๊ตฌํ์ฒด ํ ๊ณณ์ผ๋ก ํ์ ๋๋๋ก ํ์์ต๋๋ค.
โฆ
์๋๋ ChatGPT ํด๋ผ์ด์ธํธ ํด๋์ค๋ฅผ ์ถ์ํํ ์ธํฐํ์ด์ค์
๋๋ค.
public interface LanguageModelClient {
/**
* ์ ์ ํ๋กฌํํธ๋ฅผ ํตํด ์ธ์ด๋ชจ๋ธ์ ์๋ต ์์ .
* randomFactor 50%.
*
* @param prompt ์ ์ ํ๋กฌํํธ.
* @return ์ธ์ด๋ชจ๋ธ์ ์๋ต.
*/
String chat(String prompt);
/**
* ์ ์ ํ๋กฌํํธ๋ฅผ ํตํด ์ธ์ด๋ชจ๋ธ์ ์๋ต ์์ .
*
* @param prompt ์ ์ ํ๋กฌํํธ.
* @param randomFactor ์๋ต์ ๋๋ค์ฑ์ ๊ฒฐ์ ํ๋ ํ๋ผ๋ฏธํฐ. 0~100 ๋ฒ์ ์
๋ ฅ. 100์ ๊ฐ๊น์ธ์๋ก ๋ต๋ณ์ด ๋๋คํด์ง.
* @return ์ธ์ด๋ชจ๋ธ์ ์๋ต.
*/
String chat(String prompt, int randomFactor);
/**
* ๊ณผ๊ฑฐ ์ ์ ํ๋กฌํํธ ๋ฐ ์ธ์ด๋ชจ๋ธ ์๋ต์ ํฌํจํ ํ๋กฌํํธ ์
๋ ฅ์ ํตํด ์ธ์ด๋ชจ๋ธ์ ๋ต๋ณ ์์ .
* randomFactor 50%.
*
* @param messages ๊ณผ๊ฑฐ ์ ์ ํ๋กฌํํธ ๋ฐ ์ธ์ด๋ชจ๋ธ ์๋ต์ ํฌํจํ ํ๋กฌํํธ. 0๋ฒ ์ธ๋ฑ์ค๋ ์์คํ
ํ๋กฌํํธ, ํ์ ์ธ๋ฑ์ค๋ ์ ์ ํ๋กฌํํธ, ์ง์ ์ธ๋ฑ์ค๋ ์ธ์ด๋ชจ๋ธ์ ์๋ต.
* @return ์ธ์ด๋ชจ๋ธ์ ์๋ต.
* @throws ApiFailException API ์๋ต ์์ ์คํจ ์ ๋ฐ์.
*/
String chat(List<String> messages);
/**
* ๊ณผ๊ฑฐ ์ ์ ํ๋กฌํํธ ๋ฐ ์ธ์ด๋ชจ๋ธ ์๋ต์ ํฌํจํ ํ๋กฌํํธ ์
๋ ฅ์ ํตํด ์ธ์ด๋ชจ๋ธ์ ๋ต๋ณ ์์ .
*
* @param messages ๊ณผ๊ฑฐ ์ ์ ํ๋กฌํํธ ๋ฐ ์ธ์ด๋ชจ๋ธ ์๋ต์ ํฌํจํ ํ๋กฌํํธ. 0๋ฒ ์ธ๋ฑ์ค๋ ์์คํ
ํ๋กฌํํธ, ํ์ ์ธ๋ฑ์ค๋ ์ ์ ํ๋กฌํํธ, ์ง์ ์ธ๋ฑ์ค๋ ์ธ์ด๋ชจ๋ธ์ ์๋ต.
* @param randomFactor ์๋ต์ ๋๋ค์ฑ์ ๊ฒฐ์ ํ๋ ํ๋ผ๋ฏธํฐ. 0~100 ๋ฒ์ ์
๋ ฅ. 100์ ๊ฐ๊น์ธ์๋ก ๋ต๋ณ์ด ๋๋คํด์ง.
* @return ์ธ์ด๋ชจ๋ธ์ ์๋ต.
* @throws ApiFailException API ์๋ต ์์ ์คํจ ์ ๋ฐ์.
*/
String chat(List<String> messages, int randomFactor);
}
Java
๋ณต์ฌ
โฆ
์ด ์ธ์ STT, TTS, ๋ฐ์ํ๊ฐ API๋ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ถ์ํ๋ฅผ ํ์ต๋๋ค.
์ธํฐ์ ํฐ ์ธ๊ฐ(Authorization) ์ฒ๋ฆฌ ๊ณตํตํ
โข
๊ธฐ์กด ์ฝ๋๋ ๊ฐ ์ปจํธ๋กค๋ฌ๋ง๋ค ํน์ ์์์ ๋ํด ์ ์ ๊ฐ ๊ถํ์ ๊ฐ์ง๊ณ ์๋ ์ง ์ผ์ผ์ด ํ์ธํ์๊ธฐ ๋๋ฌธ์ ์ฝ๋์ค๋ณต, ์ ์ง๋ณด์์ ์ด๋ ค์ ๋ฑ ์ฌ๋ฌ ๋ฌธ์ ๊ฐ ์กด์ฌํ์ต๋๋ค.
โข
์คํ๋ง ์ธํฐ์
ํฐ์์ ์๋์ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ๊ณตํต๋ ์ธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ์ต๋๋ค.
โฆ
์ธ๊ฐ๊ฐ ํ์ํ ์์์ ์ ๊ทผํ๊ธฐ ์ํด์๋ ํด๋น ์์์ id๊ฐ ํ๋ผ๋ฏธํฐ๋ก ๋์ด๊ฐ๋ ์ ์์ ์ฐฉ์ํ์ต๋๋ค.
โฆ
ํ์ต(learning)์ด๋ผ๋ฉด learningId, ๋ฌธ์ฅ(sentence)์ sentenceId ๋ฑ์ด ๋ฉ๋๋ค.
โฆ
URI ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ, Post body ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ, PathVariable, ์ง์ ์ ์ํ Json ํ๋ผ๋ฏธํฐ ๋ฑ์ ํด๋น ์์์ id๊ฐ ํฌํจ๋์ด์์ ๊ฒฝ์ฐ ์ธ๊ฐ ๊ฒ์ฆ์ ์งํํฉ๋๋ค.
@RequiredArgsConstructor
public class AuthorizationInterceptor implements HandlerInterceptor {
private final AuthorizationService authorizationService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Authentication authentication = (Authentication) request.getAttribute("authentication");
Map<String, String[]> parameterMap = request.getParameterMap();
for (String paramKey : AuthorizationService.RESOURCE_PARAM_KEYS) {
String[] resourceIds = parameterMap.get(paramKey);
if (resourceIds != null && resourceIds.length > 0) {
authorizationService.validate(paramKey, resourceIds, authentication);
}
}
Map<String, String> pathVariables = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (pathVariables != null) {
for (String paramKey : AuthorizationService.RESOURCE_PARAM_KEYS) {
String resourceId = pathVariables.get(paramKey);
if (StringUtils.hasText(resourceId)) {
authorizationService.validate(paramKey, resourceId, authentication);
}
}
}
if (authentication != null) {
request.setAttribute("premiumType", authentication.getValidPremiumType());
}
return true;
}
}
Java
๋ณต์ฌ
โฆ
AuthorizationService์์๋ ํ๋ผ๋ฏธํฐ ํค/๊ฐ, ์ธ์ฆ(Authentication) ์ ๋ณด๋ฅผ ๋ฐ์ ๊ฒ์ฆ ์์
์ ์งํํฉ๋๋ค.
@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class AuthorizationService {
private final LearningRepository learningRepository;
private final SentenceRepository sentenceRepository;
private final CollectionSentenceRepository collectionSentenceRepository;
private final CollectionLearningRepository collectionLearningRepository;
public static final String[] RESOURCE_PARAM_KEYS =
new String[]{"learningId", "sentenceId", "collectionSentenceId", "collectionLearningId"};
public void validate(String paramKey, String paramValue, Authentication authentication) {
validate(paramKey, new String[]{paramValue}, authentication);
}
public void validate(String paramKey, String[] paramValues, Authentication authentication) {
List<Long> ids = Arrays.stream(paramValues)
.filter(StringUtils::hasText)
.map(Long::valueOf)
.toList();
if (ids.size() == 0) {
return;
}
try {
paramKey = paramKey.substring(0, 1).toUpperCase() + paramKey.substring(1);
Method method = this.getClass().getMethod("validate" + paramKey, List.class, Long.class);
boolean isValid = (boolean) method.invoke(this, ids, authentication.getMemberId());
if (!isValid) {
throw new UnauthorizedException("ํ๊ฐ๋์ง ์์ ์ ๊ทผ์
๋๋ค.");
}
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
public boolean validateLearningId(List<Long> ids, Long memberId) {
return learningRepository.findUnauthorizedOne(ids, memberId).isEmpty();
}
public boolean validateSentenceId(List<Long> ids, Long memberId) {
return sentenceRepository.findUnauthorizedOne(ids, memberId).isEmpty();
}
public boolean validateCollectionSentenceId(List<Long> ids, Long memberId) {
return collectionSentenceRepository.findUnauthorizedOne(ids, memberId).isEmpty();
}
public boolean validateCollectionLearningId(List<Long> ids, Long memberId) {
return collectionLearningRepository.findUnauthorizedOne(ids, memberId).isEmpty();
}
}
Java
๋ณต์ฌ
โฆ
์ด๋ ์๋ฐ ๋ฆฌํ๋ ์
์ ์ด์ฉํ์ฌ ๊ฒ์ฆ์ฉ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ , ๊ฒ์ฆ์ฉ ๋ฉ์๋๋ DB์์ ๊ถํ์ด ์๋ ๋ฆฌ์์ค๊ฐ ํ๋๋ผ๋ ์๋ ์ง ์กฐํํ์ฌ ์ธ๊ฐ ์ฌ๋ถ๋ฅผ boolean์ผ๋ก ๋ฐํํฉ๋๋ค.
โฆ
๊ฒ์ฆ ๊ฒฐ๊ณผ false ๋ฐํ ์ ์ปค์คํ
์ผ๋ก ๋ง๋ ์์ธ์ธ UnauthorizedException๋ฅผ ๋์ง๊ณ , exceptionResolver์์ ํด๋น ์์ธ๋ฅผ ๋ฐ์ ์๋ฌ์ฒ๋ฆฌ๋ฅผ ์งํํฉ๋๋ค.
@JsonParam ์ด๋ ธํ ์ด์ ์ ์
โข
JSON ํฌ๋งท์ผ๋ก ์์ฒญ์ ๋ฐ์ ๊ฒฝ์ฐ ํด๋น JSON ํํ์ ๋ง๋ ๊ฐ์ฒด๋ฅผ ์ผ์ผ์ด ๋ง๋ค์ด์ผ ํ๋ ์ ์ด ๋ถํธํ์ฌ ์ปจํธ๋กค๋ฌ์์ @RequestParam๊ณผ ๊ฐ์ @JsonParam ์ด๋
ธํ
์ด์
์ ์ ์ํ์ฌ ๊ฐ๋จํ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ ์๋๋ก ํ์ต๋๋ค.
@RequiredArgsConstructor
public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthorizationService authorizationService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonParam.class);
}
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String jsonBody = (String) request.getAttribute("JSON_REQUEST_BODY");
if (jsonBody == null) {
jsonBody = IOUtils.toString(request.getInputStream());
request.setAttribute("JSON_REQUEST_BODY", jsonBody);
}
String key = parameter.getParameterAnnotation(JsonParam.class).value();
// @JsonParam value ์์ฑ์ด ์์ ๊ฒฝ์ฐ controller์ ํ๋ผ๋ฏธํฐ ๋ณ์๋ช
์ผ๋ก key ์ง์
if (!StringUtils.hasText(key)) {
key = parameter.getParameterName();
}
Object value = JsonPath.parse(jsonBody).read(key, parameter.getParameterType());
validateAuthorization(key, value, request);
return value;
}
private void validateAuthorization(String key, Object value, HttpServletRequest request) {
if (Arrays.asList(AuthorizationService.RESOURCE_PARAM_KEYS).contains(key)) {
String resourceId = String.valueOf(value);
Authentication authentication = (Authentication) request.getAttribute("authentication");
authorizationService.validate(key, resourceId, authentication);
}
}
}
Java
๋ณต์ฌ
โข
์๋์ ๊ฐ์ด ์ฌ์ฉ๋ฉ๋๋ค
@ResponseBody
@PostMapping("/collection/learning/new")
public CollectionLearningListDtoResult<CollectionLearningListDto> addCollection(
@JsonParam String collectionLearningName,
@RequestAttribute Authentication authentication
) {
...
}
Java
๋ณต์ฌ
Session์์ JWT๋ก
โข
AWS์ ๋ฐฐํฌํ ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์ ์๋ฒ๋ฅผ ๋ถ์ฐํด์ ์ด์ฉํด๋ ๋ฌธ์ ๊ฐ ์๋๋ก JWT๋ฅผ ๋์
ํ์ต๋๋ค.
โข
JWT ์ฟ ํค์ HTTP ๋ฐ secure ์ค์ ์ ํ์ฌ XSS ๊ณต๊ฒฉ์ ์๋ฐฉํ์ต๋๋ค.
โข
CSRF ๊ณต๊ฒฉ ๋ฑ ๋ณด์ ์ทจ์ฝ์ ์ด ๋จ์์์ง๋ง, ์ฌ์ดํธ์์ ๋ฏผ๊ฐํ ๊ฐ์ธ์ ๋ณด๋ฅผ ์์งํ์ง ์๊ณ ์๊ณ , ํด์ปค๊ฐ ํ์ทจํ JWT๋ฅผ ํตํด ์ป์ ์ ์๋ ์ด๋์ด ๊ฑฐ์ ์๋ค๊ณ ํ๋จํ์ฌ JWT๋ฅผ ๋์
ํ์์ต๋๋ค.
โข
๋ค๋ง ๋ค์๋ถํฐ๋ ๋์นญํค๋ฅผ ์ด์ฉํด JWT์ body๋ ๋ณ๋๋ก ํด์ฑํ๋ ๊ฒ์ด ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์์ต๋๋ค.
์คํ๋ง ๋ถํธ, Spring Data JPA, QueryDSL ๋์
โข
๊ธฐ์กด ์์ ์คํ๋ง์ ์ฌ์ฉํ๋ ๊ฒ์์ ์คํ๋ง ๋ถํธ๋ฅผ ๋์
ํ์ต๋๋ค.
โข
๊ธฐ์กด MyBatis์์ Spring Data JPA, QueryDSL์ ๋์
ํ์ฌ ์์ฐ์ฑ์ ๋์์ต๋๋ค.
ํ์ด์ง ์ ํ ์ Ajax ์ ์ฉ
โข
ํ์ด์ง ์ ํ ์์ฒญ ์ ์ธ๋ถ API ํธ์ถ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ง๊ณ ์ ํํด์ผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์ต๋๋ค.
โข
์ด ๊ฒฝ์ฐ ์ธ๋ถ API ํธ์ถ์ด ์คํจํ ์ ์๊ธฐ ๋๋ฌธ์ ํ์ด์ง ์ด๋ ๋ฒํผ์ ๋ชจ๋ Ajax๋ก ์ฒ๋ฆฌํ์ฌ ์คํจ ์ ์ค๋ฅ ํ์ด์ง ๋์ ์๋ฆผ์ฐฝ์ด ๋ํ๋๋๋ก ํ์ต๋๋ค.
๋ชจ๋ฐ์ผ ์ต์ ํ(๋ฐ์ํ ๋์์ธ, ๋ฏธ๋์ด ์ ์ถ๋ ฅ)
โข
๋ถํธ์คํธ๋ฉ์ ํตํด ๋ฐ์ํ ๋์์ธ์ ๊ตฌํํ์ฌ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์๋ ์ด์ฉํ ์ ์๋๋ก ํ์์ต๋๋ค.
๋ฐ์คํฌํ, ํ๋ธ๋ฆฟ ํ๊ฒฝ
์ค๋งํธํฐ ํ๊ฒฝ
โข
๊ทธ ์ธ ๊ธฐ๊ธฐ ํํ ๋ฐ ๋ธ๋ผ์ฐ์ ๋ณ๋ก ์ง์ํ๋ ํฌ๋งท์ด ์์ดํ ๋ฏธ๋์ด ์
์ถ๋ ฅ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด ํด๊ฒฐํ์์ต๋๋ค.
โฆ
OpusMediaRecorder.js (.webm audio for STT API)
โฆ
OpusRecorder.js (.ogg audio for ๋ฐ์ํ๊ฐ API)
Redis DB ํ์ฉ
โข
๊ธฐ์กด ์ด๋ฉ์ผ ์ธ์ฆ์๋ง ์ฌ์ฉํ๋ Redis๋ฅผ ์ด์ฉํ์ฌ ์ฃผ์ ์ ํ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๋๋ฐ ์ฌ์ฉํ์ต๋๋ค.
โข
๋จผ์ ๋ฐ์ดํฐ๋ฅผ ํ
์คํธ ํ์ผ์ ์ ์ฅํ ํ ์๋ฒ๊ฐ ์์๋ ๋ Redis DB๋ก ํผ์ฌ๋ฆฌ๋๋ก ํ์ฌ ์ฃผ์ ์ ํํ๋ฉด ์กฐํ ์ Redis DB๋ฅผ ์ฌ์ฉํ๋๋ก ํ์ต๋๋ค.
Redis DB๋ก ์ฝ์ด๋ค์ผ ํ
์คํธ ํ์ผ
@Slf4j
@Component
@RequiredArgsConstructor
public class BaseDataInit {
private final TopicWritingService topicWritingService;
private final TopicDialogueService topicDialogueService;
@EventListener(ApplicationReadyEvent.class)
public void initTopicDialogue() throws IOException {
log.info("init topic dialogue base data");
topicDialogueService.deleteAll();
ClassPathResource classPathResource = new ClassPathResource("data/topic_dialogue.txt");
InputStream is = classPathResource.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
List<TopicDialogue> list = new ArrayList<>();
br.lines().forEach(line -> {
if (!StringUtils.hasText(line)) {
return;
}
String[] topics = line.split(",");
String topicCategoryKorean = topics[0];
String topicKorean = topics[1];
TopicDialogue topicDialogue = TopicDialogue.builder()
.topicCategoryKorean(topicCategoryKorean)
.topicKorean(topicKorean)
.build();
list.add(topicDialogue);
});
topicDialogueService.addAll(list);
}
...
}
Java
๋ณต์ฌ
์ด๊ธฐํ ๋ถ๋ถ
๊ธฐํ ์ถ๊ฐ ๋ฐ ๋ณ๊ฒฝ์ฌํญ
โข
์ธ๋ถ API ํธ์ถ ์ ์ฌ์ฉํ HttpURLConnection์ WebClient๋ก ๋ณ๊ฒฝ
โข
TTS API๋ฅผ Naver Clova์์ Microsoft Azure Speech๋ก ๋ณ๊ฒฝ
โข
DTO๋ฅผ JSON์ผ๋ก ๋ฐํํ๋ ์ปจํธ๋กค๋ฌ์์ ๋ฐํํ์ Result ํด๋์ค๋ก ๋ํํด์ ๋ฐํ
โข
DB๋ฅผ Oracle์์ MySQL๋ก ๋ณ๊ฒฝ
โข
์ค์ ํ์ผ์ Spring profile๋ก ๋ถ๋ฆฌ(dev, test, prod)
โข
๊ธฐ๋ฅ ์ถ๊ฐ
โฆ
๊ธ์ฐ๊ธฐ, ํํ ์ฃผ์ ์ถ์ฒ
โฆ
์ฑ์ฐ ์ฑ๋ณ ๋ฐ ์ต์ ์ ํ ๊ธฐ๋ฅ
โฆ
๋ฌธ์ฅ ๊ต์ ๋ถ๋ถ ํ์ด๋ผ์ดํ
โฆ
๋คํฌ๋ชจ๋
โฆ
๋ณด๊ดํจ ๊ธฐ๋ฅ
โฆ
๊ธฐํ ChatGPT ํ๋กฌํํธ ๋ฐ ๋ก์ง ๊ฐ์
โฆ
๋ฒ์ญ ๊ธฐ๋ฅ ์ถ๊ฐ
๋๋ ์ ๋ฐ ๊ฐ์ ํ ์
โข
๋ฆฌ์กํธ ๋ฑ ํ๋ก ํธ ํ๋ ์์ํฌ์ ๋ํ ์ง์์ด ๋ถ์กฑํ์ฌ ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง(SSR)๊ณผ ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง(CSR)์ ํผ์ฌํ์ฌ ์ ์ํ์์ต๋๋ค.
โฆ
์ถํ ์์ ํ REST API ์๋ฒ๋ก ๋์ํ ์ ์๋๋ก ํ์ฌ ์๋ฒ ๋ถํ๋ฅผ ์ค์ด๊ณ ํ๋ก ํธ/๋ฐฑ ์ฌ์ด์ ์ญํ ๊ณผ ์ฑ
์์ ๋ถ๋ฆฌํ๋ฉด ์ข๊ฒ ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
โฆ
ํนํ ์์ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ํ๋ก ํธ์ชฝ ์์
์ ํ๋ฉด์ ์ปดํฌ๋ํธ ์ฌํ์ฉ, ์ํ๊ด๋ฆฌ, ๊ฐ์ ๋ ๋ฑ SPA ํ๋ ์์ํฌ์์ ์ฌ์ฉํ๋ ๊ฐ๋
๋ค์ ํ์์ฑ์ ์ง์ ๋๊ผ์ต๋๋ค.