logo

코프링 6개월..

기존에 사용하던 자바+스프링(이하 자프링..)을 지나
코틀린+스프링(이하 코프링..) 조합으로 업무를 해온지 6개월 정도 지났습니다.

몇가지 사용하며 느낀점을 정리합니다.

사실 꽤나 많은.. 포인트 들이 있는데,
그 중에서 꽤 꽤 많이 좋았던 부분을 정리합니다.
이제 자바로 돌아갈수 없는 몸이.. 코틀린만세!

<br/>

Null !

우선 많은 분들이 말씀하시는것처럼 Null 값을 다루기가 편리합니다.

자바에서 고통을 안겨주던 Null에서 어느정도 해방 할 수 있습니다.

<br/>

코틀린은 변수 선언 시 Not Nullable, Nullable 을 강제로 선언해야합니다. 이런 강제성 확실한 장점으로 다가옵니다.

Nullable에 대한 선언에 대한 강제는 , 변수 선언 시 Nullable 신경쓰며 선언 할 수 밖에 없기 때문입니다.
모두가 약속을하지 않더라도 우리는 Nullable에 신경 쓸 수밖에 없는 코드를 작성하게 됩니다.

<br/>

또한, 코드를 보면서 이 값이 Nullable 한지 가독함으로써 파악이 가능하기도 합니다.

data class UserRead(
  val username: String,
  val telephone : String,
  val email : Strng? // 이메일은 null 일 수 있구나.
)

<br/>

이 외에도, Null을 다루기가 편리한 Null Safe Operator를 제공하는 것도 좋습니다.

자바에서는 if(value != null) 을 통한 분기처리가 꽤 많이 필요했지만, 코틀린은 이 코드를 짧고 간결하게 처리 할 수 있습니다.

// Null Safe Operator
fun function(){
   val str : String? = null   
   println(str?.toUpperCase()) // result : null,  !! 자바였다면? Exception 발생
}

// 엘비스 연산자
fun function(user : UserRead){
    val username = user.username ?: ""
    val telephone = user.telephone ?: ""
    val email = user.email ?: ""
}

<br/>

POWER 컬렉션

업무 대부분의 비즈니스로직이 사실 그렇게 복잡하지 않다는것은 사실입니다.

데이터를 가져와, 자료구조를 바꾸고 가공하고, 조합하고 등등 이런 일련의 작업들이 소스코드에 꽤나 많은 부분을 차지합니다.

기존의 자바의 스트림을 그런점에서 꽤나 많이 애용했지만, 코틀린은 자바의 스트림 보다도, 다양한 함수를 제공합니다.

정말 편하기 따로 없습니다... 만세 \ 0 /

<br/>

예를 들면, forEach를 돌면 index가 필요하다고 가정합니다.

자바에서 이렇게 했다면,

public void function(){
   List<Int> ints
   for(int index = 0; index < ints.size(); index++){
      int value = ints.get(value)
      System.out.println(value)
   }
}

코틀린에서는 이렇게 지원해줍니다.

fun function(){
   val ints = listOf<Int>()
   ints.forEachIndexed{ index, it -> // index, 값
      println(it)
   }
}

<br/>

이 외에도, 컬렉션을 다루며 자바에서는, 으.. 코드 드러워지네.. 망설여지네..

했던 코드들이 코틀린에 이미 쉽게 처리 할 수 있도록 함수에 거진 다.. 있습니다.

<br/>

만약에 없다면, 별도 컬렉션 클래스에 함수를 정의해주면 됩니다.

fun <T> List<T>.notContains(value : T): Boolean {
    return !this.contains(value)
}

<br/>

주의해야할점은 반대로 코틀린의 컬렉션 함수는 매 함수마다 새로운 컬렉션을 반환합니다

때문에 공간비용을 차지합니다.

그럴때는 asSequence() 함수를 먼저 호출한후, 진행하면, 스트림 처럼 사용할 수 있습니다.

fun function (){
    val ints  = listOf(1,2,3)
    ints.asSequence() // 이건 마치 Stream !
        .filter{ it / 2 == 0}
        .map{ it *2 }
        .toList()
}

<br/>

불변! 불변! 불변!

코틀린을 불변성 을 기본으로 합니다.

코틀린은 val라는 키워드를 통해 변수를 선언할 수 있습니다. 자바에 final 키워드를 붙이는것과 같습니다.

대부분 val를 사용하여 변수를 선언하며,

대부분 변수가 자바의 final 키워드와 함께 선언되는것 같습니다.

<br/>

또한, 컬렉션도 불변을 기본으로 합니다.

변하는 컬렉션불변하는 컬렉션을 언어차원에서 분리해서 사용 할 수 있습니다.

불변하는 컬렉션의 변경은 새로운 컬렉션을 반환합니다.

<br/>

요지는 코틀린이 불변성을 기본으로 하다보니, 코드를 작성하는데 안정감이 느껴진다는 것입니다.

<br/>

생성된 객체와 변수들은 불변함을 가정하기에, 변경에 대해서 덜 의심 할 수 있습니다.

기본적인 가정을 변했다면 -> 새롭게 생성되었다를 전제로 의심을 시작 할 수 있기 때문입니다.

<br/>

컬렉션 또한 마찬가지입니다.

어떤 컬렉션을 함수의 인자로 넘겨 무슨 행위를 하든,

기본적인 가정으로 불변 컬렉션은 변하지 않았다, 변했다면 새로운 컬렉션이다! 라는 가정이 가능합니다.

<br/>

좀 더 객체지향적으로

객체를 좀더 객체지향적으로 쓸 수 있습니다.

객체는 수동적인 존재가 아니라, 객체는 능동적인 존재여야 한다!
객체에게 묻지말고, 메세지를 보내라, 감출 수 있는 것을 모두 감추어라!

<br/>

다음과 같은 양수(PostiveNumber)클래스가 있습니다. 해당 클래스는 더하기(plus)함수만을 제공합니다.

data class PostiveNumber(
    val value : Int, 
){
    fun plus(x : PostiveNumber) : Int {
        return value + x.value
    }
}

<br/>

하지만 특정 비즈니스로직을 다루며 곱하기가 필요합니다.

그러면 곱하기(mutlple) 함수가 없는 상황에서, 자바라면 다음과 같이 할 겁니다.

class CalculatorService{
    public int multiple(PostiveNumber x, PostiveNumber y){
        return PositiveNumber(x.getvalue() * y.getValue())
    }
}

곱하기 연산을 하기위에, 객체의 값을 가져와서 외부에서 계산을 합니다.
객체는 수동적으로 다루어 집니다.
수동적인 객체는 변화를 어렵게 합니다.
나중에 0은 곱할 수 없다는 조건이 추가되면 어떨까요? 객체의 값을 가져와 곱하는 코드를 모두 수정해야합니다.

<br>

하지만 코틀린에서라면..
코틀린은 클래스 외부에서도, 클래스의 함수를 정의 할 수 있습니다.
다음과 같이, 아름답게 코드를 작성 할 수 있습니다.

class CalculatorService(){
    fun multiple(PostiveNumber x, PostiveNumber y) : PostiveNumber{
        return x.multiple(y)
    }
}

fun PostiveNumber.multiple(y : PostiveNumber){
    return PostiveNumber(this.value * y.value)
}    

외부로부터 객체 내부의 값을 보호하며, 능동적인 객체로 손쉽게 정의 할 수있습니다.
추후의 조건에도 PostiveNumber.multiple() 함수만 변경하면 될 것입니다.

<br/>

강력한 점은
이런 문법적 요소가, 자연스럽게 능동적인 객체를 작성하게되는 사고의 전환을 가져오게 되는 것 입니다.
본인도 모르게 작성하게되는, 수동적인 객체를 생성하는 습관을 버릴 수가 있습니다

과거의 코드를 보면 능동을 인지하면서도, getter() 를사용하여 수동적인 객체를 남발했다. ㅇ_0...

<br/>

또한, 캡슐화 측면에서의 장점이 한 가지 더있습니다.

캡슐화는 변화하는 모든것을 숨기는 것 입니다.

예를 들면 PostiveNumber 클래스에서, 인자로 받은 X 까지의 더하는 함수가 필요합니다.

그래서 다음과 같이 sum 함수를 추가합니다.

data class PostiveNumber(
    val value : Int, 
){ 
   //.....
    fun sum(x : PostiveNumber) : Int {
        return (value ... x.value).sum()
    }
}

그리고 CalculatorService 클래스에서 호출합니다.

class CalculatorService(){
    fun sum(PostiveNumber x, PostiveNumber y) : PostiveNumber{
        return x.sum(y)
    }
}

<br/>

하지만, PostiveNumber.sum()함수를 CalculatorService만 필요하다면 어떨까요?

캡슐화는 변화 할 수 있는 모든것을 숨기는 것입니다.
변할 수 있는것은 인터페이스도 마찬가지입니다.
인터페이스를 숨긴다는 것은, 인터페이스의 변경에 변화하는 코드가 줄어든다는 것입니다.

코틀린은 다른 곳에서 쓸 필요 없는 함수를 숨길 수 있습니다.

PostiveNumber.sum() 함수를 CalculatorService내에서 private 접근지정자로 정의합니다.

class CalculatorService(){
    fun sum(PostiveNumber x, PostiveNumber y) : PostiveNumber{
        return x.sum(y)
    }
    
    private fun PostiveNumber.sum(x : PostiveNumber) : Int {
        return (value ... x.value).sum()
    }
}

이 문법적 사용을 통해서, PostiveNumber.sum()는 다른 클래스에서는 사용하지 못할 것입니다.
다른 외부에서의 불필요한 인터페이스는 사전에 제거 해버립니다.

객체의 함수의 범위를 한정하여, 인터페이스를 캡슐화 하는 것입니다

<br/>

실제로 서비스 레이어에서 이런 사용이 가능합니다.
User 엔티티를 가져와, toResponse로 전환하는 예제입니다.

class UserService(){
    fun findById(id : Long){
        val user = userRepository.findById(id)
        user.toResponse()
    }
    
    private User.toResponse(){
        return UserResponse(this.id, this.name)
    }
}

더불어..
보통 엔티티에서 DTO로의 의존관계는 고수준에서 저수준으로 의존관계로 지양합니다.
하지만 위 예제에서는 고수준인 엔티티내에서 DTO에 의존하지 않으며, 상대적으로 낮은 수준에서 저수준로의 의존관계를 가질 수 있게합니다.

<br/>

커리함수 (Curry Function)

코틀린을 어느 정도 사용하다보니, 커리함수를 애용하게됩니다.

주로 AOP형태로 사용하게되는데, AOP를 정말 쉽게 구현할 수 있습니다.

쓰다보면 AOP 남발을..

코틀린은 메소드를 커리하게 사용할 수 있는데, 이런 모습을 가지게 됩니다.

fun <T> curry(method : () -> T) : T{
   val result = method.invoke()
   return method.invoke()
}

다음과 같이 커리하게 사용 할 수 있습니다.

fun plus(a : Int, b Int ) = curry {
    a + b
}

<br/>

음.. 그냥 AOP 아니야? 라고 쉽게 생각이 들지만,
Aspect에 해당하는 함수를 유틸성으로 정의하면, 다음과 같이 AOP를 정말 쉽게 구현 할 수 있습니다.

<br/>

만약 함수의 시작/종료 시간을 로깅하고싶다면,, 다음과 같은 형태로 사용할 수 있습니다.

object Logging{
    fun <T> time(method : () -> T) {
       logging(LocalDateTime.now())
       val result = method.invoke()
       logging(LocalDateTime.now())
       return result
    }
}
fun plus(a : Int, b : Int) = Logging.time{
    a + b
}

<br/>

때로는 예외를 스킵하고 싶은 함수가 있을 수도 있습니다. 예를 들면 트랜잭션안에 알림과 같은 로직을 호출할때 입니다.

object Aop{
    fun <T> skipException(method : () -> T) {
       try {
         return method.invoke()
       } catch(e: Excpetion){
            logging(e)
       }
    }
}
fun plus(a : Int, b : Int) = Aop.skipException{
    a + b
}

<br/>

만약 시간도 로깅하고 예외도 스킵하고 싶다. 둘이 합치는것도 가능합니다.

fun plus(a : Int, b : Int) = Logging.time { Aop.skipException {
        a + b
} }

<br/>

이외에도, 비즈니스로직을 작성하다보면 커리를 사용하여 유틸성 로직들을 애용할 수 있는 포인트들이 많습니다 (심지어 캐싱도..!)

<br/>

마무리.

코틀린 6개월차..

사용하다보니 아직 자바 호환성 이슈가 몇가지 느껴질때도 있습니다 (예를 들어, JOOQ 호환성.. 아 J가 Java의 J였지..)

하지만, 비유하자면.. 굉장히 센스(?) 있는 신축 아파트에 들어온 느낌입니다.
아.. 이런것 까지 생각하면서 인테리어 한거야? 라는 생각이 듭니다..

또 코드가 자바에 비교하자면, 굉장히 간결하게 작성 할 수 있습니다.

이 점이 생각보다 크리티컬한데..
자바로 다시 작성하면.. 아 불편하다 라고 느끼게 되버립니다.

그래서 결론은... 심플하게... 좋습니다(!) 로 마무리

CommentCount 0
이전 댓글 보기
등록
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
TOP