thumbnail thumbnail

2024-03-05 코드 안정성 (이펙티브 코틀린) (미완)

포스트

“이펙티브 코틀린” 를 읽으며 안전성과 가독성이 무엇인가 찾기 위한 정보 정리를 진행했다.

 

<코드의 안전성 이란?>

 

예외 처리

예외를 활용해 코드에 제한을 걸어라

  • 확실하게 어떤 형태로 동작해야 하는 코드가 있다면 예외를 활용해 제한을 걸자.

  • 제한을 걸면 문서를 읽지 않은 개발자도 문제를 확인할 수 있다.

  • 문제가 있을 경우 함수가 예상하지 못한 동작을 하지 않고 예외를 throw 한다. 예상하지 못한 동작을 하는 것은 예외를 throw하는 것보다 굉장히 위험하며, 상태를 관리하는 것이 굉장히 힘들다.

  • Argument

    • 일반적으로 제한 걸 때는 argument를 사용한다.

    • require함수로 제한을 확인하고 제한을 만족하지 못할 경우 예외를 throw 한다.

    • require 함수는 조건을 만족하지 못할 때 무조건 IllegalArgument Exception을 발생 시키므로 제한을 무시할 수 없다.

  • check

    • check 함수는 require과 비슷하지만 지정된 예측을 만족하지 못할 때 IllegalStateException을 throw 한다.

    • 상태가 올바른지 확인할 때 사용한다.

    • 사용자가 코드를 제대로 사용할 것이라고 믿고 있는 것 보다는 항상 문제 상황을 예측하고, 문제 상황에 예외를 throw하는 것이 좋다.

  • assert

    • 단위 테스트는 구현의 정확성을 확인하는 가장 기본적인 방법이다.

    • assert함수는 프로덕션 환경에서는 오류가 발생하지 않고 테스트 할 때만 활성화 되므로 오류가 발생해도 사용자가 알아차릴 수 없다.

    • 이 코드가 정말 심각한 오류라면 check 함수를 사용하는게 좋다.

 

결과 부족이 발생할 경우 Null과 Failure를 사용해라

  • 함수가 원하는 결과를 만들어 낼 수 없을 때가 있다.

  • 이럴 때 null 또는 Failure를 리턴하거나 예외를 throw 한다.

  • 여기서 예외를 throw 하는 방법은 좋지 않다. 예외는 정보를 전달하는 방법으로 사용해서는 안된다. 잘못된 특별한 상황을 나타내야 한다.

  • 충분히 예측할 수 있는 범위의 오류는 null 과 failure를 사용하고, 예측하기 어려운 예외적인 범위의 오류는 예외를 throw 해서 처리하는 것이 좋다.

 


<ListTask에 안정성 적용>

 

ListTaskCommand 클래스에 안정성을 적용하도록 해보았다.

Kotlin
@TaskCommand(prefix = "!LIST-TASK") class ListTaskCommand(override val taskRepository: TaskRepository) : MessageCreateCommand() { override suspend fun execute(parameter: MessageCreateParameter): CommandResult { val userId = parameter.username val tasks = taskRepository.findAllByUserId(userId = userId) val taskList = tasks.joinToString(", \\n") { it.content } return CommandResult.reply("$userId -> \\n $taskList") } } // 원본 코드

본래 코드대로 라면 tasks 를 userId 로 조회하여 taskList 의 content 부분을 출력하는 함수이다. 하지만 만약 찾아본 userId 에 어떠한 Content 도 없다면 해당 코드는 에러를 발생할 것이다. 그것을 방지하기 위해서 try catch 를 사용하여 try 로 만약 해당 tasks 가 존재 하지 않는다면 "해당하는 태스크가 검색되지 않습니다." 라는 문자열을 반환하고, 있다면 기존에 썼던 코드가 동작하도록, 마지막으로 2가지의 상황 외의 케이스에는 ignore 를 출력하도록 하였다.

(멘토가 미리 준비한 ignore 는 이런 예외들을 처리 하기 위함이지 않았을까 라는 생각을 하게 되었다.)

 

Kotlin
@TaskCommand(prefix = "!LIST-TASK") class ListTaskCommand(override val taskRepository: TaskRepository) : MessageCreateCommand() { override suspend fun execute(parameter: MessageCreateParameter): CommandResult { val userId = parameter.username try{ val tasks = taskRepository.findAllByUserId(userId = userId) if(tasks.isEmpty()){ return CommandResult.reply("해당하는 태스크가 검색되지 않습니다.") } val taskList = tasks.joinToString(", \\n") { it.content } return CommandResult.reply("$userId -> \\n $taskList") } catch (e:Exception){ return CommandResult.ignore() } } } // 수정된 코드

 

ListTaskCommand 를 수정한 뒤, 다른 코드들의 안정성에 대해 다시 생각 해보았다.

리팩토링, 수정 등을 수행 할 때에는 반드시 계획이 필요하다. 정말로 수정해야 하는가? 필요가 있는가? 2가지의 의문점에 대해 O 를 받는다면 그때는 코드의 수정을 통해서 단단하게 만들 이유가 생기는 것이다.

진행하면서 알게 된 것은 이미 정해진 데이터 조회의 간단한 처리에는 notNull 과 Null 의 경우에만 처리해주면 되겠다고 생각하게 되었다. 다만 process 가 들어간 데이터의 경우엔 원치 않는 값의 반환이 이루어지는 경우가 존재 할 수 있으므로 그러한 예외는 처리해야 한다고 생각이 들었다. 해당 코드는 이러한 task null 상태에는 대응할 수 있도록 수정하는 것이 좋다고 생각한다. 우선 확인된 사항이 아니니 롤백하여 포스팅에만 적도록 하겠다.

 


이번에는 코드를 적용하기 전, 이번에는 먼저 생각한 것을 문서로 적어보고 수행 하고자 한다.

AddTaskCommand 는 AiPrompt 를 통해 가공되는 Json 데이터를 받는다. 그리고 받은 Json 데이터를 repository 에 저장하는 Class 이다. 이 때 AddTask 가 원하는 값을 받지 못하거나 전혀 다른 내용을 불러오게 될 경우 어떻게 처리해야 할까?

 

만약 AiPrompt 가 Json 을 반환해주지 않는 상황이라면 실제 받는 값 (Argument) 를 검사하는 작업을 위해서는 require() 함수를 사용하라고 ‘이펙티브 코틀린’ 은 주장하고 있다. 그렇다면 그 작업을 위해서 받은 값을 AiPrompt 클래스에서 처리해야 하는가? 아니면 받아내는 AddTaskCommand 클래스에서 처리해야 하는가?

 

개발자 마다 원하는 처리 상황이 다르겠지만 본인은 이렇게 생각했다.

  1. 1

    Json 데이터가 아닌 경우를 검사하는 것은 Json 을 만드는 AiPrompt 에서 처리해야 한다. 만약 Json 이 아니라면 그것을 다시 OpenAiAPI 에 요청하는 것도 AiPrompt 에서 수행하도록 해야한다.

  1. 2

    그렇게 AddTaskCommand 로 넘어온 Json 의 내용을 검사하는 행위는 AddTaskCommand 에서 이루어져서 다시 요청을 보내야 한다면 다시 요청을 보내는 것은 AddTaskCommand 에서 이루어 져야 한다.

 

여기에서 내가 만든 두 가지 조건의 성질을 생각해 보았다. 우선 1번 항목의 “json 데이터가 아닌 경우를 검사하는 것” 이라는 조건은 좋지 못한 조건이다. 예외 항목을 정하는 것에는 순서가 있다. 우선 ‘Json 을 받는다’ 라는 조건이 된다면 이것은 좋은 조건이다. 왜냐하면 Json 을 받는다 라는 한 가지의 조건에는 한 개의 타입, Json 만이 값으로 들어 올 수 있게 되는 것 이고 Json 이 아닌 값을 받지 않는다 라는 건 Json 외의 모든 타입이 예외 대상이 되는 것이기 때문이다.

그렇기에 ‘아닌 값’ 을 받는 조건 보다는 ‘맞는 값’ 을 받는 조건이 관리하기 좋고 예외의 간섭에서 어느 정도 자유롭게 움직일 수 있게 된다.

조금 더 세세한 예를 들어보자

<맞는 값>

  • Json 데이터를 받아오는 코드가 있다. 이것을 검사하여 json 이라면 값을 받는다.

  • Json 데이터에는 userId 가 있어야 한다.

  • Json 데이터가 Array 형태이어야 한다.

  • Json 데이터의 Processed 는 Int 1 값을 가져야 한다.

위와 같은 조건으로 Json 데이터를 만들면

Json
[ { ‘userId’ : ‘건웅’ , ‘serverName’ : ‘응애’ , ‘processed’ : 1 } , { ‘userId’ : ‘준무’ , ‘serverName’ : ‘응애’ , ‘processed’ : 1 } ]

다음과 같은 식은 들어 올 수 있다.

반대 예시도 만들어 보겠다.

<아닌 값>

  • Json 데이터를 받아오는 코드가 있다. 이것을 검사하여 json 이 아니라면 오류를 발생 시켜야 한다.

  • Json 데이터에는 userId 외에 serverName , processed 만 있어야 한다.

  • Json 데이터는 단일 형태가 아니어야만 한다.

  • Json 데이터의 processed 의 값은 Int