diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e04d215 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM adoptopenjdk/openjdk11:latest + +ARG HEAP_SIZE +ENV HEAP_SIZE=${HEAP_SIZE:-512M} +ARG NEW_SIZE +ENV NEW_SIZE=${NEW_SIZE:-256M} + +ADD . . + +RUN ["./gradlew", "clean", "build", "-x","test"] + +ENTRYPOINT java -server -Xms${HEAP_SIZE} -Xmx${HEAP_SIZE} -XX:NewSize=${NEW_SIZE} -XX:MaxNewSize=${NEW_SIZE} -Djava.net.preferIPv4Stack=true -Djava.security.egd=file:/dev/./urandom -jar /build/libs/springwebproject-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 9a7cabb..f227dcf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.3.2.RELEASE" id("io.spring.dependency-management") version "1.0.9.RELEASE" + id("org.jetbrains.kotlin.plugin.allopen") version "1.3.61" + id("org.jetbrains.kotlin.plugin.jpa") version "1.3.61" + id( "org.jetbrains.kotlin.plugin.noarg") version "1.3.61" kotlin("jvm") version "1.3.72" kotlin("plugin.spring") version "1.3.72" } @@ -16,17 +19,28 @@ repositories { } dependencies { + implementation("com.h2database:h2") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } + testImplementation("org.assertj:assertj-core:3.13.2") +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") } tasks.withType { - useJUnitPlatform() + useJUnitPlatform { + includeTags("fast") + } } tasks.withType { @@ -35,3 +49,4 @@ tasks.withType { jvmTarget = "11" } } + diff --git a/deploy/mark.yaml b/deploy/mark.yaml new file mode 100644 index 0000000..c0e82fd --- /dev/null +++ b/deploy/mark.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mark-app + namespace: mark-space + labels: + app: mark-app +spec: + selector: + matchLabels: + app: mark-app + replicas: 1 + template: + metadata: + labels: + app: mark-app + spec: + containers: + - name: mark-app + image: idock.daumkakao.io/mark-d2hub/mark-repository:latest + imagePullPolicy: Always + resources: + limits: + cpu: 1 + memory: 300Mi + requests: + cpu: 1 + memory: 1Gi + ports: + - containerPort: 8080 + env: + - name: application_name + value: mark-app + - name: instance_name + value: "8080" + readinessProbe: + httpGet: + path: /health_check.html + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + lifecycle: + preStop: + exec: + command: ["/bin/sleep","2"] + terminationGracePeriodSeconds: 60 \ No newline at end of file diff --git a/kotlinweb/test.mv.db b/kotlinweb/test.mv.db new file mode 100644 index 0000000..84fa75b Binary files /dev/null and b/kotlinweb/test.mv.db differ diff --git a/src/main/kotlin/com/example/kotlinweb/board/controller/PostController.kt b/src/main/kotlin/com/example/kotlinweb/board/controller/PostController.kt new file mode 100644 index 0000000..9d72853 --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/controller/PostController.kt @@ -0,0 +1,32 @@ +package com.example.kotlinweb.board.controller +import com.example.kotlinweb.board.model.Post +import com.example.kotlinweb.board.service.BoardService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +class BoardController(private val boardService: BoardService) { + + @GetMapping(value = ["/board"]) + fun findAllPosts(): ResponseEntity { + return ResponseEntity(boardService.findPosts(), HttpStatus.OK); + } + + @PostMapping(value = ["/board"]) + fun savePost(@RequestBody post: Post): ResponseEntity { + println("hello") + return ResponseEntity(boardService.savePost(post), HttpStatus.CREATED); + } + + @PutMapping(value = ["/board/{id}"]) + fun updatePost(@RequestBody post: Post, @PathVariable id: String): ResponseEntity { + return ResponseEntity(boardService.updatePost(post), HttpStatus.OK); + } + + @DeleteMapping(value = ["/board"]) + fun deletePost(@RequestBody post: Post): ResponseEntity { + return ResponseEntity(boardService.deletePost(post), HttpStatus.OK); + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/event/PostEntityListener.kt b/src/main/kotlin/com/example/kotlinweb/board/event/PostEntityListener.kt new file mode 100644 index 0000000..a065f5e --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/event/PostEntityListener.kt @@ -0,0 +1,43 @@ +package com.example.kotlinweb.board.event + +import com.example.kotlinweb.board.model.Post +import javax.persistence.* + + +class PostEntityListener { + + @PostLoad + fun postLoad(post: Post?) { + println("post load: $post") + } + + @PrePersist + fun prePersist(post: Post?) { + println("pre persist: $post") + } + + @PostPersist + fun postPersist(post: Post?) { + println("post persist: $post") + } + + @PreUpdate + fun preUpdate(post: Post?) { + println("pre update: $post") + } + + @PostUpdate + fun postUpdate(post: Post?) { + println("post update: $post") + } + + @PreRemove + fun preRemove(post: Post?) { + println("pre remove: $post") + } + + @PostRemove + fun postRemove(post: Post?) { + println("post remove: $post") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/event/PostSaveEvent.kt b/src/main/kotlin/com/example/kotlinweb/board/event/PostSaveEvent.kt new file mode 100644 index 0000000..ad3b151 --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/event/PostSaveEvent.kt @@ -0,0 +1,14 @@ +package com.example.kotlinweb.board.event + +data class PostSaveEvent( + private val postId: Long, + private val message: String +) { + fun getPostId(): Long { + return postId + } + + fun getMessage(): String { + return message + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/event/PostSaveEventHandler.kt b/src/main/kotlin/com/example/kotlinweb/board/event/PostSaveEventHandler.kt new file mode 100644 index 0000000..18bc3a7 --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/event/PostSaveEventHandler.kt @@ -0,0 +1,22 @@ +package com.example.kotlinweb.board.event + +import com.example.kotlinweb.board.model.Notify +import com.example.kotlinweb.board.service.NotifyDTO +import com.example.kotlinweb.board.service.NotifyService +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class PostSaveEventHandler( + private val notifyService: NotifyService +) { + // @EventListener + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun postSaveEventListener(event: PostSaveEvent) { + println("Event Listen") + notifyService.saveNotify(Notify.of(event.getPostId().toString(), event.getMessage())) + notifyService.sendNotify(NotifyDTO(event.getMessage(), "0")) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/model/Notify.kt b/src/main/kotlin/com/example/kotlinweb/board/model/Notify.kt new file mode 100644 index 0000000..f2bdc41 --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/model/Notify.kt @@ -0,0 +1,37 @@ +package com.example.kotlinweb.board.model + +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import java.time.LocalDateTime +import javax.persistence.* + + +@Entity +class Notify( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long?, + + @Column + var postId: String, + + @Column + val message: String + +) { + companion object { + fun of(postId: String, message: String): Notify { + return Notify(null, postId, message) + } + } + + @Column + @CreationTimestamp + lateinit var createDate: LocalDateTime + + @Column + @UpdateTimestamp + lateinit var updateDate: LocalDateTime + +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/model/Post.kt b/src/main/kotlin/com/example/kotlinweb/board/model/Post.kt new file mode 100644 index 0000000..fa89d92 --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/model/Post.kt @@ -0,0 +1,36 @@ +package com.example.kotlinweb.board.model + +import com.example.kotlinweb.board.event.PostEntityListener +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import java.time.LocalDateTime +import javax.persistence.* + +@Entity +@EntityListeners(PostEntityListener::class) +class Post( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long, + @Column + var title: String, + @Column + val writer: String, + @Column + var text: String + +) { + @Column + var hitCount: Int = 0 + + @Column + @CreationTimestamp + lateinit var createDate: LocalDateTime + + @Column + @UpdateTimestamp + lateinit var updateDate: LocalDateTime + + fun increaseHitCount() = hitCount++ +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/repository/NotifyRepository.kt b/src/main/kotlin/com/example/kotlinweb/board/repository/NotifyRepository.kt new file mode 100644 index 0000000..6eb4a1d --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/repository/NotifyRepository.kt @@ -0,0 +1,10 @@ +package com.example.kotlinweb.board.repository + +import com.example.kotlinweb.board.model.Notify +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface NotifyRepository : JpaRepository { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/repository/PostRepository.kt b/src/main/kotlin/com/example/kotlinweb/board/repository/PostRepository.kt new file mode 100644 index 0000000..633cbeb --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/repository/PostRepository.kt @@ -0,0 +1,10 @@ +package com.example.kotlinweb.board.repository + +import com.example.kotlinweb.board.model.Post +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface PostRepository : JpaRepository { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/service/BoardService.kt b/src/main/kotlin/com/example/kotlinweb/board/service/BoardService.kt new file mode 100644 index 0000000..0e0fe2a --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/service/BoardService.kt @@ -0,0 +1,49 @@ +package com.example.kotlinweb.board.service + +import com.example.kotlinweb.board.event.PostSaveEvent +import com.example.kotlinweb.board.model.Post +import com.example.kotlinweb.board.repository.PostRepository +import org.springframework.context.ApplicationEventPublisher +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + + +@Service +class BoardService( + private val postRepository: PostRepository, + private val applicationEventPublisher: ApplicationEventPublisher +) { + + @Transactional + fun savePost(post: Post) { + postRepository.save(post) + applicationEventPublisher.publishEvent(PostSaveEvent(post.id, "게시글 작성이 완료 되었습니다.")) + } + + @Transactional(readOnly = true) + fun findPostById(id: Long): Post? { + return postRepository.findByIdOrNull(id) + } + + @Transactional(readOnly = true) + fun findPosts(): MutableList { + return postRepository.findAll() + } + + @Transactional + fun updatePost(post: Post) { + print(post.toString()) + var foundPost: Post = postRepository.findByIdOrNull(post.id) as Post + foundPost.text = post.text + foundPost.title = post.title + } + + @Transactional + fun deletePost(post: Post) { + var foundPost: Post = postRepository.findById(post.id) as Post + foundPost?.let { + postRepository.delete(post) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/board/service/NotifyService.kt b/src/main/kotlin/com/example/kotlinweb/board/service/NotifyService.kt new file mode 100644 index 0000000..72d4aeb --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/board/service/NotifyService.kt @@ -0,0 +1,32 @@ +package com.example.kotlinweb.board.service + +import com.example.kotlinweb.board.model.Notify +import com.example.kotlinweb.board.repository.NotifyRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.lang.RuntimeException + +@Service +class NotifyService(private val notifyRepository: NotifyRepository) { + +// 트랜잭션이 이미 커밋되거나 롤백되었지만 트랜잭션 리소스가 여전히 활성화되어 있고 액세스할 수 있을 수 있다. +// 결과적으로, 이 시점에서 트리거된 데이터 액세스 코드는 별도의 트랜잭션에서 실행해야 한다고 명시적으로 선언하지 않는 한, +// 원래 트랜잭션에 여전히 "참여"하여 일부 정리(더 이상 커밋하지 않음!)를 수행할 수 있다. +// 따라서: 여기서 호출되는 모든 트랜잭션 작업에는 REQUIRES_NEW 사용하십시오. + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun saveNotify(notify: Notify) { + notifyRepository.save(notify) +// throw RuntimeException() + } + + fun sendNotify(notifyDTO: NotifyDTO) { + println("Notify Send = $notifyDTO") + } +} + +data class NotifyDTO( + val message: String, + val userId: String +) diff --git a/src/main/kotlin/com/example/kotlinweb/config/H2ServerConfiguration.kt b/src/main/kotlin/com/example/kotlinweb/config/H2ServerConfiguration.kt new file mode 100644 index 0000000..6b225cd --- /dev/null +++ b/src/main/kotlin/com/example/kotlinweb/config/H2ServerConfiguration.kt @@ -0,0 +1,25 @@ +package com.example.kotlinweb.config + +import com.zaxxer.hikari.HikariDataSource +import org.h2.tools.Server +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class H2ServerConfiguration { + + @Bean + @ConfigurationProperties("spring.datasource.hikari") + fun dataSource(): HikariDataSource = run { + val server: Server = h2TcpServer() + return HikariDataSource() + } + + fun h2TcpServer(): Server = Server.createTcpServer( + "-tcp", + "-tcpAllowOthers", + "-ifNotExists", + "-tcpPort", "9092" + "").start() + +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/kotlinweb/controller/HealthController.kt b/src/main/kotlin/com/example/kotlinweb/controller/HealthController.kt index fed3266..c232c49 100644 --- a/src/main/kotlin/com/example/kotlinweb/controller/HealthController.kt +++ b/src/main/kotlin/com/example/kotlinweb/controller/HealthController.kt @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ResponseBody @Controller -class HealthController{ +class HealthController { @get:ResponseBody @get:GetMapping(value = ["/health_check.html"]) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..0f65d15 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + h2: + console: + enabled: true + profiles: + active: local + datasource: + hikari: + jdbc-url: jdbc:h2:mem:test + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + database-platform: H2 + show-sql: true + hibernate: + ddl-auto: update diff --git a/src/test/kotlin/com/example/kotlinweb/board/service/BoardServiceTest.kt b/src/test/kotlin/com/example/kotlinweb/board/service/BoardServiceTest.kt new file mode 100644 index 0000000..2e46407 --- /dev/null +++ b/src/test/kotlin/com/example/kotlinweb/board/service/BoardServiceTest.kt @@ -0,0 +1,73 @@ +package com.example.kotlinweb.board.service + +import com.example.kotlinweb.board.model.Post +import com.example.kotlinweb.board.repository.PostRepository +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ApplicationEventPublisher +import javax.transaction.Transactional + +@SpringBootTest +internal class BoardServiceTest( + @Autowired val repository: PostRepository, + @Autowired val applicationEventPublisher: ApplicationEventPublisher +) { + + @Test + @Tag("fast") + @Transactional + fun savePostTest() { + val post = Post(1L, "Test Title", "KDH", "memo") + val boardService: BoardService = BoardService(repository, applicationEventPublisher) + boardService.savePost(post) + val savedPost: Post? = boardService.findPostById(1L) + savedPost?.let { + Assertions.assertEquals(post.title, savedPost.title) + assertThat(post.title).isEqualTo(savedPost.title) + } + } + + @Test + @Tag("fast") + @Transactional + fun readPostsTest() { + val post1 = Post(1L, "Test Title", "KDH", "memo1") + val post2 = Post(2L, "Test Title2", "KDH2", "memo2") + val boardService: BoardService = BoardService(repository, applicationEventPublisher) + boardService.savePost(post1) + boardService.savePost(post2) + + val posts: MutableList = boardService.findPosts() + val savedPost1: Post = posts[0] + val savedPost2: Post = posts[1] + //assertion + assertAll( + "posts", + { Assertions.assertEquals(post1.title, savedPost1.title) }, + { Assertions.assertEquals(post2.writer, savedPost2.writer) } + ) + //assertJ + assertAll( + { assertThat(post1.title).isEqualTo(savedPost1.title) }, + { assertThat(post2.writer).isEqualTo(savedPost2.writer) } + ) + } + + @Test + @Tag("fast") + @Transactional + fun increaseHitCount() { + val post1 = Post(1L, "Test Title", "KDH", "memo1") + post1.increaseHitCount() + //assertion + Assertions.assertEquals(post1.hitCount, 1) + //assertJ + assertThat(post1.hitCount).isEqualTo(1) + + } +} \ No newline at end of file