Develop/Kotlin

Kotlin Void vs Unit vs Nothing | Kotlin Void vs Unit vs Nothing

Void자바의 voidjava.lang 패키지안에 있는 Void 클래스 : java의 primitive type인 void를 래핑하는 객체이다. (int wrapper인 Integer과 같다고 보면 된다.)자바에서는 void 말고 Void를 리턴해야하는 경우가 많지 않다. : 제네릭에서 Void를 사용하는 정도의 용례package java.lang;/** * The {@code Void} class is an uninstantiable placeholder class to hold a * reference to the {@code Class} object representing the Java keyword * void. * * @author unascribed * @since 1.1 */publi..

Kotlin Void vs Unit vs Nothing | Kotlin Void vs Unit vs Nothing

728x90

Void

자바의 void

java.lang 패키지안에 있는 Void 클래스 : java의 primitive type인 void를 래핑하는 객체이다. (int wrapper인 Integer과 같다고 보면 된다.)

자바에서는 void 말고 Void를 리턴해야하는 경우가 많지 않다. : 제네릭에서 Void를 사용하는 정도의 용례

package java.lang;

/**
 * The {@code Void} class is an uninstantiable placeholder class to hold a
 * reference to the {@code Class} object representing the Java keyword
 * void.
 *
 * @author  unascribed
 * @since   1.1
 */
public final
class Void {

    /**
     * The {@code Class} object representing the pseudo-type corresponding to
     * the keyword {@code void}.
     */
    @SuppressWarnings("unchecked")
    public static final Class<Void> TYPE = (Class<Void>) Class.getPrimitiveClass("void");

    /*
     * The Void class cannot be instantiated.
     */
    private Void() {}
}

Void를 코틀린에서 사용할 때

fun returnTypeAsVoidAttempt1() : Void {
    println("Trying with Void return type")
}

이때 컴파일이 되지 않는다.

Error: Kotlin: A 'return' expression required in a function with a block body ('{...}')

그래서 코틀린에서 Void 객체를 만들어서 return 해야하나. 위의 Void 클래스 정의와 같이 private constructor로 인스턴트화가 막혀있다.

따라서 위 경우에는 어쩔수 없이 Void를 nullable로 만들고 Void? null을 리턴해야한다.

fun returnTypeAsVoidAttempt1() : Void? {
    println("Trying with Void return type")
    return null
}

작동하는 솔루션이 있긴하나. java의 void와 같이 동일한 결과를 낼 수 있는 방법으로 Unit 타입이 있다. (의미 있는 것을 반환하지 않는 함수의 반환 유형)

https://www.baeldung.com/kotlin/void-type

 

Unit

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/#unit

https://kotlinlang.org/docs/functions.html#unit-returning-functions

반환값이 필요없을 때, 함수의 반환타입으로 Unit을 사용한다.

반환타입이 Unit일 경우네는, return Unit;. return; 모두 선택적으로 작성해도 된다.

fun printHello(name: String?): Unit{
    if (name != null) {
        println("hello $name")
    } else {
        println("Hi")
    }
}

java에서의 void와 대응한다. 그러나 자바에서 void는 반환 값이 없음을 의미하는 특수 타입이지만, Unit은 class로 정의된 일반타입이다.

Unit은 기본 반환 유형이므로 그리고 return 타입 명시를 안했을 때도 함수가 작동한다.

// Unit.kt
package kotlin

/**
 * The type with only one value: the `Unit` object. This type corresponds to the `void` type in Java.
 */
public object Unit {
    override fun toString() = "kotlin.Unit"
}

따라서 Unit 타입을 반환하는 함수는 return을 생략해도 암묵적으로 Unit 타입 객체를 리턴한다 (싱글턴 객체이므로 객체 생성은 하지 않는다.)

즉 기원적으로 Void는 Java를 사용할 때 만들어진 클래스, Unit은 Kotlin을 사용할 때 만들어진 클래스인 듯

 

Nothing

kotlin에서는 throw가 expression 이다. 그래서 이때 throw의 타입이 Nothing 이다.

이 타입은 값이 없으며, 도달할 수 없는 코드 위치를 표한하는데 사용된다. Nothing을 사용하면, 도달할 수 없는 코드의 위치를 컴파일단에서 체크가 가능하다.

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

컴파일단 체크 덕분에 잠재적인 버그와 좋지 않은 코드로부터 확인이 가능하다. 반환 유형이 Nothing인 함수가 호출되면 이 함수 호출 이상으로 실행되지 않고, 컴파일러에서 경고를 내보낸다.

fun invokeANothingOnlyFunction() {
    fail("nothing")
        println("hello") // Unreachable code
}

또한 Nothing은 type inference (타입추론)에도 사용이 가능하다.

  • null을 사용하여 초기화된 값일 때의 타입추론
  • 구체적인 타입을 결정하는데 사용할 수 없는 경우에서의 타입추론
val x = null // "type : Nothing?"
val l = listOf(null) // "type : List<Nothing?>"

Nothing은 java에서 대응되는 개념이 없으며, 자바에서는 주로 throw 처리를 할 때 void를 사용했었다.

// Nothing.kt
package kotlin

/**
 * Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
 * if a function has the return type of Nothing, it means that it never returns (always throws an exception).
 */
public class Nothing private constructor()

마찬가지로 Nothing도 객체를 생성할 수 없다. (값을 가지지 않는다.)

따라서 Nothing이 값을 가질 수 있는 경우는 Nothing? 에서 null 이 할당되었을 때 뿐이다.

Void

Java's void

The Void class in the java.lang package: It's an object that wraps Java's primitive type void. (Think of it like Integer being the wrapper for int.)

In Java, there aren't many cases where you need to return Void instead of void — it's mostly used in generics.

package java.lang;

/**
 * The {@code Void} class is an uninstantiable placeholder class to hold a
 * reference to the {@code Class} object representing the Java keyword
 * void.
 *
 * @author  unascribed
 * @since   1.1
 */
public final
class Void {

    /**
     * The {@code Class} object representing the pseudo-type corresponding to
     * the keyword {@code void}.
     */
    @SuppressWarnings("unchecked")
    public static final Class<Void> TYPE = (Class<Void>) Class.getPrimitiveClass("void");

    /*
     * The Void class cannot be instantiated.
     */
    private Void() {}
}

Using Void in Kotlin

fun returnTypeAsVoidAttempt1() : Void {
    println("Trying with Void return type")
}

This won't compile.

Error: Kotlin: A 'return' expression required in a function with a block body ('{...}')

So should we create a Void object and return it in Kotlin? Well, as you can see from the Void class definition above, instantiation is blocked by a private constructor.

So in this case, you have no choice but to make Void nullable as Void? and return null.

fun returnTypeAsVoidAttempt1() : Void? {
    println("Trying with Void return type")
    return null
}

While this is a working solution, there's a better way to achieve the same result as Java's void — the Unit type. (It's the return type for functions that don't return anything meaningful.)

https://www.baeldung.com/kotlin/void-type

 

Unit

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/#unit

https://kotlinlang.org/docs/functions.html#unit-returning-functions

When you don't need a return value, you use Unit as the function's return type.

When the return type is Unit, both return Unit; and return; are optional — you can write either or omit them entirely.

fun printHello(name: String?): Unit{
    if (name != null) {
        println("hello $name")
    } else {
        println("Hi")
    }
}

It corresponds to void in Java. However, while void in Java is a special type meaning "no return value," Unit is a regular type defined as a class.

Since Unit is the default return type, functions work even when you don't explicitly specify a return type.

// Unit.kt
package kotlin

/**
 * The type with only one value: the `Unit` object. This type corresponds to the `void` type in Java.
 */
public object Unit {
    override fun toString() = "kotlin.Unit"
}

So a function that returns Unit implicitly returns the Unit object even if you omit the return statement. (Since it's a singleton object, no new object is created.)

In other words, it seems like Void is a class that originated from Java, while Unit is a class that originated from Kotlin.

 

Nothing

In Kotlin, throw is an expression. And the type of that throw expression is Nothing.

This type has no value and is used to mark code locations that can never be reached. By using Nothing, unreachable code locations can be checked at compile time.

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

Thanks to compile-time checks, you can catch potential bugs and bad code. When a function with a return type of Nothing is called, execution never continues beyond that function call, and the compiler emits a warning.

fun invokeANothingOnlyFunction() {
    fail("nothing")
        println("hello") // Unreachable code
}

Nothing can also be used in type inference.

  • Type inference for values initialized with null
  • Type inference when a concrete type cannot be determined
val x = null // "type : Nothing?"
val l = listOf(null) // "type : List<Nothing?>"

Nothing has no corresponding concept in Java. In Java, void was typically used when handling throw.

// Nothing.kt
package kotlin

/**
 * Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
 * if a function has the return type of Nothing, it means that it never returns (always throws an exception).
 */
public class Nothing private constructor()

Likewise, Nothing cannot be instantiated either. (It holds no value.)

Therefore, the only case where Nothing can hold a value is when null is assigned to Nothing?.

댓글

Comments

Develop/Kotlin

Ktor 찍먹하기 - 간단 HTTP API 작성 | Trying Out Ktor - Writing a Simple HTTP API

ktor 공식문서 따라해 보는 중 : https://ktor.io/docs/gradle.html#create-entry-point Adding Ktor to an existing Gradle project | Ktor ktor.io나의 소스코드 : https://github.com/jyami-kim/ktor-sample 1. 서버 실행을 위한 기본 골격- ktor embeddedServer로 실행 : https://ktor.io/docs/gradle.html#create-embedded-server- ktor engineMain으로 실행 : https://ktor.io/docs/gradle.html#create-engine-maina. embeddedServer package com.exampleimpo..

Ktor 찍먹하기 - 간단 HTTP API 작성 | Trying Out Ktor - Writing a Simple HTTP API

728x90

ktor 공식문서 따라해 보는 중 : https://ktor.io/docs/gradle.html#create-entry-point

 

Adding Ktor to an existing Gradle project | Ktor

 

ktor.io

나의 소스코드 : https://github.com/jyami-kim/ktor-sample

 

1. 서버 실행을 위한 기본 골격

- ktor embeddedServer로 실행 : https://ktor.io/docs/gradle.html#create-embedded-server
- ktor engineMain으로 실행 : https://ktor.io/docs/gradle.html#create-engine-main

a. embeddedServer 

package com.example

import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
        }
    }.start(wait = true)
}

기본 서버 실행을 위한 골격. routing에 해당하는 부분을 주로 아래와 같이 configureXXX 와 같은 형태의 플러그인 형식으로 빼서, 분리하는 패턴을 갖고있음. (코틀린의 확장함수 사용)

// com.jyami.Application

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
    }.start(wait = true)
}

// com.jyami.plugins.Routing

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

 

위와같이 간단한 코드만 짜서 서버 RUN을 시키면? 
서버 실행 속도가 이렇게까지 빨라도 되는건가.. spring안쓰고 간단하게 서버 만들고 싶을 때 쓰면 좋을 것 같다.

 

b. engineMain (HOCON 포맷)

위 예제는 embeddedServer로 짜는 방법을 적었는데, 만약 EngineMain 방식으로 ktor 서버를 짜고 싶다면 조금 다른 방식이다. 

// main.kotlin.com.jyami

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    configureRouting()
}


// main.kotlin.com.jyami.plugins

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}


// resource

ktor {
    deployment {
        port = 8080
    }
    application {
        modules = [ com.jyami.ApplicationKt.module ]
    }
}

HOCON 포맷이라고한다. resources 폴더안에 application.conf 파일을 이용해서 실행시킬 포트나, entry point에대한 애플리케이션 설정을 작성한다. application.conf 관련 여러 설정들 : https://ktor.io/docs/configurations.html

 

2. gradle 설정 확인

gradle application 플러그인 : https://docs.gradle.org/current/userguide/application_plugin.html

 

The Application Plugin

This plugin also adds some convention properties to the project, which you can use to configure its behavior. These are deprecated and superseded by the extension described above. See the Project DSL documentation for information on them. Unlike the extens

docs.gradle.org

 

실행가능한 JVM 애플리케이션을 만들 수 있는 기능을 제공함. 
개발도중에 쉽게 애플리케이션을 시작할 수 있도록 해주며, TAR, AIP과 같이 애플리케이션 패키지도 지원해줌

application 플러그인 안에 java / distribution 플러그인이 모두 내장되어있다. 각각 main에 대한 소스 셋, 배포를 위한 패키징 기능을 가진 플러그인 인듯 하다.

application 플러그인을 사용할 때  configuration은 main class (i.e. entry point)를 지정하는 건 꼭 필요하다.

plugins {
    application
    kotlin("jvm") version "1.6.0"
}
application {
    mainClass.set("com.kakao.ApplicationKt")
}

실행시 intellij runner사용도 되고, ./gradlew run을 사용해도 된다.

 

3. 튜토리얼 1: HTTP API 만들기

build.gradle 분석

dependencies {
    implementation("io.ktor:ktor-server-core:$ktor_version")
    implementation("io.ktor:ktor-server-netty:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-serialization:$ktor_version")

    testImplementation("io.ktor:ktor-server-tests:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
  • ktor-server-core : ktor가 제공하는 핵심 컴포넌트 제공
  • ktor-server-netty : 네티엔진을 사용하여 서버 기능을 사용할 수 있게한다. 외부 애플리케이션 컨테이너에 의존하지 않고도 가능.
  • logback-classic : SLF4J 구현
  • ktor-serialization : 코틀린 객체를 JSON과 같은 직렬화 형태로 변환해준다. 
  • ktor-server-test-host : http 스택을 사용하지 않고도 서버 테스트가 가능함. unit test로 사용가능함

 

ConentNegotiation

fun Application.module() {
    install(ContentNegotiation){
        json()
    }
}
  • 클라이언트와 서버간의 미디어타입을 중계해준다. : Accept, Content-Type Header
  • kotlinx.serialization 라이브러리 혹은 그외의 것 (Gson, Jackson)을 사용하여 컨텐츠를 특정 포맷으로 serializing/deserializing한다. (json으로 하겠지)

ContentNegotiation 플러그인을 사용하기 위하여 애플리케이션 초기화 코드에 install 함수를 사용하여 해당하는 ConetentNegotiation 플러그인을 넘겨준다.

ktor에서는 Content-Type으로 다양한 내장 함수를 제공하고 있는데 

  • kotlinx.serializtion : JSON, Protobuf, CBOR
  • Gson : JSON
  • Jackson : JSON

그이외에 커스텀한 Content-Type을 제공하고 싶다면 아래와 같이 ContentNegotiation의 register 메서드를 사용하여 등록하면 된다

install(ContentNegotiation) {
    register(ContentType.Application.Json, CustomJsonConverter())
    register(ContentType.Application.Xml, CustomXmlConverter())
}

 

request, response 파라미터 제어

1. path parameter

get("{id}") {
    val id = call.parameters["id"] ?: return@get call.respondText(
        "Missing or malformed id",
        status = HttpStatusCode.BadRequest
    )
    val customer = customerStorage.find {it.id == id} ?: return@get call.respondText (
        "No customer with id $id",
        status = HttpStatusCode.NotFound
    )
    call.respond(customer)
}

인덱스 접근자 함수 : call.parameters["myParamName"]
요청 Path를 기본적으로 string으로 가져오게 된다.

 

2. request body

post {
    val customer = call.receive<Customer>()
    customerStorage.add(customer)
    call.respondText ("Customer stored correctly", status = HttpStatusCode.Created)
}

call.recevie<T> : 제네릭 변수를 사용하여 호출한 request body를 자동으로 코틀린 객체로 역직렬화 한다. 

동시에 여러 요청이 접근될 때 문제는 이 실습의 범위를 벗어난다 : 동시에 요청/스레드에서 액세스 할 수 있는 데이터 구조 혹은 코드를 작성하면 될 것

 

3. response body

get {
    if (customerStorage.isNotEmpty()) {
        call.respond(customerStorage)
    } else {
        call.respondText("No customers found", status = HttpStatusCode.NotFound)
    }
}
  • call.respond() : kotlin 객체를 가져와 해당 객체가 지정된 형식으로 직렬화하여 http 응답을 반환한다.
  • call.respondText() : Text를 응답으로 보낼 때의 respond를 간단하게 구현해둔 함수이다. 문자열 응답을 반환한다.

 

Routing

Route 확장함수를 이용하여 경로를 정의하였다. 이렇게 할 경우 아무래도 라우팅만 역할을 빼서 코드를 관리할 수 있어서 좋아보인다. 
Application.module에 있는 라우팅 블록 내부에 각 경로를 직접 추가할 수 있긴하지만, 파일 경로들을 그룹화하여 관리하는 것이 유지보수에 더 좋다. 

Route 확장함수만을 사용하여 경로를 정의할 경우 실제 Ktor 서버에는 적용되지 않는데, 맨 처음 ktor 시작하기에 했던 것처럼, route를 Application에 등록해주는 과정이 필요하다. 즉, Route.module에 등록된 경로들을 Application.module에 등록하는 방식으로 관리하자.

Application의 확장함수를 사용함으로써 application에 등록할 path를 route dsl을 이용하여 등록할 수 있으며, 이 dsl에는 사실상 Route 모듈에 등록된 경로를 Application에 경로를 등록하도록 도와주는 것으로 보인다.

fun Route.customerRouting() {
    route("/customer") {
    	get{}
    }
}

fun Application.registerCustomerRoutes() {
    routing {
        customerRouting()
    }
}

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        registerCustomerRoutes()
        install(ContentNegotiation){
            json()
        }
    }.start(wait = true)
}

 

4. Ktor 테스트

ktor-server-test-host : netty를 시작하지 않고도 endpoint 테스트가 가능하다.
테스트 request를 실행하기 위한 몇가지 헬퍼 메서드가 프레임워크에서 제공되며, 그중 하나가 TestApplication 이다.

HOCON방식 : 한번에 모든 configuration이 등록가능한 구조

internal class OrderRoutingKtTest {

    @Test
    fun testGetOrder() {
        withTestApplication({ module() }) {
            handleRequest(HttpMethod.Get, "/order/2020-04-06-01").apply {
                assertEquals(
                    """{"number":"2020-04-06-01","contents":[{"item":"Ham Sandwich","amount":2,"price":5.5},{"item":"Water","amount":1,"price":1.5},{"item":"Beer","amount":3,"price":2.3},{"item":"Cheesecake","amount":1,"price":3.75}]}""",
                    response.content
                )
                assertEquals(HttpStatusCode.OK, response.status())
            }
        }

    }
    
}

HOCON 포맷인 경우에 사용이 가능한 것으로 보인다. (embeddedServer 방식으로 짜는 경우에는 httpClient를 사용한 직접 호출 방식을 이용하는 것으로 보인다 : https://ktor.io/docs/testing.html#end-to-end)

핵심은 withTestApplication인데, 테스트로 실행하려는 애플리케이션을 주입한다. (Application.module() 형태로된 메서드면 무엇이든 주입이 가능한 것으로 보인다.)

HOCON 포맷의 경우에는, main에 넣기전 main밖에 있는 Application 확장함수에 정의되어있어, 해당 애플리케이션에 대한 모든 설정이 등록된채 테스트가 가능하고, 따라서 위와 같이 module() 을 넣고 테스트를 해도 무관하다.

 

embeddedServer 방식 : 필요모듈만 configuration에 등록하여 테스트할 수 있는 구조

fun Application.testModule(){
    registerOrderRoute()
    install(ContentNegotiation){
        json()
    }
}

@Test
fun testGetOrder() {
    withTestApplication({ testModule() }) {
        handleRequest(HttpMethod.Get, "/order/2020-04-06-01").apply {
            assertEquals(
                """{"number":"2020-04-06-01","contents":[{"item":"Ham Sandwich","amount":2,"price":5.5},{"item":"Water","amount":1,"price":1.5},{"item":"Beer","amount":3,"price":2.3},{"item":"Cheesecake","amount":1,"price":3.75}]}""",
                response.content
            )
            assertEquals(HttpStatusCode.OK, response.status())
        }
    }

}

다만 embeddedServer 방식으로 withTestApplication을 작성하는 경우에는, main() 함수안에 직접 선언이 되어있어 애플리케이션에 대한 모든 테스트는 불가능하고 위에서 내가 정의한 configureRouting(), registerCustomRouting(), registerOrderRouting()과 같이 Application 확장함수 단위로 테스트가 가능하다. 

다만 여기서 주의해야할 점은 configureRouting은 단일하게 넣어도 테스트가 가능하나, registerOrderRouting, registerCustomRouting의 경우에는 불가능하다 왜냐하면 이것들을 직접 넣고 돌리게되면, Application에 등록해둔 ContentNegotiation이 빠지게 될것이고, serialize과정에 문제가 생겨서 테스트가 실패하게 될 것이다. 따라서 테스트시 두개의 configuration을 함께 등록하여 테스트해야한다.

위와같이 테스트가 가능하게 되면서, ktor을 사용할 경우에는 모듈별로, 설정별로 테스트가 손쉽게 될 것으로 보인다.
사실, embeddedServer나 HOCON이나 하나의 Application 확장함수를 만들어서 테스트하는건 같아서 입맛대로 사용하도록하자.

 

Following along with the ktor official docs: https://ktor.io/docs/gradle.html#create-entry-point

 

Adding Ktor to an existing Gradle project | Ktor

 

ktor.io

My source code: https://github.com/jyami-kim/ktor-sample

 

1. Basic Skeleton for Running a Server

- Running with ktor embeddedServer: https://ktor.io/docs/gradle.html#create-embedded-server
- Running with ktor engineMain: https://ktor.io/docs/gradle.html#create-engine-main

a. embeddedServer 

package com.example

import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
        }
    }.start(wait = true)
}

This is the basic skeleton for running a server. The routing part is typically extracted into a plugin-style pattern like configureXXX as shown below, separating concerns. (Using Kotlin's extension functions)

// com.jyami.Application

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
    }.start(wait = true)
}

// com.jyami.plugins.Routing

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

 

If you write just this simple code and RUN the server? 
Is it even okay for the server startup to be this fast.. Seems like it would be great for quickly spinning up a server without using Spring.

 

b. engineMain (HOCON Format)

The example above showed how to write it using embeddedServer. If you want to build a ktor server using the EngineMain approach, it's a slightly different method. 

// main.kotlin.com.jyami

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    configureRouting()
}


// main.kotlin.com.jyami.plugins

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}


// resource

ktor {
    deployment {
        port = 8080
    }
    application {
        modules = [ com.jyami.ApplicationKt.module ]
    }
}

It's called the HOCON format. You write the application configuration — such as the port to run on and the entry point — in an application.conf file inside the resources folder. Various settings for application.conf: https://ktor.io/docs/configurations.html

 

2. Checking the Gradle Configuration

Gradle application plugin: https://docs.gradle.org/current/userguide/application_plugin.html

 

The Application Plugin

This plugin also adds some convention properties to the project, which you can use to configure its behavior. These are deprecated and superseded by the extension described above. See the Project DSL documentation for information on them. Unlike the extens

docs.gradle.org

 

It provides the ability to create executable JVM applications. 
It makes it easy to start the application during development and also supports application packaging like TAR and ZIP.

The application plugin has both the java and distribution plugins built in. Each seems to be a plugin that provides source sets for main and packaging functionality for distribution, respectively.

When using the application plugin, specifying the main class (i.e. entry point) in the configuration is mandatory.

plugins {
    application
    kotlin("jvm") version "1.6.0"
}
application {
    mainClass.set("com.kakao.ApplicationKt")
}

You can run it using the IntelliJ runner or by using ./gradlew run.

 

3. Tutorial 1: Building an HTTP API

Analyzing build.gradle

dependencies {
    implementation("io.ktor:ktor-server-core:$ktor_version")
    implementation("io.ktor:ktor-server-netty:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-serialization:$ktor_version")

    testImplementation("io.ktor:ktor-server-tests:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
  • ktor-server-core: Provides the core components offered by ktor
  • ktor-server-netty: Enables server functionality using the Netty engine, without depending on an external application container.
  • logback-classic: SLF4J implementation
  • ktor-serialization: Converts Kotlin objects into serialized formats like JSON. 
  • ktor-server-test-host: Allows server testing without using the HTTP stack. Can be used as unit tests.

 

ContentNegotiation

fun Application.module() {
    install(ContentNegotiation){
        json()
    }
}
  • Mediates media types between the client and server: Accept, Content-Type Header
  • Uses the kotlinx.serialization library or others (Gson, Jackson) for serializing/deserializing content into a specific format (JSON in our case).

To use the ContentNegotiation plugin, you pass the ContentNegotiation plugin using the install function in the application initialization code.

Ktor provides various built-in functions for Content-Type:

  • kotlinx.serialization: JSON, Protobuf, CBOR
  • Gson: JSON
  • Jackson: JSON

If you want to provide a custom Content-Type beyond those, you can register it using ContentNegotiation's register method like this:

install(ContentNegotiation) {
    register(ContentType.Application.Json, CustomJsonConverter())
    register(ContentType.Application.Xml, CustomXmlConverter())
}

 

Controlling Request and Response Parameters

1. path parameter

get("{id}") {
    val id = call.parameters["id"] ?: return@get call.respondText(
        "Missing or malformed id",
        status = HttpStatusCode.BadRequest
    )
    val customer = customerStorage.find {it.id == id} ?: return@get call.respondText (
        "No customer with id $id",
        status = HttpStatusCode.NotFound
    )
    call.respond(customer)
}

Index accessor function: call.parameters["myParamName"]
It retrieves the request path as a string by default.

 

2. request body

post {
    val customer = call.receive<Customer>()
    customerStorage.add(customer)
    call.respondText ("Customer stored correctly", status = HttpStatusCode.Created)
}

call.receive<T>: Uses a generic type parameter to automatically deserialize the request body into a Kotlin object. 

Handling concurrent request access is beyond the scope of this tutorial — you'd just need to write data structures or code that can handle simultaneous requests/threads.

 

3. response body

get {
    if (customerStorage.isNotEmpty()) {
        call.respond(customerStorage)
    } else {
        call.respondText("No customers found", status = HttpStatusCode.NotFound)
    }
}
  • call.respond(): Takes a Kotlin object, serializes it into the specified format, and returns it as an HTTP response.
  • call.respondText(): A convenience function for sending text as a response. Returns a string response.

 

Routing

Routes are defined using Route extension functions. This approach is nice because it lets you separate and manage just the routing logic on its own. 
While you could add each route directly inside the routing block in Application.module, grouping file paths together is better for maintainability. 

If you only define routes using Route extension functions, they won't actually be applied to the Ktor server. As we did in the initial ktor setup, you need to register the routes with the Application. In other words, manage it by registering the routes defined in Route.module into Application.module.

By using Application extension functions, you can register paths to the application using the route DSL. This DSL essentially helps register the routes defined in the Route module into the Application.

fun Route.customerRouting() {
    route("/customer") {
    	get{}
    }
}

fun Application.registerCustomerRoutes() {
    routing {
        customerRouting()
    }
}

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        registerCustomerRoutes()
        install(ContentNegotiation){
            json()
        }
    }.start(wait = true)
}

 

4. Testing in Ktor

ktor-server-test-host: Allows endpoint testing without starting Netty.
The framework provides several helper methods for executing test requests, and one of them is TestApplication.

HOCON approach: A structure where all configurations can be registered at once

internal class OrderRoutingKtTest {

    @Test
    fun testGetOrder() {
        withTestApplication({ module() }) {
            handleRequest(HttpMethod.Get, "/order/2020-04-06-01").apply {
                assertEquals(
                    """{"number":"2020-04-06-01","contents":[{"item":"Ham Sandwich","amount":2,"price":5.5},{"item":"Water","amount":1,"price":1.5},{"item":"Beer","amount":3,"price":2.3},{"item":"Cheesecake","amount":1,"price":3.75}]}""",
                    response.content
                )
                assertEquals(HttpStatusCode.OK, response.status())
            }
        }

    }
    
}

This appears to be usable with the HOCON format. (For the embeddedServer approach, it seems you'd use a direct call method with httpClient instead: https://ktor.io/docs/testing.html#end-to-end)

The key part is withTestApplication, which injects the application you want to run for testing. (It looks like any method in the form of Application.module() can be injected.)

With the HOCON format, since the Application extension function is defined outside of main before being put into main, all configurations for that application are registered and can be tested. So testing with module() as shown above works just fine.

 

embeddedServer approach: A structure where you can register only the needed modules in the configuration for testing

fun Application.testModule(){
    registerOrderRoute()
    install(ContentNegotiation){
        json()
    }
}

@Test
fun testGetOrder() {
    withTestApplication({ testModule() }) {
        handleRequest(HttpMethod.Get, "/order/2020-04-06-01").apply {
            assertEquals(
                """{"number":"2020-04-06-01","contents":[{"item":"Ham Sandwich","amount":2,"price":5.5},{"item":"Water","amount":1,"price":1.5},{"item":"Beer","amount":3,"price":2.3},{"item":"Cheesecake","amount":1,"price":3.75}]}""",
                response.content
            )
            assertEquals(HttpStatusCode.OK, response.status())
        }
    }

}

However, when writing withTestApplication with the embeddedServer approach, since everything is declared directly inside the main() function, you can't test the entire application at once. Instead, you can test at the Application extension function level — like configureRouting(), registerCustomRouting(), and registerOrderRouting() that I defined above. 

One thing to watch out for here: configureRouting can be tested on its own, but registerOrderRouting and registerCustomRouting cannot. That's because if you plug them in directly and run them, the ContentNegotiation registered in the Application will be missing, causing issues with the serialization process and making the tests fail. So you need to register both configurations together when testing.

With testing set up like this, using ktor makes it look easy to test by module and by configuration.
Honestly, whether you use embeddedServer or HOCON, it's the same in that you create one Application extension function and test with it, so just use whichever you prefer.

 

댓글

Comments

Develop/git-github

git 종종 사용하지만 까먹는 명령어들 | git commands I occasionally use but keep forgetting

+ 조금씩 추가할 예정이미 commit한 메세지 author 변경git rebase -i HEAD~N # 원하는 수정 커밋 범위 설정# 원하는 커밋 pick > e 로 수정 후git commit --amend --author="jyami-kim " # --amend로 author 변경git rebase --continue # 다음 rebase 진행 branch upstream 변경local branch가 remote branch를 추적하도록 git branch --set-upstream-to=origin/feature/TM-5644 feature/TM-5644 tag 추가git tag tag-namegit push origin tag-name git local 설정git config --local u..

git 종종 사용하지만 까먹는 명령어들 | git commands I occasionally use but keep forgetting

728x90

+ 조금씩 추가할 예정

이미 commit한 메세지 author 변경

git rebase -i HEAD~N # 원하는 수정 커밋 범위 설정

# 원하는 커밋 pick > e 로 수정 후

git commit --amend --author="jyami-kim <mor2222@naver.com>"   # --amend로 author 변경

git rebase --continue # 다음 rebase 진행

 

branch upstream 변경

local branch가 remote branch를 추적하도록

 git branch --set-upstream-to=origin/feature/TM-5644 feature/TM-5644

 

tag 추가

git tag tag-name
git push origin tag-name

 

git local 설정

git config --local user.email "mor2222@naver.com"
git config --local user.name "jyami-kim"

+ Will be updated gradually

Changing the author of an already committed message

git rebase -i HEAD~N # 원하는 수정 커밋 범위 설정

# 원하는 커밋 pick > e 로 수정 후

git commit --amend --author="jyami-kim <mor2222@naver.com>"   # --amend로 author 변경

git rebase --continue # 다음 rebase 진행

 

Changing branch upstream

Make a local branch track a remote branch

 git branch --set-upstream-to=origin/feature/TM-5644 feature/TM-5644

 

Adding a tag

git tag tag-name
git push origin tag-name

 

Git local configuration

git config --local user.email "mor2222@naver.com"
git config --local user.name "jyami-kim"

댓글

Comments

Develop/DevOps

규모 확장 시스템 설계 기본 | Fundamentals of Designing Systems for Scale

1. 단일 서버 DNS : 도메인 이름을 이용해 웹사이트에 접속한다. DNS에 질의하여 IP로 변환하는 과정이 필요하다. http 요청을 보내고 클라이언트는 응답을 받는다2. 데이터베이스 데이터베이스 : 웹/모바일 트래픽 처리 서버 (웹 계층)와 데이터베이스 서버 (데이터 계층) 분리를 시도한다.3. 로드밸런서 로드밸런서 : 부하 분산 집합에 속한 웹서버들에게 트래픽 부하를 고르게 분산한다 데이터베이스 : 다중화로 성능과 안정성을 보장한다 (master는 쓰기, slave는 읽기)4. 캐시 캐시 : 캐시를 이용해 서버의 요청이 보다 빨리 처리될 수 있게 한다. SOPF가 되지 않게 분산한다5. 콘텐츠 전송 네트워크 (CDN) CND : 정적 콘텐츠(이미지, 비디오, CSS, ..

규모 확장 시스템 설계 기본 | Fundamentals of Designing Systems for Scale

728x90

1. 단일 서버 
   DNS : 도메인 이름을 이용해 웹사이트에 접속한다. DNS에 질의하여 IP로 변환하는 과정이 필요하다. 
   http 요청을 보내고 클라이언트는 응답을 받는다

2. 데이터베이스
   데이터베이스 : 웹/모바일 트래픽 처리 서버 (웹 계층)와 데이터베이스 서버 (데이터 계층) 분리를 시도한다.

3. 로드밸런서
   로드밸런서 : 부하 분산 집합에 속한 웹서버들에게 트래픽 부하를 고르게 분산한다
   데이터베이스 : 다중화로 성능과 안정성을 보장한다 (master는 쓰기, slave는 읽기)

4. 캐시
   캐시 : 캐시를 이용해 서버의 요청이 보다 빨리 처리될 수 있게 한다. SOPF가 되지 않게 분산한다

5. 콘텐츠 전송 네트워크 (CDN)
   CND : 정적 콘텐츠(이미지, 비디오, CSS, JavaScript)는 웹 서버대신 CDN으로 성능을 보장한다

6. 무상태(stateless) 웹 계층
   웹서버 : 무상태 웹 계층을 갖게 함으로써 자동 규모 확장(autoScaling)이 가능하다
   공유 저장소 : 웹서버를 무상태 웹 계층으로 전환하면서, 필요한 상태정보들은 공유 저장소에 저장한다.

7. 데이터 센터
   로드밸런서 : 데이터 센터를 이용해 가용성을 높이고, 전 세계 어디서도 쾌적하게 사용이 가능하다. 지리적 라우팅을 이용하여 사용자의 위치에 따라 가장 가까운 위치의 데이터 센터로 안내한다.

8. 메시지 큐
   메시지 큐 : 서버간 결합을 느슨하게 하여 (loosely coupled) 규모 확장성이 보장되는 안정된 애플리케이션 구성이 가능하게 한다.
   
9. 로그, 메트릭 그리고 자동화
   도구 : 로그, 모니터링, 메트링, 자동화는 규모가 큰 서비스 관리에 용이하다

10. 데이터베이스의 규모 확장
    데이터베이스 : 샤딩으로(수평적 확장 = 서버증설) DB부하를 줄인다.

관련 책 : 가상 면접 사례로 배우는 대규모 시스템 설계 기초


   

1. Single Server 
   DNS : You access a website using a domain name. A process of querying DNS to resolve it into an IP address is needed. 
   You send an HTTP request and the client receives a response.

2. Database
   Database : We separate the server that handles web/mobile traffic (web tier) from the database server (data tier).

3. Load Balancer
   Load Balancer : Evenly distributes traffic load across web servers in the load-balanced set.
   Database : Replication ensures performance and reliability (master handles writes, slave handles reads).

4. Cache
   Cache : Uses cache so that server requests can be processed faster. Distribute caches to avoid becoming a SPOF.

5. Content Delivery Network (CDN)
   CDN : Static content (images, videos, CSS, JavaScript) is served through a CDN instead of web servers to ensure performance.

6. Stateless Web Tier
   Web Server : By making the web tier stateless, auto-scaling becomes possible.
   Shared Storage : When converting web servers to a stateless web tier, the necessary state information is stored in shared storage.

7. Data Centers
   Load Balancer : Using data centers improves availability and enables a smooth experience from anywhere in the world. GeoDNS routing directs users to the nearest data center based on their location.

8. Message Queue
   Message Queue : By loosely coupling servers, it enables building stable applications with guaranteed scalability.
   
9. Logging, Metrics, and Automation
   Tools : Logging, monitoring, metrics, and automation make it easier to manage large-scale services.

10. Database Scaling
    Database : Sharding (horizontal scaling = adding more servers) reduces the database load.

Related Book: System Design Interview – An Insider's Guide


   

댓글

Comments

Develop/DevOps

[Lettuce.io] 4.4 Reactive API 번역본

lettuce.io/core/release/reference/index.html#reactive-api Lettuce Reference GuideConnections to a Redis Standalone, Sentinel, or Cluster require a specification of the connection details. The unified form is RedisURI. You can provide the database, password and timeouts within the RedisURI. You have following possibilities to create a Rlettuce.io이 챕터의 목표 : Reacitve Stream 패턴의 이해와 reactive applica..

[Lettuce.io] 4.4 Reactive API 번역본

728x90

lettuce.io/core/release/reference/index.html#reactive-api

 

Lettuce Reference Guide

Connections to a Redis Standalone, Sentinel, or Cluster require a specification of the connection details. The unified form is RedisURI. You can provide the database, password and timeouts within the RedisURI. You have following possibilities to create a R

lettuce.io


이 챕터의 목표 : Reacitve Stream 패턴의 이해와 reactive application의 설계 방법의 전반적인 이해를 위함.

4.4.1 Motivation

비동기(Asynchronous)와 리액티브 방법론 (reactive methodolgies)는 네트워크나 디스크 IO로 인한 쓰레드의 대기시간 낭비 대신 시스템의 리소스를 좀 더 효율적으로 사용하도록 해준다. 

이런 스타일의 프로그래밍을 용이하게 하기위한 광법위한 기술이 존재하는데, java.util.concurrent.Future 부터 Akka와 같이 완전한 라이브러리들이 있다 (?)

 Project Reactor는 매우 방대한 셋의 asynchronous workflow를 위한 연산자들이 있고, 이것들은 더이상 프레임워크에 의존하지 않으며 매우 많은 Reactive Streams model을 지원한다.

4.4.2 Understanding Reactive Streams

Reactive Stream은 처음에는 표준의 non-blocking back pressure(이게 뭐지) 을 기반으로 한 asynchronous stream의 표준을 제공하기 위해 만들어졌다. 여기에는 런타임 환경 (JVM and Javascript)뿐만 아니라 네트워크 프로토콜도 포함하려하는 목표가 있었다.

Reactive Streams의 범위는 목표를 달성하는 데 필요한 작업이나 엔터티를 설명하는 최소한의 인터페이스, 메서드 및 프로토콜 집합을 찾는 것이다. 그 목표는 non-blocking back pressure 이 있는 asynchronous streams data이다. (?)

그것은 어플리케이션 코드에서 라이브러리를 연결할 필요 없이, 상호자용을 할 수 있도록 허용해주는 multiple reactive composition 라이브러리들 사이의 운영 표준이다.

Reactive Stream의 통합은 주로 Publicsher<T>Subscriber<T> 타입으로 복잡성을 숨기는 composition library (컴포지션 라이브러리)로 제공된다. Lettuce는 publisher를 Mono, Flux로 사용하는 Project Reactor를 사용한다.

Reacitve Stream에 대한 더 자세한 설명 : http://reactive-streams.org. 

4.4.3. Understanding Publishers

Aynchronouse processing은 IO작업이나 계산과 같은 작업을 호추한 스레드에서 분리한다. handle이 그 return 타입이며, 주로 java.util.concurrent.Future와 동일하거나 비슷하다. 유사하게 single object나 collection, exception을 return 한다. asynchronously하게 fetch 한 결과를 가져온 결과를 기억하는 것은 주로 한 프로우의 끝이 아니다. 한번 데이터가 확보되면 항상 혹은 조건부로 추가 요청을 발행할 수 있다. Java 8이나 Promise 패턴을 사용하면 futuers의 chaining은 연속적으로 그 이후의 asynchronous한 request가 발생하도록 할 수 있다. 한번 조건처리가 필요하면 asynchronous한 flow을 interrupted 시키고 synchronized 해야한다. 이런 접근 방식은 가능하지만 asynchronous의 장점을 완전히 활용하지는 않는다.

반면 Publisher<T> 객체는 다양하고 asynchronous한 질문에 다른방향으로 대답한다 : Pull pattern을 Push patter으로 변환한다.

push : 데이터 변경시 변경이 발새된 곳에서 데이터를 보내주는 방식 (Mono, Flux)
pull : 변경된 데이터가 있는지 질의 후 가져오는 방식 (클라이언트 요청 -> 서버 -> 응답)

Publisher<T>는 Futures처럼 single scalar value에 대한 emission 뿐만 아니라 emission sequences 심지어 infinite streams도 지원한다. 당신이 stream 작업을 시작하기만 하면 이 사실을 감사하게 여길 것이다. Project Reactor는 두개의 타입의 publisher가 사용된다 : Mono 와 Flux

Mono : 0부터 1까지의 event를 emit 한다.
Flux : 0
부터 N까지의 event를 emit 한다.

Publisher<T> 는 concurrency(동시성)나 asynchronicity(비동기성)의 특정 소스나 코드가 실행되는 방식에 편향되지 않는다. synchronous, asynchronous 둘다 ThreadPool 내에서 실행된다. Publisher<T>의 consumer는 실제 구현을 supplier에게 맡겨 나중에 supplier의 코드를 수정하지 않고도 변경이 가능하다.

Publisher<T>의 마지막 키포인트는 Publisher<T>를 가져올 때 처리가 되는 것이 아니라, Publisher<T>에 대한 observer가 subscribe하거나 신호가 보내지는 순간 프로세스가 실행된다는 것이다. 이것은 java.util.concurrent.Future와의 중요한 차이점이다. (Future는 created/obtained 가되는 순간 프로세스가 시작되기 때문이다.) 따라서 어떤 옵저버가 Publisher<T>를 subscribe 하지 않으면 아무일도 일어나지 않는다.

4.4.4. A word on the lettuce Reacitve API

모든 커맨드는 Flux<T>, Mono<T>, Mono<Void> 를 return 한다. 이것들은 Subscriber가 subscribe할 수 있다. 이 구독자는 Publisher <T>가 emit하는 아이템 또는 아이템의 시퀀스에 반응한다. 이 패턴은 Publisher<T>가 객체를 emit 할때까지 기다리는 동안 차단을 할 필요가 없어 동시작업(concurrent operations)에 용이하다. 대신, Publisher<T>가 어떤 futuer time에든 적절하게 반응하도록 준비가 되어있는 Subscriber 형태로 sentry를 만든다.

4.4.5 Consuming Publisher<T>

publisher를 사용할 때 가장먼저 고려할 일은 그들을 consume하는 것이다. Consuming a publisher의 의미는 subscribing을 의미한다. 

    @Test
    @DisplayName("Consuming Publisher 예제 : emit 된 모든 항목을 subscribe 하고 print 한다.")
    void consumingPublisher() {
        Flux.just("jyami", "java", "javabom").subscribe(new Subscriber<String>() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(3);
            }

            @Override
            public void onNext(String s) {
                System.out.println("Hello " + s + "!");
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("Complete consuming!");
            }
        });
    }
Hello jyami!
Hello java!
Hello javabom!
Complete consuming!

 

모든 구독자 또는 관찰자 (Subscriber or Observer)가 모든 이벤트에 대한 알림을 받고 완료된 이벤트도 받은걸을 확인 할 수 있다.한개의 Publisher<T>는 exception이 발생하거나, 그 Publisher<T>가 종료되었다고 onComplete()를 호출할 때까지 아이템을 내 보낸다. 그 이후에는 더이상 element가 emit되지 않는다.

subscribe의 호출로 한개의 Subscription이 등록되고 이것은 cancel을 허용하므로 더이상 이벤트를 수진하지 않는다. Publisher는 한번 Publisher<T>가 unsubscribed하면 구독 취소및 리소스 할당 해제(free resource)를 상호 운용할 수 있다. (?)

좀더 간단한 포맷의 Subscriber<T> 구현

    @Test
    @DisplayName("Subscriber<T> 의 더 간단한 구현")
    void subscriberSimpleImpl() {
        Flux.just("Ben", "Michael", "Mark").doOnNext(new Consumer<String>() {
            public void accept(String s) {
                System.out.println("Hello " + s + "!");
            }
        }).doOnComplete(new Runnable() {
            public void run() {
                System.out.println("Completed");
            }
        }).subscribe();
    }
    @Test
    @DisplayName("Subscriber<T> 의 더 간단한 구현 - 람다사용")
    void subscriberSimpleImplWithLambda() {
        Flux.just("Ben", "Michael", "Mark")
                .doOnNext(s -> System.out.println("Hello " + s + "!"))
                .doOnComplete(() -> System.out.println("Completed")).subscribe();
    }

Subscriber의 여러 연산자를 이용해서 element를 제어할 수 있다. take() 연산자는 관심이 있는 처음 N개 요소까지로 emit되는 수를 제한한다.

Hello Ben!
Hello Michael!

왜 여기서 Completed가 같이 출력이 안되는지를 모르겠다.

take 연산자는 기대하는 요소 수를() emit한 후 Publisher<T>에서 암시적으로 subscription을 취소한다.

Publisher<T> 에 대한 subscription은 다른 FluxSubscriber가 수행 할 수도 있다. 커스텀한 Publisher를 구현하지 않는한 항상 Subscriber를 사용하라.

그리고 항상 error handler를 올바르게 구현하는 것을 권장한다. 특정시점에서 일이 잘못될 수 있다 (스택트레이스가 쌓여서) 완벽하게 구현이 된 subscriber는 이벤트에 반응할 수 있게 onCompleted, onError 메서드를 모두 선언하자

4.4.6. From push to pull

위에서 햇던 예제는 publisher가 blocking, non-blocking execution에 대해서 별다른 의견없이(추가 없이) 설정되어있는 방법을 기술하였다. Flux<T>는 명시적으로 Iterable<T>로 변환하거나 block() 메서드를 이용해서 synchronized(동기화) 할 수 있다. block() 호출은 피하자. block()을 호출하면 애플리케이션에 대한 reactive chain의 모든 non-blocking 적인 장점이 사라진다.

    @Test
    @DisplayName("block() 연산자 : async -> sycn")
    void blockOperator() {
        String last = Flux.just("Ben", "Michael", "Mark").last().block();
        System.out.println(last); // Mark
    }

 

blocking 호출은 publisher의 체인을 synchronize로 사용되게 할 수 있고, 평범하고 잘 알려진 Pull 패턴으로 돌아가는 방법을 찾는다.

    @Test
    @DisplayName("block() 연산자 : async -> sycn")
    void blockOperatorCollection() {
        List<String> list = Flux.just("Ben", "Michael", "Mark").collectList().block();
        System.out.println(list); // [Ben, Michael, Mark]
    }

toList 연산자는 모든 emmited된 요소들을 모으고(collect), 그리고 그 리스트를 BlockingPublisher<T> 타입으로 패스한다.

4.4.7 Creating Flux and Mono using Lettuce

publisher를 설계하는 방법은 많이 있다. 당신은 이미 just(), take(), collectList()를 이미 보았다. Project Reactor documentation 의 참조에 따르면 Flux와 Mono를 만드는데 사용 할 수 있는 더 많은 메서드가 있다.

Lettuce publisher는 초기화나 체이닝 작업에 사용할 수 있다. Lettuce publisher를 사용하면 non-blocking 동작을 확인 할 수 있다. 이는 모든 I/O 및 커맨드 처리가 netty의 EventLoop를 사용해 aynchronously(비동기적)으로 처리되기 때문이다.

Redis에 연결하는건 매우 간단하다.

private RedisStringReactiveCommands<String, String> commands;

    @BeforeEach
    void setUp() {
        RedisClient client = RedisClient.create("redis://localhost");
        commands = client.connect().reactive();
    }

key에서 value를 가져오려면 GET 연산이 필요하다. (subscribe자리에 Consumer를 넣었다.)

 

    @Test
    @DisplayName("lettuce publisher를 사용하고, 여기서 get 연산자를 통해 redis의 key에 따른 value를 가져올 수 있다. ")
    void LettucePublisher() {
        commands.get("key")
                .subscribe(System.out::println);
    }

이것의 실행은 asynchronously(비동기적으로) 처리되며, Netty EventLoop Thread에서 작업이 완료되는 동안 호출하는 스레드(invoking thread)는 프로세스내의 다른 일을 처리할 수 있다. 분리된 특성으로 인해 호출한 메서드(calling method)는 Publisher<T>의 실행이 완료되기 전에 그대로 둘 수 있다 (?).

Lettuce의 publichser는 연결된 컨텍스트(context of chaining) 내에서 여러개의 키들을 asynchronously(비동기적으로) 로드하는데 사용될 수 있다.

댓글

Comments

Develop/Springboot

QueryDSL 슬라이싱 테스트(@DataJpaTest) / Test Bean 등록 | QueryDSL Slicing Test (@DataJpaTest) / Registering Test Beans

QueryDSL 슬라이싱 테스트 방법에 대해 기록을 남긴다.내가 주로 사용하는 Querydsl 패턴은 CustomRepository를 만드는 방식이다.docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations Spring Data JPA - Reference DocumentationExample 100. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastn..

QueryDSL 슬라이싱 테스트(@DataJpaTest) / Test Bean 등록 | QueryDSL Slicing Test (@DataJpaTest) / Registering Test Beans

728x90

QueryDSL 슬라이싱 테스트 방법에 대해 기록을 남긴다.

내가 주로 사용하는 Querydsl 패턴은 CustomRepository를 만드는 방식이다.

docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations

 

Spring Data JPA - Reference Documentation

Example 100. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

코드는 아래와 같다.

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    Optional<User> findByEmail(String email);
}
public interface UserRepositoryCustom {
    Optional<User> findByUserId(Long userId);
}
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Optional<User> findByUserId(Long userId) {
        User user = jpaQueryFactory.selectFrom(QUser.user)
                .where(QUser.user.userId.eq(userId))
                .fetchOne();
        return Optional.ofNullable(user);
    }
}

 

이렇게 짠 Querydsl 코드를 테스트하기 위한 테스트코드를 짤 때 @SpringBootTest를 하면 모든 빈이 주입되기 때문에 상관이 없지만,

아래 코드와 같이 DataJpaTest와 같은 슬라이싱 테스트를 하고싶을 때 문제가 발생한다.

@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest {

    @Autowired
    private EntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    private User settingUser() {
        User settingUser = User.builder()
                .email("jyami@ewhain.net")
                .name("jyami")
                .build();

        return userRepository.save(settingUser);
    }

    @Test
    void test() {
        settingUser();
        entityManager.clear();
        User user = userRepository.findByUserId(1L)
                .orElseThrow(() -> new ResourceNotFoundException("user", "userId", 1L));
    }



}
Error creating bean with name 'userRepositoryImpl' defined in file [/Users/jyami/Documents/commiters/commiters-ewha/commiters-ewha-api/build/classes/java/main/com/jyami/commitersewha/domain/user/UserRepositoryImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory'

결국 이유는 JpaQueryFactory가 persistenceLayer가 아니어서 빈등록이 되지않아 발생하는 문제인데, 
이때 테스트 시 특정부분의 빈만 등록해주는 방법이 있었다!

@TestConfiguration
public class TestConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

테스트에서만 사용할 용도의 @TestConfiguration을 이용해 JPAQueryFactory 만 Bean으로 생성해준다.

@DataJpaTest
@ActiveProfiles("test")
@Import(TestConfig.class)
public class UserRepositoryTest {
}

 

이후 @Import 어노테이션을 사용해 해당 테스트용 빈을 주입해주면, JpaQueryFactory에 대한 빈도 생성되므로, Querydsl의 슬라이싱 테스트가 가능해진다 :)


도움: github.com/jojoldu/blog-comments/issues/277#issuecomment-702597745

 

I'm leaving a note on how to do slicing tests with QueryDSL.

The Querydsl pattern I mainly use involves creating a CustomRepository.

docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations

 

Spring Data JPA - Reference Documentation

Example 100. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

The code looks like this.

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    Optional<User> findByEmail(String email);
}
public interface UserRepositoryCustom {
    Optional<User> findByUserId(Long userId);
}
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Optional<User> findByUserId(Long userId) {
        User user = jpaQueryFactory.selectFrom(QUser.user)
                .where(QUser.user.userId.eq(userId))
                .fetchOne();
        return Optional.ofNullable(user);
    }
}

 

When writing test code for Querydsl code like this, using @SpringBootTest would be fine since all beans get injected,

but a problem occurs when you want to do a slicing test like @DataJpaTest as shown in the code below.

@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest {

    @Autowired
    private EntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    private User settingUser() {
        User settingUser = User.builder()
                .email("jyami@ewhain.net")
                .name("jyami")
                .build();

        return userRepository.save(settingUser);
    }

    @Test
    void test() {
        settingUser();
        entityManager.clear();
        User user = userRepository.findByUserId(1L)
                .orElseThrow(() -> new ResourceNotFoundException("user", "userId", 1L));
    }



}
Error creating bean with name 'userRepositoryImpl' defined in file [/Users/jyami/Documents/commiters/commiters-ewha/commiters-ewha-api/build/classes/java/main/com/jyami/commitersewha/domain/user/UserRepositoryImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory'

The reason is that JpaQueryFactory isn't part of the persistence layer, so it doesn't get registered as a bean.
But there's a way to register only specific beans during testing!

@TestConfiguration
public class TestConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

Using @TestConfiguration, which is meant only for tests, we create just the JPAQueryFactory as a Bean.

@DataJpaTest
@ActiveProfiles("test")
@Import(TestConfig.class)
public class UserRepositoryTest {
}

 

Then, by using the @Import annotation to inject the test-specific bean, the JpaQueryFactory bean also gets created, making slicing tests with Querydsl possible :)


Credit: github.com/jojoldu/blog-comments/issues/277#issuecomment-702597745

 

댓글

Comments

Develop/Springboot

Spring Security OAuth2 Login Flow | Spring Security OAuth2 Login Flow

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/ Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.www.callicoder.com에 있는 글을 번역하여, 공부하는 용도 입니다. OAu..

Spring Security OAuth2 Login Flow | Spring Security OAuth2 Login Flow

728x90

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/

 

Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2

Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.

www.callicoder.com

에 있는 글을 번역하여, 공부하는 용도 입니다.

 

OAuth2 Login Flow 

SecurityConfig.java 파일과 관련한 OAuth2 LoginFlow. 글로 설명된 흐름을 그림으로 그려보았다.

1. OAuth2 login 플로우는 맨처음 frontend client 에서 엔드포인트에 서 요청을 보내면서 시작된다.

http://localhost:8080/oauth2/authorize/{provider}?redirect_uri=<redirect_uri_after_login>

  • provider : google, facebook, github
  • redirect_uri : OAuth2 provider가 성공적으로 인증을 완료했을 때 redirect 할 URI를 지정한다. (OAuth2의 redirectUri 와는 다르다)

2. endpoint로 인증 요청을 받으면, Spring Security의 OAuth2 클라이언트는 user를 provider가 제공하는 AuthorizationUrl로 redirect 한다.
Authorization request와 관련된 state는 authorizationRequestRepository 에 저장된다 (Security Config에 정의함)
provider에서 제공한 AutorizationUrl에서 허용/거부가 정해진다.

  • 이때 만약 유저가 앱에 대한 권한을 모두 허용하면 provider는 사용자를 callback url로 redirect한다. (http://localhost:8080/oauth2/callback/{provider}) 그리고 이때 사용자 인증코드 (authroization code) 도 함께 갖고있다.
  • 만약 거부하면 callbackUrl로 똑같이 redirect 하지만 error가 발생한다.

3. Oauth2 에서의 콜백 결과가 에러이면 Spring Security는 oAuth2AuthenticationFailureHanlder 를 호출한다. (Security Config에 정의함)

4. Oauth2 에서의 콜백 결과가 성공이고 사용자 인증코드 (authorization code)도 포함하고 있다면 Spring Security는 access_token 에 대한 authroization code를 교환하고, customOAuth2UserService 를 호출한다 (Security Config에 정의함)

5. customOAuth2UserService 는 인증된 사용자의 세부사항을 검색한 후에 데이터베이스에 Create를 하거나 동일 Email로 Update 하는 로직을 작성한다.

6. 마지막으로 oAuth2AuthenticationSuccessHandler 이 불리고 그것이 JWT authentication token을 만들고 queryString에서의 redirect_uri로 간다 (1번에서 client가 정의한 ) 이때 JWT token과 함께!

 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Autowired
    private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    
    @Autowired
    private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    /*
      By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
      the authorization request. But, since our service is stateless, we can't save it in
      the session. We'll save the request in a Base64 encoded cookie instead.
    */
    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable()
                .httpBasic()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/error",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                        .permitAll()
                    .antMatchers("/auth/**", "/oauth2/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated()
                    .and()
                .oauth2Login()
                    .authorizationEndpoint()
                        .baseUri("/oauth2/authorize")
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .and()
                    .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                    .userInfoEndpoint()
                        .userService(customOAuth2UserService)
                        .and()
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler);

        // Add our custom Token based authentication filter
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/

 

Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2

Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.

www.callicoder.com

This is a translation of the article above, for study purposes.

 

OAuth2 Login Flow 

The OAuth2 Login Flow related to the SecurityConfig.java file. I drew a diagram based on the flow described in the text.

1. The OAuth2 login flow begins when the frontend client sends a request to the following endpoint.

http://localhost:8080/oauth2/authorize/{provider}?redirect_uri=<redirect_uri_after_login>

  • provider: google, facebook, github
  • redirect_uri: Specifies the URI to redirect to when the OAuth2 provider successfully completes authentication. (This is different from OAuth2's redirectUri)

2. When the authentication request is received at the endpoint, Spring Security's OAuth2 client redirects the user to the AuthorizationUrl provided by the provider.
The state related to the authorization request is stored in the authorizationRequestRepository (defined in Security Config).
The allow/deny decision is made at the AuthorizationUrl provided by the provider.

  • If the user grants all permissions to the app, the provider redirects the user to the callback URL (http://localhost:8080/oauth2/callback/{provider}) along with the user's authorization code.
  • If the user denies, they are redirected to the same callbackUrl, but with an error.

3. If the OAuth2 callback result is an error, Spring Security invokes the oAuth2AuthenticationFailureHanlder (defined in Security Config).

4. If the OAuth2 callback result is successful and includes the authorization code, Spring Security exchanges the authorization code for an access_token and invokes the customOAuth2UserService (defined in Security Config).

5. The customOAuth2UserService retrieves the authenticated user's details and then either creates a new entry in the database or updates the existing one with the same email.

6. Finally, the oAuth2AuthenticationSuccessHandler is called, which creates a JWT authentication token and redirects to the redirect_uri from the queryString (the one defined by the client in step 1) — along with the JWT token!

 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Autowired
    private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    
    @Autowired
    private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    /*
      By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
      the authorization request. But, since our service is stateless, we can't save it in
      the session. We'll save the request in a Base64 encoded cookie instead.
    */
    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable()
                .httpBasic()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/error",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                        .permitAll()
                    .antMatchers("/auth/**", "/oauth2/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated()
                    .and()
                .oauth2Login()
                    .authorizationEndpoint()
                        .baseUri("/oauth2/authorize")
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .and()
                    .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                    .userInfoEndpoint()
                        .userService(customOAuth2UserService)
                        .and()
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler);

        // Add our custom Token based authentication filter
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

댓글

Comments

Develop/DevOps

GCP MySQL(MariaDB) DB 외부연결 | GCP MySQL (MariaDB) DB External Connection

GCP compute engine에 mysql을 연결하기 위해 삽질하면서 정리한 내용.1. compute engine에 mysql 설치sudo apt-get updatesudo apt-get install mysql-serversudo mysql_secure_installation // mysql 설정mysql_secure_installation에서 설정한 패스워드로 입력해서 접속한다.접속 방법 : root 유저로 접속하고, 패스워드를 입력하겠음sudo mysql -u root -p 2. 외부 ip 접근에 대한 포트 방화벽 열기VPC network > filewallrules 메뉴에 들어가기traffic : ingress protocols and port : 33063306 포트가 방화벽에 연결된다.소스..

GCP MySQL(MariaDB) DB 외부연결 | GCP MySQL (MariaDB) DB External Connection

728x90

GCP compute engine에 mysql을 연결하기 위해 삽질하면서 정리한 내용.

1. compute engine에 mysql 설치

sudo apt-get update
sudo apt-get install mysql-server
sudo mysql_secure_installation // mysql 설정

mysql_secure_installation에서 설정한 패스워드로 입력해서 접속한다.

접속 방법 : root 유저로 접속하고, 패스워드를 입력하겠음

sudo mysql -u root -p

 

2. 외부 ip 접근에 대한 포트 방화벽 열기

VPC network > filewallrules 메뉴에 들어가기

traffic : ingress

protocols and port : 3306

3306 포트가 방화벽에 연결된다.

소스 필터나 대상은 커스텀하게 사용하자. 난 범용적 사용을 위해 모든 인스턴스로 지정해둠

 

3. demon addresss 변경하기

ip 통해서 mysql에 접근 가능하도록 할 때 mysql 데몬이 0.0.0.0으로 떠 있어야 한다 (기본값 127.0.0.1)
이를 위해서 compute engine의 shell에서 conf 파일 수정이 필요하다.

cd /ect/mysql/mariadb.conf.d
sudo vi 50-server.cnf

해당 경로에 설정파일이 존재한다. 이곳으로 들어가서 bind-address를 변경해준다.

# bind-address = 127.0.0.1
bind-address = 0.0.0.0

이렇게 해준 후에 데몬을 재시작해주어야 한다.

service restart를 해주어도 되지만, vm에 안 깔려 있길래 다른 방법을 사용하였다.

sudo /etc/init.d/mysql restart

 

4. 접속 유저 권한 부여

위에서 sudo 권한을 이용해 db에 접속했었다. 따라서 외부 접속 전용 사용자를 추가하고, 해당 사용자에 권한을 부여 해주자

User 목록 확인

SELECT user, host FROM mysql.user;

 

사용자 계정 추가

CREATE USER 'jyami'@'%' IDENTIFIED BY 'password';
  • 사용자 명 : jyami
  • 접근 : % - 외부에서의 접근을 허용함
  • 비밀번호 : password

사용자 접근 권한

localhost local에서만의 접근을 허용함
% 모든 IP에서의 접근을 허용함
xxx.xxx.xxx.xxx 지정한 IP에서만 접근을 허용함
xxx.xxx.% 특정 IP 대역에서의 접근을 허용함

DB 권한 부여

GRANT ALL PRIVILEGES ON *.* TO 'jyami'@'%' WITH GRANT OPTION;

모든곳에서 접속하는 사용자 jyami에게 모든 권한을 부여한다.

*.* 모든 것을 할 수 잇는 권한
[database_name].* 특정 DB를 관리할 수 있는 권한
[database_name].[tablename] 특정 DB의 특정 Table을 관리할 수 잇는 권한

부여된 권한 보기

SHOW GRANTS FOR jyami@%

 

5. springboot 접속

GCE로 접근했을 때 분명 위 명령어대로 mysql을 깔았는데 직접 Mysql에 접속해보니 MariaDB [DB]> 로 떴었다. 이때 mysql connection은 안됐는데, mariaDB connection은 됐다.

[DB가 MariaDB 인 경우]

# build.gradle
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.1.2'
# application.yml

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

[DB가 MySQL 인 경우]

# build.gradle
runtimeOnly 'mysql:mysql-connector-java'
# application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

6. 추가 DB 관련 명령어

database 목록 확인

SHOW DATABASES;

사용할 database 지정

USE [database 이름];

database 생성

CREATE DATABASE [데이터베이스 이름];

 

이외 외부 접속 관련 참고 사이트

https://m.blog.naver.com/PostView.nhn?blogId=varkiry05&logNo=198610727&proxyReferer=https:%2F%2Fwww.google.com%2F

Notes from my struggles trying to connect MySQL to a GCP compute engine.

1. Installing MySQL on Compute Engine

sudo apt-get update
sudo apt-get install mysql-server
sudo mysql_secure_installation // mysql 설정

Log in using the password you set during mysql_secure_installation.

How to connect: Log in as the root user with a password

sudo mysql -u root -p

 

2. Opening Firewall Ports for External IP Access

Go to VPC network > firewall rules menu

traffic : ingress

protocols and port : 3306

Port 3306 will be opened in the firewall.

Customize the source filters and targets as you need. I set it to all instances for general-purpose use.

 

3. Changing the Daemon Address

To allow MySQL access via IP, the MySQL daemon needs to be listening on 0.0.0.0 (the default is 127.0.0.1)
To do this, you need to modify the conf file in the compute engine's shell.

cd /ect/mysql/mariadb.conf.d
sudo vi 50-server.cnf

The config file is located at this path. Go in and change the bind-address.

# bind-address = 127.0.0.1
bind-address = 0.0.0.0

After making this change, you need to restart the daemon.

You could use service restart, but since it wasn't installed on the VM, I used a different method.

sudo /etc/init.d/mysql restart

 

4. Granting User Permissions

Earlier, we connected to the DB using sudo privileges. So let's add a dedicated user for external access and grant permissions to that user.

Check User List

SELECT user, host FROM mysql.user;

 

Add a User Account

CREATE USER 'jyami'@'%' IDENTIFIED BY 'password';
  • Username: jyami
  • Access: % - allows access from external sources
  • Password: password

User Access Scope

localhost Allows access only from local
% Allows access from all IPs
xxx.xxx.xxx.xxx Allows access only from a specific IP
xxx.xxx.% Allows access from a specific IP range

Grant DB Privileges

GRANT ALL PRIVILEGES ON *.* TO 'jyami'@'%' WITH GRANT OPTION;

This grants all privileges to the user jyami connecting from anywhere.

*.* Privileges to do everything
[database_name].* Privileges to manage a specific DB
[database_name].[tablename] Privileges to manage a specific table in a specific DB

View Granted Privileges

SHOW GRANTS FOR jyami@%

 

5. Connecting from Spring Boot

When I accessed the GCE, I definitely installed MySQL following the commands above, but when I actually connected to MySQL, it showed MariaDB [DB]>. In this case, the MySQL connection didn't work, but the MariaDB connection did.

[If the DB is MariaDB]

# build.gradle
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.1.2'
# application.yml

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

[If the DB is MySQL]

# build.gradle
runtimeOnly 'mysql:mysql-connector-java'
# application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

6. Additional DB-Related Commands

Check Database List

SHOW DATABASES;

Select a Database to Use

USE [database 이름];

Create a Database

CREATE DATABASE [데이터베이스 이름];

 

Additional reference site for external access

https://m.blog.naver.com/PostView.nhn?blogId=varkiry05&logNo=198610727&proxyReferer=https:%2F%2Fwww.google.com%2F

댓글

Comments