스프링 부트 OAuth 구현을 하다가 순환참조 이슈가 발생하였다. 이 순환참조 이슈는 Spring Boot 2.6x 버전부터 발생한다고 한다. 앞으로 프로젝트 할 때 흔히 겪을 수 있는 문제라 일단 기록은 해두려 한다.

 

  • 문제에러 화면
    • 대충 circular references 어쩌고 나오면 순환참조 이슈이다. 

문제화면

  • 문제가 발생되는 코드

코드1
코드2

  • SecurityConfig를 빈으로 등록하려 하니 PrincipalOauth2UserService가 빈으로 사전에 등록되어야 주입이 가능하고 PrincipalOauth2UserService를 빈으로 등록하려 하니 BCryptPasswordEncoder가 미리 빈으로 등록되어서 주입을 해야 하는데 이도저도 안되니 결국 어떠한 빈도 생성하지 못한다. 

 

  • 해결책
    • 순환의 고리를 끊자.
      • 다른 Config 클래스에 빈으로 등록하기 
      • BCryptPasswordEncoder를 상속받는 클래스를 만들고 @Component로 빈으로 등록하기 
  • 본인은 두번째 방법으로 해결하였다. 

상속이즈굳

 

최근에 스프링부트 강의를 보는데 이전 버전 Spring Boot에서는 application.yml이 자동 생성되고 특정 버전이후부터는 application.properties 파일이 자동 생성되는 것을 확인할 수 있었다. 이 두가지 파일은 작성하는 설정의 의미는 동일하나 작성 표기법이 살짝다르다. 그리고 서로 변환이 가능해서  properties로 작성하고 싶으면 그 방식대로 작성하고 yml파일 형식으로 작성하고 싶으면 그 방식대로 작성하면 된다. 뭐가 더 편하고 좋다라는 것은 사용하는 사람들 마다 다르다. 개인적으로는 가독성은 properties 파일, 작성의 편의성은 yml파일이 좋다고 생각된다. .properties 파일은 작성법이 직관적이나 yml 파일은 초심자가 이해하는데 약간의 사전지식이 필요하다. 그리고 현재 인프런이나 패스트캠퍼스에 올라온 강의들은 yml형식으로 강의한 것들이 많으므로 이것에 대한 지식은 나름 필요하다고 생각된다. 그래서 이번 포스트에서는 application.properties파일과 application.yml파일의 차이를 알아보고 yml파일 작성법에 대해서 소개하려고 한다.

 

  • 필자는 다음과 같은 프로젝트를 생성하였고 스프링부트 버전은 2.7.13이다. 

프로젝트 생성

  • 해당 버전의 스프링부트 프로젝트에서는 application.properties가 자동생성된다. 이것을 yml 파일로 바꾸고 싶다면 파일의 확장자명만 yml으로 바꾸면 된다. 이 application.properties 파일은 별다른 설정이 없다라도 기본으로 인식이 되지만 새로 생성하는 또다른 설정 파일을 인식하기 위해선 추가로 지정해주는 과정이 필요하다. 

properties 파일 vs yml 파일  

위의 파일은 properties 형식으로 작성한 파일이고 아래 그림은 yml 형식으로 작성한 파일이다. properties 형식은 key, value 형식으로 작성되어 사이사이에 dot(.)을 넣어서 key이름을 작성한다. 사실 이것이 계층구조인데 이러한 계층구조를 properties파일 형식에선 각 계층을 모두 적어준다. 

properties

위에서 dot(.)으로 구분한 key이름은 yml 파일에선 좀 더 심플해진 계층구조로 표현이 되어 상위계층명을 생략하여 다음 라인에 입력한다. 그리고 라인마다 중간 중간에 공백이 있다. 이건 계층의 depth를 포현하며 이때 공백 한칸은 tab을 입력하기 보다는 space 두번을 쳐서 입력하는 것이 좋다. 그리고 key옆의 클론(:)과 value 사이에는 한 칸의 space 있어야 한다. 

yml

그 다음으로는 리스트를 작성할 때의 차이이다. yml 형식의 특징은 반복의 최소화이다. yml 파일의 리스트 표기는 - 로 표기를 한다. 같은 index 표기시엔 다음 라인엔 -를 쓰지 않는다. 반면에 properties 형식에서는 기존 리스트 표기형식과 동일하다. 

리스트 표현형식 : properties           >           yml

주석

  • 주석은 yml 파일, properties 파일 모두 #으로 동일하다.

직접 작성시

  • properties
    • 최대한 인텔리센스에서 제공하는 자동완성으로 작성하는 것이 좋다. 오타를 내는 것을 조심하자.

자동완성

  • yml
    • yml 파일 자동완성은 매우 편하다. 한방에 계층 구조를 만들어준다. 그리고 한라인 한라인 엔터치고 작성하면 작성 속도도 yml 파일이 월등히 빠르다고 생각한다.

자동완성 편해

yml 파일 형식 체크 사이트

 본인이 yml 파일을 잘 작성했는지 확인하려면 다음의 사이트를 확인하면 된다. 

https://jsonformatter.org/yaml-viewer

 

Best YAML Viewer Online

Free YAML Viewer is a web based viewer for YAML

jsonformatter.org

이미 작성된 yml 또는 properties 파일 변환 시 

 직접 수작업으로 변환할 수 있지만 물론 자동으로 변환시켜주는 사이트가 있다. 굉장히 유용하다.

 

  • Controller
@GetMapping("/m10")
public String m10(Model model) {
    List<String> names = mapper.getNames();
    List<BoardDTO> list = mapper.getList();
    model.addAttribute("names", names);
    model.addAttribute("list", list);
    return "m10";
}
	
@GetMapping("/m11")
public String m11(HttpSession session) {
    session.setAttribute("id", "hong");
    // session.invalidate();
    return "m11";
}

@GetMapping("/m12")
public String m12() {
    return "m12";
}
  • MyBatis
<select id="getNames" resultType="String">
	select first_name from employees where rownum &lt;= 10
</select>


<select id="getList" resultType="dto">
    select * from tblBoard
</select>
  • th:each="엘리먼트 : ${리스트배열}" 이게 기본 형태이다.
    • 태그 하나로 바로 사용가능하고 자식 태그를 두어서 사용도 가능하다.
<ul>
    <li th:each="name : ${names}" th:text="${name}">이름</li>
</ul>

<ul>
    <li th:each="name : ${names}" >
        <span th:text="${name}"></span>
    </li>
</ul>
---------------------------------------------------------------
<ul th:each="name: ${names}">
    <li th:text=${name}></li>
</ul>

결과

  • 아래의 모형이 dto 리스트를 받아서 출력하는데 th:object 사용하면 선택 변수 표현식으로 * 지정해서 사용가능 
<ul>
    <li th:each="dto : ${list}" th:text="|${dto.subject}(${dto.id})|"></li> 
</ul>
<ul>
    <li th:each="dto : ${list}" th:object="${dto}">
        <span th:text="*{subject}"></span>
    </li>
</ul>
<ul>
    <li th:each="dto : ${list}" th:object="${dto}" th:text="*{subject}">
    </li>
</ul>

결과

  • jstl 과 마찬가지로 status라는 것이 있다. index, count 꺼내쓰고 해당 넘버가 어떤 속성인지 last, odd, first even 등을 정의해놓음
<table>
    <tr>
        <th>번호</th>
        <th>아이디</th>
        <th>제목</th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
    </tr>
    <!-- 콤마로 두개 가능  -->
    <tr th:each="dto, status : ${list}">
        <td th:text="${dto.seq}"></td>
        <td th:text="${dto.id}"></td>
        <td th:text="${dto.subject}"></td>

        <td th:text="${status.index}"></td>
        <td th:text="${status.count}"></td>
        <td th:text="${status.size}"></td>

        <td th:text="${status.even}"></td>
        <td th:text="${status.odd}"></td>
        <td th:text="${status.first}"></td>
        <td th:text="${status.last}"></td>
    </tr>
</table>
  • ${#numbers.sequence(1,5)} 라는 걸 정의해서 1~5까지 출력이 가능하다. 
<th:block th:each="num : ${#numbers.sequence(1,5)}">
    <div th:text="${num}"></div>
</th:block>

결과

  • 마찬가지로 내장객체라는 것을 가리킬 수 있어야 하는데 ${  }안에 #을 붙인다. 
  • 단 session은 #을 붙이지 않는다. 
<div th:text="${#request}"></div>
<div th:text="${#response}"></div>
<div th:text="${#locale}"></div>
<div th:text="${session}"></div>
<div th:text="${#servletContext}"></div>

<div th:if="${session.id != null}">
    인증 : <span th:text="${session.id}">아이디</span>
</div>

<div th:unless="${session.id != null}">미인증</div>

결과

  • ~{    } : 조각 페이지를 삽입할 때 쓰는 include 지시자이다. 
    • insert나 replace는 거의 동일하다. ( 차이가 뭐지..? )
    • ~{파일이름} 이렇게 쓰면 확장자는 생략할 수 있다. 
    • th:insert, th:replace로 ~{ } 생략한 채로 th:insert그냥 바로 참조가 가능하다.
  • th:fragment="프래그먼트이름"
    • th:fragment 라는게 있고 이것은 조각이다. 이걸 따로 정의하고 가져다가 쓸 때는 첫번째로는 문서참조, 그 후에 : : 을 붙이고 해당 fragment 이름으로 참조한다. :: 으로 namespace를 찾는 느낌이다.
  • ★★ fragment에 파라미터를 넘겨줄 수 있다.
    • th:fragment="owner(name, tel)" 이런식으로 정의해주면 인자를 name, tel로 넘겨받겠다라는 뜻
    • 예를 들어서 username 같은거만 따로 넘겨줘서 로그인한 유저에 따라 표기를 다르게 할 수 도 있을 듯..
<!-- templates/inc/sub.html -->
<div>조각 페이지</div>
<!-- sub2.html -->
<div th:fragment="part">조각 페이지2</div>
<div th:fragment="part2">조각 페이지3</div>
<div th:fragment="owner(name, tel)">
    <div>소유주 : <span th:text="${name}"></span></div>
    <div>연락처 : <span th:text="${tel}"></span></div>
</div>
<!-- m12.html -->
<h1>Thymeleaf Fragment</h1>
<h2>insert</h2>
<div th:insert="~{inc/sub.html}"></div>
<!-- insert replace 차이 -->
<h2>replace</h2>
<div th:replace="~{inc/sub.html}"></div>
<hr>

<!-- 확장자 생략이 돤다, -->
<div th:insert="~{inc/sub}"></div>

<!-- ~{} 생략 가능 > 비권장 -->
<div th:insert="inc/sub"></div>
<div th:insert="inc/sub2.html :: part"></div>
<div th:insert="inc/sub2.html :: part2"></div>
<hr>
<!-- 사용시에 약간 함수느낌으로 인자를 넣어줌 -->
<div th:insert="~{inc/sub2::owner('아무개', '010-3333-4444')}"></div>
<div th:insert="~{inc/sub2::owner('테스트', '010-3333-4444')}"></div>

결과

 

  • Controller
@GetMapping("/m7")
	public String m7(Model model) {
		int num1= 1234567;
		double num2 = 12345.6789;
		Calendar now = Calendar.getInstance();
		model.addAttribute("num1",num1);
		model.addAttribute("num2",num2);
		model.addAttribute("now",now);
		return "m7";
}

@GetMapping("/m8")
	public String m8(Model model) {
		int seq = 10;
		String mode = "add";
		model.addAttribute("seq",seq);
		model.addAttribute("mode",mode);
		return "m8";
}
	
@GetMapping("/m9")
public String m9(Model model) {
    int num1 = 10;
    int num2 = 5;
    String mode = "add";
    model.addAttribute("num1", num1);
    model.addAttribute("num2", num2);
    model.addAttribute("mode", mode);
    return "m9";
}
  • 숫자 형식 변환하기 m7.html
    • #numbers.formatDecimal(표시 값, 최소 정수 자릿수, 최소 소수 자릿수)
    • 추가로 가능한 것
      • formatCurrency, formatPercent 이런것도 가능하다. 
      • ${#numbers.arrayFormatDecimal(numArray,3,2)}
      • ${#numbers.listFormatDecimal(numList,3,2)}
      • ${#numbers.setFormatDecimal(numSet,3,2)}
<h2>숫자</h2>
<div th:text="${num1}"></div>
<div th:text="${#numbers.formatInteger(1, 3, 'COMMA')}"></div>    
<!-- 남은 자리 0으로 채운다. 최대 3자리 --> 
<div th:text="${#numbers.formatCurrency(1)}"></div>
<div th:text="${#numbers.formatPercent(num1, 5, 1)}"></div>
<div th:text="${#numbers.formatInteger(num1, 3, 'COMMA')}"></div>
<!-- 두번쨰 인자는 표현자리수다. -->
<div th:text="${num2}"></div>
<div th:text="${#numbers.formatDecimal(num2, 3, 'COMMA', 1, 'POINT')}"></div>
<!-- 3자리씩콤마로 짜르고, 1자리 소수부분 나타낸다. -->

 

결과

  • 날짜관련
    • #dates.메서드로 날짜 객체를 받아서 사용한다.
<h2>날짜</h2>
<div th:text="${now}"></div> 
<div th:text="${#dates.year(now)}"></div>
<div th:text="${#dates.month(now)}"></div>
<div th:text="${#dates.monthName(now)}"></div>
<div th:text="${#dates.monthNameShort(now)}"></div>
<div th:text="${#dates.day(now)}"></div>
<div th:text="${#dates.hour(now)}"></div>
<div th:text="${#dates.minute(now)}"></div>
<div th:text="${#dates.second(now)}"></div>
<div th:text="${#dates.millisecond(now)}"></div>
<div th:text="${#dates.dayOfWeek(now)}"></div>
<div th:text="${#dates.dayOfWeekName(now)}"></div>
<div th:text="${#dates.dayOfWeekNameShort(now)}"></div>
<div th:text="${#dates.format(now)}"></div>
<div th:text="${#dates.format(now, 'yyyy-MM-dd HH:mm:ss')}"></div>
<div th:text="${#dates.format(now, 'yyyy-MM-dd aa hh:mm:ss')}"></div>

결과

  • Link URL Expression
    • @{   }  
    • a태그에서 URL 표현
      • 매개변수 처리가 쉽고 Context Root Path가 자동으로 삽입된다. 
      • 쿼리 스트링을 붙이는 표현 가능
      • ★★★ th:href와 같이 쓴다. 
      • @{url(key1=value1, key2=value2)}
  • m8.html
 <div><a href="/m7">이전 페이지</a></div>
 <div><a href="/spring/m7">이전 페이지</a></div>

 <!-- root context를 붙여준다. -->
 <div><a th:href="@{/m7}">이전 페이지</a></div>
 <hr>
 <h3>QueryString, 매개변수</h3>
 <div>
    <a href="/m7?seq=100">이전 페이지</a>
    <a href="/m7?seq=${seq}">이전 페이지</a> <!-- 잘못된 표현 -->
    <a th:href="@{/m7(seq=${seq})}">이전 페이지</a>

    <a href="/m7?seq=100&mode=add">이전 페이지</a>
    <a th:href="@{/m7(seq=${seq}, mode=${mode})}">이전 페이지</a>
 </div>
 <!-- 
    기본 앱(QueryString)
    - /m7?seq=10

    REST(Path Variable)
    - /m7/10
  -->
 <div>
    <!-- 바인딩이 되버린다. -->
    <a th:href="@{/m7/{seq}(seq=${seq})}">이전 페이지</a>
    <a th:href="@{/m7/{mode}/{seq}(seq=${seq},mode=${mode})}">이전 페이지</a>
 </div>

소스검사

  • th:if, th:unless
    • 조건을 만족하면 해당 태그가 만들어지고 만족하지 못하면 해당 태그가 사라진다. 
    • if와 else 연달아서는 아래처럼 쓴다.
      • <div th:if="${num1 > 0}">양수</div>
      •  <div th:unless="${num1 > 0}">음수</div>
  • th:switch
    • th:switch로 받아서 th:case로 매칭
  • m9.html
 <h2>if</h2>
 <div th:if="${num1 > 0}">num1 : 양수</div>
 <div th:if="${num2 < 0}">num2 : 양수</div>
 <div th:if="${num1 > 0}">
    <span th:text="'양수' + ${num1} + '입니다.'"></span>
 </div>
 <!-- 조건을 만족못하면 테그 자체가 사라진다. 조건을 만족하면 태그가 만들어진다. -->
 <div th:if="${num1 > 0}" th:text="'양수' + ${num1} + '입니다.'"></div>
 <div th:if="${num1 > 0}" th:text="|양수 + ${num1} + 입니다.|"></div>
 <div th:if="${num1 > 0}">양수 [[${num1}]]입니다.</div>
 <hr>	 
 <div th:if="${num1 > 0}">양수</div>
 <div th:unless="${num1 > 0}">음수</div>
 <!-- 이걸 만족하지 못했을 때 실행된다. -->
 <hr>
 <h2>switch</h2>
 <div th:switch="${mode}">
    <div th:case="add">추가하기</div>
    <div th:case="remove">삭제하기</div>
    <div th:case="*">기타</div>
 </div>

소스검사

  • Controller
@GetMapping("/m5")
	public String m5(Model model) {
		model.addAttribute("name", "age");
		model.addAttribute("size", 30);
		model.addAttribute("color", "cornflowerblue");
		return "m5";
}
  • HTML 속성조작
    • th:text="값"
    • th:HTML속성명="값"
    • 기존에 동일한 속성이 선언되어 있으면 대체한다.
    • 기존에 동일한 속성이 선언되어 있지 않으면 추가한다. 
    • ★ 대부분의 경우에는 서버에서 Model로 전송하여 받은 값은 그냥 사용할 수 없다. 
      • 직접 표현식으로 ${key} 형태로는 바로 못쓴다.(일반속성에 표현식 적용이 안된다.)
      • th:속성으로 표현식을 넣어야 가능하다. 
<div>${name}</div>
<div th:text="${name}"></div>

<input type="text" name="age">     <!-- 이건 정적 -->
<input type="text" th:name="age">  <!-- 이건 타임리프가 손을 대서 만듬 -->
<input type="text" name="${name}"> <!-- 이건 타임리프 속성이라 안된다. -->
<input type="text" th:name="${name}"> <!-- 이건 타임리프 속성이다. -->
<input type="text" th:name="${name}" th:size="${size}">  
<input type="text" th:value="${color}">
  • 위의 코드는 아래와 같이 해석된다. th:name="age" 이렇게 타임리프 속성에 정적인 값을 넣어도 적용이 되지만 일반 속성에 표현식을 넣으면 적용이 되지 않는다.

해석

 

  • th:class 
    • 특이한 점이 있다면 한번더 정의하게 되면 나중에 나온것이 적용이 된다. 
<div class="one">Box 1</div>
<div th:class="one">Box 2</div>
<div class="one" th:class="two">Box 3</div>

적용

  • 기존에 jquery 하던 class append 같은 것들을 하는 append 속성
    • th:attrappend > 뒤
    • th:attrprepend > 앞 
    • th:classappend > 뒤
<div class="one" th:attrappend="class=' two'">Box 4</div>
<div class="one" th:attrprepend="class='two '">Box 5</div>
<div class="one" th:classappend="two">Box 6</div>

결과

  • 속성 설정 및 css 스타일 설정
    • th:checked는 타임리프에 의해서 속성이 뭔가 새롭게 가공되는 느낌이다. 
      • true면 checked="checked", false면 아예 나타나지 않는다. 
    • th:style을 넣을 때는 그냥 맘 편하게 |  |을 사용하고 그렇게 쓰지 않으면 문자열 + 로 처리
<input type="checkbox" name="cb" th:checked="true">
<input type="checkbox" name="cb" th:checked="false">

<div th:style="'background-color:' +${color}">Box 7 </div> 
<div th:style="|background-color:${color}|">Box 7 </div>

결과

  • Controller
@GetMapping("/m6")
	public String m6(Model model) {
		String name = mapper.getTxt();
		BoardDTO dto = mapper.getDTO();
		String txt = "안녕하세요. &lt;i&gt;홍길동</i> 입니다.";
		int num = 100;
		List<String> names = mapper.getNames();
		model.addAttribute("name", name);
		model.addAttribute("dto", dto);
		model.addAttribute("txt", txt);
		model.addAttribute("num", num);
		model.addAttribute("names", names);
		return "m6";
}
  • getNames 부분의 쿼리정의 ↓
<select id="getNames" resultType="String">
    select first_name from employees where rownum &lt;= 10
</select>
  • th:text는 기본적으로 escape 되어 텍스트가 출력된다.
    • 해석이 안되고 escape 된다. > CDATA 느낌
  • th:utext 
    • unescaped text으고 PCDATA로 해석이 되어 출력이 된다. >> script injection이 된다.
<div th:text="${txt}"></div>
<!-- 이스케이프 되서 출력 -->
<div th:utext="${txt}"></div>
<!-- 이스케이프 안되서 출력 i태그도 나온다.-->

결과

  • th:inline
    • 1. th:inline="text" // html 쪽에서 쓴다. 사용 잘 안한다.
    • 2. th:inline="javascript" // script 쪽에서 쓴다. 많이 쓴다.(필수) ★★★★
  • escape text, unescape text를 태그 사이 내용으로 넣을 때 아래의 키워드를 쓴다. (th:inline이 함께 쓰이면서) 안쪽에 표현식을 넣는다. 
    • [[ ]] : escaped text
    • [( )] : unescaped text
    • ★★★ 이런 것들 주석처리는 타임리프 주석처리로 해야한다. <!-- --> 와 같은 일반주석 처리시에는 에러난다. 
    • <!--/*        주석할 내용            */--> 
    • <!--/* <div th:inline="text">[[${a}]] + [[${b}]] = [[${a + b}]]</div> */-->
<div th:inline="text">[[${name}]]</div>
<div th:inline="text">[(${name})]</div>

<div th:inline="text">[[${txt}]]</div>
<div th:inline="text">[(${txt})]</div>

<div>[(${txt})]</div>
<div th:inline="text">[[${num}]] + [[${num}]] = [[${num + num}]]</div>

결과

  • th:inline="javascript"를 쓴 것과 안쓴 것의 차이 ★
    • label1, label2에 값을 넣는 예제인데 기존에 표현식을 script 단에서 썼을 때 문자열 표현식을 '  '로 감싸야 했던 상황이 있었는데 th:inline="javascript"을 쓰면 문자열은 자동으로 " "를 붙여서 script단에서 동작한다. 숫자는 그냥 나온다. ( th:inline="javascript"에서 [[]] 사용시 한글은 퍼센트 인코딩이 되어서 나오니까, 주의할 것! )
    • th:inline="javascript"을 사용하고 [[ ]], [( )]을 가지고 적절하게 맞게 사용해야 한다.
      • [( )] 형태로 꺼내쓰면 인코딩 없이 데이터가 나온다. [[ ]] 로 꺼내쓰면 인코딩 되어 나온다.
      • [( )] 형태로 쓰려거든 ' '를 직접 붙여줘야 한다. 
<div id="label1"></div>
<div id="label2"></div>
	
	
<script>
	let name1 = '[[${name}]]';
	let num1 = [[${num}]];
	let names1 = '[[${names}]]';
	let dto1 = '[[${dto}]]';
	// 소스 보기에서 자바스크립트 배열이 아니다. 
	document.getElementById('label1').textContent = name1;	
</script>

<script th:inline="javascript">
    //let name2 = '[[${name}]]'; // 이거 쓰면 " " 붙어서 나온다.
    let name2 = [[${name}]]; // 이거 쓰면 " " 붙어서 나온다.
    let num2 = [[${num}]];
    let names2 = [[${names}]];
    let dto2 = [[${dto}]];
    let dto3 = '[(${dto})]'; // ' '를 안쓰면 문자열화가 안되어서 에러난다. 
    // 소스 보기에서 자바스크립트 배열이다. 손댈것이 없다.  
    document.getElementById('label2').textContent = name2;
</script>

 

결과

 

프로젝트 셋팅 및 코드는 이전 글 내용을 참고한다.

 

Thymeleaf 연산자 정리하기

  • Controller 부분
@GetMapping("/m4")
	public String m4(Model model) {
		int a = 10;
		int b = 3;
		model.addAttribute("a", a);
		model.addAttribute("b", b);
		return "m4";
}
  • m4.html 
    • th:text 에서는 일종의 텍스트 느낌인데 ${ } 안쪽에 ' + ' 이런 스트링을 넣어도 되고 ${a} 이렇게 분리된 형식으로 넣어도 된다. 
<div>10 + 3 = 13</div>
<div><span th:text="${a}"></span> + <span th:text="${b}"></span> = <span th:text="${ a + b }"></span></div>
<div th:text="${a + ' + ' + b + ' = ' + ( a + b )}"></div>
<div th:text="${a} + ' + ' + ${b} + ' = ' + ${ a + b }"></div>

결과

  • |   | 이건 th:text에서 문자열 취급을 시켜준다.
    • 위의 것과 다른 느낌이 있다면 의도적으로 ' '를 +에 씌워준 것과 다르게 내부에 있는 것을 문자열 처리 해주되 ${a}이런건 결과 기준으로 문자열화 해준다.
<div th:text="홍길동"></div>
<div th:text="|홍길동|"></div>
<div th:text="|${a}|"></div>
<!-- 이러면 || 안에 있는거 문자열로 취급한다. 템플릿 스트링 느낌 -->
<div th:text="|${a} + ${b} = ${a+b}|"></div>

결과

  • 산술 연산자 
    • 문자열 처리화를 하지 않으면 "  "안쪽에서 연산자로 인식한다. 
<div th:text="${a} + ${b}"></div>
<div th:text="${a} - ${b}"></div>
<div th:text="${a} * ${b}"></div>
<div th:text="${a} / ${b}"></div>
<div th:text="${a} % ${b}"></div>

결과

  • 비교 연산자
    • ${ }를 따로따로 쓰나, ${ }안에 다 처리하는거나 같다. 
<div th:text="${a > b}"></div>
<!-- 이방식으로 하나 아래 방식으로 하나 같다.  -->
<div> ---- </div> 
<div th:text="${a} > ${b}"></div>
<div th:text="${a} >= ${b}"></div>
<div th:text="${a} < ${b}"></div>
<div th:text="${a} <= ${b}"></div>
<div th:text="${a} == ${b}"></div>
<div th:text="${a} != ${b}"></div>

결과

  • 논리 연산자
<div th:text="${a > 5} and  ${b < 10}"></div>
<div th:text="${a > 5} or  ${b < 10}"></div>
<div th:text="${a > 0} ? '양수' : '음수'"></div>
<div th:text="${a > 0 ? '양수' : '음수'}"></div>

결과

  • 삼항연산자(말그대로 3항)
    • <span th:text="${data} ? ${data} : '데이터가 없음'"></span> 
    • data가 true면 data 출력, false면 데이터가 없음
  • Elvis 연산자(2개 항이다.)
    • <span th:text="${data} ?: '데이터가 없음'"></span>
    • data가 true면 해당 값을 출력하고 false면 뒤에 있는 값을 치환하여 출력한다. 
  • No-Operation(언더바 짝대기)
    • <span th:text="${data} ?: _">데이터가 없음</span> 
    • data가 true면 data가 false라면 HTML 내용 그대로를 출력한다. 
  • Elvis나 No-Operation은 true면 자신 그대로가 나오는 형태이다. 
<!-- c는 여기서 정의되어 있지 않다. -->
<div th:text="${c} != null ? ${c} : '데이터가 없음'" ></div>
<div th:text="${c} ? ${c} : '데이터 없음'" ></div>
<div th:text="${c} ?: '데이터 없음'"></div> <!-- Elvis 연산자 -->
<!-- 위에 세개 다 똑같은 문장이다. 아래것만 기억하자 -->
<div th:text="${c} ?:_">데이터 없음</div><!-- No Operation -->

결과

왜 Thymeleaf를 써야하는지는 이전 글의 jsp 설정에서 봤듯이 STS4에서 jsp 셋팅을 기본으로 지원을 하지 않는다는 점이다. 반면에 Thymeleaf는 초기 프로젝트 설정에서 Dependency로 Thymeleaf만 추가해도 셋팅이 된다. 인프런에 있는 많은 강의에서도 대부분 부트 입문자에게 Thymeleaf쪽을 공부하는 것을 추천한다. Thymeleaf는 JSP와 비슷한 것이 많은데 추가적으로 몇가지 편리한 기능들이 추가되어 JSP에서는 불편했던 것들이 Thymeleaf에서는 좀 더 작업하기 편한 것들이 있다. 무조건 Thymeleaf만을 쓰라는 것은 아니다. FreeMarker나 Groovy, Mustache 등의 템플릿 엔진도 있다. 나중에 기회가 되면 이러한 것들도 공부해보고 싶긴하다.

 

Thymeleaf 플러그인 설치

설치해

  • STS4에서 thymeleaf 관련 속성들의 인텔리센스는 위의 플러그인 설치로 인해 가능해진다. 참고하기

설치되면 이게 가능

프로젝트 셋팅

  • 프로젝트 생성
* STS4 > New 탭 > Spring Start Project
  - Name > boot-thymeleaf
  - Type > Maven
  - Packaging > Jar
  - Java Version > 11
  - Language > Java
  - Group > com.test.thymeleaf
  - Artifcat > boot-mybatis
  - Package > com.test.thymeleaf
  - Spring boot version > 2.7.13
  
* Dependency 
  - Spring Web
  - Oracle Driver
  - MyBatis Framework
  - Lombok
  - Thymeleaf 
  - Spring Boot DevTools
  • 설정파일 셋팅 > application.properties
    • classpath이건 스프링과 동일하게 기본적으로 src/main/resources 이다. 
    • 작업할 땐 spring.thymeleaf.cache=false
# 서버 포트 번호
server.port = 8092

# JSP View Resoler, webapp, WEB-INF 폴더가 없음 
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# HikariCP settings
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.maximumPoolSize=20
spring.datasource.hikari.idleTimeout=30000
spring.datasource.hikari.maxLifetime=2000000
spring.datasource.hikari.connectionTimeout=30000

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=hr
spring.datasource.password=java1234

#Thymeleaf 
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

# 이전에 내용을 캐싱시켜준거 보여주는데 속도 빠르게 해주는데
# 개발할때는 고친게 눈에 바로 보여야 한다. 캐시 활성화 끈다. 
spring.thymeleaf.cache=false
spring.thymeleaf.encoding=UTF-8

# 이설정을 하면 mybatis 다루는데 필요한 설정이 가능 
mybatis.config-location=classpath:mybatis-config.xml

SQL, Mapper 파일

  • typeAliase 설정
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0/EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
	<typeAliases>
		<typeAlias type="com.test.domain.BoardDTO" alias="dto"/>
	</typeAliases>
</configuration>
  • sql 파일 
create table tblBoard (
   seq     number      primary key,
   subject varchar2(1000) not null,
   content varchar2(2000) not null,
   regdate           date not null,
   id      varchar2(30) not null
);
-- drop sequence seq;
create sequence seq;

desc tblBoard;

insert into tblBoard values(seq.nextVal, '제목입니다.', '내용입니다.', sysdate, '멍멍이');
insert into tblBoard values(seq.nextVal, '제목입니다.2', '내용입니다.2', sysdate, '야옹이');
insert into tblBoard values(seq.nextVal, '제목입니다.3', '내용입니다.3', sysdate, '어흥이');

select * from tblBoard;
  • mapper.xml쪽
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.test.mapper.ThymeleafMapper">
	<select id="getNum" resultType="Integer">
		select salary from employees where rownum=1
	</select>
	
	<select id="getTxt" resultType="String">
		select first_name from employees where rownum = 1
	</select>
	
	<!-- application.properties에서 typeAlias를 지정해서 패키지명을 생략한 것이다.  -->
	<!-- mybatis-config.xml에 엘리아스를 지정해서 가능하다. -->
	<select id="getDTO" resultType="dto">
		select * from tblBoard where rownum = 1 
	</select>
	
	<select id="getNames" resultType="String">
		select first_name from employees where rownum &lt;= 10
	</select>
	
	
	<select id="getList" resultType="dto">
		select * from tblBoard
	</select>
</mapper>

프로젝트 파일셋팅

★ 자바단
com.test.controller > ThymeleafController.java
com.test.domain > BoardDTO.java
com.test.mapper > ThymeleafMapper.java(I)
com.test.thymeleaf > BootThymeleafApplication.java

★ 매퍼단
src/main/resources
com/test/mapper > ThymeleafMapper.xml

★ 뷰단
templates 폴더에 하나씩 추가

자바단 및 프론트단 파일

  • 기본적인 Controller 틀 
    • 설정파일에서 thymeleaf prefix, suffix를 써놓은대로 > 앞뒤로 string이 붙는다. 
@Controller
public class ThymeleafController {
	
	@GetMapping("/m1")
	public void m1() {
		
		// 요청 메소드의 반환값 > void > m1.jsp 호출 
		// 요청 메소드의 반환값 > void > m1.html 호출
		System.out.println("m1");
	}
    
    // 위와 아래는 동일한 표현이다. > void 반환값, String 반환값
    
    /*@GetMapping("/m1")
	public String m1() {
		System.out.println("m1");
		return "m1"; // 이것도 마찬가지로 m1.html 호출이다.
	}*/
    
}
  • ★★★ Thymeleaf Standard Expression
1. Variable Expression, 변수 표현식 
    - ${}
    - 컨트롤러 > 전달된 값 > 출력하는 역할
			
* 우리가 쓰던 EL과 비슷하다!
			
2. Selection Variable Expression, 선택 변수 표현식
	- *{}
	- 객체/맵 프로퍼티  > 출력 
	- th:object 속성과 같이 사용
			
3. Message Expression, 메세지 표현식
	- #{}
	- 스프링 메세지 > 전용 출력
			
4. Link URL Expression, 링크 주소 표현식 
	` @{}
	- 링크의 URL > 전용 출력
			
5. Fragment Expression, 조각 표현식
	- ~{}
	- 조각 페이지 삽입(include 지시자 or 타일즈 > 유사 ) 
	- 타임리프 > th:XXX 옆엔 속성이다.
  • "/m3" Controller 부분
    • 기존 스프링과 동일 model에 담아서 전송, map을 사용해도 되고 int나 String으로 넣어도 model에 넣는데 차이 없음
@GetMapping("/m3")
	public String m3(Model model) {
		int num = mapper.getNum();
		String txt = mapper.getTxt();
		BoardDTO dto = mapper.getDTO();
		
		Map<String, String> map = new HashMap<String, String>();
		map.put("dog","강아지");
		map.put("cat","고양이");
		
		model.addAttribute("num", num);
		model.addAttribute("txt", txt);
		model.addAttribute("now", Calendar.getInstance());
		model.addAttribute("dto", dto);
		model.addAttribute("map", map);
		
		return "m3";
	}
<h2>변수 표현식</h2>
<div>${num}</div>
<div>10</div>

<!--★ EL 표현은 th:text와 함께 적어야 한다. 그렇지 않으면 변환없이 그대로 나온다. -->

<div th:text="${num}">24000</div>
<div th:text="${num}">200</div>
<div th:text="100"></div>
<!-- 
th 접두어가 붙어야 한다. 태그 내부에 200이라는 값은 th:text="${num}"으로 덮어써진다.
프론트엔드 개발자가 작업하던 페이지를 넘겨주면 백엔드 개발자는 th:text 부분만 채우면 된다.
-->

<h2>타입별</h2>
<div>숫자 : 100</div>
<div th:text="${num}">숫자 : </div>

<div>숫자 : <span th:text="${num}"></span></div>
<div>문자 : <span th:text="${txt}"></span></div>
<div>객체 : <span th:text="${dto}"></span></div>
<div>멥 : <span th:text="${map}"></span></div>				

<!-- th:text를 붙여야 하는 것은 중요하다.
model에 넣은 이름대로 꺼내서 쓴다. -->
<!-- setter를 정의해야하고 .멤버변수명으로 꺼내쓰는건 동일하고 
getSubject() 이렇게 메서드 방식으로 사용하는 방법도 중요함 -->

<h2>복합 데이터(객체, 맵) 프로퍼티</h2>

 <!-- 변수 표현식 -->
 <div>제목 : <span th:text="${dto.getSubject()}"></span></div>
 <div>제목 : <span th:text="${dto.subject}"></span></div>
 <div>아이디 : <span th:text="${dto.id}"></span></div>
 <div>날짜 : <span th:text="${dto.regdate}"></span></div>

 <!-- 
 *{} > 선택 변수 표현식 앞에 : 부분을 생략한다. 반복되면 이득이다.th:object와 함께 쓰이는 것 확인
 -->
 <div th:object="${dto}">
    <!-- <div>제목 : <span th:text="${dto.subject}"></span></div> -->
    <div>제목 : <span th:text="*{subject}"></span></div>
    <div>아이디 : <span th:text="*{id}"></span></div>
    <div>날짜 : <span th:text="*{regdate}"></span></div>
 </div>
 
<!-- 객체가 가진 method를 뷰단에서도 유지가 된다는걸 인지하고 있어야함 -->
 <div th:text="${map.get('dog')}"></div>
 <div th:text="${map.dog}"></div>
 <div th:text="${map.cat}"></div>

 <div th:object="${map}">
    <div th:text="*{dog}"></div>
    <div th:text="*{cat}"></div>
 </div>
  • 실행결과

다국어처리

  • application.properties에 아래 내용 추가
    • 스프링 메시지를 사용하는 방식인데 이걸 정리하기엔 양이 방대하다. 나중에 따로 타픽을 잡어서 해야할 듯?
# 스프링 메시지
spring.messages.basename=messages
spring.messages.encoding=UTF-8
  • messages_en.properties
language=English

item.name=Mouse
item.color=Black
item.price=30000

hello=Hello, I m {0}.
  • messages_ja.properties
language=日本語

item.name=マウス
item.color=黒
item.price=30000

hello=こんにちは、私は{0}です。
  • messages.properties
language=한국어

item.name=마우스
item.color=검정색
item.price=30000

hello=안녕하세요. 저는 {0}입니다.
  • m3.html
	 <h2>다국어 지원<small>스프링 메세지</small></h2>
	 <div class="message" title="사용 언어" th:text="#{language}"></div>
	 
	 <div class="message" title="상품">
	 	<div>상품명 : <span th:text="#{item.name}"></span></div>
	 	<div>색상 : <span th:text="#{item.color}"></span></div>
	 	<div>가격 : <span th:text="#{item.price}"></span></div>
	 </div>
	 
	 <div th:text="#{hello('홍길동')}"></div>
	 <div th:text="#{hello(#{item.name})}"></div>
  • 결과로 브라우저 기본 언어 우선순위에 따라서 적절한 message.properties를 골르고 내용을 읽어 번역이 된다. 

언어 우선순위

  • 일본어를 우선순위 1등으로 했을 때

 

스프링 부트에선 jsp를 기본으로 지원하지 않는다. dependency에 따로 추가해야 하는 것들과 만들어야할 폴더들과 설정이 필요하다. 다음의 순서대로 jsp 사용을 위한 설정을 해보자. 

프로젝트셋팅

  • Dependency 추가 
    • Spring Web

디펜던씨

  • STS4에서 import
    • Maven > Existing Maven Projects 클릭해서  해당 프로젝트 위치 넣고 pom.xml 체크하여 import 한다. 

임포트

  • pom.xml 파일에 아래의 의존성 추가 
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
  • application.properties에 View Resolver 설정 추가
# 서버 포트 번호
server.port = 8092

# JSP View Resoler, webapp, WEB-INF 폴더가 없음 
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
  • src/main 폴더에 webapp 폴더 만들어서 그 안에 WEB-INF, 그 안에 views 폴더 만들어서 test.jsp 파일 만들기

이런구조

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<title>Insert title here</title>
</head>
<body>
<!-- localhost:8092 -->
	<h1>Spring Boot</h1>
	<!-- test.jsp -->
	<div>${name}</div>
</body>
</html>
  • com.test.bootjsp.controller 패키지를 만들고 TestController.java 작성
  • 브라우저로 http://localhost:8092/test.do 접속하면 끝
    • boot는 context root를 별다른 설정을 안하면 / 로 지정이 된다. 
    • application.properties에 아래 한줄을 추가하면 그 path로 컨텍스트루트가 지정이 된다.
      • server.servlet.context-path=/지정할컨텍스트루트

 

● 실행결과

+ Recent posts