코틀린, 저는 이렇게 쓰고 있습니다

코틀린, 저는 이렇게 쓰고 있습니다

요약: 서버 개발자 카펀은 코틀린을 사용하여 안정적인 백엔드 서비스를 개발하고 운영합니다. 직접 코틀린을 사용하며 느낀 값 검증 객체 생성, 안전한 null 처리, 확장 함수를 사용한 유틸리티 라이브러리 제작, 효율적인 단위 테스트 작성 등 코틀린의 매력을 소개합니다. 안정적인 웹 서비스 개발에 관심 있는 백엔드 개발자라면 읽어 보는 걸 추천합니다.

💡 리뷰어 한줄평

yun.cheese 코틀린 매력에 푹 빠진 개발자의 생생한 경험을 통해, 실제 서비스 개발에 바로 적용할 수 있는 코틀린 활용 꿀팁들을 얻어가세요!

noah.you 복잡한 보험의 요구사항들을 해결하기 위한 이유 있는 코틀린 사용 사례! 좋은 사례들 많으니 많이 보고 가세요!

ari.a 카카오페이에서는 왜 서비스의 백엔드를 개발할 때 코틀린을 사용하는 걸까요? 궁금하신 분들에게 카펀의 코틀린 사용 사례를 추천드립니다!

시작하며

안녕하세요. 카카오페이 보험마켓파티에서 보험 비교 추천, 내 차 관리 서비스 등을 개발하고 운영하는 카펀입니다.

여러분은 어떤 언어를 사용하여 개발을 하고 계신가요? 카카오페이에서는 저희 보험 서비스를 비롯한 다양한 서비스의 백엔드 서비스 개발에 코틀린을 사용하고 있습니다.

카카오페이에 합류하기 전에는 코틀린을 경험해 본 적이 없었지만, 실제로 사용하면서 코틀린의 다양한 장점과 편리함에 매료되었습니다. 예를 들면 다음과 같은 코틀린의 매력이 있습니다.

  • 코틀린을 사용함으로써 서비스를 보다 견고하고 안정적이며, 효율적으로 운영하고 있습니다.
  • 특정 도메인에 대한 내용을 쉽게 모아서 관리하거나, 저희 서비스만을 위한 라이브러리를 간편하게 만들었습니다.
  • 목적과 대상이 명확한 테스트 코드 작성 역시 코틀린을 통해 손쉽게 작성하였습니다.

카카오페이에 합류해 코틀린으로 여러 서비스를 개발하고 운영하면서 직접 경험한 코틀린의 매력을 소개해 보고자 합니다. 여러 매력 포인트 중 코틀린의 4가지 매력 포인트를 정리했습니다. 저와 마찬가지로 안정적인 웹 서비스 개발에 관심 있는 백엔드 개발자 분들이 읽어 보시면 좋습니다.

생성 시 값이 검증되는 객체 만들기

어떤 값을 나타내는 VO1를 만들 때, 이따금 입력 값을 변환하거나 검증이 필요한 경우가 있습니다. 검증 로직을 별도 클래스로 분리할 수도 있지만, 매번 별도로 검증 로직을 호출해야 한다면 실수로 검증을 누락할 가능성이 있습니다. 코틀린의 다양한 기능을 활용하여 간결하면서도, 검증을 통과하였음이 보장되도록 작성하는 예시를 소개합니다.

자동차 번호를 나타내는 VO가 있습니다. 코틀린의 Value Class를 사용합니다.

@JvmInline
value class CarNumber(val input: String)

Value Class는 코틀린에서 값을 나타내기 위한 wrapper 클래스입니다. 단 하나의 불변 필드만을 가질 수 있고, JVM 환경에서 컴파일 시 class를 벗겨 내고 내부의 값으로 대체합니다. 덕분에 primitive 타입의 값을 객체와 같이 다룰 수 있으며, 동시에 wrapper 클래스 사용 시의 오버헤드 문제를 해결할 수 있습니다. 자세한 내용은 Reference의 Project Valhalla 내용을 확인해 주세요.

자동차 번호에는 아래와 같은 규칙이 있다고 가정하겠습니다.

  • 공백 포함 금지
  • 자동차 번호 형식은 아래 중 하나여야 함
    • 12가1234
    • 123가1234
    • 서울1가1234
    • 서울12가1234

CarNumber 인스턴스 생성 시, 아래 과정을 거치고 싶습니다.

  • 공백 제거
  • 지역명은 주어진 목록 내에서만 사용 가능(서울, 경기, 대전 등등), 아닐 경우 예외 발생
  • 지역명이 없는 경우 앞 숫자는 2~3자리, 뒤 숫자는 4자리
  • 지역명이 있는 경우 앞 숫자는 1~2자리, 뒤 숫자는 4자리

이를 팩토리 메서드 내 검증으로 추가하였습니다.

@JvmInline
value class CarNumber(val value: String) {
    companion object {
        private val CAR_NUMBER_REGEX = Regex("(\\d{2,3})([가-힣])(\\d{4})")
        private val OLD_CAR_NUMBER_REGEX = Regex("^([가-힣]{1,2})?(\\d{1,2})([가-힣])(\\d{4})\$")
        private val LOCATION_NAMES = setOf("서울", "부산", "대구", "인천", "광주", "대전", "울산", "경기", "강원", "충북", "충남", "전북", "전남", "경북", "경남", "제주")

        fun from(carNumber: String): CarNumber {
            return CarNumber(carNumber.removeSpaces())	// 공백은 제거
        }
    }

    init {
        validateCarNumber(value)
    }

    private fun validateCarNumber(number: String) {
        val oldCarNumberMatch = OLD_CAR_NUMBER_REGEX.matchEntire(number)
        if (oldCarNumberMatch != null) {
            val (location, _, _) = oldCarNumberMatch.destructured
            require(location in LOCATION_NAMES) { "알 수 없는 등록 지역입니다." }	// 예전 자동차 번호의 지역명이 목록에 없을 경우 예외 발생
        } else {
            require(CAR_NUMBER_REGEX.matches(number)) { "자동차 번호 형식을 확인해 주세요." }	// 자동차 번호 형식과 다를 경우 예외 발생
        }
    }
}

코드가 다소 복잡해 보이지만, 순서를 소개하면 다음과 같습니다.

  • CarNumber.from() 팩토리 메서드를 호출하여 하이픈과 공백 제거
  • init { } 블록으로 인스턴스 생성 시 로직 호출
    • 두 정규식(신 자동차 번호, 구 자동차 번호) 중 하나와 일치 여부 검증
    • 구 자동차 번호일 경우 지역명은 목록 내 있는지 검증

검증에 사용하는 정규식(regex) 및 지역명은 companion object 내 작성하여 싱글톤으로 사용합니다.

위와 같이 코드를 작성하면, 모든 문제가 해결될까요? 아쉽지만 아직 생성자가 공개되어 있다는 문제점이 남았습니다.

val carNumber = CarNumber("123 가 4567")	// 공백이 제거되지 않아 예외 발생

생성자를 직접 호출하게 되면, 앞서 팩토리 메서드 내에 만든 공백 제거 처리 과정을 거칠 수 없습니다. 다행히 생성자를 직접 호출하지 못하도록 막을 수 있는데요. 생성자에 private constructor 접근 제어자를 붙여 주면 됩니다.

value class CarNumber private constructor(val value: String) {

private constructor를 붙여, 팩토리 메서드를 통해서만 CarNumber 인스턴스를 생성하도록 강제할 수 있습니다. 다만 이는 CarNumber 사용자에게 다소 불친절한 구현이기도 합니다. 일반적으로 사용자는 CarNumber("123 가 4567")와 같이 생성자를 통해 인스턴스를 생성하고자 시도하지만, 코드를 작성하기 전까지는 생성자가 private으로 막혀 있고, 대신 CarNumber.from("123 가 4567")와 같이 팩토리 메서드를 사용해야 한다는 점을 알 수 없기 때문입니다.

이는 코틀린의 invoke 연산자를 오버로딩 하여 해결할 수 있습니다. 코틀린에서는 ‘함수 타입의 값은 invoke 연산자를 통해 불러올 수 있다.‘고 안내합니다.

@JvmInline
value class CarNumber private constructor(val value: String) {
    companion object {
        // ...

        @JsonCreator
        fun from(carNumber: String): CarNumber {
            return CarNumber(carNumber.removeSpacesAndHyphens())
        }
      
        operator fun invoke(carNumber: String): CarNumber = from(carNumber)
    }
}

// 사용 예시
val carNumber = CarNumber("123 가 4567")	// 실제로는 생성자 대신 from을 호출

이로서 사용자는 생성자를 직접 호출하듯 CarNumber 인스턴스를 생성할 수 있지만, 내부적으로는 팩토리 메서드 from을 호출하도록 감추어 두게 되었습니다.

경우에 따라 팩토리 메서드 from 역시 private으로 감추어도 됩니다. 이제 사용자는 CarNumber 인스턴스를 만들 때 생성자를 써야 할지, 팩토리 메서드를 써야 할지 고민하지 않아도 됩니다. 동시에 정합성이 깨진 CarNumber 인스턴스가 생성되는 일 역시 방지할 수 있습니다.

마지막으로, jackson을 사용해 직렬화, 역직렬화할 때 해당 팩토리 메서드를 사용하도록 from@JsonCreator 어노테이션을 붙여 줍니다. 이를 통해 의도한 조건을 모두 만족하는 VO를 완성할 수 있습니다. 아래는 최종적으로 완성된 CarNumber 클래스입니다.

@JvmInline
value class CarNumber private constructor(val value: String) {
    companion object {
        private val CAR_NUMBER_REGEX = Regex("(\\d{2,3})([가-힣])(\\d{4})")
        private val OLD_CAR_NUMBER_REGEX = Regex("^([가-힣]{1,2})?(\\d{1,2})([가-힣])(\\d{4})\$")
        private val LOCATION_NAMES =
            setOf("서울", "부산", "대구", "인천", "광주", "대전", "울산", "경기", "강원", "충북", "충남", "전북", "전남", "경북", "경남", "제주")

        @JsonCreator
        fun from(carNumber: String): CarNumber {
            return CarNumber(carNumber.removeSpacesAndHyphens())
        }
      
        operator fun invoke(carNumber: String): CarNumber = from(carNumber)
    }

    init {
        validateCarNumber(value)
    }

    private fun validateCarNumber(number: String) {
        val oldCarNumberMatch = OLD_CAR_NUMBER_REGEX.matchEntire(number)
        if (oldCarNumberMatch != null) {
            val (location, _, _) = oldCarNumberMatch.destructured
            require(location in LOCATION_NAMES) { "알 수 없는 등록 지역입니다." }
        } else {
            require(CAR_NUMBER_REGEX.matches(number)) { "자동차 번호 형식을 확인해 주세요." }
        }
    }
}

위 예와 같이 코틀린으로 코드를 작성할 경우, 다양한 값에 대해 VO를 만들어 두고 인스턴스 생성 전 검증 및 변환을 수행할 수 있습니다. 이렇게 코드를 작성해 두면, 자동차 번호와 관련된 모든 역할과 책임을 위 VO에 위임할 수 있습니다. 자동차 번호 관련 정책 등이 변경되는 경우 CarNumber 클래스만 확인하면 됩니다. 자연스레 의도하지 않은 값이 자동차 번호로 사용되는 등의 오동작을 방지하고, 안정적인 서비스를 제작할 수 있습니다.

예시

간단한 예시로 조금 더 살펴보겠습니다. 아래와 같이 사용자로부터 입력을 받는 API가 있습니다. request 클래스 내 carNumber 필드를 String이 아닌 CarNumber VO로 선언해 주면, 클라이언트에서 조건에 벗어나는 자동차 번호로 API를 호출했을 때, 즉시 예외를 발생시키게 됩니다. 코드를 작성하는 입장에서는 별도의 검증 로직을 호출할 필요도 없으며, CarNumber 인스턴스가 성공적으로 생성된 경우, 자동차 번호 값이 유효함을 보장할 수 있습니다.

@RestController
class CarController {
    @PostMapping("/car")
    fun carInformation(@RequestBody request: CarInformationRequest) {
        // ...
    }
}

data class CarInformationRequest(
    val carNumber: CarNumber
)

Null 여부 확인을 확실하게

코틀린은 null을 안전하게 다루기 위해 다양한 방법을 제공합니다. 이 중에서 값의 불변성과 스마트 캐스팅2을 통해 로직 작성 및 파악에 도움이 되는 예시를 소개합니다.

기존 요청을 다시 보내는 retryLogic이 있습니다. 이 메서드는 아래 역할을 수행합니다.

  1. 전달받은 카테고리 코드에 걸맞은 retryUseCase 탐색
    • 찾지 못한 경우 예외 발생
  2. 찾은 retryUseCase를 사용하여 재조회 요청을 보냄

코드로 보면 아래와 같습니다.

fun retryLogic(
    categoryCode: CategoryCode,
    transactionId: String,
    request: RetryRequest
) {
    val retryUseCase: UseCase? = activeUseCases().firstOrNull { it.type == categoryCode }
    requireNotNull(retryUseCase) { "현재 가능하지 않은 재조회 요청입니다." }

    // 별도 비지니스 로직

    return retryUseCase.getPrice(transactionId, request)
}

val retryUseCaseUseCase? 타입의 값입니다. 이는 해당 값이 UseCase일 수도 있고, null일 수도 있다는 것을 나타냅니다. 코틀린에서는 값의 타입 뒤에 별도로 ?로 nullable함을 지정해 주지 않는 이상, 기본적으로는 값에 null이 들어갈 수 없습니다.

다음으로, 받아온 값의 null check를 합니다. 일반적으로 익숙한 방법은 if를 사용해 null 여부를 확인하는 것입니다.

if (retryUseCase == null) throw IllegalArgumentException("현재 가능하지 않은 재조회 요청입니다.")

코틀린에서는 requireNotNull이라는 계약(contracts)을 사용하여 위와 완전히 동일한 기능을 작성할 수 있습니다.

null 여부 확인 후에도 문제가 해결된 것은 아닙니다. 만약 null 여부 확인 이후 중간에 값이 바뀐다면, 앞에서 retryUseCase에 대해 null 여부 확인을 했어도, 이후에 null이 아니라고 확신할 수 없습니다. 위 코드에서는 ‘별도 비지니스 로직’으로 나타낸 영역입니다.

코틀린이 이 부분에서 특히 강력한 이유는 코틀린의 불변성(immutability) 때문입니다. 코틀린에서는 val 혹은 var로 값을 선언하는데, val로 선언한 값은 불변입니다. 즉, 값이 할당된 이후 바뀌지 않습니다. 이는 null 여부를 확인할 때도, 한 번 null이 아님이 확인된 값은 이후로도 null이 아님을 보장할 수 있습니다. 위 예시 코드에서는 requireNotNull(retryUseCase) 이후부터 해당 값이 null이 아님이 보장됩니다.

이를 코틀린 언어적으로도 스마트 캐스팅을 통해 확인해 줍니다.

스마트 캐스팅을 통해 코틀린 컴파일러에서는 requireNotNull(retryUseCase) 이후, retryUseCase의 타입을 UseCase?가 아닌 UseCase로 취급하게 됩니다. 위 사진에서 초록색으로 강조된 부분이 이를 나타냅니다. 덕분에 코드를 작성하거나 디버깅을 할 때, null이 될 수 있는 값에 대해서도 확인 후 null이 아님을 확신하고 로직을 전개할 수 있습니다. 서비스의 안정적인 운영에 큰 도움이 되는 것은 물론입니다.

확장 함수를 사용한 유틸리티 라이브러리 만들기

보험 서비스를 개발하며 공통으로 사용하게 되는 여러 유틸리티 코드가 있는데요. 특히 primitive 타입 필드나 String에 대해 무언가 조작을 하는 경우가 많습니다. 이러한 내용을 모아서 insurance-common이라는 라이브러리를 만들게 되었습니다.

이렇게 라이브러리를 만들 때 활용할 수 있는 것이 코틀린의 확장 함수와 object declaration입니다. 코틀린의 확장 함수는 별도의 디자인 패턴이나 특정 클래스에 대한 상속 없이 메서드를 확장할 수 있게 해 줍니다. 객체 선언(Object Declaration)은 특정 인스턴스 상태에 독립적인 내용을 담을 때 사용합니다. 언어 레벨에서 싱글톤으로 선언되며, 같은 내용이 불필요하게 여러 번 생성되는 것을 방지하기에 좋습니다.

예를 들어, 문자열 중 특정 패턴을 발견하면 해당 코드를 마스킹하는 maskingName이라는 메서드를 작성합니다.

private val maskingNameRegex = Regex("(?i)Name=[^,)]++[,)]")

/**
* 문자열 내 "Number="와 "," 또는 ")"으로 둘러싸인 숫자를 첫 자리만 제외하고 마스킹 처리
*/
fun maskingName(input: String): String {
    return input.replace(maskingNameRegex) { "${it.value.substring(0, 6)}*${it.value.last()}" }
}

// 사용 예시
val maskedValue = maskingName(userName)

위 코드를 코틀린의 확장 함수를 이용해 리팩토링 한다면, 아래와 같이 수정할 수 있습니다.

fun String.maskingName() = this.replace(maskingNameRegex) { "${it.value.substring(0, 6)}*${it.value.last()}" }

// 사용 예시
val maskedValue = userName.maskingName()

String이라는 클래스에 마스킹 메서드를 확장하였습니다. 이 메서드는 특정 인스턴스의 상태와는 무관한 메서드입니다. 즉, 싱글톤3으로 선언 후 재활용하는 것이 리소스 관리 상 유리합니다. 코틀린의 object declaration을 사용해서 싱글톤으로 사용할 수 있습니다.

object StringUtils {
    private val maskingNameRegex = Regex("(?i)Name=[^,)]++[,)]")

    /**
     * 문자열 내 "Number="와 "," 또는 ")"으로 둘러싸인 문자열을 첫 자리만 제외하고 마스킹 처리
     */
    fun String.maskingName() = this.replace(maskingNameRegex) { "${it.value.substring(0, 6)}*${it.value.last()}" }
}

이를 사용하는 예시는 단위 테스트를 통해 확인할 수 있습니다.

    @DisplayName("문자열 내 Number=와 , 또는 )으로 둘러싸인 문자열을 첫 자리만 제외하고 마스킹 처리")
    @Test
    fun maskingName() {
        // given
        val name = "김춘식"
        val text = "userName=$name, result=\"success\""
        val lowerText = "name=$name, result=\"success\""

        // when
        val result = text.maskingName()
        val lowerResult = lowerText.maskingName()

        // then
        val expectedMaskedResult = "김*"

        assertThat(result).isEqualTo("userName=$expectedMaskedResult, result=\"success\"")
        assertThat(lowerResult).isEqualTo("name=$expectedMaskedResult, result=\"success\"")
    }

이런 방식으로 보험 서비스 전역에서 사용하는 기능들을 라이브러리화 하여 사용하고 있습니다. 운영 중인 여러 프로젝트 간 중복 코드를 줄일 수 있었고, 이들을 별도의 성능 저하와 같은 성능적 손해 없이 사용하고 있습니다. 이는 모두 코틀린의 확장 함수와 object declaration 덕분입니다. 이렇게 관리된 코드는 읽기 쉽고 유지보수가 용이하여, 서비스 운영에 큰 도움이 됩니다.

data class를 사용한 간단하고 효율적인 단위 테스트

단위 테스트를 작성할 때, 테스트를 위해 주어진 상황을 설정하는데 data class를 유용하게 사용할 수 있습니다. 코틀린의 data class는 이름 그대로 데이터를 표현하기 위한 클래스입니다. 일반 클래스와는 다르게 equals(), hashCode()가 재정의되며, 그 밖에 copy() 등 다른 메서드도 자동으로 생성됩니다. DTO4 등 데이터를 나타내는 클래스를 작성할 때 data class를 사용하면 유용합니다.

아래와 같이 테스트하고자 하는 DTO가 있습니다.

data class UserInformation(
    val name: String,
    val age: Int,
    val birthDate: LocalDate,
    val address: String,
    val gender: Gender,
    val isDisplay: Boolean
) {
    enum class Gender {
        MALE,
        FEMALE;
    }

    init {
        require(age >= 18)
    }
}

이때, 나이 검증 여부를 테스트하고자 합니다. 나이가 18세 이상일 경우, 아무런 예외도 던지지 않고, 18세 미만일 경우 IllegalArgumentException을 발생시켜야 합니다.

class WhateverTest() {
    @Test
    fun `나이가 18세 미만이면 IllegalArgumentException을 던진다`() {
        assertThrows<IllegalArgumentException> {
            val userInformation = UserInformation(
                name = "정카펀",
                age = 17,
                birthDate = LocalDate.of(2022, 12, 19),
                address = "카카오 판교 아지트",
                gender = UserInformation.Gender.MALE,
                isDisplay = true
            )
        }
    }

    @Test
    fun `나이가 18세 이상이면 예외를 던지지 않는다`() {
        assertDoesNotThrow {
            val userInformation = UserInformation(
                name = "정카펀",
                age = 18,
                birthDate = LocalDate.of(2022, 12, 19),
                address = "카카오 판교 아지트",
                gender = UserInformation.Gender.MALE,
                isDisplay = true
            )
        }
    }
}

테스트를 작성했습니다. 문제점이 보이시나요?

  1. 테스트하고자 하는 대상이 불명확하다.
  2. 불필요한 코드가 반복된다.

UserInformation에서 어떤 값이 예외 발생에 기여하는지 위 테스트 코드를 보고서는 파악하기 어렵습니다. 주석을 달아 주는 것으로 일부 해결할 수 있겠지만, DTO의 필드가 수십 개가 있다면 어떤 필드에 주석이 붙어있는지조차 한눈에 들어오지 않습니다. 또한 UserInformation 인스턴스 생성을 위해, age 외 다른 필드에도 적당한 값을 할당해 주어야 합니다.

이를 해결하기 위해 data class의 copy()를 사용할 수 있습니다. copy()는 아래와 같은 특성을 가집니다.

  • 완전히 동일한 data class 인스턴스를 하나 생성합니다. 기존의 인스턴스와 equals() 비교 시 서로 같습니다.
  • copy() 호출 시, 파라미터에 값을 지정할 수 있습니다. 이 경우, 해당 파라미터의 값만 지정한 값으로 설정하여 복사됩니다.

다시 위의 코드로 돌아가서 공통 부분을 따로 빼고, 테스트 시에는 테스트 대상이 명확하도록 수정해 보겠습니다.

class WhateverTest() {
    @Test
    fun `나이가 18세 미만이면 IllegalArgumentException을 던진다`() {
        val invalidAge = 17
        assertThrows<IllegalArgumentException> {
            val userInformation = successUserInformation.copy(age = invalidAge)
        }
    }

    @Test
    fun `나이가 18세 이상이면 예외를 던지지 않는다`() {
        val validAge = 18
        assertDoesNotThrow {
            val userInformation = successUserInformation.copy(age = validAge)
        }
    }
  
    private val successUserInformation = UserInformation(
        name = "정카펀",
        age = 28,
        birthDate = LocalDate.of(2022, 12, 19),
        address = "카카오 판교 아지트",
        gender = UserInformation.Gender.MALE,
        isDisplay = true
    )
}

비교를 위해 코드를 좌우로 배치했습니다.

차이가 느껴지시나요? copy() 사용을 통해서

  • 반복되는 코드를 줄였고,
  • 테스트에 영향을 미치는 대상을 명확히 드러냈습니다.

이는 테스트 작성의 허들을 낮추고, 테스트의 가독성을 높여 줍니다. 덕분에 테스트의 문서로서의 역할을 더욱 기대할 수 있고, 다양한 테스트를 통한 안정적인 서비스를 작성할 수 있습니다. given-when-then, 또는 arrange-act-assert로 사용되는 BDD5 스타일로 테스트를 구분하여 작성하는 경우에도 이는 유효합니다.

마치며

서비스의 서버를 책임지는 입장으로서 제 관심사는 안정적이고, 읽기 쉬우며, 확장이 용이한 서비스의 운영입니다. 코틀린을 처음 사용한 이래 지난 2년간, 카카오페이에서 여러 서비스를 개발하며 이와 같은 관심사를 하나씩 충족하고 있습니다. 더 많은 분들이 안정적으로 서비스를 개발하고 운영하실 수 있도록, 기술 블로그를 통해 제 경험을 공유했습니다. 코틀린에 관심이 있거나 더 안정적인 서비스를 제작하고 싶으신 분들께 제 경험이 도움이 되면 좋겠습니다.

Reference

Footnotes

  1. Value Object. 특정 값(엔티티)을 나타내는 객체를 의미합니다.

  2. 컴파일러가 불변 값들에 대해 타입 체크와 명시적 형변환을 트래킹하고, 필요한 경우 묵시적 형변환을 추가하는 코틀린의 주요 기능을 가리킵니다.

  3. Singleton Pattern. 특정 클래스가 하나의 인스턴스만이 생성되어, 전역적으로 사용하는 디자인 패턴.

  4. Data Transfer Object. 데이터를 전송하기 위한 객체를 뜻합니다.

  5. Behaviour Driven Development. 테스트를 작성할 때 도메인 언어를 사용하여 코드의 행동을 묘사하는 것을 가리킵니다.

katfun.joy
katfun.joy

카카오페이 서버 개발자 카펀입니다. 어려움과 불편함을 기술을 통해 해소하는 것을 매우 좋아합니다. 한 줄의 코드마다 근거를 가지고 작성하고자 노력합니다.

태그