์๋ ํ์ธ์ dev_writer์ ๋๋ค.
์์ฆ ํ์ฌ ์ผ๊ณผ ๋ค๋ฅธ ์ผ๋ค๋ก ์ธํด ๋ธ๋ก๊ทธ ์์ฑ์ ์ ์ ํ์ง ์์์๋๋ฐ์, ์ด๋ฒ์ ๋ค์ EatToFit ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ Spring AI๋ฅผ ์ฐ๊ฒฐํ ๊ณผ์ ์ด ์์ด ์ด ๋ถ๋ถ์ ๊ณต์ ๋๋ฆฌ๊ณ ์ ํฉ๋๋ค.
๊ฐ๋ฐ ๊ณ๊ธฐ: ์์ AI API ๋ง๋ฃ ๋ฌธ์ ๋ฐ์ ๐
์ฌ์ค Spring AI๋ฅผ ์ ์ฉํ๋ ๊ฒ์ ์์ ๊ฒ์์ด ์๋๋ผ ์์๊ณผ ํ์์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ด๋ ํ๋์ ์์ฑํ ๋์๋ง ์งํํ ๊ณํ์ด์์ต๋๋ค.
๊ธฐ์กด์๋ ์์ AI ํ์ฌ๋ก๋ถํฐ API๋ฅผ ๋ฐ๊ธ๋ฐ์์, ๋ถ์ ํ์ง๊ณผ ์๋, ์ ๋ขฐ์ฑ ๋ฉด์์ ๋ชจ๋ ์ฐ์ํ ์์ AI ํ์ฌ์ ์๋น์ค๋ฅผ ์ด์ฉํ ๊ณํ์ด์์ต๋๋ค.
๊ทธ๋ฌ๋ API๋ฅผ ๋ฐ์๋ ์์ ์ด 5์์ด์๋๋ฐ, ๊ทธ๋์์ธ์ง ์ฌ์๋ํ ๊ฒฐ๊ณผ ๋ง๋ฃ๋์๋ค๋ ๋ฌธ์ ๊ฐ ๋ฐ์๋์ด ๋น์ฅ์ ํ๋ก์ ํธ์ ์ฌ์ฉํ ์ ์๊ฒ ๋์์ต๋๋ค. (์์ AI ํ์ฌ์ ๋ฌธ์๋ฅผ ๋๋ฆฌ๊ธด ํ์ง๋ง ๋ต๋ณ์ด ์ค๋ ๋ฐ์๋ ์ถ๊ฐ ์๊ฐ์ด ๊ฑธ๋ฆด ๊ฒ์ผ๋ก ์๊ฐํ์ต๋๋ค.)
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ์ฐ์ ์ ์์์ ๊ฒ์ํ๋ ๊ธฐ๋ฅ์๋ Spring AI๋ฅผ ์ด์ฉํด ๋ณด์๋ ๊ฒฐ๋ก ์ ๋ด๋ฆฌ๊ฒ ๋์์ต๋๋ค.
Spring AI ์ ์ฉํ๊ธฐ (1.0.0 M2 ๋ฒ์ )
๋จผ์ , Spring AI๋ 24๋ 9์ ๊ธฐ์ค์ผ๋ก M2 ๋ฒ์ ์ด ๋์์ต๋๋ค.
๊ทธ๋์์ธ์ง build.gradle ์์ฑ ์ฝ๋๊ฐ ์กฐ๊ธ ๋ฌ๋ผ์ก๋๋ฐ, ์์นซํ๋ฉด ๊ธฐ์กด์ ์ฌ์ฉ ์ค์ด๋ lombok ๋ฐ ๋ค๋ฅธ ์คํ๋ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ถฉ๋ํ ์ ์๋ค๋ ์ฌ์ค์ ๋ฐ๊ฒฌํ์ต๋๋ค.
์คํ๋ง๋ถํธ ์คํํฐ ์ฌ์ดํธ์์ ์ฒ์๋ถํฐ Spring AI ๋ฐ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (lombok ๋ฑ)๋ฅผ ํจ๊ป ์ ์ฉํ์ค ๋ถ๋ค์ ๊ด์ฐฎ์ผ์๊ฒ ์ง๋ง, ์ ์ฒ๋ผ ์ดํ์ Spring AI๋ฅผ ์ ์ฉํ๋ ค๋ ๋ถ๋ค์ ์ํด build.gradle ์ฝ๋๋ฅผ ๊ณต์ ํด ๋๋ฆฌ๊ฒ ์ต๋๋ค.
build.gradle
repositories {
mavenCentral()
// ์ถ๊ฐ
maven { url 'https://repo.spring.io/milestone' }
}
ext {
jwtVersion = '0.12.6'
snippetsDir = file('build/generated-snippets')
// ์ถ๊ฐ
set('springAiVersion', "1.0.0-M2")
}
dependencies {
...
// ์ถ๊ฐ
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}
// ์ถ๊ฐ
dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}
...
application.yml (OpenAI ๊ธฐ์ค)
application.yml์ด๋ application-local.yml ์ด๋ ๊ด๊ณ์์ด spring.ai.openai.api-key์ OpenAI ํค๋ฅผ ์์ฑํด ๋์๋ฉด ๋ฉ๋๋ค.
AiConfig.java
๋ค์์ผ๋ก๋ Spring AI์ ChatClient๋ฅผ ์ ์ํ AiConfig ์ค์ ํ์ผ์ global/config์ ์์ฑํ์ต๋๋ค.
@Configuration
public class AiConfig {
@Value("${spring.ai.openai.api-key}")
private String apiKey;
@Bean
public ChatClient chatClient() {
ChatModel chatModel = new OpenAiChatModel(new OpenAiApi(apiKey));
return ChatClient
.builder(chatModel)
.build();
}
}
ChatClient๋ ์ค์ ์ ์ผ๋ก Spring AI๋ก๋ถํฐ ํธ์ถํ ์ ์๋ ์ธํฐํ์ด์ค์ด๋ฉฐ, ChatModel์ ์ฌ์ฉํ AI ๊ตฌํ์ฒด๋ฅผ ์ง์ ํ ์ ์์ต๋๋ค.
์ด์ ์ ์์ฑํ ๊ธ์ ์ฐธ๊ณ ํ์๋ฉด ChatClient์ ChatModel์ ๋ํด ์ตํ์ค ์ ์์ต๋๋ค.
JSON ํฌ๋งทํ ์ ์ด๋ป๊ฒ ํ ๊น?
LLM์๊ฒ ํน์ JSON ํฌ๋งท์ผ๋ก ๋ฐํํด ๋ฌ๋ผ๋ ๊ฒ์ ๊ฐ๋ฐ์๋ค์๊ฒ ๋น๋ฒํ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ๊ธฐ์กด์๋ ํ๋กฌํํธ ๋ง๋ฏธ์ ์ด๋ค JSON ํํ๋ก ๋ฐํํด ๋ฌ๋ผ๊ณ ์ง์ ์ ์๋ฅผ ํด์ค์ผ ํ์ต๋๋ค.
๋ง์ฝ ์ผ์ข ์ JSON ๋ณํ๊ธฐ๊ฐ ์๋ค๋ฉด, ์ด ๋ถ๋ด์ ์ฝ๊ฒ ๋์ด๋ผ ์ ์์ง ์์๊น์?
์ด๋ฅผ ์ํด Spring AI์์๋ BeanOutputConverter๋ฅผ ์ ์ํ๊ณ ์์ต๋๋ค.
1. BeanOutputConverter ์์ฑ
๋ง์ง๋ง์ ์๋ BeanOutputConverter(Class<T> clazz)๋ฅผ ํตํด, ํน์ ํด๋์ค์ ๋ง๋ ํํ๋ก ๋ณํํ ์ ์์ต๋๋ค. (ํด๋น ์ฝ๋๋ฅผ ๋ ๋ณด์๋ฉด, ์ปค์คํ objectMapper๋ ๋ฐ์ ์ ์์ต๋๋ค.)
2. getFormat ํจ์ ํธ์ถ
getFormat ํจ์์์๋ LLM์๊ฒ ์ ๋ฌํ ๋ช ๋ น์ด๋ฅผ ํฌํจํ์ฌ ์ผ์ข ์ ํ๋กฌํํธ๋ก ๋ง๋ค์ด๋ ๋๋ค.
์ฆ, ์๊ตฌํ๋ json ์คํค๋ง์ ๋ง์ถ์ด ์๋ตํ๋ผ๊ณ ๋ช ๋ นํ ์ ์๊ฒ ๋ฉ๋๋ค. (์ ํฌ๊ฐ ์ง์ JSON์ผ๋ก ์๋ตํด์ผ ํ๋ค๊ณ ์์ฑํ์ง ์์๋ ๋ฉ๋๋ค.)
3. convert ํจ์ ํธ์ถ
์ดํ, convert ํจ์์์ ์ฃผ์ด์ง text๋ฅผ objectMapper๋ฅผ ์ด์ฉํ์ฌ ํด๋์ค๋ก ๋ง๋ค์ด๋ ๋๋ค. ๋ฐํ ๋์ค ์์ธ (JsonProcessingException)๊ฐ ๋ฐ์ํ ์ ์ผ๋ฐ์ ์ธ RuntimeException์ด ๋ฐ์ํฉ๋๋ค.
์ ๋ฆฌ
๋ฐ๋ผ์ BeanOutputConverter๋ฅผ ์ด์ฉํ๋ ์์๋ ์๋์ ๊ฐ์ต๋๋ค.
- BeanOutputConverter๋ฅผ ์๊ตฌํ๋ ํด๋์ค์ ๋ง๊ฒ ์ ์ธ
- getFormat ํจ์๋ฅผ ํธ์ถํ์ฌ ํ๋กฌํํธ ๊บผ๋ด๊ธฐ, LLM์ ์ ๋ฌ
- ๋ฐํ๋ LLM ์๋ต์ convert ํจ์๋ฅผ ํธ์ถํ์ฌ ์๊ตฌํ๋ ํด๋์ค๋ก ๋ณํ
์ด๋ฏธ์ง ๋ถ์ํ๊ธฐ
์๋ฆฌ
๋ค์์ผ๋ก๋ ์ด๋ฏธ์ง ๋ถ์์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
์ฒ์์๋ ์ด๋ฏธ์ง์ ๊ด๋ จ๋๊ธฐ ๋๋ฌธ์ chat ๊ด๋ จ ๋ถ๋ถ์ด ์๋๋ผ image ๊ด๋ จ ๋ถ๋ถ์ ์์๋ด์ผ ํ๋ ๊ฒ์ผ๋ก ์๊ฐํ์๋๋ฐ, Spring AI์ image๋ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค์ด๋ด๋ ๋ฐ ์ง์ค๋์ด ์์ผ๋ฉฐ, ์ด๋ฏธ์ง๋ฅผ ๋ถ์ํ๋ ๊ฒ์ chat๊ณผ ๊ด๋ จ๋์ด ์๋ค๋ ์ฌ์ค์ ๋ฐ๊ฒฌํ์์ต๋๋ค.
์ ํฌ๋ ์ด๋ฏธ์ง์ ๋ํ ์ ๋ณด๋ฅผ URL๋ก์จ ์ ๊ณตํ ๊ฒ์ผ๋ก ์ค์ ํฉ๋๋ค. ChatClient์๋ ๋ ๊ฐ์ง์ prompt ๋ฐฉ์์ด ์๋๋ฐ, Promt ์ธ์๋ฅผ ๋ฐ๋ ๋ฐฉ์๊ณผ ๋ฐ์ง ์๋ ๋ฐฉ์์ด ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Prompt ์ธ์๋ฅผ ๋ฐ์ง ์๋ ๋ฐฉ์์ ChatClientRequestSpec์ ๋ฐํํ๋ฉฐ, Prompt ์ธ์๋ฅผ ๋ฐ๋ ๋ฐฉ์์ ChatClientPromptRequestSpec์ ๋ฐ์ต๋๋ค.
์์ ์ Spring AI ๊ธ์ ์์ฑํ์ ๋ ๋ณด์ ๋ถ๋ค์ ์๊ฒ ์ง๋ง, Prompt๋ ์ผ๋ฐ ์ฑํ ๋ฉ์์ง๋ฅผ ์๋ฏธํฉ๋๋ค.
๊ทธ๋ ๊ธฐ์ ๋ง์ฝ ํ์ผ์ ์ฌ๋ฆฌ๋ ๋ฑ ๋ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ๋๊ธฐ๊ณ ์ ํ๋ค๋ฉด, Prompt๋ฅผ ๋ฐ์ง ์๋ prompt ํจ์๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
์ด ๋ง์ ๊ณง PromptTemplate์ ์ด์ ์ ๋๋ฆด ์ ์๋ค๋ ๋ป์ด๊ธฐ๋ ํฉ๋๋ค. ๋ฌผ๋ก ํ๋กฌํํธ (ํ ์คํธ)๋ฅผ ๋ง๋๋ ๊ณผ์ ์์ ๋ถ๋ถ์ ์ผ๋ก PromptTemplate์ ์ธ ์๋ ์๊ฒ ์ง๋ง, prompt ํจ์๋ฅผ ์ด์ฉํ ๋์๋ ์ฒซ ๋ฒ์งธ์ prompt ํจ์๋ฅผ ์จ์ผ ํฉ๋๋ค. (ํ์ผ ๋ฑ ๋ถ๊ฐ ์ ๋ณด๊ฐ ํ์ํ๋ค๋ฉด)
Prompt๋ฅผ ๋ฐ๋ ํจ์์ ๋ฐํ์ธ ChatClientPromptRequestSpec์ ๋ณ๋์ ์ ๋ณด๋ฅผ ์ถ๊ฐํ ์ ์์ผ๋ฉฐ, ๋ฐ๋ก call์ ํ์ฌ ์๋ต์ ๋ฐ๋ ๊ฒ๋ง ๋์ต๋๋ค.
๋ฐ๋ฉด Prompt๋ฅผ ๋ฐ์ง ์๋ ํจ์์ ๋ฐํ์ธ ChatClientRequestSpec์์๋ ์ฐ์์ ์ผ๋ก ์ถ๊ฐ ์ ๋ณด๋ฅผ ์์ฑํ ์ ์๋ ๊ฒ ๋ณด์ด๋ฉฐ, ์ด ์ค Consumer<PromptUserSpec> consumer๋ฅผ ๋ฐ๋ ํจ์๋ฅผ ์ด์ฉํ๋ฉด ํ์ผ ์ ๋ณด๋ฅผ ๋ถ์ผ ์ ์์ต๋๋ค.
์ด๋ PromptUserSpec์ media๋ฅผ ๋ฐ์ ์ ์๋ ํจ์๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ค์ต (JSON ์ปจ๋ฒํฐ ํฌํจ)
๊ทธ๋ฌ๋ฉด ์ด์ ์ต์ข ์ค์ต์ ํด ๋ณด๊ฒ ์ต๋๋ค.
1. ์๋ต DTO ์ ์
๋จผ์ , ํ๋ก์ ํธ์ ํ์ํ ์๋ต DTO๋ฅผ ์ ์ํฉ๋๋ค.
์ ๋ ์์ ๋ถ์์ ๋ง๊ฒ FoodSearchResponse๋ฅผ ์ ์ํ๊ณ , ์ด๋ฅผ ๋ฆฌ์คํธ๋ก ๊ฐ์ง๊ณ ์๋ PredictFoodSearchResponse๋ฅผ ์ถ๊ฐ๋ก ์ ์ํ์์ต๋๋ค.
// eattofit > food > infrastructure > dto
public record PredictFoodSearchResponse(
List<FoodSearchResponse> predictFoods
) {
}
public record FoodSearchResponse(
String name,
BigDecimal servingSize,
String unit,
BigDecimal kcal,
BigDecimal carbohydrate,
BigDecimal protein,
BigDecimal fat,
BigDecimal sodium
) {
}
2. FoodSearchManager ์ ์
๋ค์์ผ๋ก๋ ์๋ต์ ๋ฐ์ ์ ์๋๋ก ๊ธฐ๋ฅํ๋ ๊ตฌํ์ฒด๋ค์ ์ถ์ํํ๊ธฐ ์ํด FoodSearchManager๋ฅผ ์ ์ํ์์ต๋๋ค. (ํฅํ ์์ AI ํ์ฌ ๋ฑ ๋ค๋ฅธ ๊ตฌํ์ฒด๊ฐ ๋ค์ด์ฌ ์ ์์ ๊ฒ์ด๋ผ ํ๋จ)
// eattofit > food > domain
public interface FoodSearchManager {
PredictFoodSearchResponse search(String url);
}
3. FoodService ์ ์
FoodSearchManager๋ฅผ ์๋น์ค์ ์ฐ๊ฒฐํฉ๋๋ค.
// eattofit > food > application
@RequiredArgsConstructor
@Service
public class FoodService {
private final FoodSearchManager foodSearchManager;
public PredictFoodSearchResponse foodSearch(final String url) {
return foodSearchManager.search(url);
}
}
4. FoodController ์ ์
์๋น์ค๋ฅผ ์ปจํธ๋กค๋ฌ์ ์ฐ๊ฒฐํฉ๋๋ค.
// eattofit > food > ui
@RequiredArgsConstructor
@RequestMapping("/api/foods")
@RestController
public class FoodController {
private static final String IMAGE_URL = "image_url";
private final FoodService foodService;
@GetMapping("/search")
public ResponseEntity<PredictFoodSearchResponse> search(final @RequestParam(value = IMAGE_URL) String imageUrl) {
PredictFoodSearchResponse response = foodService.foodSearch(imageUrl);
return ResponseEntity.ok()
.body(response);
}
}
5. OpenAiFoodSearchManager ์ ์ (ํต์ฌ)
FoodSearchManager๋ฅผ ๊ตฌํํ OpenAiFoodSearchManager๋ฅผ ์์ฑํฉ๋๋ค.
AiConfig.java๋ฅผ ํตํด OpenAI ์ ์ฉ์ ChatClient๋ฅผ ๋ฐ์์ผ๋, ์ฃผ์ ํ ์ ์๊ฒ ๋ฉ๋๋ค.
// eattofit > food > infrastructure
@RequiredArgsConstructor
@Component
public class OpenAiFoodSearchManager implements FoodSearchManager {
private final ChatClient chatClient;
@Override
public PredictFoodSearchResponse search(final String url) {
BeanOutputConverter<PredictFoodSearchResponse> parser = new BeanOutputConverter<>(PredictFoodSearchResponse.class);
String response = chatClient.prompt()
.user(userSpec -> {
try {
userSpec.text(
"""
<role>
๋๋ 20๋
์ด์ ์์ ๊ฒฝ๋ ฅ์ ๊ฐ์ง ์๋ฆฌ์ฌ ๊ฒธ ์์์ฌ๋ค. ์์ ์ฌ์ง์ ์ฃผ๋ฉด, ์ ๊ณต๋ format์ ๋ง์ถฐ ์๋ตํด์ผ ํ๋ค.
</role>
<instruction>
1. ์์ธก ๊ฐ๋ฅํ ์์์ ์ต์ 3๊ฐ ์ด์ ๋์์ผ ํ๋ค.
2. unit์ 'g'๋ก ๊ณ ์ ํ๋ค.
3. servingSize๋ gram ๊ธฐ์ค์ผ๋ก ๊ณ์ฐํ๋ค.
4. servingSize, kcal, carbohydrate, protein, fat, sodium์ ๋ชจ๋ ์์์ ๋์งธ ์๋ฆฌ๊น์ง ํํ๋์ด์ผ ํ๋ค.
5. ์์์ ๋์งธ ์๋ฆฌ๋ค๋ก ํํ์ ํ ๋, ์ด๊ฒ๋ค์ด ๋ชจ๋ 0์ผ๋ก ๋ ๊ฒฝ์ฐ๊ฐ ์๋ ๊ฒฝ์ฐ๋ ์์ ์ ์๋ค. (0์ผ๋ก ๋ ๊ฒฝ์ฐ๊ฐ ์์ ์๋ ์๋ค.)
6. name์ ํ๊ธ ๊ธฐ์ค์ผ๋ก ๋์์ผ ํ๋ค.
</instruction>
<example>
์์ ์๋ต์ ์๋ ค์ฃผ๊ฒ ๋ค. ์์ ์๋ต๊ณผ ๊ฐ์ ์์์ด ๋์ค๋๋ผ๋ ์ด ์์ ๊ฐ์ ๋๊ฐ์ด ํ์ฉํ์ง๋ ๋ง๋ผ.
{
"predictFoods": [
{
"name": "๋น๋น๋ฐฅ",
"servingSize": 488.02,
"unit": "g",
"kcal": 635.31,
"carbohydrate": 97.13,
"protein": 24.21,
"fat": 16.23,
"sodium": 1248.24
},
{
"name": "์ฐ์ฑ๋น๋น๋ฐฅ",
"servingSize": 433.24,
"unit": "g",
"kcal": 596.83,
"carbohydrate": 93.21,
"protein": 17.82,
"fat": 16.23,
"sodium": 1135.35
},
{
"name": "๋์ฅ๋น๋น๋ฐฅ",
"servingSize": 430.24,
"unit": "g",
"kcal": 656.51,
"carbohydrate": 93.23,
"protein": 17.39,
"fat": 18.66,
"sodium": 1135.35
}
]
}
</example>
format์ ์๋์ ๊ฐ๋ค.
""" + parser.getFormat())
.media(MimeTypeUtils.IMAGE_JPEG, new UrlResource(url));
} catch (MalformedURLException e) {
throw new BadImageUrlException();
}
})
.call()
.content();
return parser.convert(response);
}
}
- ChatClient๋ฅผ ์ฃผ์ ๋ฐ์์ต๋๋ค.
- PredictFoodSearchResponse์ ๋ง์ถ BeanOutputConverter๋ฅผ ์ ์ํ์์ต๋๋ค.
- chatClient.prompt()๋ฅผ ํธ์ถํ๊ณ , user ๋ฉ์๋๋ฅผ ํธ์ถํจ๊ณผ ๋์์ userSpec์ ์ด์ฉํ์ฌ ๊ธฐ๋ณธ ํ๋กฌํํธ๋ฅผ ์์ฑํ์ต๋๋ค.
- ํ๋กฌํํธ ๋ง์ง๋ง์๋ BeanOutputConverter์ json ์คํค๋ง๋ฅผ ๋ฃ์์ต๋๋ค.
- media ๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ํ์ผ ์ ๋ณด๋ฅผ ๋ฃ์์ต๋๋ค. ํ์ผ์ URL์ ๋ฃ๊ณ , ์ ๋ฌ์ IMAGE_JPEG๋ก ๊ณ ์ ํ์์ต๋๋ค.
- ๋น๊ทผ์์์ LLM ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ฐธ๊ณ ํ์ฌ ํ๋กฌํํธ๋ฅผ ์์ฑํด ๋ดค์ต๋๋ค. ์ญํ ์ ๋ถ์ฌํ๊ณ , ๋ช ๋ น์ ๋ถ์ฌํ ๋ค ์์๋ฅผ ์ ๊ณตํ์์ต๋๋ค.
- call() ๋ฉ์๋๋ฅผ ํตํด ์๋ต์ ๋ฐ๊ณ , content() ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์๋ต ๋ณธ๋ฌธ์ ๋ฐ์ต๋๋ค. (String)
- BeanOutputConverter์ convert ๋ฉ์๋๋ฅผ ํตํด PredictFoodSearchResponse์ ๋ฐํํ๋๋ก ํ์์ต๋๋ค.
๊ฒฐ๊ณผ
์ต์ข ์ ์ผ๋ก ๋ฒ๊ฑฐ ์ฌ์ง์ ์ฃผ๊ณ ํ ์คํธํด ๋ณด๋ฉด, ์๋์ ๊ฐ์ด dto์ ๋ง์ถ์ด ๋ฐํ๋จ์ ๋ณด์ค ์ ์์ต๋๋ค.
์ด๊ฒ์ ํตํด ์ด๋ฏธ์ง ๋ถ์, DTO ํ์ฑ, ํ์ผ ์ ์ก ๋ฑ์ ๋ฐฐ์ธ ์ ์์์ต๋๋ค. ์ฝ์ด์ฃผ์ ๋ถ๋ค ๊ฐ์ฌ๋๋ฆฝ๋๋ค!
์ถ๊ฐ๋ก ํ์ฌ ํ๋กฌํํธ๊ฐ ์ง์ text๋ก ๊ด๋ฆฌํ๋ ๋ถ๋ด์ด ์์ต๋๋ค. ์ด๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด ์คํ๋ง์ AOP๋ฅผ ์ด์ฉํ์ฌ, ํ์ผ (ํ๋กฌํํธ)์ ์ฃผ์๋ง ๋ฃ์ผ๋ฉด ํด๋น ํ์ผ์ ๋ด์ฉ์ ์กฐํํด ์ฃผ๋ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๊ณ ์ ํฉ๋๋ค. ์ด ๋ถ๋ถ์ AOP์ ๊ด๋ จ ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ๊ธ์์ ๋ค๋ฃจ๊ฒ ์ต๋๋ค.
Reference