Kotlin 자주 사용하는 패턴 정리

Posted by Yun on 2024-05-06

불변 객체의 효율적 관리: copy() 메소드 활용

불변 객체는 데이터의 안정성과 예측 가능성을 제공하여 소프트웨어 개발에서 권장되는 패턴입니다. 특히, 암호화와 같은 데이터의 보안적 처리에 있어서 불변 객체를 사용하면 변경될 필요가 없는 정보의 무결성을 유지할 수 있습니다. Kotlin의 data class는 이러한 불변 객체를 다루기 위한 유용한 기능 중 하나로 copy() 메소드를 제공합니다. 이 메소드를 사용하면 객체의 일부만을 변경한 새로운 객체를 생성할 수 있어, 기존 객체의 불변성을 해치지 않으면서 필요한 부분만 업데이트가 가능합니다.

코드 예시 및 설명

아래의 테스트 코드는 User라는 데이터 클래스의 인스턴스를 생성한 후, copy() 메소드를 사용하여 이메일 주소만을 암호화된 형태로 변경하는 예를 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
fun `불변 객체의 유지보수를 위한 copy 활용 예시`() {
val user = User(
name = "name",
email = "email@asd.com"
)

val userCopy = user.copy(
email = "email@asd.com 암호화"
)

// User(name=name, email=email@asd.com)
println("user: $user")
// User(name=name, email=email@asd.com 암호화)
println("userCopy: $userCopy")

// 428039780
println("user: ${System.identityHashCode(user)}")
// 48361312
println("userCopy: ${System.identityHashCode(userCopy)}")
}
  • 원본 객체 출력: user: User(name=name, email=email@asd.com)
  • 복사 후 업데이트된 객체 출력: userCopy: User(name=name, email=email@asd.com 암호화)
  • 객체 식별자 비교: 두 객체의 System.identityHashCode 값을 출력하여 각각 다른 객체임을 확인할 수 있습니다.

포인트 정리

  • copy() 메소드는 원본 객체의 일부 속성을 변경하여 새로운 객체를 생성합니다. 이 방식은 기존 객체의 불변성을 유지하면서 필요한 데이터만 갱신할 수 있는 효율적인 방법을 제공합니다.
  • val 키워드를 사용하여 불변성을 명시하는 것은 데이터 보호 및 버그 방지에 중요합니다. 특히 암호화와 같이 데이터 보안이 중요한 작업에서는 불변 객체의 사용이 더욱 중요합니다.

이 방법은 데이터의 무결성을 유지하면서도 효율적인 데이터 관리를 가능하게 하여, 유지보수성을 높이고 시스템의 안정성을 강화합니다. 불변 객체와 copy() 메소드의 적절한 사용은 모던 소프트웨어 개발의 중요한 측면 중 하나입니다.

Pair와 Triple 객체의 유용성과 효율적 사용

코틀린에서는 간단한 객체를 빠르게 생성하고 사용할 수 있도록 PairTriple이라는 두 가지 유틸리티 클래스를 제공합니다. 이러한 클래스는 특히 서비스 내부 로직에서만 사용되는 임시 데이터를 다룰 때, 매번 새로운 DTO(Data Transfer Object)를 만드는 것보다 더 효율적일 수 있습니다. 또한, 멀티 모듈 프로젝트에서 여러 모듈 간에 DTO 클래스를 공유해야 할 때 이러한 객체의 사용이 유용합니다.

코드 예시 및 설명

다음은 UserPointCalculator 클래스의 구현 예시입니다. 이 클래스는 사용자 정보를 MySQL 데이터베이스에서 가져오고, 사용자의 포인트 정보는 Redis에서 가져와 계산을 진행합니다. 이 과정에서 Triple 객체를 활용하여 각 사용자의 이름, 이메일, 포인트 정보를 효과적으로 관리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserPointCalculator(
private val userRepository: UserRepository,
private val userPointRepository: UserPointRepository
) {
fun calculate() {
val users = userRepository.findUserByIds(listOf(1, 2, 3))
val points = userPointRepository.findUserPoint(listOf(1, 2, 3))
.associateBy { it.id }

val userPoints = users.map {
Triple(
first = it.name,
second = it.email,
third = points[it.id]!!.point
)
}

for (userPoint in userPoints) {
println("user name: ${userPoint.first}, user email ${userPoint.second}, user point ${userPoint.third}")
}
}
}

분석: 이 예에서 Triple 객체는 각 사용자의 이름, 이메일, 포인트를 저장하는데 사용됩니다. 이는 데이터베이스와 다른 저장소에서 정보를 읽어와 조합할 때 유용하게 사용됩니다.

Pair와 Triple 사용 시 가독성 향상

Pair와 Triple은 기본적으로 first, second, third라는 속성명을 사용합니다. 이 속성명은 코드의 가독성을 저하시킬 수 있으므로, 구조 분해 할당(destructuring declaration)을 사용하여 보다 의미 있는 변수명을 사용하는 것이 좋습니다.

1
2
3
4
5
6
7
8
fun calculate() {
// 반복 처리
for ((userName, userEmail, userPoint) in userPoints) {
println("user name: ${userName}, user email ${userEmail}, user point $userPoint")
}
// 개별 처리
val (userName, userEmail, userPoint) = userPoints.first()
}

이와 같은 처리 방식은 PairTriple을 사용할 때 코드의 명확성을 향상시키고, 데이터를 보다 효율적으로 다루는 데 도움이 됩니다. 데이터를 직관적으로 알아볼 수 있도록 이름을 명확하게 지정함으로써, 코드의 가독성과 유지보수성을 크게 개선할 수 있습니다.

이 방법은 간단한 데이터 구조를 사용하면서도 프로그램의 복잡성을 줄이고, 클린 코드를 유지하는 데 기여합니다.

포인트 정리

PairTriple 객체는 임시 데이터 또는 내부 로직에서만 사용되는 데이터를 간편하게 다루기 위한 우수한 도구입니다. 이들은 DTO를 정의하는 복잡성을 피할 수 있으며, 특히 간단한 데이터 그룹을 빠르게 다루어야 할 때 효율적입니다. 하지만, 이들을 사용할 때는 변수명을 명확하게 지정하여 코드의 가독성을 유지하는 것이 중요합니다.

이 글은 Pair와 Triple 객체의 적절한 사용 사례를 통해 개발자들이 코드의 간결성과 유지보수성을 향상시키는 방법을 제시합니다.

typealias를 활용한 코드 개선

코틀린의 typealias 기능은 복잡한 타입 선언에 대한 간결하고 의미 있는 이름을 제공함으로써 코드의 가독성과 유지보수성을 크게 향상시킵니다. 특히, 프로젝트 내에서 자주 사용되는 타입 조합에 별칭을 부여함으로써, 코드의 일관성을 유지하고 타입 변경 시의 유연성을 높일 수 있습니다.

코드 예시 및 설명

다음 예제에서는 사용자 정보(User)와 사용자의 포인트(UserPoint)를 연결하는 PairUserPointAssociation이라는 typealias를 사용합니다. 이는 Pair<User, UserPoint>의 사용을 간소화하고, 의미를 명확히 합니다.

1
2
3
4
5
6
7
8
9
10
typealias UserPointAssociation = Pair<User, UserPoint>

val userPointAssociations = users.map {
UserPointAssociation(
first = it,
second = points[it.id]!!
)
}

val (user, userPoint) = userPointAssociations.first()
  • UserPointAssociation 타입 별칭은 Pair<User, UserPoint>를 대체하여 코드의 목적을 더 명확하게 표현합니다.
  • map 함수 내에서 UserPointAssociation 생성자를 사용하여 각 사용자와 해당 포인트 객체를 쌍으로 묶습니다. 이는 데이터의 논리적 연관성을 직관적으로 보여줍니다.
  • 구조 분해 할당을 통해 useruserPoint 변수에 각각 사용자 정보와 포인트 정보를 할당함으로써, 코드의 가독성을 더욱 향상시킵니다.

포인트 정리

  • 명확성: UserPointAssociation과 같은 타입 별칭을 사용하면, 복잡한 타입 조합도 의미 있는 이름으로 대체되어 코드의 목적이 분명해집니다.
  • 유연성: 타입 별칭을 사용하면, 기본 타입 구조에 변경이 필요할 때 별칭 정의만 수정하면 되므로 코드 전체에 걸쳐 유연하게 변경을 적용할 수 있습니다.
  • 재사용성: 일단 정의된 타입 별칭은 프로젝트 전반에 걸쳐 재사용될 수 있으며, 코드의 일관성을 유지하는 데 도움이 됩니다.

typealias는 복잡한 타입을 간소화하고, 코드의 의도를 명확히 전달하는 데 큰 도움을 줍니다. 이는 특히 크고 복잡한 프로젝트에서 타입 관리를 효과적으로 수행하는 데 중요한 역할을 합니다.

runCatching 함수와 Result 객체를 활용한 안전한 처리

코틀린의 runCatching 함수는 예외 발생 가능성이 있는 코드 블록을 실행하고 그 결과를 Result 타입으로 캡처합니다. 이 기능은 HTTP 통신과 같은 네트워크 요청에서 매우 유용하게 사용될 수 있습니다. Result 객체는 성공적인 결과 또는 발생한 예외를 안전하게 처리할 수 있는 API를 제공합니다.

코드 예시 및 설명

다음 예제는 HTTP API를 통해 사용자 데이터를 요청하고 결과를 처리하는 과정을 보여줍니다:

1
2
3
4
5
fun getUser(userId: Long): User {
return runCatching { userClient.getUser(userId) }
.onFailure { throw IllegalArgumentException("Failed to fetch user data for user ID $userId") }
.getOrThrow()
}
  • userClient.getUser(userId) 함수는 HTTP 요청을 통해 사용자 정보를 가져옵니다. 이 함수는 네트워크 에러나 데이터 문제로 예외를 발생시킬 수 있습니다.
  • runCatching은 이 요청을 감싸 실행하며, 요청 중 발생하는 예외를 Result 객체로 캡처합니다.
  • onFailure 블록은 Result 객체가 예외를 캡처했을 경우 실행됩니다. 여기서는 사용자 정의 예외를 던져, 오류 발생을 명확히 알립니다.
  • getOrThrowResult 객체에서 값을 추출합니다. 만약 Result가 실패를 나타내는 경우, onFailure에서 설정한 예외가 발생됩니다.

포인트 정리

  • 안전한 실행과 예외 처리: runCatchingResult 객체를 사용하면 예외 처리를 안전하고 효율적으로 수행할 수 있습니다. 이를 통해 프로그램의 견고성이 증가합니다.
  • 결과 처리의 유연성: Result 타입은 getOrThrow, getOrElse, getOrNull 등 다양한 방법으로 결과를 처리할 수 있는 확장 함수를 제공합니다. 이 함수들은 각 상황에 맞게 결과를 유연하게 처리할 수 있도록 도와줍니다.
  • 코드의 간결성과 명확성: runCatching을 사용함으로써 전통적인 try-catch 블록보다 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다.
  • 에러 핸들링의 명확성: onFailure를 통해 에러 발생 시 명확한 처리 로직을 구현할 수 있으며, 에러 메시지를 통해 오류의 원인을 더욱 분명히 할 수 있습니다.

코틀린의 Result 타입은 개발자가 더 유연하고 견고한 코드를 작성할 수 있도록 지원합니다. 특히, HTTP 통신을 많이 다루는 서비스에서는 이러한 패턴을 적극적으로 활용하여 애플리케이션의 안정성과 유연성을 동시에 향상시킬 수 있습니다. 더 자세한 내용과 실용적인 설계 전략은 카카오페이 기술 블로그의 MSA 환경에서의 유연한 HTTP 클라이언트 설계 전략 글에서 확인하실 수 있습니다. 이 글에서는 Result 타입을 활용하여 MSA 환경에서 HTTP 클라이언트를 유연하게 설계하는 방법을 소개하고 있습니다.

안정적인 서비스 운영을 위해 runCatching과 같은 코틀린의 기능을 적극 활용해보시길 권장드립니다. 이러한 패턴들은 예외가 발생할 가능성이 있는 네트워크 요청을 처리할 때 특히 유용하며, 시스템의 전체적인 에러 관리 능력을 개선할 수 있습니다.

코틀린에서 by 키워드를 활용한 로깅 설정

코틀린에서 by 키워드는 위임 패턴(delegation)을 간편하게 구현할 수 있도록 도와줍니다. 특히, 로깅과 같은 반복적으로 사용되는 기능을 클래스에 쉽게 통합할 수 있게 하는 강력한 도구입니다. by 키워드를 사용하면 인스턴스 생성을 위임함으로써 코드의 중복을 줄이고 유지보수를 용이하게 만들 수 있습니다.

코드 예시 및 설명

아래 예제는 by 키워드를 사용하여 Logger 인스턴스를 생성하고 이를 클래스에서 쉽게 사용할 수 있도록 보여줍니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.slf4j.Logger
import org.slf4j.LoggerFactory

// 로거 인스턴스 생성을 위한 제네릭 확장 함수
fun <A : Any> A.logger(): Lazy<Logger> = lazy { LoggerFactory.getLogger(this.javaClass) }

// 클래스 내부에서 위임을 통해 로그 인스턴스 사용
class MyClass {
private val log by logger()

fun performAction() {
log.info("Action performed")
}
}
  • logger() 함수는 Lazy<Logger>를 반환합니다. 이 함수는 호출하는 객체의 클래스 이름을 사용하여 Logger 인스턴스를 생성합니다.
  • private val log by logger() 표현은 실제 로그 인스턴스가 필요할 때까지 로그 객체의 생성을 지연시킵니다(lazy를 사용).
  • 이 방식을 통해 클래스 내부에서 log를 직접 사용할 수 있으며, 로그 호출 시점에만 로거 인스턴스가 초기화됩니다.

포인트 정리

  • 효율적인 자원 사용: lazy를 사용함으로써 로거의 초기화를 실제 로깅이 필요한 시점까지 지연시킬 수 있습니다. 이는 자원을 효율적으로 사용하게 합니다.
  • 코드 중복 감소: logger() 확장 함수를 사용하면 모든 클래스에서 동일한 로깅 구성을 쉽게 재사용할 수 있습니다. 이는 코드 중복을 크게 줄여줍니다.
  • 유지보수의 용이성: 로그 인스턴스 생성 코드를 한 곳에 집중시키므로, 로거 설정을 변경할 때 다수의 클래스를 수정할 필요가 없습니다. 이는 전체적인 유지보수를 간단하게 만듭니다.

by 키워드의 사용은 코틀린의 강력한 기능 중 하나로, 개발자가 코드를 보다 효과적이고 깔끔하게 관리할 수 있도록 돕습니다. 위의 예제처럼 by 키워드를 사용하는 것은 반복되는 코드 패턴을 단순화하고 프로젝트의 전반적인 품질을 향상시키는 데 큰 도움이 됩니다.

코틀린에서 초기화 지연을 안전하게 관리하기

Delegates.notNull()은 코틀린에서 프로퍼티가 사용되기 전에 초기화되어야 함을 보장하는 위임 메커니즘입니다. 이 방법은 특히 프로퍼티의 초기화 시점이 명확하지 않을 때 유용하며, 초기화되지 않은 상태에서의 접근을 방지하여 안전성을 높입니다.

코드 예시 및 설명

다음 예제는 QuerydslCustomRepositorySupport 클래스에서 Delegates.notNull()을 사용하는 방법을 보여줍니다. 스프링 프레임워크의 의존성 주입 기능을 사용하여 EntityManager가 주입된 후 JPAQueryFactory를 초기화합니다. 이런 경우, 의존성 주입의 시점이 런타임에 결정되므로 Delegates.notNull()을 활용하여 안전하게 초기화를 보장할 수 있습니다. 이는 EntityManager가 설정되기 전에 queryFactory가 사용되는 것을 방지하며, 초기화되지 않은 상태에서의 접근을 효과적으로 차단합니다.

1
2
3
4
5
6
7
8
9
10
abstract class QuerydslCustomRepositorySupport(domainClass: Class<*>) : QuerydslRepositorySupport(domainClass) {

protected var queryFactory: JPAQueryFactory by Delegates.notNull()

@PersistenceContext
override fun setEntityManager(entityManager: EntityManager) {
super.setEntityManager(entityManager)
this.queryFactory = JPAQueryFactory(entityManager)
}
}
  • queryFactory 프로퍼티는 JPAQueryFactory 타입으로 선언되어 있으며, Delegates.notNull()을 통해 위임되고 있습니다. 이는 queryFactory가 사용되기 전에 반드시 초기화되어야 함을 보장합니다.
  • setEntityManager 메소드는 EntityManager를 받아 super 클래스의 같은 메소드를 호출한 후, queryFactory를 초기화합니다. 이 메소드는 @PersistenceContext 애노테이션을 통해 JPA의 영속성 컨텍스트에서 EntityManager가 주입될 때 자동으로 호출됩니다.
  • 만약 queryFactorysetEntityManager 메소드 호출 전에 사용되려고 하면, IllegalStateException이 발생하여 개발자에게 초기화 문제를 즉시 알려줍니다.

포인트 정리

  • 안전성 보장: 초기화되지 않은 프로퍼티의 사용을 방지하여 애플리케이션의 안정성을 향상시킵니다.
  • 명시적인 오류 처리: 초기화되지 않은 프로퍼티에 접근하려고 할 때 즉각적으로 예외가 발생함으로써, 초기화 로직의 오류를 빠르게 파악하고 수정할 수 있습니다.
  • 초기화 유연성: 특정 메소드나 조건 하에서만 초기화가 가능한 경우에 Delegates.notNull()을 사용하여 유연하게 초기화를 관리할 수 있습니다.

이러한 특성 덕분에 Delegates.notNull()은 코틀린에서 프로퍼티의 초기화를 안전하고 효과적으로 관리할 수 있는 강력한 방법을 제공하며, 특히 늦은 초기화가 필요한 상황에서 그 가치가 더욱 빛납니다.