타임리프

 

Thymeleaf is a modern server-side Java template engine.
The main goal of Thymeleaf is to provide highly-maintainable way of creating templates.
it builds on the concept of Natural Templates.

 

타임리프는 순수 HTML로 사용할 수 있으면서 서버 사이드 렌더링(SSR)으로서 뷰 템플릿의 역할도 하는 네츄럴 템플릿이다.

스프링과 자연스럽게 통합되어 스프링의 다양한 기능을 편리하게 사용할 수 있는 장점이 있다.

 

 

 

기본 표현식

  • 타임리프는 기본적으로 HTML 태그의 속성에 기능을 정의해서 동작한다.
  • 뷰 템플릿으로 실행하지 않을 때는 기본 HTML 태그로 동작하기 때문에 네츄럴 템플릿이라고 한다.

 

 

텍스트

<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력 = [[${data}]]</li>

 

HTML 엔티티

  • HTML 태그를 순수 문자 용도로 사용하도록 변경(이스케이프)하는 방법
  • th:text, [[...]] 의 경우 기본적으로 이스케이프를 적용
  • 이스케이프를 적용하지 않으려면 th:utext, [(...)] 로 사용 (HTML 태그로 동작함)
  • 이스케이프를 적용하지 않을 경우 HTML 렌더링 오류 위험

 

 

변수 표현

  • 스프링이 제공하는 변수 표현식 SpringEL
<li> user 객체 프로퍼티 접근 <span th:text="${user.username}"></span></li>
<li> List 접근 <span th:text="${users[0].username}"></span></li>
<li> Map 접근 <span th:text="${userMap['userA'].username}"></span></li>
  • 지역 변수 선언
<!-- 변수(user)는 선언한 태그 내에서만 사용 가능 -->
<div th:with="user=${users[0]}">
	<p><span th:text="${user.username}"></span></p>
</div>

 

유틸리티 객체

  • 문자, 숫자, 날짜, URI, 객체, 배열등을 편리하게 다루는 다양한 유틸리티 객체들 (thymeleaf.org 참고)
<!-- LocalDateTime Format Example -->
<span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></span>

 

URL 링크

<!-- path variable + query parameter -->
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}"></a></li>

 

리터럴

  • 문자 리터럴은 작은 따옴표(' ')로 감싸거나 리터럴 대체 문법으로 처리를 해야 된다.
<li>따옴표 사용<span th:text="'hello ' + ${data}"></span></li>
<li>리터럴 대체<span th:text="|hello ${data}|"></span></li>

 

Checked

<!-- HTML에서는 checked 속성이 false여도 체크가 되는데 타임리프는 false일 경우 checked 옵션이 사라짐 -->
<input type="checkbox" name="active" th:checked="false" />

 

반복문

  • th:each 는 List 뿐만 아니라 배열, Map, java.util.Iterable java.util.Enumeration 을 구현한 모든 객체를 반복에 사용 가능
  • 두번째 파라미터를 설정해서 상태 확인 가능 (생략 시 변수명 + 'Stat', 예제에서는 userStat)
  • index, count(1부터 시작 index), size, current (현재 객체) 등 다양한 반복 상태 나타내는 기능
<!-- userStat 생략 가능 -->
<tr th:each="user, userStat : ${users}">
	<td th:text="${user.username}">username</td>
    <td th:text="${user.age}">age</td>
</tr>

 

조건문

  • th:if, th:unless
  • 조건 충족하지 않을 경우 해당 태그 자체가 사라짐
  • 스위치문 th:switch, th:case, th:*(default)

 

블록

  • HTML이 아닌 유일한 타임리프 자체 태그
  • HTML 태그 사용하기 애매한 경우에 사용, th:block은 렌더링시 제거
<th:block th:each="user : ${users}"></th:block>

 

자바스크립트 인라인

  • 자바스크립트에서 타임리프를 편리하게 사용할 수 있게 지원해주는 자바스크립트 인라인 기능
  • 객체를 JSON으로 자동 변환
<script th:inline="javascript">

	<!-- 렌더링시 주석이 제거되면서 [[${user.username}]] 출력 -->
    var username2 = /*[[${user.username}]]*/ "test username";
    
    
    <!-- 자바스크립트 인라인 each문 -->
    [# th:each="user, stat : ${users}"]
    	var user[[${stat.count}]] = [[${user}]];
    [/]
      
</script>

 

 

템플릿 조각

 <footer th:fragment="copy">
 	footer content
 </footer>

<footer th:fragment="copyParam (param1, param2)">
	<p th:text="${param1}"></p>
    <p th:text="${param2}"></p>
</footer>
<!-- ~{template/fragment/footer :: copy} = ~{'파일경로' :: 'fragment name'} -->
<!-- insert 사용 시 현재 태그 내부에 생성 -->
<div th:insert="~{template/fragment/footer :: copy}"></div>

<!-- replace 사용 시 현재 태그를 대체 -->
<div th:replace="~{template/fragment/footer :: copy}"></div>

<!-- 파라미터 넘기기 -->
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터 2')}"></div>

 

템플릿 레이아웃

  • 공통으로 사용하는 css javascript 같은 정보들이 있는데이러한 공통 정보들을 한 곳에 모아두고공통으로 사용하지만각 페이지마다 필요한 정보를 더 추가해서 사용하고 싶은 경우 템플릿 레이아웃 적용
  • html 자체를 레이아웃 하는 것도 가능
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)">
	<title th:replace="${title}">레이아웃 타이틀</title>
	
    <!-- 공통 -->
    <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
  	<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
  	<script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

	<!-- 추가 -->
	<th:block th:replace="${links}" />
</head>
<!-- ::title 은 현재 페이지의 title 태그들을 전달한다.-->
<!-- ::link 는 현재 페이지의 link 태그들을 전달한다.-->
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
	<title>메인 타이틀</title>
	<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
	<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>

 

폼 기능

  • th:object 커맨드 객체를 지정
  • *{...} 선택 변수 식, th:object 에서 선택한 객체에 접근한다.
  • th:field : HTML 태그의 id name value 속성을 자동으로 처리
  • th:field는 정상 상황에는 모델 객체의 값을 사용하지만, 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력

 

체크 박스

  • HTML 체크 박스는 체크가 안되면 서버로 값 자체를 보내지 않기 때문에 경우에 따라 사용자가 체크되어 있던 값을 해제한 건지 판단하기가 어려움
  • 스프링 MVC는 이를 위해 _open 이라는 히든 필드를 추가로 하나 만들어서 체크 해제를 인식할 수 있다. (히든 필드는 항상 전송)
  • 따라서 체크를 해제하면  open 은 전송되지 않고 _open 이 전송되는데이 경우 체크를 해제했다고 판단할 수 있다.
  • 타임리프를 사용하면 히든 필드(_open)를 자동으로 생성
<input type="checkbox" id="open" name="open" class="form-check-input">
<!-- 히든 필드 추가 -->
<input type="hidden" name="_open" value="on"/>
<!-- 타임리프는 체크박스의 히든 필드도 자동으로 해결 -->
<input type="checkbox" id="open" th:field="${item.open}" class="form-check-input">

 

멀티 박스

  • @ModelAttribute를 컨트롤러에 있는 별도의 메서드에 적용하면 해당 컨트롤러를 요청할 때 메서드에서 반환한 값이 자동으로 model에 담기게 된다.
  • 멀티 체크박스를 th:each로 생성 시 id를 임의로 item1, item2, item3 .. 이런식으로 생성
<!-- multi checkbox -->
<div>
	<div th:each="item : ${items}" class="form-check form-check-inline">
		<input type="checkbox" th:field="*{items}" th:value="${item.key}" class="form-check-input">
		<label th:for="${#ids.prev('items')}" th:text="${item.value}" class="form-check-label">서울</label>

 

 

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

DispatcherServlet

  • HttpServlet을 상속 받아서 서블릿으로 동작 (프론트 컨트롤러 역할)
  • 서블릿 호출 시, HttpServlet 이 제공하는 service()를 시작으로 DispacherServlet.doDispatch()가 실행
  • 스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다 (인터페이스 제공)
 

 

DispacherServlet.doDispatch()

  • 핸들러 조회
  • 핸들러 어댑터 조회 -> 핸들러를 처리할 수 있는 어댑터
  • 핸들러 어댑터 → 핸들러 실행
  • ModelAndView 반환
  • View Resolver 실행 (View 조회)
  • View 렌더링

 

핸들러 매핑과 핸들러 어댑터

 

스프링 MVC는 이미 내부적으로 구현되어 있다.

  • 가장 우선순위가 높은 RequestMappingHandlerMapping,RequestMappingHandlerAdapter
  • RequestMappingHandler은 @RequestMapping 또는 @Controller가 붙어있는 스프링 빈을 보고 매핑 정보로 인식

 

View Resolver

  • 스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록
  • application.properties에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정 정보를 사용해서 등록
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

그 외 간단 정리

  • @Controller : 컨트롤러는 반환 값이 String이면 뷰 이름으로 인식, 그래서 뷰를 찾고 뷰가 랜더링
  • @RequestMapping : URL 요청 정보를 매핑 (메서드단에 적용해서 컨트롤러 클래스를 유연하게 통합 가능)
  • @RequestParam : HTTP 요청 파라미터를 애노테이션으로 받을 수 있음 (생략 가능)
  • @PathVariable : URL 경로를 템플릿화하여 조회 가능 (members/{memberId})
  • @ModelAttribute : 요청 파라미터를 전달 받고, 객체를 생성해서 Model에 저장하는 과정을 자동화 (생략 가능)

 

 

HTTP 메시지 컨버터

  • view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력
  • RequestMapping 핸들러 어댑터에서 핸들러(컨트롤러)로 넘어가기 전 ArgumentResolver 에서 HTTP 메시지 컨버터 호출
  • ArgumentResolver컨트롤러의 파라미터, 애노테이션을 기반으로 필요한 데이터를 생성하고 컨트롤러에 전달
  • ReturnValueHandler컨트롤러 응답 값을 전달 받아 변환하고 처리
  • @RequestBody : HTTP 메시지 컨버터가 HTTP 메시지 바디를 원하는 문자나 객체 등으로 변환 (json도 객체로 변환 가능)
  • @ResponseStatus(HttpStatus.OK) 애노테이션으로 응답 코드도 설정 가능하지만 동적으로 응답 코드를 생성하는 경우 ResponseEntity를 사용
  • 컨트롤러에서 @ReqeustBody 사용 시, Message Converter가 해당 메시지를 읽을 수 있는지 확인하기 위해 canRead() 또는 canWrite()를 호출하고 가능하면 read() 또는 write() 호출

 

 

[참고]인프런 김영한님 강의를 공부한 내용입니다.

서블릿 (Servlet)

  • 동적 웹 페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
  • 웹 요청과 응답의 흐름을 간단한 메서드 호출만으로 체계적으로 다룰 수 있게 지원
  • HTTP 요청을 통해 매핑된 URL이 호출되면 service() 메서드를 실행
  • HTTP 요청, 응답 메시지를 편리하게 사용할 수 있도록 도와주는 HttpServletRequest, HttpServletResponse 객체

 

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
	@Override
    protected void service(HttpServletRequest request, HttpServletResponse
  response) throws ServletException, IOException {
  	...
   
  }
}

 

HTTP 통신 과정 변화

  • 서버 TCP/IP 연결 대기, 소켓 연결
  • HTTP 요청 메시지 파싱 (헤더부, 바디부 ..)Request 객체
  • 비즈니스 로직
  • HTTP 응답 메시지 생성Response 객체
  • TCP/IP 응답 전달, 소켓 종료

 

MVC 패턴

  • 비즈니스 로직과 뷰 렌더링을 한번에 처리 -> 너무 많은 역할
  • 비즈니스 로직을 수정하는 일과 UI를 수정하는 일은 각각 다르게 발생할 경우가 많고 대부분 서로에게 영향을 주지 않음
  • MVC 패턴Controller와 View 영역으로 서로 역할을 나눈 것
  • Controller : HTTP 요청을 받아서 파라미터 검증, 비즈니스 로직(Service 계층), View에 전달할 데이터를 Model에 저장
  • Model : View에 전달할 데이터를 저장
  • View : Model에 담겨있는 데이터를 사용해서 화면을 렌더링하는데 집중

 

Servlet + 기본 MVC 패턴의 문제점

  • View로 이동하는 dispatcher.forward(request, response) 코드 중복 호출
  • 모든 컨트롤러에 Servlet 선언 (종속성)
  • 공통 처리가 어려움 -> 컨트롤러 호출 전에 공통 기능을 처리해줄 무언가 필요 (프론트 컨트롤러)

 

프론트 컨트롤러

  • 스프링 MVC의 DispatcherServlet 역할
  • 하나의 서블릿으로 사용 (공통 처리)
  • URL 요청을 전부 이곳에서 받아 요청에 맞는 컨트롤러를 호출 -> 서블릿 종속성 제거
  • 각각의 컨트롤러에서 View 네임을 반환하면 프론트 컨트롤러에서 경로(viewPath)를 생성 -> 코드 중복 제거
  • 프론트 컨트롤러에서만 서블릿 선언을 해서 나머지 컨트롤러에서 서블릿 종속성 제거
  • 모든 컨트롤러에서 Model을 반환하지 않도록 프론트 컨트롤러에서 따로 Model 객체를 생성해서 전달
  • 여러 컨트롤러를 처리할 수 있도록 핸들러 어댑터 생성

 

@WebServlet(name = "FrontControllerSerlvetV5", urlPatterns = "/front-controller/v/*")
public class FrontControllerSerlvetV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    // 핸들러 매핑 등록, 핸들러 어댑터 등록
    public FrontControllerSerlvetV5() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }


    // 핸들러(컨트롤러) 초기화
    private void initHandlerMappingMap() {
    	//v3 버전 컨트롤러
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

		//v4 버전 컨트롤러
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    // 어댑터 초기화
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 핸들러 조회
        Object handler = getHandler(request);

        // 요청 URL에 맞는 핸들러 객체가 없으면 종료
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 3. 핸들러에 맞는 핸들러 어댑터 조회
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 5. 핸들러 어댑터에 전달
        // 어댑터는 request 파라미터를 Map 객체에 담고 model 객체를 따로 생성해서 전달
        // 핸들러는 Servlet 종속성이 제거되고 ViewName만 반환하면 됨
        ModelView mv = adapter.handle(request, response, handler);

        // 6. ViewName(논리 이름)으로 View Resolver(풀 경로 생성) 실행
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        // 7. View 렌더링
        view.render(mv.getModel(), request, response);
    }

    // 2. 요청 URL에 맞는 핸들러 객체를 handlerMappingMap에서 조회, 핸들러 객체 반환
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    // 4. 해당 핸들러를 지원하는 핸들러 어댑터가 있는지 확인
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("Error");
    }

    //ViewPass생성
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

 

[참고]인프런 김영한님 강의를 공부한 내용입니다.

빈 생명주기 콜백

  • 스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음 초기화 작업을 할 수 있는 준비가 된다.
  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 기능을 제공한다.
  • 또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 주어 안전하게 종료 작업을 할 수 있게 지원한다.

 

스프링이 지원하는 빈 생명주기 콜백 방법

 

1. 인터페이스

  • InitializingBean, DisposableBean
  • 스프링 초창기에 주로 사용하던 방법
  • 스프링 인터페이스에 의존적, 외부 라이브러리 적용 불가 등의 단점

 

2. 설정 정보

  • @Bean(initMethod = "init", destroyMethod = "close")
  • 메서드 이름 자유롭게 사용, 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 적용 가능
  • destroyMehtod의 경우 따로 적지 않아도 close, shutdown 이라는 메서드 이름을 추론해서 호출

 

3. 애노테이션

  • @PostConstruct, @PreDestroy
  • 자바 표준 기술로 가장 권장하는 방법
  • 유일한 단점으로는 외부 라이브러리에 적용 못하기 때문에 이 경우는 @Bean 설정 정보 사용

 

 

빈 스코프

  • 생성된 빈이 존재할 수 있는 범위
  • 일반적으로 스프링 빈은 싱글톤 스코프로 생성
  • @Scope

 

스프링이 지원하는 스코프

  • 싱글톤 : 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 기본 스코프
  • 프로토 타입 : 스프링 컨테이너가 의존관계 주입, 초기화까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 스코프 : request, session, application 등 특정 웹 요청까지 유지되는 스코프

 

프로토타입 빈

 

1. 특징

  • 스프링 컨테이너 생성 시점에 초기화가 되는 싱글톤 빈과 달리, 프로토타입 빈은 스프링 컨테이너에서 빈은 조회할 때 생성
  • 조회를 할 때마다 새로운 스프링 빈이 생성, 초기화
  • 프로토타입 빈을 조회한 클라이언트가 해당 빈을 관리

 

2. 문제점

  • 스프링 컨테이너 의존관계 주입 시점에 싱글톤 빈에 프로토타입 빈이 생성되어 주입
  • 이후 클라이언트가 프로토타입 빈의 메서드 요청 시 새로운 프로토타입 빈이 생성되는 것이 아니라 기존 빈 호출
  • 프로토타입 빈이 싱글톤 빈과 함께 계속 유지되는 문제 발생

 

3. 해결 방안

  • 의존관계 주입(D.I)이 아닌 의존관계 조회(Depengency Lookup)을 통해 필요시마다 프로토타입 빈을 새로 조회, 생성해야 한다.
  • 스프링이 제공하는 ObjectProvider (기존 ObjectFactory + 편의 기능 추가)
  • 지정한 빈을 컨테이너에서 대신 찾아주는 D.L 기능 제공
  • getObject() 메서드를 통해 항상 새로운 프로토타입 빈이 생성
  • 스프링에 의존적이지만 기능이 단순해서 단위테스트, mock 테스트 만들기 편리
  • 싱글톤 스코프 빈을 거의 사용하기 때문에 직접적으로 프로토타입 빈을 사용할 경우는 적음 

 

 

스코프와 프록시

 

  • @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
  • 스프링 컨테이너가 CGLIB (바이트코드를 조작하는 라이브러리)를 사용해서 가짜 프록시 객체를 생성
  • 실제 객체를 상속받은 프록시 객체를 스프링 빈으로 등록, 싱글톤 빈처럼 사용
  • 싱글톤을 사용하는 것 같지만 실제로는 다르게 동작하므로 주의해서 사용
  • 프록시 객체는 요청이 올 경우 그 때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • ObjectProvider를 사용하든, Proxy를 사용하든 핵심은 실제 객체 조회를 필요한 시점까지 지연 처리 한다는 점

 

 

 

참고(https://www.inflearn.com/users/@yh)

+ Recent posts