Kotlin에서 코루틴을 사용할 때 가장 기본적이면서도 중요한 두 가지 개념이 있습니다. 바로 runBlocking과 CoroutineScope입니다. 이 글에서는 이 두 가지의 차이점과 각각의 사용 방법에 대해 알아보겠습니다.

runBlocking

runBlocking은 현재 스레드를 차단(block)하고, 블록 내의 모든 코루틴이 완료될 때까지 기다립니다. 주로 테스트나 간단한 예제에서 사용되며, 메인 함수에서 코루틴을 동기적으로 실행할 때 유용합니다.

특징

  • 차단(blocking): runBlocking 블록 내의 모든 코드가 완료될 때까지 현재 스레드를 차단합니다.
  • 동기적(synchronous): 블록 내의 코드가 완료될 때까지 다음 코드로 진행하지 않습니다.
  • 주로 테스트용: 주로 테스트 코드나 예제 코드에서 사용됩니다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    // "World!"가 출력될 때까지 runBlocking은 메인 스레드를 차단합니다.
}

CoroutineScope

CoroutineScope는 비차단(non-blocking) 방식으로 코루틴을 실행할 수 있는 컨텍스트를 제공합니다. CoroutineScope는 일반적으로 애플리케이션 내에서 코루틴을 관리하고, 특정 라이프사이클(예: ViewModel, Activity)에 따라 코루틴의 생명주기를 제어하는 데 사용됩니다.

특징

  • 비차단(non-blocking): CoroutineScope 내의 코루틴은 비차단 방식으로 실행되며, 스코프의 컨텍스트에 따라 코루틴을 관리합니다.
  • 유연성: 다양한 코루틴 빌더(launch, async 등)를 사용하여 비동기 작업을 처리할 수 있습니다.
  • 라이프사이클 관리: 스코프는 특정 객체(ViewModel, Activity 등)의 라이프사이클에 따라 코루틴을 취소하거나 정리할 수 있습니다.
import kotlinx.coroutines.*

fun main() {
    val scope = CoroutineScope(Dispatchers.Default)

    scope.launch {
        delay(1000L)
        println("World!")
    }

    println("Hello,")
    Thread.sleep(2000L) // 메인 스레드가 종료되지 않도록 잠시 대기
}

주요 차이점

차단 여부

  • runBlocking: 현재 스레드를 차단합니다. 블록 내의 모든 작업이 완료될 때까지 기다립니다.
  • CoroutineScope: 비차단 방식으로 코루틴을 실행합니다. 코루틴은 백그라운드에서 실행되며, 스레드를 차단하지 않습니다.

사용 목적

  • runBlocking: 주로 테스트나 간단한 콘솔 애플리케이션에서 동기적으로 코루틴을 실행할 때 사용됩니다.
  • CoroutineScope: 실제 애플리케이션에서 비동기 작업을 처리하고, 객체의 라이프사이클에 따라 코루틴을 관리할 때 사용됩니다.

라이프사이클 관리

  • runBlocking: 블록이 끝날 때까지 모든 코루틴이 완료되기를 기다립니다.
  • CoroutineScope: 스코프가 종료되면 해당 스코프 내의 모든 코루틴이 취소됩니다. 예를 들어, Android에서 ViewModel의 viewModelScope는 ViewModel이 클린업될 때 자동으로 취소됩니다.

결론

  • runBlocking: 주로 동기적 실행이 필요한 간단한 테스트나 예제에서 사용됩니다. 현재 스레드를 차단하므로 실제 애플리케이션에서는 잘 사용되지 않습니다.
  • CoroutineScope: 비차단 방식으로 코루틴을 실행하며, 실제 애플리케이션에서 비동기 작업을 처리하고, 라이프사이클 관리가 필요한 경우에 사용됩니다.

실제 애플리케이션 개발에서는 CoroutineScope를 사용하여 비동기 작업을 관리하는 것이 좋습니다. 이를 통해 비차단 방식으로 코루틴을 실행하고, 애플리케이션의 다른 부분과 자연스럽게 통합할 수 있습니다.

'Kotlin' 카테고리의 다른 글

JDK의 다양한 종류와 그 역사적 배경  (0) 2024.05.26
Kotlin 기본 개념 정리  (0) 2024.05.26

Gradle에서 Groovy의 역할

Gradle은 의존성 관리와 빌드 자동화를 도와주는 도구로 널리 사용됩니다. Gradle 스크립트를 작성할 때는 Groovy 또는 Kotlin DSL(Domain Specific Language)을 사용할 수 있습니다. 이번 글에서는 Groovy가 무엇인지, 그리고 Gradle에서 어떤 역할을 하는지에 대해 알아보겠습니다.

Groovy란?

Groovy는 Java와 높은 호환성을 가지는 스크립팅 언어로, 간결하고 유연한 문법을 가지고 있습니다. Gradle의 설정 파일(build.gradle)은 기본적으로 Groovy로 작성되며, 이를 통해 프로젝트의 빌드 구성, 의존성 관리, 플러그인 적용 등을 정의할 수 있습니다.

Groovy를 이용한 Gradle 스크립트 예제

다음은 Groovy로 작성된 Gradle 스크립트의 예제입니다:

plugins {
    id 'java'
}

group 'com.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'junit:junit:4.12'
}

위의 스크립트는 Java 플러그인을 적용하고, 프로젝트의 그룹과 버전을 설정하며, Maven Central 저장소에서 의존성을 가져오는 예제입니다. 이 모든 설정은 Groovy 문법으로 작성되었습니다.

Kotlin DSL을 이용한 Gradle 스크립트 예제

최근에는 Gradle 스크립트를 Kotlin DSL로도 작성할 수 있는데, 이는 build.gradle.kts 파일에서 사용됩니다. Kotlin DSL은 정적 타입 언어의 장점을 활용할 수 있어, IDE에서 더 나은 자동 완성과 오류 검사를 제공하는 이점이 있습니다. 다음은 같은 설정을 Kotlin DSL로 작성한 예제입니다:

plugins {
    id("java")
}

group = "com.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    testImplementation("junit:junit:4.12")
}

마무리

요약하자면, Groovy는 Gradle 스크립트를 작성하는 데 사용되는 언어 중 하나로, Gradle의 설정과 동작을 정의하는 데 중요한 역할을 합니다. Kotlin DSL을 사용할 수도 있으며, 이는 개발자의 선호도에 따라 선택할 수 있습니다.

JDK의 다양한 종류와 그 역사적 배경

Kotlin 프로젝트를 IntelliJ에서 설정할 때 OpenJDK, Amazon Corretto, Oracle JDK 등 여러 종류의 JDK 옵션이 보이는 이유가 궁금하신 적이 있으신가요? 왜 이렇게 다양한 JDK가 존재하는지, 그 배경을 이해하면 좀 더 명확한 선택을 할 수 있습니다.

JDK의 종류

1. OpenJDK

OpenJDK는 오픈 소스 자바 개발 키트로, 대부분의 Java 개발자들에게 기본 옵션입니다. 자유롭게 사용할 수 있으며 다양한 플랫폼을 지원합니다.

2. Amazon Corretto

Amazon Corretto는 아마존에서 제공하는 OpenJDK 배포판입니다. 성능 최적화와 보안 패치가 잘 되어 있으며, AWS 환경과의 호환성이 뛰어납니다.

3. Oracle JDK

Oracle JDK는 Oracle에서 제공하는 공식 JDK입니다. 상업적 지원과 추가 기능을 제공하지만, 상용 라이선스가 필요합니다.

4. AdoptOpenJDK

AdoptOpenJDK는 다양한 플랫폼에 대해 빌드된 OpenJDK입니다. 다양한 버전과 설정을 제공하여 개발 환경에 맞추기 쉽습니다.

JDK가 나뉘어진 역사적 배경

Sun Microsystems와 Oracle

Java는 원래 Sun Microsystems에서 개발되었습니다. Sun이 Oracle에 인수된 후, Oracle은 Java의 상용화와 라이선스 변경을 시도했습니다. 이에 따라 OpenJDK라는 오픈 소스 프로젝트가 더 강조되었고, Oracle JDK와는 별도로 유지 관리되기 시작했습니다.

오픈 소스 커뮤니티의 성장

Java 커뮤니티는 OpenJDK를 중심으로 활성화되었고, 다양한 기업들이 이 프로젝트에 기여하게 되었습니다. 각 기업은 자신들의 요구에 맞는 최적화와 기능을 추가한 JDK 배포판을 만들기 시작했습니다.

기업의 요구사항

각 기업은 자신들의 클라우드 환경, 배포 환경, 보안 정책 등에 맞추어 최적화된 JDK를 원했습니다. 예를 들어, Amazon Corretto는 AWS 환경에 최적화된 JDK이고, Azul Zulu는 다양한 플랫폼을 지원하는 JDK입니다.

오픈 소스와 상용화의 균형

Oracle JDK는 상업적 지원을 제공하면서 일부 기능을 추가로 제공하지만, 이에 대한 비용이 발생합니다. 반면, OpenJDK 기반 배포판들은 무료로 사용 가능하고, 각기 다른 벤더가 지원을 제공합니다.

기술적 차별화

성능, 보안 패치 주기, 호환성 등 기술적인 차별화를 제공하여 특정 환경에서 더 나은 성능을 발휘하거나 특정 요구사항을 충족시킬 수 있습니다.

결론

다양한 JDK 배포판은 각각의 개발 환경과 요구 사항에 맞춰 선택할 수 있는 다양한 옵션을 제공합니다. 각 JDK는 기본적으로 동일한 Java 표준을 따르지만, 벤더별 최적화와 추가 기능이 다를 수 있습니다. 자신의 프로젝트와 환경에 맞는 JDK를 선택하면 최적의 개발 경험을 누릴 수 있습니다.

'Kotlin' 카테고리의 다른 글

Kotlin Coroutine: runBlocking과 CoroutineScope의 차이점  (0) 2024.05.26
Kotlin 기본 개념 정리  (0) 2024.05.26

해당 게시글은 인프런의 코틀린 문법 총 정리 강의를 들으면서 작성 되었습니다.

해당 강의 중 맨 마지막 섹션인 코루틴 섹션은 내용을 추가로 조사하여 작성 하였음을 알립니다.

컴파일 상수

const val num = 20

fun main() {
    println("Hello, World")
}

main() 함수 상단부에 작성된 const valmain() 함수보다 우선해서 컴파일되어 성능에 유리한 이점을 가져올 수 있다.

문자열에서 첫 번째 문자 다루기

fun main() {
    val name = "abc"

    println("${name[0].uppercase()}")
}

name[0]와 같이 문자열 접근시 문자열 변수에서 첫 번째 문자를 접근할 수 있다.

min, max

초기 버전의 코틀린에서는 자바 코드의 Math.max 함수를 사용하였지만, 최근에는 코틀린 코드로 작성된 min, max 함수를 사용하면 된다.

import kotlin.math.max
import kotlin.math.min

fun main() {
    val i = 10
    val j = 20

    println(max(i, j))
    println(min(i, j))
}

import 구문에 kotlin.math.*가 포함돼 있음을 체크하자.

Random

import kotlin.random.Random

fun main() {
    val randomNumber = Random.nextInt(0, 100) // 0 ~ 99
    val randomDouble = Random.nextDouble(0.0, 1.0) // 0.0 ~ 0.9
}

Scanner

값을 입력받는 Scanner 클래스이다.

import java.util.Scanner

fun main() {
    val reader = Scanner(System.`in`)
    reader.nextInt()
}

코틀린에서 in 키워드는 예약어라서 자동으로 백틱(`)이 붙게된다.

if, when 제어문

코틀린에서는 3항 연사자가 따로 없다. if 또는 when 제어문을 사용하면 된다.

fun main() {
    val i = 5

    // when으로 변경 가능
    // 3항 연산자를 따로 사용하지 않는다.
    val result = if (i > 10) {
        "10 보다 크다"
    } else if (i > 5) {
        "5 보다 크다"
    } else {
        "!!!"
    }

    val result2 = when {
        i > 10 -> "10 보다 크다"
        i > 5 -> "5 보다 크다"
        else -> "!!!"
    }

    println(result)
    println(result2)
}

두개의 코드 블록 모두 동일한 코드이다. 마지막 줄의 코드가 리턴되기 때문에 각각 result, result2 변수에 값이 담기게 된다.

for, forEach

리스트의 각 아이템들을 출력시키는 다양한 방법이다. 정통적으로 사용되는 for (var i = 0; i < items.length; i++)과 같은 형태는 지양한다.

더 직관적이면서 다양한 문법들이 지원되기 때문이다. 그럼에도 불구하고 정통적인 반복문 형태의 포맷을 찾는다면 예시의 맨 마지막 형태가 정통적인 형태와 가장 유사하다.

fun main() {
    val items = listOf(1, 2, 3, 4, 5)

    for (item in items) {
        print(item)
    }

    items.forEach {
        println(it)
    }

    items.forEach { item ->
        println(item)
    }

    items.forEach(::println)

    println()

    for (i in 0..(items.size - 1)) {
        print(items[i])
    }
}

List

fun main() {
    val items = listOf(1, 2, 3, 4, 5) // 변경안됨

    val mItems = mutableListOf(1, 2, 3, 4, 5)
    mItems.add(6)
    mItems.remove(3)
}

Array

fun main() {
    val items = arrayOf(1, 2, 3) // 배열은 실질적으로 잘 사용하지 않고, List를 사용하면 된다.

    println(items.size)
    items[0] = 2

    try {
        val item = items[4]
    } catch (e: Exception) {
        println(e.message)
    }
}

배열(Array)은 실질적으로 잘 사용하지 않고, List를 사용하면 된다.

배열과 리스트의 차이

  • 배열은 고정된 크기를 가지며, 인덱스를 통해 각 요소에 직접 접근하고 변경할 수 있습니다. 배열의 크기는 생성 시에 결정되며 이후에는 변경할 수 없습니다.
  • 성능의 관점에서 배열은 메모리에서 연속된 블록을 사용하기 때문에 성능이 약간 더 좋을 수 있습니다. 그러나 이러한 차이는 대부분의 애플리케이션에서 큰 영향을 미치지 않습니다.
  • 리스트는 더 많은 기능과 유연성을 제공하지만, 성능 측면에서 배열보다 약간 느릴 수 있습니다. 특히, MutableList는 내부적으로 배열을 사용하기 때문에 성능 차이가 크지 않습니다.
  • 대부분의 경우 리스트가 더 사용하기 편리하고(map, filter, reduce), Kotlin의 컬렉션 API와 더 잘 통합됩니다.

Null Safety

코틀린에서 nullable 타입을 표현하기 위해서는 String?과 같이 물음표를 붙여준다.

다만, Nullable 타입은 명시된 타입을 바로 할당할 수 없기 때문에 ?.let { it } 구문을 사용해 코드를 작성하는 것이 관례이다.

그 밖에 Null이 아님을 강제하는 !! 문법을 사용할 수 있지만, 이는 개발자에 실수가 될 수 있기 때문에 지양하도록 하자.

fun main() {
    var nameNullable: String? = null
    var name: String = ""

    nameNullable?.let { // null이 아니라면, 블록을 실행하자.
        name = it
    }
}

함수 작성하기

파일에 작성된 함수들을 Top-Level 함수라 한다. 어느 파일에서나 사용가능하다.

아래의 sum, sum2 함수들을 호출하는 문법들은 모두 유효한 문법들이다.

fun main() {
    println(sum(1, 2))
    println(sum2(a = 10, b = 20))
    println(sum2(b = 30, a = 10))
}

// Top-Level 함수, 어느 파일에서나 사용가능하다.
fun sum(a: Int, b: Int): Int {
    return a + b
}

fun sum2(a: Int, b: Int, c: Int = 0) = a + b + c

클래스, Data 클래스

fun main() {
    val jhon = Person("Jhon Smith", 28, "private name")
    val jhon2 = Person("Jhon Smith", 28, "private name")

    println(jhon) // Person@65b54208
    println(jhon2) // Person@1be6fc3
    println(jhon2 == jhon2) // false

    val jhon3 = DataPerson("a", 1)
    val jhon4 = DataPerson("a", 1)
    println(jhon3 == jhon4) // `data class`는 주소 참조가 아닌 값 참조를 하기 때문에 `true`가 된다.

    jhon.some()
    println(jhon.hobby) // 취미: 농구
}

class Person(
    private val name: String,
    private val age: Int,
    private val name2: String,
) {
    var hobby = "축구"
        private set // 외부에서 set이 불가능해 짐
        get() = "취미: $field"

    init {
        // 초기화 구문, 객체 생성시 실행된다.
        println("My name is $name and I am $age")
    }

    fun some() {
        hobby = "농구"
    }
}

data class DataPerson(
    val name: String,
    val age: Int,
)

상속을 위한 open 그리고 interface

  • 일반 클래스를 상속 가능하게 하려면 open 키워드를 붙여야 한다.
  • 추상 클래스에서 작성된 메소드 또한 오버라이드가 가능하게 하려면 open 키워드를 붙여야 한다.
interface Drawable {
    fun draw()
}

abstract class Animal {
    open fun move() {
        println("move")
    }
}

class Dog: Animal(), Drawable {
    override fun move() {
        println("살금")
    }

    override fun draw() {
        TODO("Not yet implemented")
    }
}

class Cat: Animal(), Drawable {
    override fun draw() {
        TODO("Not yet implemented")
    }
}

타입 체크 is

위의 작성된 예시에서 타입을 체크 하려면 다음과 같이 작성해 볼 수 있다.

fun main() {
    val dog: Animal = Dog()
    val cat = Cat()

    if (dog is Dog) {
        println("멍멍이")
    }

    cat as Animal // type casting

    if (dog is Animal) {
        println("Animal")
    }
}

Generic 클래스 생성

fun main() {
    val box = Box(10)
    val box2 = Box("asdf")

    println(box.value)
    println(box2.value)
}

class Box<T>(val value: T)

고차함수

fun main() {
    myFunc(10) { // Lambda 식
        println("Hello World")
    }
}

// 콜백함수(고차함수)
fun myFunc(a: Int, callback: () -> Unit) {
    println("함수 시작")
    callback()
    println("함수 끝")
}

코루틴(coroutine)

코루틴?

  • 코루틴은 비동기 프로그래밍을 쉽게 할 수 있도록 도와주는 프로그래밍 구조이다.
  • 쉽게 말해, 코루틴은 멈췄다가 나중에 다시 시작할 수 있는 함수이다.
  • 코루틴을 사용하면 코드가 멈추지 않고 다른 작업을 할 수 있다.

코루틴으로 가능한 것들

  1. 메인 스레드가 멈추지 않음: 네트워크 요청이나 파일 읽기 같은 시간이 오래 걸리는 작업을 하면서도 메인 스레드가 멈추지 않아서 UI가 끊기지 않는다.
  2. 간편한 비동기 코드 작성: 콜백이나 복잡한 스레드 관리를 할 필요 없이, 동기 코드처럼 읽기 쉬운 비동기 코드를 작성할 수 있다.
  3. 효율적인 자원 사용: 필요할 때만 코드를 실행하므로, 자원을 효율적으로 사용할 수 있다.

코루틴의 작동 방식

  1. 시작: suspend 함수로 표시된 코루틴 블록을 시작한다.
  2. 멈춤: suspend 키워드가 있는 부분에서 멈춘다. 예를 들어, 네트워크 요청을 기다리는 동안 다른 작업을 한다.
  3. 재개: 요청이 완료되면 멈췄던 지점부터 다시 시작한다.
import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        val result = fetchData()
        println("Data: $result")
    }
    println("Waiting for data...")
    Thread.sleep(2000) // 메인 스레드가 종료되지 않도록 잠시 대기
}

suspend fun fetchData(): String {
    delay(1000) // 네트워크 요청 시뮬레이션
    return "Hello, Coroutine!"
}

이 예제에서는 fetchData 함수가 suspend로 표시되어 있고, 이 함수는 delay를 통해 잠시 멈췄다가 1초 후 다시 실행된다. 메인 스레드는 멈추지 않고 "Waiting for data..."를 출력하고, 이후 fetchData의 결과가 출력된다.

💡 PHP 8 이상의 버전에서 유효한 예시 코드들이 작성되어 있습니다.

 

라라벨에서 DTO(Data Transfer Object)를 작성하기 위해 해당 글에서는 spatie/laravel-data 오픈소스 라이브러리를 사용합니다. PHP의 기본 클래스 만으로도 충분하지만 프로그래밍틱하고 라라벨과 호환이 잘 되어 자주 사용하는 라이브러리 중 하나입니다. 👍

 

오픈소스 개발 회사 spatie에서 개발된 laravel-data를 다음과 같이 소개하고 있습니다.

Powerful data objects for laravel
… can be used in various ways. Using this package you only need to describe your data once:
instead of a form request, you can use a data objectinstead of an API transformer, you can use a data objectinstead of manually writing a typescript definition, you can use… 🥁 a data object
Laravel을 위한 강력한 데이터 객체
… 여러 가지 방식으로 사용할 수 있습니다. 이 패키지를 사용하면 데이터를 한 번만 정의하면 됩니다:
폼 요청 대신 데이터 객체를 사용할 수 있습니다.API 변환기 대신 데이터 객체를 사용할 수 있습니다.직접 타입스크립트 정의를 작성하는 대신… 🥁 데이터 객체를 사용할 수 있습니다.

설치

composer require spatie/laravel-data

기본 사용법

그럼 laravel-data 라이브러리를 통해 객체를 생성하는 방법에 대해 간단히 알아보겠습니다.

<?php

namespace App\Http\Controllers\Requests;

use Spatie\LaravelData\Data;

class Feedback extends Data
{
    public function __construct(
        public readonly int $id,
        public readonly int $point,
    ) {
    }
}

 

기본적인 Data 클래스 생성 방법은 위와 같습니다.. Spatie\LaravelData\Data 클래스를 상속 받고 정의할 데이터 타입들을 생성자에 나열합니다. 데이터 클래스가 객체로 생성된 이후에는 속성 값이 변하지 않는다는 "불변 객체 패턴"을 적용하기 위해 readonly 속성을 추가적으로 작성해 주었습니다.

 

불변 객체 패턴이란? 불변 객체 패턴은 객체의 상태가 한 번 설정되면 변경할 수 없도록 하는 디자인 패턴으로, 이는 코드의 안정성과 예측 가능성을 높여줍니다. Java에서는 final 키워드, C#에서는 readonly 키워드를 사용해 구현할 수 있습니다. 불변 객체를 사용하면 동시성 문제가 줄어들고, 객체를 안전하게 공유할 수 있는 장점이 있습니다.

 

생성된 데이터 클래스는 아래와 같이 from static 메소드를 통해 객체를 생성할 수 있다.

$feedback = Feedback::from([
    'id' => 1,
    'point' => 5,
]);

 

PHPStorm IDE 기준으로 from 메소드 안에서의 연관 배열 작성시 자동완성이 지원되기 때문에 전혀 불편함이 없습니다.
(VSCode는 요즘 사용하지 않아 잘 모르겠네요. 상용 소프트웨어의 맛을 알아버린 이후에 VSCode를 잘 사용하지 않게되네요. 🥲)

Request 및 Validation 클래스로 분리

전통적인 라라벨의 Request를 받아 유효성(Validation) 검사를 진행하는 방법은 아래와 같습니다.

<?php

use Illuminate\Http\Request;

class Controller {
    public function getFeedback(Request $request)
    {
        $validated = $request->validate([
            'id' => 'required|integer',
            'point' => 'require|integer',
        ]);

        ...
    }
}

 

간결하고 작성하기 빠른 방법입니다. 하지만, 저는 개인적으로 복잡한 유효성 검사가 들어간 코드는 기본 로직에서 많이 벗어나기 때문에 분리 되었으면 좋겠다는 니즈가 있었습니다. 이럴때 다음과 같이 spatie/laravel-data 라이브러리를 통해 분리가 가능합니다.

<?php

namespace App\Http\Controllers\Requests;

use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;

#[MapInputName(SnakeCaseMapper::class)]
class PointRequest extends Data
{
    public function __construct(
        #[Required, IntegerType]
        public readonly int $id,

        #[Required, In([1, 2, 3])]
        public readonly int $point,

        #[Nullable, StringType, Max(50)]
        public readonly ?string $userName = null,
    ) {
    }
}

 

코드를 살펴보면 기본적으로 Spatie\LaravelData\Data 클래스를 상속 받고, PHP 8에서 추가된 Attributes을 이용해서 PointRequest 클래스에 추가적인 동작이 되도록 코드가 작성되었습니다.

#[MapInputName(SnakeCaseMapper::class)] 이는 API 요청 파라메터들의 규칙은 snake_case로 요청되지만 내부 코드는 camelCase 규칙을 맞추기 위한 부가적인 기능입니다. 예를 들면 다음과 같습니다.

 

요청 설명
/api/point?id=1&point=5&user_name=Hello user_name 파라메터는 public readonly ?string $userName과 매핑됨

 

이렇게 작성한 Request 클래스는 컨트롤러에서 다음과 같이 작성이 가능합니다. 정확히 Controller와 Request 파일이 분리되어 보다 규모가 큰 애플리케이션에서는 관리를 보다 효율적으로 할 수 있도록 도와줍니다.

<?php

use App\Http\Controllers\Requests\PointRequest;

class Controller {
    public function getFeedback(PointRequest $request)
    {
        dd($request); // validation이 수행된 후 생성된 객체

        ...
    }
}

 

컨트롤러 레이어까지 진입하기 전에 유효성 검사가 수행됩니다. (유효성 검사가 통과되지 못하면 라라벨은 422 Unprocessable Entity를 반환함)

본격 DTO(Data Transfer Object) 객체로의 이용

DTO 객체로 사용할 수 있습니다. 기본 사용 방법과 비슷합니다. 여기서는 리스트 형태의 데이터를 DTO를 만들어 어떻게 DTO 인스턴스를 생성하는지에 대해 알아보도록 하겠습니다.

 

BagDto 클래스를 작성해 봅니다.

<?php

namespace App\Services\Product\Dto;

use Carbon\Carbon;
use App\Services\Product\Dto\BagItemDto;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Illuminate\Support\Collection;

#[MapOutputName(SnakeCaseMapper::class)]
class BagDto extends Data
{
    public function __construct(
        public readonly int $count,

        /** @var Collection<BagItemDto> */
        public readonly Collection $items,
    ) {
    }
}

 

array 타입 또는 라라벨의 기본 Collection 클래스를 이용해 리스트 타입을 정의할 수 있습니다. 여기서 PHPDoc을 이용해 타입을 명시해주는걸 권장합니다. 그래야 IDE에서 정확히 타입 추론이 가능합니다. (PHP가 Generic을 아직 지원하지 않는 것은 매우 안타깝게 생각합니다. 🥲)

 

그리고 BadItemDto 클래스 파일은 아래와 같이 작성합니다.

<?php

namespace App\Services\Product\Dto;

use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;

#[MapOutputName(SnakeCaseMapper::class)]
class BagItemDto extends Data
{
    public function __construct(
        public readonly int $id,
        public readonly string $itemName,
    ) {
    }
}

 

두 가지의 Dto 클래스를 만들었고, 데이터를 다음과 같이 매핑할 수 있습니다.

$items = [
    ['id' => 1, 'itemName' => 'item1'],
    ['id' => 2, 'itemName' => 'item2'],
];

$bag = BagDto::from([
    'count' => 2,
    'items' => $items,
]);

 

 

BagDto::from(); 메소드를 호출함으로써 $items에 연관배열 리스트가 자동으로 내부 리스트 객체로 생성되게 됩니다.

 

또한, 각 Dto 클래스 파일 상단에 작성돼 있는 #[MapOutputName(SnakeCaseMapper::class)] 코드를 통해 객체가 라라벨에서 출력될때 다음의 예시와 같이 snake_case로 변환되어 출력됩니다.

 

이는 내부의 컨벤션을 camelCase로 유지하면서 API 응답 컨벤션은 snake_case로 컨벤션을 유지하는데 유용합니다. 라라벨 Resources 기본 기능을 활용해도 되지만, DTO 작성을 통해 이런 세세한 부분까지 컨트롤할 수 있다는 점이 제겐 큰 장점으로 보였고, Resources 기능 대신 Attributes을 통해 컨벤션을 유지하고 있습니다.

 

조금 더 클래스 친화적인 코드가 되는 것 같습니다. 👍

Typescript Definition 자동 생성하기

잘 작성된 DTO 파일들에 Attributes를 추가하면 다음의 명령어를 통해 Typescript 타입 정의 파일 또한 출력 가능한 부가적인 기능이 존재합니다.

php artisan typescript:transform

아! 해당 부가 기능을 사용하기 위해서는 spatie/laravel-typescript-transformer 라이브러리 설치가 필요합니다.

composer require --dev spatie/laravel-typescript-transformer

즉 아래의 DTO 클래스 파일은

<?php

namespace App\Http\Controllers\Requests;

use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[MapInputName(SnakeCaseMapper::class), TypeScript]
class PointRequest extends Data
{
    public function __construct(
        #[Required, IntegerType]
        public readonly int $id,

        #[Required, In([1, 2, 3])]
        public readonly int $point,

        #[Nullable, StringType, Max(50)]
        public readonly ?string $userName = null,
    ) {
    }
}

 

(TypeScript Attributes를 추가됨)

export interface PointRequest {
  id: number;
  point: '1' | '2' | '3';
  userName?: string;
}

 

와 같이 변환된 파일을 출력합니다.

 

이는 회사의 개발 환경에 따라 프론트엔드 개발자와 협업할때 도움이 될 수도 그다지 필요하지 않을 수도 있습니다. 😅

심화편: 값을 변경하여 객체 복제하기

위의 모든 예시들은 "불변 객체 패턴"을 적용하고 있기 때문에 readonly 속성을 정의해 주었습니다. 당연히 값이 변할 필요가 있는 경우에 새로운 값으로 속성을 덮으려고 하면 오류가 발생합니다. 그럴땐 아래와 같은 방법이 도움이 될 수도 있습니다.

#[MapOutputName(SnakeCaseMapper::class)]
class BagItemDto extends Data
{
    public function __construct(
        public readonly int $id,
        public readonly string $itemName,
    ) {
    }

    public function withItemName(string $itemName): self
    {
        $bagItem = $this->with(); // 원래의 데이터들

        $bagItem['itemName'] = $itemName; // 값 변경

        return self::from($bagItem); //  새로운 인스턴스 반환
    }
}
$bagItem = BagItemDto::from([
    'id' => 1,
    'itemName' => 'item1',
]);

$bagItem->withItemName('item2');

마치며

어디까지나 해당 라이브러리를 채택하고 사용하고는 선택사항입니다. 규모에 따라서 그리고 필요성에 따라서 기본적인 라라벨 프레임워크의 기능만으로 충분할 수 있습니다.

 

많은 개발자분들과 같이 협업이 되어야 하는 상황이라면 모두의 동의를 구해야 할 것이고, 1인 규모에서는 불필요한 노동이 될 수도 있습니다.

 

충분히 검토 후 사용하시는 현명한 개발자가 되시길 바랍니다. :)

 

모든 글은 다음의 문서 링크에서 보다 정확한 내용 그리고 추가적인 부가 기능을 확인할 수 있습니다.

 

https://spatie.be/docs/laravel-data/v4/introduction

참조

해당 글은 제 개인 서버 페이지를 참조하여 재작성 되었습니다.

설정 > 키보드 > [편집] 진입
[설정 > 키보드 > 편집]

[설정 > 키보드 > 편집] 진입 후 "맞춤법 자동 수정"을 끄거나 "맞춤법: U.S English"로 변경해 주자.

 

근데 맥 자체의 소프트웨어 결함인 건 맞다. 리부팅이 정답이지만 그냥 익숙해지자..

https://rubjo.github.io/victor-mono/

 

Victor Mono

 

rubjo.github.io

서체는 가늘고 선명하며 좁고 x 높이가 크고 구두점이 명확하여 읽기 쉽고 코드에 이상적입니다. 7가지 웨이트와 Roman, Italic 및 Oblique 스타일로 제공됩니다.

Victor Mono

1Password SSH 키 저장

장비를 새로 지급 받거나 포맷을 한 경우 일반적으로 개인키를 따로 백업해 놓지 않기 때문에 새로 생성 후 Github 및 AWS 같은 사이트에 새로 등록해준다. 귀찮다. 1Password의 SSH Agent 기능을 활용해보자.

 

일단 +새 항목 선택 후 앞으로 쭈욱 사용할 SSH를 한 개 등록해준다. (위 스크린샷) [Command + ,] 키를 입력하여 환경설정 창을 오픈한다.

 

1Password 환경설정 윈도우

[개발자] 메뉴로 진입하여 체크해야 할 것 같은 느낌의 옵션들을 체크준 후 중앙에 위치한 스니펫을 복사하여 ~/.ssh/config 에 추가해준다.

 

설정은 끝났다. 이제 ~/.ssh/id_rsa.pub 파일은 없어도된다. 1Password와 해당 SSH 에이전트만 활성화돼 있으면된다. 🤫

+ Recent posts