Thymeleaf Sample 예제

|

Thymeleaf 라이브러리

타임리프(Thymeleaf) 라이브러리는 템플릿을 이용해서 HTML 페이지를 편리하게 만들 수 있도록 도와줍니다. Spring Initializer 웹사이트에서 ‘Thymeleaf’ 항목을 선택하고 프로젝트를 생성하면 됩니다.


SampleController.kt

@Controller
class SampleController {

    @GetMapping("/hello")
    fun hello(model: Model) {
        model.addAttribute("message", "Hello, SnowDeer.")
    }
}


hello.html

resources/templates/ 디렉토리 아래에 hello.html 파일을 생성합니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
<h2>Thymeleaf Template Sample</h2>
</body>
</html>

그런 다음 웹브라우저에서 http://localhost:8080/hello 페이지에 접속하면 방금 생성한 hello.html 페이지가 뜨는 것을 확인할 수 있습니다.

그리고 위 코드를 아래처럼 수정을 하면,

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
<h2 th:text="${message}">Thymeleaf Template Sample</h2>
</body>
</html>

SampleController에서 model.addAttribute 메소드로 넘긴 파라메터가 잘 전송된 것을 확인할 수 있습니다.


utext 명령어

utext 명령어는 HTML 문자열을 출력하는 명령어입니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
<h2 th:text="${message}">Thymeleaf Template Sample</h2>

th:text
<div th:text='${"<h3>" + user.id + "</h3>"}'/>

<br>

th:utext
<div th:utext='${"<h3>" + user.id + "</h3>"}'/>

</body>
</html>

를 실행해보면

th:text
<h3>snowdeer</h3>

th:utext
snowdeer

와 같이 출력되는 것을 확인할 수 있습니다.


리스트 출력하기

리스트를 위한 반복문은 th:each 명령어를 이용해서 처리할 수 있습니다.

@Controller
class SampleController {

    @GetMapping("/userList")
    fun userList(model: Model) {

        val list = ArrayList<UserVO>()
        for (i in 0 until 10) {
            val user = UserVO(uno = i.toLong(), id = "snowdeer$i", password = "1234", name = "Seo $i")

            list.add(user)
        }

        model.addAttribute("list", list)
    }
}


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>

<table border="1">
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>RegDate</td>
    </tr>

    <tr th:each="user : ${list}">
        <td th:text="${user.id}"/>
        <td th:text="${user.name}"/>
        <td th:text="${#dates.format(user.regDate, 'yyyy-MM-dd hh-mm-ss')}"/>
    </tr>

</table>

</body>
</html>

그리고 th:each에서 추가로 반복 상태에 대한 변수 값을 아래의 예제처럼 사용할 수도 있습니다.

    <tr th:each="user, iter : ${list}">
        <td th:text="${iter.index}"/>
        <td th:text="${iter.size}"/>
        <td th:text="${user.id}"/>
        <td th:text="${user.name}"/>
        <td th:text="${#dates.format(user.regDate, 'yyyy-MM-dd hh-mm-ss')}"/>
    </tr>

그 외에도 count, odd, even, first, last 등의 값을 사용할 수도 있습니다.


특정 범위내 유효한 변수

특정 범위내에 유효한 변수는 th:with를 이용해서 구현할 수 있습니다. 예를 들면 다음과 같습니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>

<table border="1" th:with="target='snowdeer5'">
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>RegDate</td>
    </tr>

    <tr th:each="user : ${list}">
        <td th:text="${user.id == target ? 'SNOWDEER': user.id}"/>
        <td th:text="${user.name}"/>
        <td th:text="${#dates.format(user.regDate, 'yyyy-MM-dd hh-mm-ss')}"/>
    </tr>

</table>

</body>
</html>


if, ~unless 구문

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>

<table border="1" th:with="target='snowdeer5'">
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>RegDate</td>
    </tr>

    <tr th:each="user : ${list}">
        <td th:text="${user.id == target ? 'SNOWDEER': user.id}"/>
         <td th:if="${user.name}">
            <a href="/profile" th:if="${user.name=='Seo 3'}">Seo 3 Profile</a>
            <p th:unless="${user.name=='Seo 3'}">Nothing</p>
        </td>
        <td th:text="${#dates.format(user.regDate, 'yyyy-MM-dd hh-mm-ss')}"/>
    </tr>

</table>

</body>
</html>


자바스크립트에 연결하기

@Controller
class SampleController {

    @GetMapping("/useJavascript")
    fun useJavascript(model: Model) {
        val text = "Hello"

        model.addAttribute("text", text)

    }
}


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>

<script th:inline="javascript">
    var text = [[${text}]]
    alert(text)
</script>

</body>
</html>

No identifier specified for entity 문제 해결

|

No identifier specified for entity 문제 해결 방법

만약

nested exception is org.hibernate.AnnotationException: No identifier specified for entity: com.snowdeer.database.board.Member

와 같은 오류가 발생하면 해당 클래스의 @Id 어노테이션 항목을 살펴봐야 합니다.

import org.springframework.data.annotation.Id
import javax.persistence.Entity
import javax.persistence.Table

@Entity
@Table(name = "tbl_members")
data class Member(@Id
                  var uid: String = "",
                  var upw: String = "",
                  var uname: String = "")

만약 이와 같이 import org.springframework.data.annotation.Idimport되어 있다면, import javax.persistence.Id로 수정하면 됩니다.

JPA를 사용한 예제

|

Spring Data JPA

JPA(Java Persistence API)를 사용할 경우 데이터베이스 종류에 무관하게 객체 지향 프로그램의 객체를 다루듯이 데이터베이스의 엔티티를 쉽게 다룰 수 있습니다.

여기서는 H2 데이터베이스를 이용해서 JPA 예제를 사용해봅니다.


H2 데이터베이스 포함된 프로젝트 생성

Spring Initializer 웹사이트에서 ‘H2 Database’ 항목과 ‘Spring Data JPA’ 항목을 선택하고 프로젝트를 생성해줍니다.


application.yaml 수정

application.properties 파일을 application.yaml 파일로 변경하고 파일 안에 다음 내용을 작성합니다.

spring:
  h2:
    console:
      enabled: true
      path: /h2_db

  datasource:
    hikari:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:file:./h2_db
      username: snowdeer
      password: 1234
      connection-test-query: SELECT 1

  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: false
    show-sql: true

logging:
  level:
    org:
      hibernate: info


DatabaseConfiguration.kt

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.PropertySource
import javax.sql.DataSource

@Configuration
@PropertySource("classpath:application.yaml")
class DatabaseConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    fun hikariConfig(): HikariConfig {
        return HikariConfig()
    }

    @Bean
    @Throws(Exception::class)
    fun dataSource(): DataSource {
        val dataSource = HikariDataSource(hikariConfig())
        println(dataSource.toString())
        return dataSource
    }
}



Board 엔티티 생성

Board.kt 파일을 생성합니다.

import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.sql.Timestamp
import javax.persistence.*

@Entity
@Table(name = "tbl_boards")
data class Board(@Id
                 @GeneratedValue(strategy = GenerationType.AUTO)
                 var bno: Long = 0,

                 var title: String = "",
                 var writer: String = "",
                 var content: String = "",

                 @CreationTimestamp
                 var regdate: Timestamp? = null,

                 @UpdateTimestamp
                 var updatedate: Timestamp? = null)


BoardRepository.kt

import com.snowdeer.database.board.Board
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository

interface BoardRepository : CrudRepository<Board, Long> {

    fun findBoardByTitle(title: String): List<Board>

    fun findByWriter(writer: String): List<Board>

    fun findByWriterContaining(writer: String): List<Board>

    fun findByTitleContaining(title: String): List<Board>

    fun findByTitleContainingOrContentContaining(title: String, content: String): List<Board>

    fun findByBnoGreaterThanOrderByBnoDesc(bno: Long): List<Board>

    fun findByBnoGreaterThanOrderByBnoDesc(bno: Long, paging: Pageable): List<Board>

    fun findByBnoGreaterThan(bno: Long, paging: Pageable): List<Board>

    @Query("SELECT b FROM Board b WHERE b.title LIKE %?1% AND b.bno > 0 ORDER BY b.bno DESC")
    fun findByTitle(title: String): List<Board>
}


Unittest로 확인

위와 같이 인터페이스만 선언해줘도 각 메소드들은 잘 동작합니다. 정말 잘 동작하는지 Unittest를 이용해서 확인해보도록 합시다.

BoardRepositoryTests.kt라는 이름으로 파일을 만들었습니다.

import com.snowdeer.database.board.Board
import com.snowdeer.database.repository.BoardRepository
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort

@SpringBootTest
class BoardRepositoryTests {

    @Autowired
    private lateinit var boardRepo: BoardRepository

    @Test
    fun inspect() {
        val clz = boardRepo.javaClass
        println("[snowdeer] inspect - ${clz.name}")

        val interfaces = clz.interfaces
        for (inter in interfaces) {
            println("[snowdeer] inspect - ${inter.name}")
        }

        val superClass = clz.superclass
        println("[snowdeer] inspect - ${superClass.name}")
    }

    @Test
    fun testInsert() {
        val board = Board()
        board.title = "제목"
        board.content = "내용"
        board.writer = "snowdeer"

        boardRepo.save(board)
    }

    @Test
    fun testRead() {
        val board = boardRepo.findById(1L)
        println("[snowdeer] testRead - $board")
    }

    @Test
    fun testUpdate() {
        val board = boardRepo.findById(1L)
        board.get().title = "수정된 제목"
        boardRepo.save(board.get())
    }

    @Test
    fun testByTitle() {
        println("[snowdeer] testByTitle")
        val list = boardRepo.findBoardByTitle("제목")
        for (b in list) {
            println("[snowdeer] ${b.bno}. ${b.title} - ${b.regdate}")
        }
    }

    @Test
    fun testByWriter() {
        println("[snowdeer] testByWriter")
        val list = boardRepo.findByWriter("snowdeer")
        for (b in list) {
            println("[snowdeer] ${b.bno}. ${b.title} - ${b.regdate}")
        }
    }

    @Test
    fun testByTitleLike() {
        println("[snowdeer] testByTitleLike")
        val list = boardRepo.findByTitleContaining("목")
        for (b in list) {
            println("[snowdeer] ${b.bno}. ${b.title} - ${b.regdate}")
        }
    }

    @Test
    fun testBnoOrderByPaging() {
        println("[snowdeer] testBnoOrderByPaging")
        val paging = PageRequest.of(0, 10)
        val list = boardRepo.findByBnoGreaterThanOrderByBnoDesc(0L, paging)

        for (b in list) {
            println("[snowdeer] ${b.bno}. ${b.title} - ${b.regdate}")
        }
    }

    @Test
    fun testBnoPagingSort() {
        println("[snowdeer] testBnoPagingSort")
        val paging = PageRequest.of(0, 10, Sort.Direction.ASC, "bno")
        val list = boardRepo.findByBnoGreaterThan(0L, paging)

        for (b in list) {
            println("[snowdeer] ${b.bno}. ${b.title} - ${b.regdate}")
        }
    }

    @Test
    fun testFindByTitle() {
        println("[snowdeer] testFindByTitle")
        val list = boardRepo.findByTitle("제목")

        for (b in list) {
            println("[snowdeer] ${b.bno}. ${b.title} - ${b.regdate}")
        }
    }
}

Spring Data JPA

|

Spring Data JPA

JPA(Java Persistence API)는 Java를 이용해서 데이터를 관리하는 기법을 스펙으로 정리한 표준입니다. 데이터베이스와 관련된 기술과 스펙은 오랫동안 이슈가 되어왔고, Java 진영에서는 EJB라는 기술 스펙으로 Entity Bean이라는 데이터 처리 스펙을 정했으며 이게 JPA의 시초라고 볼 수 있습니다.


ORM

ORM(Object Relation Mapping)은 데이터베이스의 개체와 객체지향에서의 객체가 아주 유사하기 때문에 데이터베이스와 객체 지향을 한 번에 처리하기 위해서 나온 개념입니다.

ORM은 특정 언어에 종속적인 개념이 아니기 때문에 다양한 언어에서 ORM 지원 프레임워크를 많이 볼 수 있습니다.


JPA와 ORM

JPA는 ORM의 개념을 Java에서 구현하기 위한 스펙이라고 볼 수 있습니다. 기존의 JDBC 등을 이용해서 SQL 쿼리 등을 직접 작성했다면, ORM을 통해서 추상화된 객체 형태로 데이터베이스를 관리할 수 있습니다.


스프링 부트와 JPA

JPA는 하나의 스펙이기 때문에 다양한 프레임워크들에서 개별로 구현을 하고 있습니다. 스프링부트에서는 Hibernate를 이용해서 JPA를 지원하고 있습니다.


JPA의 특징

JPA를 이용하면 다음과 같은 장점이 있습니다.

  • 데이터베이스 관련 코드에 대한 유연함 획득
  • DB 설계와 Java 설계를 동시에 할 수 있음
  • 데이터베이스 종류와 독립적 - 특정 벤더에 종속적이지 않음

반대로 다음과 같은 단점도 있습니다.

  • 높은 러닝 커브(Learning Curve)
  • 객체지향 설계 사상이 반영되어야 함
  • 특정 데이터베이스에 최적화된 방법을 사용할 수 없음

Error 처리하기

|

Error 처리하는 방법

Json 파싱 에러 처리

ErrorHandler.kt 파일을 만들고 다음과 같이 작성하면 됩니다.

@ControllerAdvice
class ErrorHandler {

    @ExceptionHandler(JsonParseException::class)
    fun JsonParseExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<String> {
        return ResponseEntity("JSON Parsing Error", HttpStatus.BAD_REQUEST)
    }
}

하지만 이 경우는 String으로 리턴하기 때문에 RESTful API에는 어울리지 않습니다. 에러도 JSON 포맷으로 리턴하는 것이 좋습니다.


ErrorResponse.kt

data class ErrorResponse(val error: String, val message: String)


ErrorHandler.kt

@ControllerAdvice
class ErrorHandler {

    @ExceptionHandler(JsonParseException::class)
    fun JsonParseExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<ErrorResponse> {
        return ResponseEntity(
                ErrorResponse("JSON Parsing Error", exception.message ?: ""),
                HttpStatus.BAD_REQUEST)
    }
}


다양한 Exception 처리

여기에 좀 더 다양한 Exception 처리를 하려면 다음과 같이 추가로 예외 처리를 작성할 수 있습니다.

ErrorResponse.kt

data class ErrorResponse(val error: String, val message: String)


UserNotFoundException.kt

class UserNotFoundException(message: String) : Exception(message)


ErrorHandler.kt

@ControllerAdvice
class ErrorHandler {

    @ExceptionHandler(JsonParseException::class)
    fun JsonParseExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<ErrorResponse> {
        return ResponseEntity(
                ErrorResponse("JSON Parsing Error", exception.message ?: ""),
                HttpStatus.BAD_REQUEST)
    }

    @ExceptionHandler(UserNotFoundException::class)
    fun UserNotFoundExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<ErrorResponse> {
        return ResponseEntity(
                ErrorResponse("User Not Found", exception.message ?: ""),
                HttpStatus.NOT_FOUND)
    }
}


UserController.kt

@RestController
class UserController {

    @Autowired
    private lateinit var service: UserService

    @GetMapping("/user/{id}")
    fun getUser(@PathVariable id: Int): ResponseEntity {
        val user = service.getUser(id) ?: throw UserNotFoundException("user($id) does not exist.")
        return ResponseEntity(user, HttpStatus.OK)
    }
}
</pre>