=> /resources/아래에messages_en.properties, messages_ko.properties, messages.properties라는 파일을 만들어두면 자동으로 인식하여 메시지 기능에 사용함
=> messages.properties을 기본 값으로 사용
messages.properties
hello=안녕
hello.name=안녕 {0}
messages_en.properties
hello=hello
hello.name=hello {0}
=> {0} 은 파라미터임
MessageSourceTest
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
@Test
void helloMsg() {
String resultMsg = ms.getMessage("hello", null, Locale.KOREA);
// String resultMsg = ms.getMessage("hello", null, null);
assertThat(resultMsg).isEqualTo("안녕");
}
// msg 찾지 못하면 NoSuchMessageException
@Test
void notFoundMsgCode() {
assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
.isInstanceOf(NoSuchMessageException.class);
}
// 메시지가 없는 경우 기본 메시지 (기본MSG)
@Test
void notFoundMsgCodeDefaultMsg() {
String resultMsg = ms.getMessage("no_code", null, "기본MSG", null);
assertThat(resultMsg).isEqualTo("기본MSG");
}
// 파라미터 사용
@Test
void argMsg() {
String resultMsg = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
assertThat(resultMsg).isEqualTo("안녕 Spring");
}
// Locale별로 메시지 가져오기
@Test
void defaultMsg() {
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
}
}
=> getMessage() 메서드를 활용하여 messages.properties, messages_en.properties에서 알맞은 메시지를 불러옴
메시지 적용해보기
messages.properties
label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량
label.item.open=판매여부
label.item.regions=판매지역
label.item.itemType=상품종류
label.item.deliveryCode=배송방식
page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정
button.save=저장
button.cancel=취소
Thymeleaf로 메시지 불러오기 ( #{...} )
- 메시지 표현식인 #{...}을 사용하면 메시지를 불러올 수 있음
ex)
<h2 th:text="#{page.addItem}">상품 등록</h2>
국제화
messages_en.properties
label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity
label.item.open=open
label.item.regions=region
label.item.itemType=itemType
label.item.deliveryCode=delivery
page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update
button.save=Save
button.cancel=Cancel
=> 국제화 적용은 그냥 message_en.properties만 만들어두면 끝난 것임
Spring의 국제화 메시지 선택 방법
- Locale정보를 알아야 messages.properties을 적용할지 messages_en.properties을 적용할지 선택 가능
- th:object를 사용하기 위해서(HTML 렌더링을 위해서)는 Controller로부터 객체를 넘겨받아야 함
- 등록 화면(addForm)을 띄우는 Controller에 model에 빈 Item객체를 넘기는 코드를 추가
변경 전
@GetMapping("/add")
public String addForm(Model model) {
return "form/addForm";
}
변경 후
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item()); // form 렌더링(th:object 를 위해 빈 객체 넘기기)
return "form/addForm";
}
=> th:object="${item}"으로 해당 <form>에서 사용한 객체를 선택함 (Controller에서 넘긴 빈 Item객체를 선택함)
=> th:field=*{itemName}"을 사용하여 ${item}의 itemName을 적용함 (th:field="${item.itmeName}"과 동일한 내용)
=> th:field=*{itemName}"으로 해당 <input>에 id, name, value속성을 자동으로 추가할 수 있음 => 개발이 간편함
=> value속성에는 실제 Controller에서 넘겨준 Item객체의 값이 들어감 (지금은 빈 객체이므로 value="")
=> <form action="item.html" th:action ... >에서 th:action으로 인해 th:action=""으로 렌더링이 되는데, HTML에서 <form>의 action속성 값이 없는 경우, 해당 페이지에 접근한 URL + method속성 값으로 <form> 내용을 제출함
- <script th:inline="javascript"> : Javascript에 Thymeleaf를 적용하여 넘어온 값을 편리하게 사용할 수 있도록 도움
<script th:inline="javascript">
@GetMapping("/javascript")
public String javascript(Model model) {
model.addAttribute("user", new User("userA", 23));
addUser(model);
return "basic/javascript";
}
private void addUser(Model model) {
ArrayList<User> list = new ArrayList<>();
list.add(new User("aaa", 12));
list.add(new User("bbb", 22));
list.add(new User("ccc", 32));
model.addAttribute("users", list);
}
<script>
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
=> 결과 소스
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = userA;
var age = 23;
//자바스크립트 내추럴 템플릿
var username2 = /*userA*/ "test username";
//객체
var user = BasicController.User(username=userA, age=23);
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script>
var username = "userA";
var age = 23;
//자바스크립트 내추럴 템플릿
var username2 = "userA";
//객체
var user = {"username":"userA","age":23};
</script>
=> 사용 전 : userA에 " 가 없어서 오류가 발생함 > var username = "[[${user.username}]]"; 직접 " 로 감싸줘야 하는 번거로움
=> 사용 후 : Thymeleaf가 알아서 문자 타입으로 인식하여 " 를 포함시켜 줌, javascript에서 문제가 될 수 있는 문자는 escape 처리도 해줌
model에는 user라는 이름으로 User객체가 담겨있는데,
=> 사용 전 : 그냥 toString으로 생성됨 ( var user = BasicController.User(username=userA, age=23); )
=> 사용 후 : 객체를 json형태로 바꿔줌 ( var user = {"username":"userA","age":23}; )
자바스크립트 인라인 each
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
=> 결과
<!-- 자바스크립트 인라인 each -->
<script>
var user1 = {"username":"aaa","age":12};
var user2 = {"username":"bbb","age":22};
var user3 = {"username":"ccc","age":32};
</script>
템플릿 조각
- th:fragment, th:insert, th:replae
- 웹 페이지들에는 서로 동일한 공통 영역이 많음(상단바, 좌측 nav 등등)
@GetMapping("/fragment")
public String fragment() {
return "template/fragment/fragmentMain";
}
public class SingleTonWithPrototypeTest1 {
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientSingleTonBean.class, PrototypeBean.class);
ClientSingleTonBean clientSingleTonBean = ac.getBean(ClientSingleTonBean.class);
int count1 = clientSingleTonBean.logic();
assertThat(count1).isEqualTo(1);
int count2 = clientSingleTonBean.logic();
assertThat(count2).isEqualTo(2);
// => 동일한 prototype 빈을 사용하고 있음
}
// 싱글톤 빈에 프로토타입 빈을 주입하는 방법
@Scope("singleton")
static class ClientSingleTonBean {
private final PrototypeBean prototypeBean; // singleton 빈 안에있는 prototype 빈
@Autowired // prototype 빈을 주입받음, 이때 컨테이너가 prototype 빈을 생성해서 주입해줌
public ClientSingleTonBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
- 싱글톤 빈 ClientSingleTonBean에서 PrototypeBean을 주입받아 사용하게 됨
- singleTonClientUsePrototype() 메서드에서 logic()을 두 번 사용하는데, 원하는 동작은 logic() 호출시마다 새로운 프로토타입 빈이 생성되길 원했지만, 그렇게 동작하지 않음 (count2 값이 2 = 기존 프로토타입 빈의 count값을 증가시켰기 때문)
=> 스프링이 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되는데, 싱글톤 빈은 생성시점에만 의존관계 주입을 받고 변경되지 않기 때문에, 프로토타입 빈이 새로 생성되기는 하지만 싱글톤 빈과 함께 유지되는 문제점이 있음
사용할 때마다 항상 새로운 프로토타입 빈을 생성하는 방법
- Dependency Lookup : 의존관계를 주입받지 않고, 직접 필요한 의존관계를 찾는 것 (싱글톤 빈에서 필요한 프로토타입 빈을 직접 찾아서 받음)
- Provider를 사용하여 구현 가능
ObjectProvider
@Scope("singleton")
static class ClientSingleTonBean {
@AutoWired
private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
- ObjectProvider.getObject()를 호출하면 스프링 컨테이너를 통해 해당 프로토타입 빈을 찾아서 받아옴
Provider
@Scope("singleton")
static class ClientSingleTonBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
- Provider는 자바 표준임, 스프링에 의존적이지 않음
- javax.inject:javax.inject:1 라이브러리 필요
- Provider.get()을 호출하면 스프링 컨테이너를 통해 해당 프로토타입 빈을 찾아서 받아옴
웹 스코프
- 웹 스코프는 웹 환경에서만 동작
- 해당 스코프의 시작~종료까지 컨테이너에서 관리됨, 종료 메서드가 호출됨
request : http 요청하나 가 들어와서 나갈때까지유지, 각 http요청마다서로 다른빈이 생성되고 관리됨
session : http session과 동일한 생명주기
application : ServletContext와 동일한 생명주기
websocket : 웹 소켓과 동일한 생명주기
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String msg) {
System.out.println("["+uuid+"]"+"["+requestURL+"]"+msg);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("["+uuid+"] request scope bean create" + this);
}
@PreDestroy
public void destroy() {
System.out.println("["+uuid+"] request scope bean close" + this);
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger);
myLogger.setRequestURL(requestURL);
myLogger.log("LogDemoController Test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String testId) {
myLogger.log("LogDemoService id = " + testId);
}
}
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP request와 상관없이 가짜 프록시 클래스를 미리 주입해둠
public class MyLogger {
...
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final MyLogger myLogger; // 가짜 MyLogger 프록시 클래스를 미리 주입받음
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger); // myLogger = hello.core.common.MyLogger@6634cbd4 // 가짜 MyLogger 프록시 클래스
myLogger.setRequestURL(requestURL);
myLogger.log("LogDemoController Test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger; // 가짜 MyLogger 프록시 클래스를 미리 주입받음
public void logic(String testId) {
myLogger.log("LogDemoService id = " + testId);
}
}
- @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)를 사용하면 스프링 컨테이너는 CGLIB 바이트코드 조작 라이브러리로 MyLogger가짜 프록시 클래스를 생성해서 빈 등록함
- LogDemoController, LogDemoService에 MyLogger의 가짜 프록시 클래스가 주입됨
- Client에서 실제로 사용하는 요청이 오면 그때 내부에서 진짜 MyLogger빈을 찾아서 위임함
=> Provider, 프록시의 핵심은 진짜 빈 조회를 실제로 필요한 시점까지 지연할 수 있다는 점
=> 아주 간단한 어노테이션 설정으로 가짜 프록시 객체를 만들 수 있는 것이 다형성, DI컨테이너의 큰 장점