본문 바로가기
Study/WEB

[Spring Boot With Kotlin] Web Server Tutorial (Feat. Intellij)

by Becoming a Hacker 2022. 7. 23.
반응형

가장 먼저 jetbrains에서 Intellij IDEA를 설치합니다.

 

다운로드 IntelliJ IDEA: 우수성과 인체 공학이 담긴 JetBrains Java IDE

 

www.jetbrains.com

 

상용 버전의 경우 Spring을 바로 설정할 수 있지만 커뮤니티 버전의 경우 https://start.spring.io/ 사이트에 접속하여 Spring 프로젝트를 생성해야만 합니다.

 

저는 Spring Boot와 Kotlin을 이용하여 웹을 개발할 예정이기 때문에 아래와 같이 설정한 뒤, GENERATE를 이용하여 압축 파일로 저장하였습니다.

Project : Gradle Project

Language : Kotlin

Spring Boot : 2.7.2

Java : 17

Dependencies : Spring Web, Mustache, Spring Data JPA, Spring Boot DevTools, H2 Database

 

저장한 압축 파일을 해제한 뒤, Intellij의 Import 기능을 이용하여 해당 프로젝트를 불러옵니다.

 

간단한 웹 서버를 만들기 위하여 패키지 내에 컨트롤러를 생성합니다.

위치 : src/main/kotlin/{package}/

HtmlController.kt

package com.kotlinspring.vulnweb.vulnweb

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping

@Controller
class HtmlController {

    @GetMapping("/")
    fun index(model:Model): String{
        model["title"] = "title"
        return "index" // templates file name without file extension
    }
}
 

그리고 해당 결과를 사용자에게 보여줄 Mustache 템플릿도 생성합니다.

위치 : src/main/resources/templates/

header.mustache

<html>
<head>
    <title>{{title}}</title>
</head>
<body>

 

footer.mustache

</body>
</html>

 

index.mustache

{{> header}}
<h1>{{title}}</h1>
{{> footer}}

 

이후, 프로젝트를 실행 시 http://localhost:8080/에 웹 서버가 연결된 것을 확인할 수 있습니다.


JPA를 통한 DB와 웹 서버 연결

Spring Boot 2.x 버전부터는 CGLIB Proxy 방식으로 Bean을 관리하고 있으며, CGLIB Proxy는 Target Class를 상속받아 생성하기 때문에 "open" 예약어를 이용하여 해당 클래스를 상속이 가능한 상태로 만들어야 합니다.

 

all-open 플러그인은 아래와 같은 특정 어노테이션이 있을 경우 open을 자동으로 추가시키기 때문에 편의성을 크게 증가시킬 수 있습니다.

  • @Component
  • @Async
  • @Transactional
  • @Cacheable
  • @SpringBootTest
  • @Configuration, @Controller, @RestController, @Service, @Repository, @Component

 

all-open 플러그인을 사용하기 위해 build.gradle.kts 에 아래와 같은 문구를 추가합니다.

plugins {
  ...
  kotlin("plugin.allopen") version "1.6.21"
}

allOpen {
  annotation("javax.persistence.Entity")
  annotation("javax.persistence.Embeddable")
  annotation("javax.persistence.MappedSuperclass")
}

 

그리고 속성과 생성자 매개변수를 동시에 선언할 수 있는 Kotlin의 문법을 이용하여 Entity를 만듭니다.

위치 : src/main/{package}/

Entities.kt

package com.kotlinspring.vulnweb.vulnweb

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
class Article (
        var title: String,
        var content: String,
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long? = null
        )

@Entity
class Users(
        var loginId: String,
        var password: String,
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long? = null
)

 

Spring Data JPA 저장소를 선언합니다.

위치 : src/main/{package}/

Repositories.kt

package com.kotlinspring.vulnweb.vulnweb

import org.springframework.data.repository.CrudRepository

interface ArticleRepository: CrudRepository<Article, Long>{
    fun findByTitle(title: String): Article?
}

interface UserRepository: CrudRepository<Users, Long>{
    fun findByLoginId(loginId: String): Users?
}

 

이제 Mustache의 템플릿을 조금 더 있어보이게 바꿔보겠습니다.

위치 : src/main/resources/templates/

index.mustache

{{> header}}
<h1>{{title}}</h1>
<div class="articles">
    {{#articles}}
            <section>
                <header class="article-header">
                    <h2 class="article-title"><a href="/article/{{title}}">{{title}}</a></h2>
                </header>
            </section>
    {{/articles}}
</div>
{{> footer}}

 

article.mustache

{{> header}}
<section class="article">
    <header class="article-header">
        <h1 class="article-title">{{article.title}}</h1>
    </header>
    <div class="article-description">
        {{article.content}}
    </div>
</section>
{{> footer}}

 

수정된 템플릿을 처리할 수 있도록 HtmlController도 업데이트 합니다.

위치 : src/main/kotlin/{package}/

HtmlController.kt

package com.kotlinspring.vulnweb.vulnweb

import org.springframework.http.HttpStatus
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.server.ResponseStatusException

@Controller
class HtmlController(private val repository: ArticleRepository) {

    @GetMapping("/")
    fun index(model:Model): String{
        model["title"] = "HackSMS"
        model["articles"] = repository.findAll().map{ it.render() }
        return "index" // templates file name without file extension
    }

    @GetMapping("/article/{title}")
    fun article(@PathVariable title: String, model: Model): String{
        val article = repository
                .findByTitle(title)
                ?.render()
                ?:throw ResponseStatusException(HttpStatus.NOT_FOUND,"This article does not Exist")
        model["title"] = article.title
        model["article"] = article
        return "article"
    }

    fun Article.render() = RenderedArticle(
            title,
            content
    )

    data class RenderedArticle(
            val title: String,
            val content: String
    )
}

 

마지막으로 데이터를 초기화해줄 클래스를 추가합니다.

위치 : src/main/kotlin/{package}/

DataConfiguration.kt

package com.kotlinspring.vulnweb.vulnweb

import org.springframework.boot.ApplicationRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class DataConfiguration {
    @Bean
    fun databaseInitializer(userRepository: UserRepository, articleRepository: ArticleRepository) = ApplicationRunner{
        userRepository.save(Users("admin","admin123"))
        articleRepository.save(Article("New Article","Hellow World!"))
        articleRepository.save(Article("Second Article","Bye Bye~"))
    }
}

 

이후, 프로젝트 실행 시 정상적으로 Repository에 저장된 게시글이 보이는 것을 확인할 수 있습니다.


RestController를 이용한 API 서버 구축

이제 @RestController Annotation을 이용하여 HTTP API를 구현해보겠습니다.

위치 : src/main/kotlin/{package}/

HttpControllers.kt

package com.kotlinspring.vulnweb.vulnweb

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException

@RestController
@RequestMapping("/api/article")
class HttpControllers(private val repository: ArticleRepository) {

    @GetMapping("/")
    fun findAll() = repository.findAll()

    @GetMapping("/{title}")
    fun findOne(@PathVariable title: String) = repository.findByTitle(title) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND,"This article does not exist")
}

@RestController
@RequestMapping("/api/users")
class UsersController(private val repository: UserRepository){

    @GetMapping("/")
    fun findAll() = repository.findAll()

    @GetMapping("/{loginId}/{password}")
    fun findOne(@PathVariable loginId: String, @PathVariable password: String) : Users{
        val ret = repository.findByLoginId(loginId)
        if (ret != null) {
            if(ret.password != password){
                ret = throw ResponseStatusException(HttpStatus.BAD_REQUEST,"incorret Password")
            }
        }else{
            ret = throw ResponseStatusException(HttpStatus.NOT_FOUND,"This user does not exist")
        }
        return ret
    }
}

 

이후, 프로젝트 실행 시 /api 경로로 접근할 경우 RestAPI가 정상적으로 동작하는 것을 확인할 수 있습니다.

댓글