DevLog ๐ถ
[Spring] ๋ฉ์์ง์ ๊ตญ์ ํ๋ฅผ ํตํด ์ธ์ด ์ค์ ์ปค์คํ ํ๊ธฐ ๋ณธ๋ฌธ
[Spring] ๋ฉ์์ง์ ๊ตญ์ ํ๋ฅผ ํตํด ์ธ์ด ์ค์ ์ปค์คํ ํ๊ธฐ
dolmeng2 2022. 8. 21. 16:53๊น์ํ ๋์ '์คํ๋ง MVC 2ํธ - ๋ฐฑ์๋ ์น ๊ฐ๋ฐ ํ์ฉ ๊ธฐ์ '์ ๋ณด๊ณ ์ ๋ฆฌํ ๊ธ์ ๋๋ค ๐
- ์ง๋ ํฌ์คํ ๊ณผ ์ด์ด์ง๋๋ค :D
| ๋ฉ์์ง, ๊ตญ์ ํ ์ฒ๋ฆฌํ๊ธฐ
- ์ง๋ ํฌ์คํ ์์ ๋ง๋ค์๋ ์ํ ๊ด๋ฆฌ ํผ์ ๋ณด๋ฉด, ์ํ ID์ ๊ฒฝ์ฐ ID, ์ด๋ฆ์ Name, ๊ฐ๊ฒฉ์ Price, ์๋์ Quantity๋ก ํํํ์๋ค.
- ๋ง์ฝ ๊ธฐํ์๊ฐ itemName, itemPrice, itemQuantity๋ก ์ผ๊ด ๋ณ๊ฒฝํ๊ธฐ๋ฅผ ์ํ๋ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น?
- ์ง์ ๊ฐ ํ์ผ์์ ์์ ํ์ง ๋ง๊ณ , ๋ฉ์์ง๋ฅผ ํ๋์ ๊ณต๊ฐ์์ ๊ด๋ฆฌํด๋ณด์.
- ๋ํ, ์์ด-ํ๊ธ๋ ์์ ๋กญ๊ฒ ์ง์ํ๋๋ก ๋ง๋ค์ด๋ณด์!
- ๊ธฐ๋ณธ์ ์ผ๋ก, ๋ฉ์์ง ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ MessageSource๋ฅผ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํด์ผ ํ๋ค.
- ์ด๋, MessageSource๋ ์ธํฐํ์ด์ค์ด๊ธฐ ๋๋ฌธ์ ๊ตฌํ์ฒด์ธ ResourceBundleMessageSource๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ์.
- ๊ตฌํ์ฒด๋ก๋ ๊ทธ์ธ์๋ ReloadableResourceBundleMessageSource, StaticMessageSource๊ฐ ์๋ค.
- ์คํ๋ง ๋ถํธ์์๋ ์๋์ผ๋ก ResourceBundleMessageSource๋ฅผ ๋ฑ๋กํด์ค๋ค!
- application.properties์ ๋ฉ์์ง ์์ค๋ฅผ ์ค์ ํด์ฃผ๋ฉด ๋๋ค.
- ๋ํดํธ๋ก๋ 'messages'๋ผ๋ ์ด๋ฆ์ผ๋ก ๊ธฐ๋ณธ ๋ฑ๋ก๋๋ค.
โ ๋ฉ์์ง ์ค์ ํ์ผ
- /src/main/resources ๋๋ ํ ๋ฆฌ์ [file_name]_[lang_code]_[country_code].properties ํ์์ผ๋ก ๋ฑ๋กํ๋ค.
- ๋ํดํธ๊ฐ 'messages'์ด๊ธฐ ๋๋ฌธ์, messages_en.properties, messages_en_UK.properties ๊ฐ์ ํ์์ผ๋ก ๋ฑ๋กํ๋ค.
- ๊ธฐ๋ณธ์ผ๋ก๋ messages.properties์ด๋ค.
[messages.properties]
hello=์๋
hello.name=์๋
{0}
[messages_en.properties]
hello=hello
hello.name=hello {0}
- ๊ธฐ๋ณธ์ ์ผ๋ก key-value ํํ๋ก ์์ฑํ๋ค.
- {0}์ ๊ฒฝ์ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ด๊ธฐ ์ํด์ ์ฌ์ฉํ๋ค.
- ํ ์คํธ๋ฅผ ์งํํด๋ณด์.
- ์ฐธ๊ณ ๋ก, intellij ๊ธฐ์ค ํ๊ธ ์ฌ์ฉ ์ file encoding์์ UTF-8๋ก ๋ณ๊ฒฝํด์ค์ผ ํ๋ค.
- ์ด๊ฑฐ ์ค์ ํ ๋ค์ ํ๋กํผํฐ ํ์ผ์์ ํ๊ธ ๊นจ์ก๋์ง ๊ผญ ํ์ธํ๊ธฐ!
[MessageSourceTest.java]
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
@Test
public void hello() throws Exception {
String result = ms.getMessage("hello", null, null);
String result2 = ms.getMessage("hello.name", new String[]{"spring"}, null);
assertThat(result).isEqualTo("์๋
");
assertThat(result2).isEqualTo("์๋
spring");
}
}
- ๋งค๊ฐ๋ณ์๋ก๋ code, args, locale ์ ๋ณด๊ฐ ๋ค์ด๊ฐ๋ค.
- locale ์ ๋ณด๋ฅผ ์ค์ ํ์ง ์์ผ๋ฉด ๋ํดํธ์ธ 'messages' -> messages.properties์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ค.
@Test
public void noMessage() throws Exception {
assertThatThrownBy(() -> ms.getMessage("hihi", null, null))
.isInstanceOf(NoSuchMessageException.class);
String result = ms.getMessage("hihi", null, "hello!", null);
assertThat(result).isEqualTo("hello!");
}
- ๋ฉ์์ง๊ฐ ์๋ ๊ฒฝ์ฐ์๋ NoSuchMessageException์ด ๋ฐ์ํ์ง๋ง, defaultMessage๋ฅผ ์ค์ ํด์ฃผ๋ฉด ํด๋น ๋ฉ์์ง๋ก ์นํ๋๋ค.
- defaultMessage๋ ์ธ ๋ฒ์งธ ์ธ์๋ก ๋ฃ์ ์ ์๋ค.
@Test
public void international() throws Exception {
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("์๋
");
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
๊ตญ์ ํ ์ ์ฉ์ด๋ค. Locale.KOREA๋ฅผ ํ๋ฉด messages_ko.properties๋ฅผ ์ฐพ์ง๋ง, ์๊ธฐ ๋๋ฌธ์ default์ธ messages_properties์์ ์ฐพ๋๋ค. Locale.ENGLISH๋ฅผ ํ๋ฉด messages_en.properties๋ฅผ ์ฐพ๊ธฐ ๋๋ฌธ์ ์ฐ๋ฆฌ๊ฐ hello="hello", key๊ฐ์ด hello์ธ value๊ฐ = hello๊ฐ ๋์จ๋ค.
| ์ค์ ๋ก ์ ์ฉํ๊ธฐ
- ์ง๋ ํฌ์คํ ์์ ๋ง๋ค์๋ ์ํ ๊ด๋ฆฌ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฉ์์ง, ๊ตญ์ ํ๋ฅผ ์ ์ฉํด๋ณด์.
[message.properties]
label.item=Item
label.item.id=ItemID
label.item.itemName=Name
label.item.price=Price
label.item.quantity=Quantity
page.items=Item List
page.item=Item Detail
page.addItem=Item Save Form
page.updateItem=Item Edit Form
start.message=๐๐ปHello {0}!
โ ํ์๋ฆฌํ - ๋ฉ์์ง ์ ์ฉํ๊ธฐ
- ํ์๋ฆฌํ์ #{...}์ ์ฌ์ฉํ๋ฉด ๋ฉ์์ง ์กฐํ๊ฐ ๊ฐ๋ฅํ๋ค.
- th:text="#{...}" ์ผ๋ก ๋ฉ์์ง๋ฅผ ์ ์ฉํด์ฃผ์๋ค.
[addForm.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
...
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 th:text="#{page.addItem}" class="font-monospace fw-bold">Item Save Form</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemName"
th:text="#{label.item.itemName}">Name</label>
<input type="text"
th:field="*{itemName}"
class="form-control text-center"
placeholder="Please Enter an Item Name.">
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price"
th:text="#{label.item.price}">Price</label>
<input type="text"
th:field="*{price}"
class="form-control text-center"
placeholder="Please Enter an Item Price.">
</div>
<div class="col text-center">
<label for="quantity"
th:text="#{label.item.quantity}">Quantity</label>
<input type="text"
th:field="*{quantity}"
class="form-control text-center"
placeholder="Please Enter an Item Quantity.">
</div>
</div>
...
</body>
</html>
[editForm.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
...
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold"
th:text="#{page.updateItem}">Item Edit Form</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="id"
th:text="#{label.item.id}">Id</label>
<input type="text"
th:field="*{id}"
class="form-control text-center"
readonly>
</div>
<div class="col text-center">
<label for="itemName"
th:text="#{label.item.itemName}">Name</label>
<input type="text"
th:field="*{itemName}"
class="form-control text-center">
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price"
th:text="#{label.item.price}">Price</label>
<input type="text"
th:field="*{price}"
class="form-control text-center">
</div>
<div class="col text-center">
<label for="quantity"
th:text="#{label.item.quantity}">Quantity</label>
<input type="text"
th:field="*{quantity}"
class="form-control text-center">
</div>
</div>
...
</div>
</body>
</html>
[item.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
...
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold"
th:text="#{page.item}">Item Detail</h2>
</div>
<h2 class="font-monospace fw-bold text-center text-danger"
th:if="${param.status}" th:text="'Save Success!'"></h2>
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemId"
th:text="#{label.item.id}">ID</label>
<input type="text" id="itemId" name="itemId"
class="form-control text-center"
value="1" th:value="${item.id}" readonly>
</div>
<div class="col text-center">
<label for="itemName"
th:text="#{label.item.itemName}">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
value="์ํA" th:value="${item.itemName}" readonly>
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price"
th:text="#{label.item.price}">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
value="10000" th:value="${item.price}" readonly>
</div>
<div class="col text-center">
<label for="quantity"
th:text="#{label.item.quantity}">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
value="10" th:value="${item.quantity}" readonly>
</div>
</div>
...
</body>
</html>
[items.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
...
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h1 class="font-monospace fw-bold mb-3"
th:text="#{start.message(${userName})}">๐๐ป Hello</h1>
<hr class="my-4">
<h2 class="font-monospace fw-bold"
th:text="#{page.items}">Item List</h2>
...
</div>
<div class="mt-1 font-monospace">
<table class="table table-hover table-bordered">
<thead>
<tr class="text-center">
<th th:text="#{label.item.id}">ID</th>
<th th:text="#{label.item.itemName}">์ํ๋ช
</th>
<th th:text="#{label.item.price}">๊ฐ๊ฒฉ</th>
<th th:text="#{label.item.quantity}">์๋</th>
</tr>
</thead>
...
</table>
</div>
</div>
</body>
</html>
- ์๋จ์ ์ฌ์ฉ์์๊ฒ ์ธ์ฌํ๋ ๋ฌธ๊ตฌ๊ฐ ์์ผ๋ฉด ์ข์ ๊ฒ ๊ฐ์์ ์ถ๊ฐํ์๋ค.
- ํ๋ผ๋ฏธํฐ๋ ๋ค์๊ณผ ๊ฐ์ด #{message()}๋ก ์ ๋ ฅ์ด ๊ฐ๋ฅํ๋ค!
[SItemController.java] - ์์
@GetMapping
public String items(Model model) {
List<SItem> items = repository.findAll();
model.addAttribute("items", items);
model.addAttribute("userName", "Spring");
return "basic/items";
}
- ๊ฐ๋จํ๊ฒ ์ฌ์ฉ์์ ์ด๋ฆ์ ๋๊ฒจ์ฃผ์๋ค.
- ํ์ธํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ ๋ณ๊ฒฝ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
- ์ด๋ฒ์๋ ๊ตญ์ ํ๋ฅผ ์ ์ฉํด๋ณด์! ์ผ๋ณธ์ด๋ฅผ ์ ์ฉํด๋ณผ ์์ ์ด๋ค. (ํ๊ธ์ ๋ปํ๋๊น...ใ ใ )
[messages_ja.properties]
label.item=้
็ฎ
label.item.id=้
็ฎID
label.item.itemName=ๅๅ
label.item.price=ไพกๆ ผ
label.item.quantity=ๆฐ้
page.items=ใขใคใใ ใชในใ
page.item=ใขใคใใ ่ฉณ็ดฐ
page.addItem=ใขใคใใ ไฟๅญใใฉใผใ
page.updateItem=ใขใคใใ ็ทจ้ใใฉใผใ
start.message=ใใใซใกใฏ {0}<(๏ผฟ๏ผฟ)>!
- ํํ๊ณ ์ ๋์์ ๋ฐ์ ์์ฑํ์๋ค.
- ํฌ๋กฌ์์ ์ธ์ด ์ค์ ์ ์ผ๋ณธ์ด๋ก ๋ฐ๊พผ ๋ค์์ ํ ๋ฒ ๋๋ ค๋ณด์.
- ์ด๋ฌ๋ฉด ์์ฒญ ์ Accept-Language ๊ฐ์ด ๋ณ๊ฒฝ๋๊ณ , ์คํ๋ง์ Locale ์ ๋ณด๋ฅผ ์ด ํค๋์์ ๊ฐ์ ธ์ ์ฐธ๊ณ ํ๋ค.
- ์คํ๋ง ๋ถํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก LocaleResolver ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด์ธ AccpetHeaderLocaleResolver๋ฅผ ์ฌ์ฉํ๋ค.
- ์ด๋๊ฐ ์ด์ง๊ฐ์ด ๋ค์ง๋ง ์๋ฌดํผ ์ ์ ์ฉ๋์๋ค.
- ์ฌ์ฉ์๊ฐ ์ง์ ์ธ์ด๋ฅผ ์ ํํ ์ ์๋๋ก ์กฐ๊ธ ๋ ์ปค์คํ ํด๋ณด์๋ค.
[messages.properties]
select.lang.en=ENGLISH
select.lang.ja=JAPANESE
lang=LANGUAGE
[messages_ja.properties]
select.lang.en=่ฑ่ช
select.lang.ja=ๆฅๆฌ่ช
lang=่จ่ช
[Items.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
...
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<div class="float-end fw-bold" th:text="#{lang}">์ธ์ด ์ ํ</div>
<select id="lang" name="lang" class="form-select mb-4"
onchange="if(this.value) location.href=(this.value);">
<option value="">== SELECT LANGUAGE TYPE ==</option>
<option value="?lang=en" th:text="#{select.lang.en}">์์ด</option>
<option value="?lang=ja" th:text="#{select.lang.ja}">์ผ๋ณธ์ด</option>
</select>
<hr class="my-4">
<h1 class="font-monospace fw-bold mb-3"
th:text="#{start.message(${userName})}">๐๐ป Hello</h1>
<hr class="my-4">
...
</div>
...
</div>
</body>
</html>
- select box์์ ์ฌ์ฉ์๊ฐ ์ ํํ ์ต์ ์ onchange๋ก ๊ฐ์งํ์ฌ ๊ฐ์ง๋ ๊ฐ์ value ๊ฐ์ผ๋ก ์ด๋ํ๋๋ก ํ์๋ค.
- ์ด๋ฌ๋ฉด ์์ด ์ ํ ์ /basic/items?lang=en, ์ผ๋ณธ์ด ์ ํ ์ /basic/items?lang=ja๋ก ๋์ด๊ฐ๋ค.
[WebMvcConfig.java]
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.getDefault());
resolver.setCookieName("lang");
return resolver;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang"); // lang={country_code}
registry.addInterceptor(localeChangeInterceptor);
}
}
- ๋ค์๊ณผ ๊ฐ์ด ์ค์ ์ ๋ณด๋ฅผ ์ถ๊ฐํ๋ค.
- ๋จผ์ , CookieLocaleResolver๋ฅผ ์ฌ์ฉํ์ฌ Cookie๋ฅผ ์ด์ฉํด Locale ์ ๋ณด๋ฅผ ์ ์ฅํด์ฃผ์๋ค.
- ๊ธฐ๋ณธ์ accept-header์์ ์ฝ์ด์ค์ง๋ง, ์ด๋ฅผ ์ฌ์ฉํ๋ฉด cookie์์ ๊ฐ์ ์ฝ์ด์จ๋ค.
- ์ฟ ํค ์ด๋ฆ์ lang์ผ๋ก ์ค์ ์ผ๋ก ์ค์ ํ์์ผ๋ฉฐ, ์ปค์คํ ์ธํฐ์ ํฐ๋ฅผ ์ถ๊ฐํ์๋ค.
- LocaleChangeInterceptor๋ฅผ ํ์ฉํ์ฌ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์ lang ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ๋์ํ๋๋ก ์ง์ ํ์๋ค.
- ์ฆ, ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด ๋ค์ด์จ ์ธ์ด๊ฐ์ ๋ฐ๋ผ์ ์ธ์ด ์ค์ ์ ์งํํ ์ ์๋ค.
- ๋ด๊ฐ ์๊ฐํ ๊ฒ ๋ง๋ค๋ฉด ์ด๋ฐ ๋๋...
- ์ฌ์ฉ์๊ฐ ๋ฒํผ ํด๋ฆญ -> ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ถ๊ฐ -> ์ธํฐ์ ํฐ ๋์ -> ์ฟผ๋ฆฌํ๋ผ๋ฏธํฐ lang์ ์๋ ์ธ์ด ์ ๋ณด๋ฅผ ํ์ธํ๊ณ ์ธ์ด ์ค์
-> ํด๋น ์ธ์ด ์ ๋ณด๋ ์ฟ ํค์ ์ ์ฅ, ๊ทธ๋ฆฌ๊ณ ์ฟ ํค์ ์๋ ๊ฐ ๊ณ์ ์ฌ์ฉ... ์ด๋ฐ ๋ก์ง์ธ ๊ฒ ๊ฐ๋ค!
- ๋ค์ ํฌ์คํ ์์๋ ๊ฒ์ฆ ์ฒ๋ฆฌ๋ฅผ ์งํํด๋ณด์.