아래는 개인적인 의견이며, 실제 다수의 개발 환경과는 차이가 있을 수 있습니다.

Python

  • 사용범위: 데이터 과학, 웹 개발, 인공지능, 자동화 스크립트
  • 특징: 초보자에게 친숙하고 다양한 라이브러리를 제공
  • 선정 이유: AI와 빅데이터 분야의 성장으로 인해 지속적으로 수요가 증가

파이썬은 함수형 프로그래밍부터 객체 지향 프로그래밍까지 다양한 스타일을 수용하는 다목적 프로그래밍 언어로 주목받고 있습니다YouTube, Google 검색, iRobot 머신과 같은 유명한 플랫폼은 Python의 견고함 덕분에 존재할 수 있었습니다

HackerRank의 데이터에 따르면 Python은 미주 지역 채용 관리자가 두 번째로 많이 찾는 언어입니다사용자 친화적인 특성 덕분에 초보자와 숙련된 개발자 모두에게 인기가 높습니다

Python의 풍부한 라이브러리는 미리 빌드된 명령어로 작업을 간소화하며, 대화형 특성은 실시간 코드 테스트에 도움이 됩니다채용 관리자의 50% 이상이 파이썬 기술을 우선시하는 등 광범위한 수요를 고려할 때, 파이썬은 프로그래밍 환경의 주요 자산으로 남아 있습니다.

JavaScript

  • 사용범위: 웹 프론트엔드, 백엔드(Node.js), 모바일 애플리케이션(React Native)
  • 특징: 웹 표준 기술로서의 지위와 비동기 처리에 강점
  • 선정 이유: 웹 개발의 필수 언어로서의 지속적인 인기

스택 오버플로의 2020년 개발자 설문조사에 따르면 자바스크립트는 전 세계에서 가장 많이 사용되는 언어로 69.7%로 1위를 차지했습니다이러한 우위는 미주 지역의 채용 관리자들 사이에서 자바스크립트에 대한 높은 수요를 통해 더욱 두드러집니다

자바스크립트는 개발자가 애니메이션 그래픽, 인터랙티브 맵 등과 같은 동적 요소를 제작할 수 있도록 웹 페이지의 상호작용성을 향상시키는 데 중추적인 역할을 합니다HTML 및 CSS와 결합된 자바스크립트는 향상된 웹사이트 제어 기능을 제공하여 뛰어난 사용자 탐색 및 가독성을 보장합니다자바스크립트가 널리 사용되는 이유는 대부분의 웹 브라우저와 통합되고 초보자도 쉽게 사용할 수 있기 때문입니다방대한 온라인 커뮤니티와 광범위한 지원 리소스는 자바스크립트의 최상위 프로그래밍 언어로서의 입지를 더욱 공고히 해줍니다.

Java

  • 사용범위: 엔터프라이즈급 백엔드 시스템, 안드로이드 앱 개발

  • 특징: 강력한 크로스 플랫폼 지원과 안정적인 성능

  • 선정 이유: 대규모 시스템 개발과 안드로이드 시장의 확대

    Java는 어디서나 한 번만 작성하면 실행할 수 있다는 철학으로 엔터프라이즈급 애플리케이션과 안드로이드 앱 개발에서 확고한 입지를 다지고 있습니다강력한 커뮤니티 지원과 이식성 덕분에 지속적인 인기를 누리고 있습니다

C#

  • 사용범위: 윈도우 애플리케이션, 게임 개발(Unity), 웹 개발(ASP.NET)

  • 특징: 강력한 개발 환경과 통합된 개발 경험

  • 선정 이유: 게임 개발과 .NET 생태계의 확장

    C++는 시스템 프로그래밍, 게임 개발, 고성능 애플리케이션의 초석이 되고 있습니다높은 수준의 추상화를 통해 낮은 수준의 조작이 가능하므로 성능이 중요한 작업에 적합합니다

    TypeScript

  • 사용범위: 대규모 웹 애플리케이션 개발

  • 특징: JavaScript의 상위 집합으로 타입 안정성 제공

  • 선정 이유: JavaScript의 복잡성 관리와 안정적인 개발을 위한 수요 증가

    자바스크립트의 상위 집합인 타입스크립트는 정적 타이핑을 추가하여 대규모 애플리케이션에 선호되는 선택지입니다기존 JavaScript 코드베이스와의 호환성 및 향상된 개발자 경험으로 인해 그 중요성이 날로 높아지고 있습니다

Swift

  • 사용범위: iOS 및 macOS 애플리케이션 개발

  • 특징: Apple 플랫폼에 최적화된 성능과 간결한 문법

  • 선정 이유: Apple 생태계 내 앱 개발의 필수 언어

    Apple의 프로그래밍 언어인 Swift는 iOS 및 macOS 앱 개발에 필수적인 언어입니다구문이 간결하고 표현력이 뛰어나 개발자들이 원활하고 직관적인 사용자 환경을 만들기 위해 선호하는 언어입니다

Kotlin

  • 사용범위: 안드로이드 앱 개발, 서버 사이드 애플리케이션

  • 특징: 간결하고 표현력 있는 문법, 자바와의 상호 운용성

  • 선정 이유: 안드로이드 공식 개발 언어로의 지정과 개발자 커뮤니티의 성장

    Google이 Android 개발을 위해 승인한 Kotlin은 간결함과 Java와의 상호 운용성으로 인기를 얻고 있습니다Kotlin을 배우면 효율적이고 현대적인 Android 애플리케이션을 만들 수 있는 길이 열립니다

Go (Golang)

  • 사용범위: 시스템 프로그래밍, 분산 시스템, 클라우드 서비스
  • 특징: 간결한 문법과 빠른 실행 속도, 병행성(concurrency) 지원
  • 선정 이유: 클라우드 기반 서비스와 마이크로서비스 아키텍처의 인기

Rust

  • 사용범위: 시스템 프로그래밍, 웹 어셈블리, 임베디드 시스템
  • 특징: 메모리 안전성과 병행성을 보장하는 현대적인 시스템 언어
  • 선정 이유: 안전한 시스템 개발에 대한 수요 증가

Rust는 스택 오버플로와 같은 플랫폼에서 꾸준히 호평을 받고 있으며, 종종 가장 선호도가 높은 상위 프로그래밍 언어 중 하나로 꼽히기도 합니다사용자의 86%에 달하는 상당수가 Rust에 대한 전문성을 강화하는 데 큰 관심을 표명하고 있습니다그러나 이러한 매력에도 불구하고 설문조사에 참여한 개발자 중 단 3%만이 Rust를 사용한다고 답해, 광범위한 프로그래밍 커뮤니티에서 틈새 시장으로 인식되고 있음을 알 수 있습니다

Rust는 구문은 C++와 비슷하지만 응용 가능성이 더 넓은 '다중 패러다임' 언어로 구분됩니다신속한 컴파일, 향상된 크로스 플랫폼 기능 또는 더 밝은 커리어의 길을 우선시하든, Rust를 채택하는 것은 전략적인 선택이 될 수 있습니다.

SQL

  • 데이터베이스 관리
    • 영업 보고서
    • 비즈니스 관리

일반적으로 구조화된 쿼리 언어로 알려진 SQL은 프로그래머가 데이터베이스와 상호 작용할 수 있는 특수 도구로 사용됩니다특히 관계형 데이터베이스 관리 시스템(RDBMS)에 맞게 설계된 SQL은 데이터 검색, 레코드 업데이트, 추가 및 삭제와 같은 작업에 탁월합니다

SQL의 기능은 칭찬할 만하지만, SQL은 소규모 데이터베이스에서 가장 효율적이며 더 크고 복잡한 데이터베이스에서는 한계에 직면할 수 있다는 점에 유의할 필요가 있습니다그럼에도 불구하고 SQL은 프로그래밍 분야에서 세 번째로 많이 사용되는 언어이며, 개발자의 54.7%가 SQL의 기능을 활용하고 있을 정도로 널리 사용되고 있다는 사실은 부인할 수 없습니다.

NOSQL

  • 데이터베이스 관리
  • 영업 보고서
  • 비즈니스 관리

비관계형 SQL의 약자인 NoSQL은 사용자 친화적인 특성을 유지하면서 SQL의 확장성을 향상시키는 것을 목표로 하는 솔루션으로 등장했습니다SQL은 조작과 검색을 위해 데이터를 테이블로 구성하는 관계형 데이터베이스 시스템에 의존하지만, NoSQL은 이러한 테이블 구조를 따르지 않는다는 점에서 차별화됩니다

이러한 유연성 덕분에 NoSQL은 계층적 네트워크 데이터 저장이나 광범위한 클라우드 기반 애플리케이션 지원과 같은 작업에 특히 능숙합니다이러한 적응성을 고려할 때 SQL과 NoSQL은 모두 가장 인기 있는 프로그래밍 언어 중 하나입니다.

반응형

'개발' 카테고리의 다른 글

코딩 AI 랭킹  (0) 2024.03.09
Mac이 AI PC로서 최고인 이유  (0) 2024.02.03
gitlab 설치(apache --proxy--> gitlab)  (0) 2023.12.27
elixir vs rust 비교  (1) 2023.12.27
vscode dev containers  (1) 2023.12.22

개인적으로 새로운 언어를 공부하는 것은 너무나 재미있습니다. 새로운 대륙을 향해 돛단배를 몰고가는 것과 같습니다. 어디로 갈지 모르지만, 미지의 세계를 목표로 출발하는 마음은 셀렘이라는 감정일 것입이다.

이 글에서는 두 가지 언어의 기능, 장단점, 코드 예제를 통해 이 두 가지 언어를 비교해 보겠습니다. 이 글을 읽고 나면 어떤 프로그래밍 언어가 여러분의 필요에 맞는지 더 잘 알 수 있기를 바랍니다.

엘릭서

Elixir는 Erlang VM에서 실행되는 함수형 프로그래밍 언어입니다. Ruby와 동일한 스크립팅 스타일을 사용하지만, Ruby는 객체 지향 언어 패러다임을 따르고 Elixir는 함수형 패러다임을 따른다는 차이점이 있습니다.

특징

  • Elixir는 처음부터 동시성을 가장 잘 처리하는 데 중점을 두었습니다.
  • Elixir는 함수형 프로그래밍 패러다임을 따르기 때문에 반복적인 코드 없이 쉽게 작성할 수 있는 언어를 만들기 위해 노력합니다.
  • Elixir는 내결함성(결함 감내 시스템: Fault tolerant system)을 갖도록 설계되어 문제가 발생했을 때 재시작할 수 있습니다.
  • Elixir는 확장성이 뛰어납니다. 예를 들어, GPU에서 실행할 수 있습니다.
  • 대화형 또는 컴파일된 형태로 Repl에서 Elixir를 실행할 수 있습니다.

Elixir는 확장 가능하고 내결함성 있는 애플리케이션을 구축하는 데 적합한 강력하고 다재다능한 프로그래밍 언어입니다. 함수형 프로그래밍 패러다임, 동시성 기능, Erlang 에코시스템과의 호환성 덕분에 다양한 도메인 및 산업에서 널리 사용되고 있습니다.

엘릭서를 사용하는 곳

Elixir는 Pinterest, Bleacher Report, Discord를 비롯한 다양한 기업과 조직에서 사용하고 있습니다. 대량의 트래픽을 처리할 수 있고 내결함성이 뛰어나 웹 애플리케이션과 분산 시스템을 구축하는 데 널리 사용됩니다.

문서에 따르면 주목할 만한 Elixir 사용자는 다음과 같습니다.

  1. 펩시코: PepsiCo는 소비재 판매를 위한 전자상거래 도구로 Elixir를 사용한다고 보고했습니다. 이들은 Elixir가 강력하고, 간단하며, 배우기 쉽고, 효율적이라는 점을 발견했습니다. Elixir를 사용한 펩시코 지점은 2019년에 20억 달러의 수익을 올렸습니다.
  2. Spotify: Spotify는 백엔드 개발에 Elixir를 사용합니다. 방대한 데이터베이스와 수많은 활성 사용자로 인해 초당 수천 건의 요청을 처리하고 처리할 수 있는 기술이 필요했습니다. Elixir는 필요한 수준의 동시성을 제공하여 세계적인 음악 및 엔터테인먼트 플랫폼으로 성장하는 데 도움을 주었습니다.
  3. Discord: 게이머를 위한 채팅 플랫폼인 Discord 역시 Elixir를 사용하고 있습니다. 대용량 데이터와 엄청난 트래픽을 처리할 수 있는 기술이 필요했습니다. Elixir를 통해 수백만 명의 사용자도 실시간으로 처리할 수 있었습니다. Elixir는 기술 스택의 주요 언어가 되었습니다.
  4. WhatsApp: 전 세계에서 가장 인기 있는 메시징 앱 중 하나인 WhatsApp은 Elixir를 사용합니다. 이 애플리케이션은 처음에 Erlang으로 완전히 작성된 후 Elixir 기반 요소를 도입하여 개선되었습니다. Elixir는 WhatsApp이 방대한 트래픽과 대량의 요청을 처리하는 데 도움이 됩니다.
  5. Heroku: Elixir와 Phoenix 프레임워크를 사용하여 대시보드를 구동하는 Heroku

Elixir 장단점

Elixir의 장점

  • 내결함성(결함 감내 시스템: Fault tolerant system): 동시성을 사용하면 시스템은 종종 많은 문제에 직면합니다. 이러한 문제 중 하나는 하드웨어 오류, 소프트웨어 버그, 네트워크 문제 또는 리소스 제약으로 인해 시스템의 일부가 다운될 수 있다는 것입니다. Elixir는 시스템의 안정성과 장애에 대한 복원력을 보장하는 데 탁월합니다.
  • 확장성(Scalability): Elixir는 동시성 시스템 개발에 항상 우선순위를 두어온 Erlang VM(BEAM)에서 실행되므로 확장성이 뛰어납니다. 이 언어는 서로 통신하는 경량의 격리된 스레드에서 실행됩니다. 또한 Elixir를 사용하여 여러 머신에서 프로세스를 실행하고 통신할 수 있으므로 수평적 확장성을 달성할 수 있습니다.
  1. 웹 API
  2. 강력하고 동적으로(런타임에 확인) 입력됩니다.
  3. 소프트 실시간 서비스는 챗봇이나 Google 문서와 같은 소켓 연결로 구축할 수 있습니다. 하드 실시간 서비스에는 적합하지 않습니다(두 서비스의 차이점은 여기에 있습니다).
  4. 높은 확장성과 내결함성 지원을 제공합니다.
  5. 동시성은 액터 모델을 기반으로 합니다. 이는 Agha의 클래식 액터 모델과 대체로 유사합니다. 이러한 유형의 액터 모델을 프로세스 기반 액터라고 합니다. 이는 시작부터 완료까지 실행되는 계산으로 정의됩니다. 대신 Agha의 클래식 액터 모델은 액터를 거의 행동의 상태 머신과 그 사이를 전환하는 로직으로 정의합니다. 프로세스 기반 액터 모델은 에릭슨에서 Erlang을 개발하면서 처음에 개발되었습니다.
  6. 또한 패턴 매칭을 지원하므로 더 복잡한 데이터 유형을 분해하는 데 도움이 될 수 있습니다.
  7. 안정적인 라이브러리와 지원 커뮤니티가 있지만 Nodejs나 Python과 같지는 않지만 충분히 괜찮은 수준입니다(인기 있는 커뮤니티는 거의 없음).
  8. 개발자 경험이 좋습니다.

Elixir의 단점

  • 소규모 커뮤니티: Elixir 프로젝트에서 문제가 발생하면 커뮤니티가 상대적으로 작기 때문에 해결책을 찾거나 질문에 대한 답을 찾기가 어려울 수 있습니다.
  • 구직 시장에서 낮은 수요: 향후에는 달라질 수 있지만, 개발자 커뮤니티의 많은 부분에서 숙련된 Elixir 개발자에 대한 수요가 적기 때문에 구직 시장에서 이들을 찾기가 어려울 수 있습니다.

    Exlixir의 코드

Elixir의 코드는 스레드보다 가볍고 저렴한 프로세스를 통해 실행되므로 수천 개 또는 수백만 개의 프로세스를 생성할 수 있습니다. Elixir에서 프로세스는 격리되어 메모리를 공유하지 않고 서로에게 메시지를 전송하여 통신합니다.
아래 코드 블록에 따라 함수를 인수로 받고 프로세스 식별자(PID)를 반환하는 spawn/1 함수를 사용하여 새 프로세스를 생성하려면 다음과 같이 하세요:

process_pid = spawn(fn ->
    IO.puts("Starting process")
end)

IO.puts("PID: #{inspect(process_pid)}") # PID: #PID<0.98.0>

한 프로세스에서 다른 프로세스로 메시지를 보내려면 수신자의 PID와 메시지를 지정하여 send/2 함수를 사용할 수 있습니다. 메시지를 받으려면 지정된 패턴과 일치하는 메시지가 올 때까지 기다리는 receive/1 함수를 사용할 수 있습니다. 메시지는 패턴과 순차적으로 매칭되므로 메시지 내용에 따라 수신할 메시지를 선택할 수 있습니다.
엘릭서에서 메시지를 보내는 방법은 다음과 같습니다:

send(process_pid, {:print_hello})

Elixir 프로세스는 동시에 실행되므로 겹치는 방식으로 실행될 수 있지만 반드시 여러 CPU 코어에서 병렬로 실행되지는 않습니다. 동시성 모델은 프로세스가 메시지를 전달하여 통신하는 액터 모델을 기반으로 합니다. 이 모델은 공유 상태를 없애고 메시지를 통해 통신하는 격리된 프로세스에 집중함으로써 동시 프로그래밍을 단순화합니다.
Elixir는 작업을 동시에 실행하기 위해 서로 다른 프로세스에서 작업을 예약하는 Task 모듈을 제공합니다. Task 모듈을 사용하여 여러 작업을 병렬로 실행하는 방법은 다음과 같습니다:

tasks = [
Task.async(fn -> IO.puts("Doing work 1") end),
Task.async(fn -> IO.puts("Doing work 2") end)
]

for task <- tasks do
 Task.await(task)
end

주어진 URL에 대한 모든 링크를 찾는 웹 크롤러를 만들어 보겠습니다.

우리가 구축하는 크롤러는 주어진 URL의 모든 링크를 가져오는 것을 목표로 주어진 URL에 대한 모든 링크를 크롤링합니다. 피해야 할 사항은 다음과 같습니다:

  • 중복 크롤링 링크 없음
  • 크롤러가 동시에 실행되며 다음과 같이 구성할 수 있습니다.
  • 크롤러의 실행 시간과 메모리 사용량 비교

이 튜토리얼에는 모든 코드가 포함되어 있지는 않지만 가장 중요한 부분을 보여드리겠습니다. 먼저 Elixir는 함수형 언어이므로 웹사이트를 재귀적으로 크롤링할 것입니다:

def run(start_url, scraper_fun, max_concurrency) do
    Stream.resource(
      fn -> {[start_url], []} end,
      fn
        {[], _found_urls} ->
          {:halt, []}
        {urls, found_urls} ->
          {new_urls, data} = crawl(urls, scraper_fun, max_concurrency)
          new_urls =
            new_urls
            |> List.flatten()
            |> Enum.uniq()
            |> Enum.reject(&diff_host?(URI.parse(start_url), &1))
            |> Enum.map(&to_string/1)
            |> Enum.reject(&Enum.member?(found_urls, &1))
          {data, {new_urls, new_urls ++ found_urls}}
      end,
      fn _ -> IO.puts("Finished crawling for '#{start_url}'.") end
    )
  end

  defp crawl(urls, scraper_fun, max_concurrency) when is_list(urls) do
    urls
    |> Task.async_stream(&crawl(&1, scraper_fun, max_concurrency),
      ordered: false,
      timeout: 15_000 * max_concurrency,
      max_concurrency: max_concurrency
    )
    |> Enum.into([], fn {_key, value} -> value end)
    # |> Enum.map(&crawl(&1, scraper_fun)) # To run without concurrency
    |> Enum.reduce({[], []}, fn {scraped_urls, scraped_data}, {acc_urls, acc_data} ->
      {scraped_urls ++ acc_urls, scraped_data ++ acc_data}
    end)
  end

따라서 크롤러는 중복 링크를 필터링하여 크롤링하고, 크롤러를 현재 도메인으로만 제한하며, 동시성으로 실행합니다. 다음은 Elixir에서 크롤러의 메모리 사용량입니다:

Rust

Rust는 정적이고 강력한 타이핑 및 컴파일 언어입니다. 그 결과 커뮤니티가 매우 빠르게 성장했습니다.

  • 구조는 완전히 객체 지향이 아닌 객체 지향 위에 존재합니다. Java의 인터페이스와 같은 특성이 존재합니다. 상속은 없지만 a/is 모델을 사용하여 상속을 지원합니다.
  • 컴파일 시 가비지 컬렉션으로 인해 메모리 안전성을 제공하고 성능이 향상됩니다.
  • Rust에는 가비지 컬렉션이 없지만 차용 검사기가 있습니다.
  • 멀티 스레드가 스레드 안전하지 않은 경우 컴파일 시 유형 오류가 발생합니다.

    Rust 의 장점

  1. 강력한 타입 시스템 언어
  2. 동시성이 가능하며 행위자 모델을 따릅니다.
  3. 컴파일러는 훌륭하고 사전에 상황을 파악합니다.
  4. 배포를 위해 출력에 정적 바이너리를 생성합니다.
  5. CPU 집약적인 애플리케이션과 같이 효율성성능이 필요한 시스템을 위한 시스템입니다.
  6. Async io는 Tokio 모듈과 함께 안정화되고 있습니다.
  7. 사용하지 않는 기능에는 비용을 지불하지 않습니다.
  8. 추상화 비용이 들지 않습니다.

Rust의 단점

  1. 학습 곡선이 더 높고, 코드 작성이 복잡합니다.
  2. 성장이 필요한 라이브러리와 커뮤니티
  3. 느린 컴파일러
  4. 100% 자체 지원 컴파일러는 아니며 LLVM 컴파일러에서 구현됩니다.

    Elixir와 Rust 비교

Rust는 안정적이고 효율적인 소프트웨어를 구축하는 프로그래밍 언어입니다. Rust의 초점 중 하나는 프로그램이 메모리 안전성을 보장하는 것입니다. Rust는 Elixir나 Go처럼 스레드나 코루틴을 지원하지는 않지만, 런타임을 구현하기 위한 하위 수준의 빌딩 블록을 제공합니다.
오늘은 동시 런타임을 빌드하지는 않겠지만, Rust에서 가장 많이 사용되는 비동기 런타임 중 하나를 사용하겠습니다: Tokio를 사용하겠습니다. 비동기와 동시성은 다르지만, Tokio 라이브러리의 일부를 사용하여 동시 런타임처럼 작동하도록 만들 수 있습니다.

다음은 Tokio 상자를 사용하여 동시 프로그램을 만드는 방법입니다:

  • 프로세스: Tokio에서 프로세스를 실행하려면 tokio::spawn을 사용할 수 있습니다. Tokio는 현재 스레드에서 실행할지 다른 스레드에서 실행할지 처리합니다. 또한 하나의 스레드에서만 실행하거나 여러 스레드에서 실행하도록 구성할 수도 있습니다.
  • 메시지: 프로세스 간 통신을 위해, Gol에서 채널처럼 작동하는 Tokio 메시지 채널 모듈 tokio::sync::mpsc를 사용할 수 있습니다.
  • 공유 상태: 공유 상태는 위험한 작업이지만 다행히도 Rust는 우리가 올바르게 수행하고 있는지 확인하는 데 도움을 줄 것입니다. Tokio를 사용하면 tokio::sync::Mutex를 사용하여 공유 상태에 안전하게 액세스할 수 있습니다.

다음은 Tokio를 사용한 간단한 비동기 작업의 예입니다:

use tokio::time::{sleep, Duration};

#[tokio::main] // 새 Tokio 런타임을 시작합니다.
async fn main() {
    let task = tokio::spawn(async {
    // 일부 작업을 시뮬레이션합니다.
    sleep(Duration::from_millis(50)).await;
    println!("작업이 완료되었습니다");
});

// 작업이 완료될 때까지 기다림
task.await.unwrap();
}

이 예제에서 tokio::spawn은 Tokio 런타임에서 새 작업을 시작하고, tokio::time::sleep은 일정 시간 후에 완료되는 퓨처를 생성합니다. await 키워드는 이러한 퓨처가 완료될 때까지 기다리는 데 사용되지만, 스레드를 차단하지 않고 다른 작업을 실행할 수 있도록 허용합니다.
동시성 프로그래밍에서는 여러 작업 간에 상태를 공유해야 하는 경우가 많습니다. 이를 위해 일반적으로 메시지 전달을 사용하는 것이 더 안전하며 코드를 더 쉽게 추론할 수 있기 때문에 선호되는 방법입니다. 하지만 상태 공유가 필요한 경우도 있습니다.
작업 간에 상태를 공유하려면, std::sync의 일부인 Arc를 제외한 tokio::sync 모듈의 일부인 Arc, RwLock, Mutex 및 Semaphore와 같은 유형을 사용할 수 있습니다. 이러한 유형을 사용하면 여러 작업에서 변경 가능한 상태를 안전하게 공유할 수 있습니다:

  1. Arc(원자 참조 카운트): Arc를 사용하면 작업이나 스레드에서 공유할 모든 타입이나 구조체를 래핑할 수 있습니다. 예를 들어 Arc는 User를 힙에 저장합니다. 그런 다음 해당 인스턴스를 복제하여 힙에서 User를 참조하는 새 Arc 인스턴스를 만들 수 있습니다.
  2. Mutex(상호 제외): 한 번에 최대 하나의 스레드만 일부 데이터에 액세스하거나 변경할 수 있도록 합니다.
  3. RwLock(읽기-쓰기 잠금): 쓰기자가 잠그지 않는 한, 여러 작업 또는 스레드가 데이터를 읽을 수 있도록 합니다.
  4. 세마포어: 세마포어는 동시성에서와 같이 워커를 구현하는 데 사용할 수 있습니다. 예를 들어, 데이터가 1,000개인데 1,000개의 워커를 스폰하고 싶지 않은 경우 세마포어를 사용하면 스폰할 워커 수를 제한할 수 있습니다. 세마포어는 워커가 많다고 해서 프로젝트가 더 빨리 작동하는 것은 아니기 때문에 유용합니다.

ArcMutex의 예제

use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::spawn;

#[tokio::main]
async fn main() {
    let data = Arc::new(Mutex::new(0));

    let data1 = Arc::clone(&data);
    let task1 = spawn(async move {
        let mut lock = data1.lock().await;
        *lock += 1;
    });

    let data2 = Arc::clone(&data);
    let task2 = spawn(async move {
        let mut lock = data2.lock().await;
        *lock += 1;
    });

    task1.await.unwrap();
    task2.await.unwrap();

    assert_eq!(*data.lock().await, 2);
}

이 예제에서는 일부 데이터를 저장하기 위해 Arc<Mutex<i32>>가 생성됩니다. 그런 다음 각각 데이터를 증가시키는 두 개의 작업이 생성됩니다. Mutex는 한 번에 하나의 작업만 데이터에 액세스할 수 있도록 보장하기 때문에 데이터 경합이 발생하지 않으며 최종 결과는 예상대로 나타납니다.
한 가지 주목할 점은 Tokio에서 제공하는 잠금(예: Mutex 및 RwLock)은 비동기식이며, 경합이 발생할 때 전체 스레드를 차단하지 않는다는 점입니다. 대신 현재 작업만 차단하여 다른 작업이 동일한 스레드에서 계속 실행될 수 있도록 합니다.
이제 Rust와 Tokio를 사용해 간단한 웹 크롤러를 만들어 보겠습니다. 먼저 크롤러를 실행할 특성을 정의해 보겠습니다:

use crate::error::Error;
use async_trait::async_trait;

pub mod web;

#[async_trait]
pub trait Spider: Send + Sync {
    type Item;

    fn name(&self) -> String;
    fn start_url(&self) -> String;
    async fn scrape(&self, url: String) -> Result<Vec<Self::Item>, Error>;
}

Spider를 구현합니다.

impl WebSpider {
    pub fn new(start_url: String, worker: usize) -> Self {
        let http_timeout = Duration::from_secs(4);

        let http_client = Client::builder()
            .timeout(http_timeout)
            .user_agent(
                "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
            )
            .pool_idle_timeout(http_timeout)
            .pool_max_idle_per_host(worker)
            .build()
            .expect("WebSpider: Building HTTP client");

        WebSpider {
            http_client,
            start_url,
        }
    }
}

#[async_trait]
impl super::Spider for WebSpider {
    type Item = String;

    fn name(&self) -> String {
        String::from("WebSpider")
    }

    fn start_url(&self) -> String {
        self.start_url.to_string()
    }

    async fn scrape(&self, url: String) -> Result<Vec<String>, Error> {
        println!("Scraping url: {}", &url);
        let raw_url = Url::parse(&url)?;
        let host = raw_url.scheme().to_owned() + "://" + raw_url.host_str().unwrap();

        let start = Instant::now();
        let body_content = self.http_client.get(&url).send().await?.text().await?;

        let seconds = start.elapsed().as_secs_f64();
        if seconds > 3.0 {
            println!(
                "Parsing res body: {} in \x1B[32m{:.2}s\x1B[0m",
                &url, seconds
            );
        }
        let parser = Html::parse_document(body_content.as_str());
        let selector = Selector::parse("a[href]").unwrap();

        let links: Vec<String> = parser
            .select(&selector)
            .filter_map(|element| element.value().attr("href"))
            .filter_map(|href| {
                let parsed_link = raw_url.join(href);
                match parsed_link {
                    Ok(link) => {
                        let mut absolute_link = link.clone();
                        absolute_link.set_fragment(None);
                        absolute_link.set_query(None);
                        if absolute_link.to_string().starts_with(&host) {
                            Some(absolute_link.to_string())
                        } else {
                            None
                        }
                    }
                    Err(_) => None,
                }
            })
            .collect();

        Ok(links)
    }
}

실행

$ ./crawler_rust --work 10 --url https://react.dev

Elixir, Rust 성능의 벤치마크

지금까지 웹 크롤러를 Rust, Elixir에 구현했습니다. 이제 각각의 성능을 확인하기 위해 벤치마크를 실행해 보겠습니다. 작업을 완료하는 데 걸리는 시간과 테스트 중에 사용되는 메모리 양이라는 두 가지를 측정하고자 합니다. 크롤러는 https://react.dev 도메인의 모든 링크를 가져옵니다. 127개의 링크를 가져올 것으로 예상됩니다.

시간 측정 벤치마크

10개의 동시성 작업자를 사용하여 각 웹 크롤러가 링크를 가져오는 데 걸린 시간은 다음과 같습니다:

  • Elixir: 7.6초
  • Rust: 7.2초

HTTP 요청을 수행하는 데 시간이 걸립니다. 이는 각 연결에서 SSL/TLS 핸드셰이크가 생성되기 때문입니다. 기본 Go HTTP 라이브러리는 이 두 가지를 동시에 실행하기 때문에 이 시나리오에서 Go의 성능이 가장 우수합니다.
Elixir와 Rust를 모두 최적화하여 TLS/TLS 핸드셰이크를 한 번만 생성하고 워커 전체에서 사용하도록 할 수 있습니다:

TLS/SSL 핸드셰이크를 한 번만 만들고 작업자 전체에서 사용해야 합니다:

Web Crawler Performance With Elixir, Rust, And Go

다음은 50명의 워커의 결과입니다. 더 많은 동시성 워커를 사용한다고 해서 크롤러의 속도가 향상되는 것은 아니며, 네트워크 지연에 의해 제한되기 때문입니다

Web Crawler Result With 50 Workers

다음은 동시성(Concurency)를 조외한 밴치마크 입니다.

Web Crawler Without Concurrency

메모리 사용 밴치마크

메모리 사용량과 관련해서는 흥미로운 점이 있습니다:

  1. Elixir: 295MB
  2. Rust: 50MB

메모리 사용량에서는 Rust가 더 나은 성능을 보이는 반면, Elixir의 성능은 비슷하게 떨어지는 것을 알 수 있습니다:

Memory Usage Comparison Across Elixir, Rust, And Go

결론

이 튜토리얼에서는 Elixir, Rust로 동시 프로그램을 실행하는 방법을 배웠습니다. 각 언어마다 동시성을 처리하는 방법에 대한 고유한 아이디어가 있지만, Go의 기본 라이브러리는 동시성을 훌륭하게 지원하지만 메모리를 많이 사용한다는 것을 알 수 있었습니다. 이는 힙에 메모리를 할당하고 가비지 컬렉션을 사용하여 메모리를 정리하기 때문입니다. 이로 인해 GC가 정리를 수행할 때 메모리 사용량이 많아지고 일부 결함이 발생할 수 있습니다.

Elixir에서는 프로세스를 스폰하고 프로세스 간 공유 상태를 관리하는 것이 얼마나 쉬운지 확인했습니다. 하지만 동시에 Elixir에서 동시 프로그램의 성능을 향상시키는 것은 쉽지 않습니다. Elixir는 가비지 컬렉션을 사용해 메모리를 관리하기 때문에 Go과 비슷한 성능을 발휘합니다.

Rust로 동시 프로그램을 작성하는 것은 쉽지 않지만, Rust의 소유권 및 타입 시스템 덕분에 메모리 안전성을 확보할 수 있다는 이점이 있습니다. Rust는 가비지 컬렉션을 사용하지 않기 때문에 가비지 컬렉션을 사용하는 골랑이나 엘릭서보다 메모리 사용 측면에서 더 효율적입니다.

이 글의 전체 소스코드는 여기에서 확인할 수 있습니다(https://github.com/ahmadrosid/elixir-go-rust). 궁금한 점이나 공유하고 싶은 추가 정보가 있으면 언제든지 알려주세요.

반응형

'개발' 카테고리의 다른 글

2024년 프로그래밍 랭킹  (1) 2024.01.02
gitlab 설치(apache --proxy--> gitlab)  (0) 2023.12.27
vscode dev containers  (1) 2023.12.22
svn to git 마이그레이션 (맥)  (0) 2023.12.13
intelliJ gitlab plugin(merge request) 연결 오류  (0) 2023.08.23

유용한 학습 사이트

텍스트

문자열

문자열의 자료형은 &'static str'이다.

  • &은 메모리 내의 장소를 참조하고 있다는 의마(mut가 없으므로 값의 변경을 허용하지 않음)
  • 'static'은 문자열 데이터가 프로그램이 끝날 때까지 유효하다는 의미이다 (절대로 Drop 되지 않는다)
  • str은 언제가 유효한 utf-8 바이트 열을 가리키고 있다는 의미
    fn main() {
      let a: &'static str = "hi";
      println!("{} {}", a, a.len());
    }

예외 처리 문자

어떤 문자들은 시각적으로 표현하기 어려우므로, 예외 처리 코드로 대체해서 쓴다.

  • \n: 줄바꿈
  • \r: 캐리지 리턴
  • \t: 탭
  • \\: 역슬래시
  • \O: NULL
  • \': 작은 따옴표
fn main() {
    let a: &'static str = "Ferris says:\t\"hello\"";
    println!("{}", a);
}

원시 문자열

원시 문자열은 r#"로 시작하고 "#로 끝나며 문자열을 있는 그대로 쓰는데 사용한다.

fn main() {
    let a: &'static str = r#"
        <div class="advice">
            Raw strings are useful ofr some situations.
        </div>
    "#;
    println!("{}", a);
}

파일에서 몬자열 가져오기

매우 큰 텍스트가 필요하다면 include_str! 매크ㄹㅗㄹㅡ 이용해 ㄹㅗ컬 파일에서 텍스트를 읽어오는 방식을 고려해보자

fn main() {
    let html = include_str!("test.html");
    println!("{}", html);
}

문자열 슬라이스

메모리 상의 바이트 열에 대한 참조이며, 언제나 유효한 UTF-8이어야 한다.
&str에서 자주 사요하는 메소드는 다음과 같다

  • len은 문자열 바이트 길이를 가겨온다. (글자수 아님)
  • start_withends_with는 기본적인 비교에 쓰인다
  • is_empty는 길이가 0일 경우 true를 리턴한다.
  • find는 주어진 텍스트가 처음 등장하는 위치인 Option<usize> 값을 리턴한다.
fn main() {
    let a = "hi.";
    println!("{}", a.len());
    let first_word = &a[0..2];
    let secord_word = &a[3..7];
    println!("{} {} ", first_word, secord_word);
}

문자

rust에서는 utf-8 바이트 열을 char 타일의 벡터로 돌려주는 기능을 제공하다.
char 하나는 4byte다.

fn main() {
    let chars = "hi there 😄".chars().collect::<Vec<char>>();
    println!("{}", chars.len());
    println!("{}", chars[9] as u32);
}

스트링

UTF-8 바이트 열을 힙 메모리에 소유하는 구조체
문자열과는 달리 변경하거나 기타 등들을 할 수 있다. (메모리가 힙에 있기 때문)
자주 사용하는 메소드는 다음과 같다.

  • push_str은 스트링의 맨 뒤에 UTF-8 바이트들을 더 붙일 때 사용한다.
  • replce는 utf-8 바이트 열을 다른 것으로 교체할 때 사용한다.
  • to_lowercaseto_uppercase는 대소문자를 비교할 때 사용한다.
  • tim은 공백을 제거할 때 사용한다.
fn main() {
    let mut helloworld = String::from("hello      ");
    helloworld.push_str("world ");
    helloworld = helloworld + "!";
    println!("{}", helloworld);
    let mut trim_to_right = helloworld.trim().to_string();
    trim_to_right = trim_to_right + "!";
    println!("{}", trim_to_right);
}

함수 매개변수로서의 텍스트

문자열과 스트링은 일반적으로 함수에 문자열 슬라이스 형태로 전달된다.
이 방법은 소유권을 넘길 필요가 없어 대부분의 경우에 유연한다.

fn say_it_loud(msg: &str) {
    println!("{}!!!!", msg.to_string().to_uppercase());
}
fn main() {
    say_it_loud("hello");
    say_it_loud(&String::from("goodbye"));
}

스트링 만들기

concatjoin은 스트링을 만드는 간단한지만 강력한 방법

fn main() {
    let helloworld = ["Hello", " ", "World", "!"].concat();
    let abc = ["a", "b", "c"].join("");
    println!("{}", helloworld);
    println!("{}", abc);
}

스트링 양식 만들기

format! 매크로는 값이 어디에 어떻게 놓일지 매개번수화된 스트링을 정의해 생성한다.
println!과 같이 매개변수화된 스트링을 사용한다

fn main() {
    let a: i32 = 42;
    let f: String = format!("secret to lite: {}", a);
    println!("{}", f);
}

##스트링 변환
to_string을 사용해 많은 데이터 바입을 스트링으로 변환할 수 있다.
제네릭 함수 parse로 스트링이나 문자열을 다른 데이터 타입을 갖는 값으로 변환할 수 있다. 이 함수는 실패할 수도 있기 때문에 Result를 리턴한다.

fn main() -> Result<(), std::num::ParseIntError> {
    let a = 42;
    let a_string = a.to_string();
    let b = a_string.parse::<i32>()?;
    println!("{} {}", a, b);
    Ok(())
}

객체 지향 프로그래밍(OOP)

OOP란 무엇인가

객체 지향프로그래밍은 다음과 같은 상징적 특징을 갖는 프로그래밍 언어를 뜻한다.

  • 캡슐화(Encapulation): 객체라 불리는 단일 타입의 개념적 단위에 데이터와 함수를 연결
  • 추상화(Abstraction): 데이터와 함수를 숨겨 객체의 상세 구현 사항을 알기 어렵게 함
  • 다형성(Polymorphism): 다른 기능적 관점에서 객체와 상호 작용하는 능력
  • 상속(Inheritance): 다른 객체로부터 데이터와 동작을 상속받는 능력

RUST는 OOP가 아니다

Rust에서는 어떠한 방법으로도 데이터와 동작의 상속이 불가능하다.

  • 구조체는 부모 구조체로부터 필드를 상속받을 수 없다.
  • 구조체는 부모 구조체로부터 함수를 상속받을 수 없다.

메소드 갭슐화하기

Rust는 메소드가 연결된 구조체인 객체라는 개념을 지원한다.
모든 메소드의 처번째 매개변수는 메소드 호출과 연관된 인스턴스에 대한 참조여야한다.

  • &self: 인스턴스에 대한 변경 불가능한 참조
  • &mut self: 인스턴스에 대한 변경 가능한 참조
    메소드는 impl 키워드를 쓰는 구현 블록 안에 정의 한다.
struct SeaCreature {
    noise: String,
}

impl SeaCreature {
    fn get_sound(&self) -> &str {
        &self.noise
    }
    fn set_sound(&mut self, new_noise: &str) {
        self.noise = new_noise.to_string();
    }
}
fn main() {
    let creature = SeaCreature {
        noise: String::from("blub"),
    };
    println!("{}", creature.get_sound());
}

선택적 노출을 통한 추상화

Rust는 객체의 내부 동작을 숨길 수 있다
기본적으로, 필드와 메소드들은 그들이 속한 모듈에서만 접근 가능하다.
pub 키워드는 구조체의 필드와 메소드를 모듈 밖으로 노출시킨다.

struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
    fn set_sound(&mut self, new_noise: &str) {
        self.noise = new_noise.to_string();
    }
}
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    println!("{}", creature.get_sound());
}

다형성과 트레잇

  • Rust는 트레잇으로 다형성을 지원한다. 트레잇은 메소드의 집합을 구조체 데이터 타입에 연결할 수 있게 해준다.
  • 먼저 트레잇 안에 메소드 원형을 정은한다. 구조체가 트레잇을 구현할 때, 실제 데이터 타입이 무엇인지 알지 못하더라도 트레잇 데이터 타입을 통해 간접적으로 구조체와 상호 작용할 수 있도록 협약을 맺게 된다.
  • 구조체의 구현된 트레잇 메소드들은 구현 블록 안에 정의된다.
struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

trait NoiseMaker {
    fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound())
    }
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    creature.make_noise();
}

트레잇에 구현된 메소드

  • 트레잇에 메소드를 구현해 넣을 수 있다
  • 함수가 구조체 내부의 필드에 직접 접근할 수는 없지만, 트레잇 구현체들 사이에서 동작을 공유할 때 유용하게 쓰인다.
    struct SeaCreature {
      pub name: String,
      noise: String,
    }
    

impl SeaCreature {
pub fn get_sound(&self) -> &str {
&self.noise
}
}

trait NoiseMaker {
fn make_noise(&self);
fn make_alot_of_noise(&self) {
self.make_noise();
self.make_noise();
self.make_noise();
}
}

impl NoiseMaker for SeaCreature {
fn make_noise(&self) {
println!("{}", &self.get_sound())
}
}

fn main() {
let creature = SeaCreature {
name: String::from("Ferris"),
noise: String::from("blub"),
};
creature.make_alot_of_noise();
}


## 동적 vs 정적 디스패치

- 메소드는 다음의 두 가지 방식으로 실행된다.
    - 정잭 디스패치(Static Dispatch): 인스턴스의 데이터 트입을 알고 있는 경우, 어떤 함수를 호출해예 하는지 정확히 알고 있다.
    - 동적 디스패치(Dynamic Dispatch): 인스턴스의 데이터 타입을 모르는 경우, 올바른 함수를 호촐할 방법을 찾아야 한다.
- 트레잇의 자료형인 `&dyn MyTrait`은 동적 디스패치를 통해 객체의 인스턴스들을 간접적으로 작동시킬 수 있게 해준다.
- Rust에서는 동적 디스패치를 사용할 경우 사람들이 알 수 있도록 트레잇 자료형 앞에 `dyn`을 붙일 것을 권고한다.

```rust
struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

trait NoiseMaker {
    fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound())
    }
}
fn static_make_noise(creature: &SeaCreature) {
    creature.make_noise();
}
fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) {
    noise_maker.make_noise();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    static_make_noise(&creature);
    dynamic_make_noise(&creature);
}

트레잇 객체

  • 객체의 인스턴스를 &dyn MyTrait 데이터 타입을 가진 매개 변수로 넘길 때, 이를 트레잇 객체라고 한다.
  • 트레잇 객체는 인스턴스의 올바른 메소드를 간접적으로 호출할 수 있게 해주며, 인스턴스에 대한 포인터와 인스턴스 메소드들에 대한 함소 포인터 목록을 갖는 구조체이다

크기를 알 수 없는 데이터 다루기

  • 트레잇은 원본 구조체를 알기 어렵게 하느라 원래 크기 또한 알기 어렵다.
  • Rust에서 크기를 알 수 없는 값이 구조체에 저장될 때는 두 가지 방법으로 처리된다.
    • generics: 매개 변수의 데이터 타입을 효과적으로 활용해 알려진 데이터 타입 및 크기의 구조체/함수를 생성한다
    • indirection: 인스턴스를 힙에 올림으로써 실제 데이터 타입의 크기 걱정이 없이 그 포인터만 저장할 수 있는 간접적인 방법을 제공한다.

제네릭 함수

  • Rust의 제네릭은 트레잇과 함께 작동한다. 개매 변수 데이터 타입 T를 정의할 때 해당 인자가 어떤 트레잇을 구현해야 하는지 나열함으로써 인자에 어떤 데이터 타입을 쓸 수 있는지 제한할 수 있다.
  • 제네릭을 이용하면 컴파일 시 데이터 타입과 크기를 알 수 있는 정적 데이터 타입이 함수가 만들어지며, 따라서 정적 디스패치와 함께 크기가 정해진 값으로 저장할 수 있게 된다.
struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

trait NoiseMaker {
    fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound())
    }
}
fn generic_make_noise<T>(creature: &T)
where
    T: NoiseMaker,
{
    creature.make_noise();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    generic_make_noise(&creature);
}

제네릭 함수줄여쓰기

fn generic_make_noise<T>(creature: &T)
where
    T: NoiseMaker,
{
    creature.make_noise();
}

// 아래와 같이 변경 가능

fn generic_make_noise(creature: &impl NoiseMaker) {
    creature.make_noise();
}

스마트 포인터

BOX

  • BOX는 스택에 있는 데이터를 힙으로 옮길 수 있게 해주는 자료 구조다.
  • 스마트 포인터로도 알려진 구조체이며 힙에 있는 데이터를 가리키는 포인터를 들고 있다.
  • 크기가 알려져 있는 구조체이므로(왜냐하면 포인터만 들고 있으므로) 필드의 크기를 알아야하는 구조체에 뭔가의 참조를 저장할 때 종종 사용된다.
struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

trait NoiseMaker {
    fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound())
    }
}

struct Ocean {
    animals: Vec<Box<dyn NoiseMaker>>,
}

fn main() {
    let ferris = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    let sarah = SeaCreature {
        name: String::from("Sarah"),
        noise: String::from("swish"),
    };
    let ocean = Ocean {
        animals: vec![Box::new(ferris), Box::new(sarah)],
    };
    for a in ocean.animals.iter() {
        a.make_noise();
    }
}

참조 다시보기

  • 참조는 근본적으로 메모리 상의 어떤 바이트들의 시작 위치를 가리키는 숫자일 뿐이며, 유일한 용도는 특정 타입의 데이터가 어디에 존재하는지에 대한 개념을 나타내는 것이다.
  • 일반 숫자와 차이점은 Rust에서 참조가 가리키는 값보다 더 오래 살지 않도록 수명을 검증한다는 거다. ( 안그러면 그걸 사용핼을 때 오류가 날 것이다)

원시 포인터

  • 참조는 더 원시적인 자료형인 원시 포인터로 변환될 수 있다
  • 원시 포인터는 숫자와 마찬가지고 거의 제한없이 여기저기 복사하고 이동할 수 있다.
  • Rust는 원시 포인터가 가리키는 메모리 위치의 유효성을 보증하지 않는다.
  • 원시 포인터에는 두 종류가 있다
    • *const T: 데이터 타입 T의 데이터를 가리키는 절대 변경되지 않는 원시 포인터
    • *mut T: 데이터 타입 T의 데이터를 가리키는 변경될 수 있는 원시 포인터
  • 원시 포인터는 숫자와 상호 변환이 가능하다. (예: uszie)
  • 원시 포인터는 안전하지 않은 코드의 데이터에 접근할 수 있다
fn main() {
    let a = 42;
    let memory_location = &a as *const i32 as usize;
    println!("memory location: {}", memory_location);
}![[Javascript _ Null 과 undefined]]

역참조

  • 참조(예: &i32) 를 통해 참조되는 데이터를 접근/변경하는 과정을 역참조라고 한다.
  • 참조로 데이터를 접근/변경하는 데에는 두 가지 방법이 있다.
    • 변수 할당 중에 참조되는 데이터에 접근
    • 참조되는 데이터의 필드나 메소드에 접근
  • Rust에는 이를 가능케 하는 강력한 연산자가 있다.

* 연산자

  • * 연산자는 참조를 역참조하는 명시적인 방법이다.
fn main() {
    let a: i32 = 42;
    let ref_ref_ref_a: &&&i32 = &&&a;
    let ref_a: &i32 = **ref_ref_ref_a;
    let b: i32 = *ref_a;
    println!("{}", b);
}
  • .연산자는 참조의 필드와 메소드에 접근하는 데에 쓰인다. (좀 더 미묘하게 동작하는데, 참조 열을 자동으로 역참조한다.)
struct Foo {
    value: i32,
}

fn main() {
    let f = Foo { value: 42 };
    let ref_ref_ref_f = &&&f;
    println!("{}", ref_ref_ref_f.value);
    // -> println!("{}", (***ref_ref_ref_f).value);
}

스마트 포인터

  • Rust에서는 & 연산자로 이미 존재하는 데이터의 참조를 생성하는 기능과 대불어, 스마트 포인터라 불리는 참조 같은 구조체를 생성하는 기능을 제공한다.
  • 고수준에서 보자면 참조는 다른 데이터 타입에 대한 접근을 제공하는 데이터 타입이라고 볼 수 있다. 스마타 포인터가 일반적인 참조와 다른 점은, 프로그래머가 작성하는 내부 로직에 기반해 동작하는 거다.
  • 일반적으로 스마트 포인터는 구조체가 *.연산자로 연참조될 때 무슨 일이 발생할 지 지정하기 위해 Deref, DerefMut, Drop 트레잇을 구현한다.
use std::ops::Deref;

struct TattleTell<T> {
    value: T,
}

impl<T> Deref for TattleTell<T> {
    type Target = T;

    fn deref(&self) -> &T {
        println!("{} was used!", std::any::type_name::<T>());
        &self.value
    }
}

fn main() {
    let foo = TattleTell {
        value: "secret message",
    };
    println!("{}", foo.len());
}
// &str was used!
// 14

위험한 스마트 코드

  • 스마트 포인터는 안전하지 않는 코드를 꽤 자주 쓰는 경향이 있다. 앞서 말했듯이, 스마트 포인터는 rust에서 가장 저수준의 메모리를 다루기 위한 일반적인 도구다.

  • 무엇이 안전하지 않은 코드일까? 안전하지 않는 코드는 rust 컴파일러가 보증할 수 없는 몇가지 기능이 있다는 예외사항을 제외하고는 일반적인 코드와 완전히 똑같이 동작한다.

  • 안전하지 않는 코드의 주 기능은 원시 포인터를 역참조하는 것이다. 이는 원시 포인터를 메모리 상의 위치에 가져다 놓고, “데이터 구조가 여기 있다!”고 선언한 뒤 사용할 수 있는 데이터 표현으로 변환하는 걸 의미한다. (즉, *const u8u8로)

  • Rust에서는 메모리에 쓰여지는 모든 바이트의 의미를 추적하는 방법이 없다. 원시 포인터로 쓰이는 임의의 숫자에 무엇이 존재하는지 보증할 수 없기 때문에, 역참조를 unsafe { ... } 블록 안에 넣는다.

    fn main() {
      let a: [u8; 4] = [86, 14, 63, 12];
      let pointer_a = &a as *const u8 as usize;
      println!("Data memory location: {}", pointer_a);
    
      let pointer_b = pointer_a as *const f32;
      let b = unsafe { *pointer_b };
      println!("I swear this is a pie! {}", b);
    }
    

// Data memory location: 140701859722372
// I swear this is a pie! 0.00000000000000000000000000000014718419


## 익숙한 친구들

- 이미 본 적이 있는 `Vec<T>`나 `String` 같은 스마트 포인터를 생각해 보자.
- `Vec<t>` 는 바이트들의 메모리 영역을 소유하는 스마트 포인터다. Rust 컴파일러는 이 바이트들에 뭐가 존재하는지 모른다. 스마트 포인터는 관리하는 메모리 영역에서 내용물을 꺼내기 위해 자기가 뭘 의미하는지 해석하고, 데이터 구조가 그 바이트들 내 어디에서 시작하고 끝나는지 추척하며, 마지막으로 원시 포인터를 데이터 구조로, 또 쓰기 편한 깔금한 인터페이스로 역참조한다. (예: `my_vec[3]`)
- `String`는 바이트들의 메모리 영역을 추적하며, 쓰여지는 내용물이 언제나 유효한 UTF-8이도록 프로그램적으로 제한하며, 그 메모리 영역을 `&str` 데이터 타입으로 역참조할 수 있도록 도와준다.
- 두 데이터 구조 모두 원시 포인터에 대한 안전하지 않은 역참조를 사용한다.
- 
```rust
use std::alloc::{alloc, Layout};
use std::ops::Deref;

struct Pie {
    secret_recipe: usize,
}

impl Pie {
    fn new() -> Self {
        let layout = Layout::from_size_align(4, 1).unwrap();
        unsafe {
            let ptr = alloc(layout) as *mut u8;

            ptr.write(86);
            ptr.add(1).write(14);
            ptr.add(2).write(15);
            ptr.add(3).write(92);
            Pie {
                secret_recipe: ptr as usize,
            }
        }
    }
}
impl Deref for Pie {
    type Target = f32;
    fn deref(&self) -> &f32 {
        let pointer = self.secret_recipe as *const f32;
        unsafe { &*pointer }
    }
}

fn main() {
    let p = Pie::new();
    println!("{:?}", *p);
}
// result:
// 1.6106674e17

힙에 할당 된 메모리 Box::

Box는 데이터를 스택에서 힙으로 옮길 수 있게 해주는 스마트 포인터다. 이를 역참조하면 마치 원래 데이터 타입이었던 것처럼 힙에 할당 된 데이터를 편하게 쓸 수 있다.

struct Pie;

impl Pie {
    fn eat(&self) {
        println!("tastes better on the heap!");
    }
}
fn main() {
    let heap_pie = Box::new(Pie);
    heap_pie.eat();
}
// result : tastes better on the heap!

참조 카운팅 Rc::

  • Rc는 스택에 있는 데이터를 힙으로 옮겨주는 스마트 포인터다. 이는 heap에 놓인 데이터를 변경 불가능하게 대여하는 기능을 갖는 다른 Rc 스마트 포인터를 복제할 수 있게 해준다.
  • 마지막 스마트 포인터가 Drop될 때에만 힙에 있는 데이터가 할당 해제된다.
use std::rc::Rc;

struct Pie;

impl Pie {
    fn eat(&self) {
        println!("tastes better on the heap!");
    }
}
fn main() {
    let heap_pie = Rc::new(Pie);
    let heap_pie2 = heap_pie.clone();
    let heap_pie3 = heap_pie2.clone();

    heap_pie3.eat();
    heap_pie2.eat();
    heap_pie.eat();
    println!(
        "heap_pie3 = {:p}, heap_pie2 = {:p}, heap_pie = {:p}",
        heap_pie3, heap_pie2, heap_pie
    );
}
// result
// tastes better on the heap!
// tastes better on the heap!
// tastes better on the heap!
// heap_pie3 = 0x7fa007f05c80, heap_pie2 = 0x7fa007f05c80, heap_pie = 0x7fa007f05c80

접근 공유하기 RefCell::

  • RefCell은 보통 스마트 포인터가 보유하는 컨테이너 구조로서, 데이터를 가져오거나 안에 있는 것에 대한 변경 가능한 또는 불가능한 참조를 대여할 수 있게 해준다.
  • 데이터를 대여할 때, Rust는 런타임에 메모리 안전 규칙을 적용해 남용을 방지한다.
    “단 하나의 변경 가능한 참조 또는 여러 개의 변경 불가능한 참조만 허용하며, 둘 다는 안됨”
    이 규칙을 어기면 RefCell은 패닉을 일으킨다.
use std::cell::RefCell;

struct Pie {
    slices: u8,
}

impl Pie {
    fn eat(&mut self) {
        println!("tastes better on the heap!");
        self.slices -= 1;
    }
}
fn main() {
    let pie_cell = RefCell::new(Pie { slices: 8 });
    {
        let mut mut_ref_pie = pie_cell.borrow_mut();
        mut_ref_pie.eat();
        mut_ref_pie.eat();
    }

    let ref_pie = pie_cell.borrow();
    println!("{} slices left", ref_pie.slices);
}
// result:
// tastes better on the heap!
// tastes better on the heap!
// 6 slices left

스레드 간에 공유하기 Mutex::

  • Mutex는 보통 스마트 포인터가 보유하는 컨테이너 데이터 구조로서, 데이터를 가져오거나 안에 있는 것에 대한 변경 가능한 또는 불가능한 참조를 대여할 수 있게 해준다.
  • 잠긴 대여를 통해 운영체제가 동시에 오직 하나의 CPU만 데이터에 접근 가능하도록 하고, 원래 스레드가 끝날 때까지 다른 스레드들은 막음으로써 대여남용을 방지한다.
  • Mutex는 여러 개의 CPU 스레드가 같은 데이터에 접근하는 걸 조율하는 방법이다.
  • 특별한 스마트 포인터인 Arc도 있는데, 스레드-안전성을 가진 참조 카운타 증가 방식을 사용한다는 걸 제외하고는 Rc와 동일하다.
    • 동일한 Mutex에 다수의 참조를 가질 때 종종 사용되곤 한다.
use std::sync::Mutex;

struct Pie;
impl Pie {
    fn eat(&self) {
        println!("tastes better on the heap!");
    }
}
fn main() {
    let mutex_pie = Mutex::new(Pie);

    let ref_pie = mutex_pie.lock().unwrap();
    ref_pie.eat();
}
// result
// tastes better on the heap!

스마트 포인터 조합하기

스마트 포인터는 한계가 있는 것처럼 보이지만, 조합해서 사용하면 매우 강력해질 수 있다.

  • Rc<Vec<Foo>> : 힙에 있는 변경 불가능한 데이터 구조의 동일한 벡터를 대여할 수 있는 복수의 스마트 포인터를 복제할 수 있게 해준다.
  • Rc<RefCell<Foo>>: 복수의 스마트 포인터가 동일한 Foo구조체를 변경 가능하게 또는 불가능하게 대여할 수 있게 해 준다.
  • Arc<Mutex<Foo>>: 복수의 스마트 포인터가 임시의 변경 가능한 또는 불가능한 대여를 CPU 스레드 독점 방식으로 잠글 수 있게 해준다.
use std::{cell::RefCell, rc::Rc};

struct Pie {
    slices: u8,
}
impl Pie {
    fn eat_slice(&mut self, name: &str) {
        println!("{} ate a slice of pie!", name);
        self.slices -= 1;
    }
}
struct SeaCreature {
    name: String,
    pie: Rc<RefCell<Pie>>,
}
impl SeaCreature {
    fn eat(&self) {
        let mut p = self.pie_borrow_mut();
        p.eat_slice(&self.name);
    }
    fn pie_borrow_mut(&self) -> std::cell::RefMut<Pie> {
        self.pie.borrow_mut()
    }
}
fn main() {
    let pie = Rc::new(RefCell::new(Pie { slices: 8 }));
    let ferris = SeaCreature {
        name: String::from("ferris"),
        pie: pie.clone(),
    };
    let sarah = SeaCreature {
        name: String::from("sarah"),
        pie: pie.clone(),
    };
    ferris.eat();
    sarah.eat();

    let p = pie.borrow();
    println!("{} slices left", p.slices)
}
// result
// ferris ate a slice of pie!
// sarah ate a slice of pie!
// 6 slices left/

프로젝트 구성과 구조

프로그램/라이브러리 작성하기

  • 프로그램은 main.rs 라 불리는 파일에 최상위 모듈을 갖고 있다.
  • 라이브러리는 lib.rs라 불리는 최상위 모듈을 갖고 있다.

다른 모듈과 crate 참조하기

  • 모듈 내의 항목은 전체 모듈 경로인 std::f64::consts::PI를 이용해 참조할 수 있다.
  • 더 간단한 방법은 use 키워드를 사용하는 거다. ㅣㅇ를 이용하면 모듈에서 쓰고자 하는 특정 항목을 전체 경로를 쓰지 않고도 코드 어디에서든 사용할 수 있다.
    • 예를 들어, use std::f64::consts:PI를 쓰면 main 함수에서 PI만으로 사용할수 있다
  • std는 유용한 데이터 구조 및 OS와 상호 작용할 수 있는 함수로 가득한 표준 라이브러리(Standard Library)의 crate다.

여러 개의 항목은 참조하기

복수의 항목을 하나의 모듈 경로로 참조하고 싶다면 다음과 같이 사용하면 된다.

use std::f64::consts::{PI, TAU}

모듈 작성하기

코드를 생각할 때 보통은 디렉토리로 구성된 파일 구조를 떠올린다.
Rust에서는 모듈을 선언하는 두가지 방법이 있다.

  1. foo.rs 라는 파일이름
  2. foo라는 이름의 디렉토레이 들어있는 파일 mod.rs

모듈 계층구조

한 모듈은 다른 모듈에 의존할 수 있다. 모듈과 하위 모듈 사이에 관계를 지어주려면, 부모 모듈에 코드 mod foo;를 작성하면 된다.
foo.rs 파일이나 foo/mod.rs 파일을 찾아 범위 내의 foo 모듈 안에 그 내용물을 삽입한다.

인라인 모듈

  • 하위 모듈은 모듈의 코드 내에 직접 치환 될 수 있다.
  • 인라인 모듈의 가장 흔한 용도는 유닛 테스트를 만들 때다. Rust가 테스트에 쓰일 때에만 존재하는 인라인 모듈을 만들 수 있다.
#[cfg(test)]
mod tests {
    use super::*;
    // test code
}

내부 모듈 참조하기

rust에서는 use경로에 사용할 수 있는 몇가지 키워드를 통해 원하는 모듈을 빠르게 가져다 쓸 수 있다.

  • create : 최상위 모듈
  • super: 현재 모듈의 부모 모듈
  • self: 현재 모듈

내보내기

기본적으로 모듈의 구성원들은 외부에서 접근이 불가능하다. (자식 모듈에게까지도!) pub 키워드를 사용하면 모듈의 구성원들을 접근 가능하게 할 수 있다.
기본적으로 crate의 구성원들도 외부에서 접근이 불가능하다. 크레이트의 최상위 모듈에 pub를 표시하면 구성원들을 접근 가능하게 할 수 있다.

구조체 가시성

구조체도 함수와 마찬가지로 pub를 사용해 모듈 외부로 무엇을 노출할 지 선엉할 수 있다

pub struct SeaCreature {
    pub animal_type: String,
    pub name: String,
    pub arms: i32,
    pub legs: i32,
    weapon: String,
}

Prelude 모듈

use로 가져오지 않았는데, 어떻게 Vec, Box를 사용할 수 있을까? 이는 표준 라이브러리의 prelude 모듈 덕분이다.
Rust의 표준 라이브러리에서는 std::prelude::**로 내보내기 된 모든 것들이 어디에서든 자동으로 사용 가능하다. Vec, Box가 바로 이런 경우이며, 다른 것들 (Opton, Copy 등 )도 마찮가지다.

반응형

'개발 > rust' 카테고리의 다른 글

RUST 학습 1주차  (0) 2022.10.17
RUST 소개  (0) 2022.10.09
rust 유용한 명령어  (0) 2022.10.07

Tour of RUST #1

유용한 학습 사이트

Cargo project 만들기

바이너리 파일을 생성하는 프로젝트

$ cargo new [프로젝트명]
# Cargo.toml, main.rs 생성

라이브러리 파일을 생성하는 프로젝트

$ cargo new [프로젝트명] -lib
# Cargo.toml, lib.rs 가 생성

유용한 도구

rustfmt

  • Rust 팀에서 개발, 관ㄹㅣ하고 있는 공식 포맷터(Formatter)
  • 공식 스타일 가이드라인을 참고해서 자동으로 코드 스타일을 수정
cargo fmt

clippy

  • Rust 팀에서 개발, 간리하고 있는 코드 린터 (Linter)
  • 현재 코드의 문제점을 파악하고, 자동으로 수정할 수 있다.
cargo clippy

변수

let 키워드 사용

변수의 자료형을 대부분 유추할 수 있다.

변수 숨김(Variable Shadowing)을 지원

변수의 이름은 언제나 snake_case형태로 짓는다.

fn main() {
    let x = 13;
    println!("{}", x);

    let x: f64 = 3.14159;
    println!("{}", x);

    let x;
    x = 0;
    println!("{}", x);
}

Rust에서 변수는 기본적으로 변경 불가(Immutable) 타입이다.

변경 가능(Mutable)한 값을 원한다면 mut키쿼드로 표시해줘야 한다.

fn main() {
    let mux x = 42;
    println!("{}", x);
    x = 13;
    println!("{}", x);
}

2. 기본 데이터 구조

기본 자료형

부울값: bool

부호가 없는 정수형: 양의 정수를 나타내는 u8, u16,u32, u64, u128

부호가 있는 정수형 - 양/음의 정수를 나타내는 i8, i16, i32, i64, i128

포인터 사이즈 정수: 메모리에 있는 값들의 인덱스와 크리를 나타내는 usize, isize

부동소수점: f32, f64

튜플(tuple): stack에 있는 값들의 고정된 순서를 전달하기 위한 (value, value, …)

배열(array): 컴파일 타임에 정해진 길이를 갖는 유사한 원소들의 모음(Collection)인 [value, value, …]

슬라이스(slice): 런타임에 길이가 정해지는 유사한 원소들의 collection

str(문자열 slice): 런타임에 길이가 정해지는 텍스트

자료형 변환을 할 때는 as 키워드를 사용한다. ( Rust에서는 숫자형 자료형을 쓸 때 명시적으로 사용해야 한다.)

fn main{
    let a = 13u8;
    let b = 7u32;
    let c = a as u32 + b;
    println!("{}", c);

    let t = true;
    println!("{}", t as u8);
}

상수

상수는 변수와 달리 반드시 명시적으로 자료형을 지정해야 한다.

상수의 이름은 언제나 SCREAMING_SNAKE_CASE형태로 짓는다.

const PI: f32 = 3.14159;

fn main() {
    println!(
        PI
    );
}

배열

고정된 길이로 된 모든 같은 자료형의 자료를 갖는 Collection

[T; N]으로 표현한다.

  • T는 원소의 자료형
  • n은 컴파일 타임에 주어지는 고정된 길이

각각의 원소는 [x] 연산자로 가져올 수 있다

fn main() {
    let nums: [i32; 3] = [1,2,3];
    println!("{:?}", nums);
    println!("{}", nums[1]);
}

함수

함수의 0개 또는 그 이상의 인자를 가진다.

함수의 이름은 언제나 snake_case형태로 짓는다.

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

fn main() {
    println!("{}", add(42,13));
}

여러개의 리턴 값

함수에서 튜플(Tuple)을 리턴하면 여러개의 값을 리턴할 수 있다.

fn swap(x: i32, y: i32) -> (i32, i32) {
    return (y, x);
}

fn main() {
    let result = swap(123, 321);
    println!("{} {}", result.0, result.1);

    let (a,b,) = swap(result.0, result.1);
    println!("{} {}", a, b);
}

아무것도 리턴하지 않기

함수에 리턴형을 지정하지 않는 경우 빈 튜플을 리턴하는데, ()로 표현한다.

fn make_nothing() -> () {
    return ();
}

fn make_nothing2() {
    // Do nothing
}

fn main() {
    let a = make_nothing();
    let b = make_nothing2();

    pritnln!("The value of a: {:?}", a);
    println!("The value of b: {:?}", b);
}

if/else if/else

조건문에 괄호가 없다

fn main() {
    let x = 42;
    if x < 42 {
        println!("Less then 42");
    } else if x == 42 {
        println!("Eqeual 42");
    } else {
        println!("Greater than 42");
    }
}

loop

무한 반복문이 필요할 때 사용

fn main() {
    let mux x = 0;
    loop {
            x += 1;
            if x == 49 {
                break;
            }
    }
    println!("{}", x);
}

while

반복문에 조건을 간단히 넣을 수 있다

조건의 평가 결과가 false인 경우, 종료한다.

fn main() {
    let mux x = 0;
    while x != 42 {
            x += 1;
    }
}

for

..연산자는 시작 숫자에서 끝 숫자 전까지의 숫자들을 생성하는 반복자를 만든다.

..=연산자는 시작 숫자에서 끝 숫자까지들을 생성하는 반복자를 만든다.

fn main() {
    for x in 0..5 {
        println!("{}", x);
    }

    for x in 0..=5 {
        println!("{}", x);
    }
}

match

switch를 대체하는 구문

모든 케이스를 빠짐없이 처리해야 한다.

fn main() {
    let x = 41;

    match x {
        0 => {
            println!("Foudn 0");
        }
        1 | 2 => {
            println!("Fount 1 or 2!");
        }
        3..=9 => {
            println!("Fount between 3 and 9!");
        }
        matched_num @ 10..=100 => {
            println!("Fount {} between 10 and 100!", matched_num);
        }
        _ => {
            println("Found something else!");
        }
    }
}

구조체

필드(Filed)들의 Collection

메모리 상에 필드들을 어떻게 배치할 지에 대한 컴파일러의 청사진

struct SeaCreature {
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

3. 기초적인 흐름 제어

메소드 호출하기

스태틱 메소드(Static Methods)

  • 자료형 그 자체에 속하는 메소드
  • ::연산자를 이용해 호출

인스턴스 메소드(Instance Methods)

  • 자료형의 인스턴스에 속하는 메소드
  • .연산자를 이용해 호출
fn main() {
    let s = String::from("Hello world!");
    println!("The length of {} is {}.", s, s.len());
}

메모리에 데이터 생성하기

코드에서 구조체를 인스턴스화(Instaniate)하면 프로그램은 연관된 필드 데이터들을 메모리 상에 나란히 생성한다.

구조체의 필드값들은 .연산자를 통해 접근한다.

struct SeaCreature {
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

fn main() {
    let ferris = SeaCreature {
        animal_type: String::from("crab"),
        name: String::from("Ferris"),
        arms: 2,
        legs: 4,
        weapon: String::from("claw"),
    }

열거형

enum키워드를 통해 몇 가지 태그된 원소의 값을 갖는 새로운 자료형을 생성할 수 있다

match와 함께 사용하면 품질 좋은 코드를 만들 수 있다.

enum Species {
    Crab,
    Octopus,
    Fish,
    Clam,
}

struct SeaCreature {
    spcies: Species,
    name; String,
}

fn main() {
    let ferris = SeaCreate {
        species: Species::Crab,
        name: String::from("Ferris"),
    }
    match ferris.species {
        Species::Crab => println("{} is Crab", ferries,name),
        Species::Octopus => println("{} is Octopus", ferries,name),
        Species::Fish => println("{} is Fish", ferries,name),
        Species::Clam => println("{} is Clam", ferries,name),
    }
}

4. Generic 자료형

Generic 자료형

structenum을 부분적으로 정의해, 컴파일러가 컴파일 타임에 코드 사용을 기반으로 완전히 정의된 버전을 만들 수 있게 해준다.

struct BagOfHolding<T> {
    item: T,
}

fn main() {
    let i32_bag = BagOfHolding::<i32> { tiem: 42 };
  let bool_bag = BagOfHolding::<bool> { item: true };
  let float_bag = BagOfHolding { item: 3.14 };
  let bag_in_bag = BagOfHolding {
    item: BagOfHolding { item: "boom!" },
  };

  println!(
    "{} {} {} {}",
    i32_bag.item, bool_bag.item, float_bag.item, bag_in_bag.item.item
  );
}

Option

null을 쓰지 않고도 Nullable한 값을 표현할 수 있는 내장된 Generic 열거체

enum Option<T> {
    None,
    Some(T),
}
struct BagOfHolding<T> {
    item: Option<T>,
}

fn main() {
    let i32_bag = BagOfHolding::<i32> { item: None };
    if i32_bag.item.is_none() {
        println!("Nothing!")
    } else {
        println!("Found Something!")
    }

    let i32_bag = BagOfHolding::<i32> { item: Some(42) };
    if i32_bag.item.is_some() {
        println!("Found Something!")
    } else {
        println!("Nothing!")
    }

    match i32_bag.item {
        Some(v) => println!("Found {}!", v),
        None => println!("Nothing!"),
    }
}

Result

실패할 가능성이 있는 값을 리턴할 수 있도록 해주는 내장된 Genric 열거체

enum Result<T, E> {
    Ok(T),
    Err(E),
}
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(3.14)
    } else {
        Err(String::from("Not match!"))
    }
}

fn main() {
    let result = do_something_that_might_fail(12);
    match result {
        Ok(value) => println!("Success: {}", value),
        Err(error) => println!("Error: {}", error),
    }
}

우아한 오류 처리

Result와 함께 쓸 수 있는 강력한 연산자 ?

do_somrthing_that_might_fail()?

match do_something_that_might_fail() {
    Ok(v) => v,
    Err(e) => return Err(e),
}
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("Not match!"))
    }
}

fn main() -> Result<(), String>{
    let v = do_something_that_might_fail(42)?;
    println!("Found {}", v);
    Ok(())
}

추한 옵션/결과 처리

간단한 코드를 작성 할 때에도 Option/Result를 쓰는 것은 귀찮은 일일 수 있다

unwap이라는 함수를 사용해 빠르고 더러운 방식으로 값을 가져올 수 있다.

  • Option/Result 내부의 값을 꺼내오고
  • enumNone/Err인 경우에는 panic!
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("Not match!"))
    }
}

fn main() -> Result<(), String> {
    let v = do_something_that_might_fail(42).unwrap();
    println!("Found {}", v);

    let v = do_something_that_might_fail(1).unwrap();
    println!("Found {}", v);
    Ok(())
}

벡터

  • Vec구조체로 표현하는 가변 크기의 리스트
  • Vec!머크로를 통해 손쉽게 생성할 수 있다.
  • iter()메소드를 통해 반복자를 생성할 수 있다.
fn main() {
    let mut float_vec = Vec::new();
    float_vec.push(1.3);
    float_vec.push(2.4);
    float_vec.push(3.5);

    let string_vec = vec![String::from("Hello"), String::from("World")];

    for word in string_vec.iter() {
        println!("{}", word);
    }
}

5. 소유권과 데이터 대여

소유권 및 범위 기반 리소스 관리

자료형을 인스턴스화해 변수명에 할당(Binding)하면, Rust 컴파일러가 전체 생명주기(Lifetime) 동안 검증할 메모리 리소스를 생성한다.

할당된 변수는 리소스의 소유자(Owner)라고 한다.

Rust는 범위(scope)가 끌나는 곳에서 리소스를 소멸하고 할당 해제한다.

이 소멸과 할당 해제를 의미하는 용어로 drop을 사용한다. ( C++ 에서는 Resource Acquisition Is Initialization(RAII)라고 부른다)

구조체가 Drop될 때 구조체 자신이 제일 먼저 Drop되고, 이후 자식들이 각각 Drop된다.

소유권 이전

소유자가 함수의 인자로 전달되면, 소유권은 그 함수의 매개 변수로 이동(Move)된다.

이동된 이후에는 원래 함수에 있던 변수는 더 이상 사용할 수 없다.

struct Foo {
    x: i32,
}

fn do_somerhing(f: foo) {
    println!("{}", f.x);
}

fn main() {
    let foo = Foo { x: 42 };
    do_something(foo);
}

소유권 리턴하기

소유권은 함수에서도 리턴될 수 있다.

struct Foo {
    x: i32,
}

fn do_somerhing() -> {
    Foo {x: 42}
}

fn main() {
    let foo =    do_something(foo);
}

참조로 소유권 대여하기

&연산자를 통해 참조로 리소스에 대한 접근 권한을 대여할 수 있다

참조도 다른 리소스와 마찬가지로 Drop된다.

struct Foo {
    x: i32,
}

fn main() {
    let foo = Foo { x: 42};
    let f = &foo;
    println!("{}", f.x);
}

참조로 변경 가능한 소유권 대여하기

&mut연산자를 통해 리소스에 대해 변경 가능한 접근 권한도 대여할 수 있다.

리소스의 소유자는 변경 가능하게 대여된 상태에서 이동되거나 변경될 수 없다.

struct Foo {
    x: i32,
}

fn do_something(f: Foo) {
    println!("{}", f.x);
}

fn main() {
    let mut foo = Foo { x:42 };
    let f= &mut foo;

    // do_something(foo); // error: use of moved value: `foo`
    // foo.x = 13;
    f.x = 13;
    println!("{}", foo.x);

    foo.x = 7;
    do_something(foo);
}

역참조

&mut참조를 이용해 *연산자로 소유자의 값을 설정할 수 있다.

*연산자로 소유자의 값의 복사본도 가져올 수 있다.(복사 가능한 경우만)

fn main() {
    let mut foo =42;
    let f = &mut foo;
    let bar = *f;
    *f = 13;
    println!("{}", bar);
    println!("{}", foo);
}

대여한 데이터 전달하기

Rust의 참조 규칙

  • 단 하나의 변경 가능한 참조 또는 여러개의 변경 불가능한 참조만 허용하며, 둘다는 안된다.
  • 참조는 그 소유자보다 더 오래 살 수 없다.

보통 함수로 참조를 넘겨줄 때에는 문제가 되지 않는다.

struct Foo {
    x: i32,
}

fn do_something(a: &Foo) -> &i32 {
    return &a.x;
}
fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    let y = do_something(&foo);
    println!("{}", y);
}

명시적인 생명주기

Rust 컴파일러는 모든 변수의 생명 주기를 이해하며, 참조가 절대로 그 소유자보다 더 오래 존재하지 못하도록 검증을 시도한다.

함수에서 어떤 매개 변수와 리턴 값이 서로 같은 생명 주기를 공유하는지 식별할 수 있도록 심볼로 표시해 명시적으로 셩명 주기를 지정할 수 있다.

생명 주기 지정자는 언제나로 시작한다. (ex: ‘a, ‘b, ‘c)

struct Foo {
    x: i32,
}

fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
    return &foo.x;
}

fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 43;
    let y = do_something(&foo);
    println!("{}", y);
}

여러 개의 생명 주기

생명 주지 지정자는 컴파ㅊ일러가 스스로 함수 매개 변수들의 생명 주기를 판별하지 못하는 경우, 이를 명시적으로 지정할 수 있게 도와준다.

struct Foo {
    x: i32,
}

fn do_something<'a, 'b>(foo_a: &'a Foo, foo_b: &'b Foo) -> &'b i32 {
    println!("{}", foo_a.x);
    println!("{}", foo_b.x);
    return &foo_b.x;
}

fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 12 };
    let x = do_something(&foo_a, &foo_b);
    println!("{}", x);
}

정적인 생명주기

static변수는 컴파일 타임에 생성되어 프로그램의 시작부터 끝까지 존재하는 메모리 리소스다. 이들은 명시적으로 자료형을 지정해 주어야 한다.

static생명 주기는 프로그램이 끝날 때까지 무한정 유지되는 메모리 리소스다. 따라서 static이라는 특별한 생명주기 지정자를 갖는다.

static한 리소스는 절대 drop 되지 않는다.

만약 static 생명 주기를 갖는 리소스가 참조를 포함하는 경우, 그들도 모두 static이어야 한다. (그 이하의 것들은 충분히 오래 살아남지 못한다)

static PI: f64 = 3.1415;

fn main() {
    static mut SECRET: &'static str = "swordfish";

    let msg: &'static str = "Hello World";
    let p: &'static f64 = &PI;
    println!("{} {} ", msg, p);
    unsafe {
        SECRET = "abracadbra";
        println!("{}", SECRET);
    }
}

데이터 자료형의 생명주기

함수와 마찬가지로 데이터 자료형의 구성원들도 생명 주기 지정자로 지정할 수 있다.

Rust는 참조가 품고 있는 데이터 구조가 참조가 가리키는 소유자보다 절대 오래 살아남지 못하도록 검증한다.

아무것도 아닌 것을 가리키는 참조를 들고 다니는 구조체는 있을 수 없다.

struct Foo<'a> {
    i:&'a i32
}

fn main() {
    let x = 42;
    let foo = Foo {
        i: &x
    };
    println!("{}", foo.i);
}
반응형

'개발 > rust' 카테고리의 다른 글

RUST 학습 2주차  (0) 2022.10.24
RUST 소개  (0) 2022.10.09
rust 유용한 명령어  (0) 2022.10.07

RUST_소개

1. RUST란

  • https://www.rust-lang.org
  • 모질라 재단에서 2010년 7월 7일 처음 발표
  • 현재는 러스트 재단으로 독립해서 개발되고 있음

RUST 언어의 특징

  • 안전한 메모리 관리
  • 철저한 예외나 에러 관리
  • 특이한 enum 시스템
  • 트레이트
  • 하이지닉 매크로
  • 비동기 프로그래밍
  • 제네릭

Rust Playground

설치방법

  • Window
  • Linux subsystem for linux
  • Linux and MacOS

Rust 디스어셈블리

반응형

'개발 > rust' 카테고리의 다른 글

RUST 학습 2주차  (0) 2022.10.24
RUST 학습 1주차  (0) 2022.10.17
rust 유용한 명령어  (0) 2022.10.07

rust 유용한 명령어

업데이트와 제거

업데이트

$ rustup update

제거

$ rustup self uninstall

Cargo

프로그램 체크

빌드 보다 적은 시간을 사용하면서, 문법이나 실행이 되는지 확인할 수 있다.

$ cargo check

빌드

$ cargo build

rustfmt

  • Rust 팀에서 개발, 관ㄹㅣ하고 있는 공식 포맷터(Formatter)
  • 공식 스타일 가이드라인을 참고해서 자동으로 코드 스타일을 수정
cargo fmt

clippy

  • Rust 팀에서 개발, 간리하고 있는 코드 린터 (Linter)
  • 현재 코드의 문제점을 파악하고, 자동으로 수정할 수 있다.
cargo clippy

변수

패키지 문서 만들기

현재 의존 패키지들이 참조하는 문서들을 빌드해서 브라우저 문서로 만들어 줍니다.

$ cargo doc --open
        Finished dev [unoptimized + debuginfo] target(s) in 0.02s
        Opening /Users/forteleaf/works/rust/hello_cargo/target/doc/hello_cargo/index.html
반응형

'개발 > rust' 카테고리의 다른 글

RUST 학습 2주차  (0) 2022.10.24
RUST 학습 1주차  (0) 2022.10.17
RUST 소개  (0) 2022.10.09

리눅스 서버를 다루다보니, 터미널을 매일사용하고 있습니다. 저는 기본으로 fish, tmux. alacritty 을 이용해서 작업을 하고 있습니다. 제가 사용하는 것들의 주요 특징들은 가볍고 빠릅니다.
iterm2는 전혀 성에 차지 않습니다. wasp와 같은 터미널 프로그램은 쓸만합니다. 하지만, 기본기로 돌아가면 alacritty 사용을 권하고 싶습니다. (rust 로 작성되어 있다는 점도 무시할 수 없습니다.)

현재 사용중인 stack

오늘부터 도전!

자주가는 reddit.comr/unixpron에는 커스텀화 된 화면들을 공유하고 자신들의 설정을 공유하는 곳입니다. 리눅스 사용자들을 위한 곳입니다. 리눅스도 같이 사용하고 있는 저에게는 지루한 랩탑을 좋은 놀이깜으로 만들어주는 곳입니다. (리눅스에 대해서도 학습하면서...)
아래 적용하려고 하는 도구들도 이 곳에서는 많이 사용하는 것들입니다.

여기서 최신 도구들을 몇개 사용해 보려고 합니다. 선정이유는 다들 rust로 작성되어 있는 점입니다. rust를 사랑하는 개발자로서 이 세팅은 반드시(!) 좋을 것 같습니다.

아래 TOOLS 를 사용하려고 하고, 장점에 대해서 설명하려고 합니다.

  • nushell
  • startship
  • zellij

nushell

rust로 작성된 최신 데이터 지향 쉘입니다. 기본적인 구성만으로 자동완성 및 기타 유용한 기능을 사용하기 위해 여러 설정을 할 필요가 없습니다.

아직 사용하기에는 여러문제들이 보임.

설치

$ brew install nushell
.......
$ nu
✘  ~  nu
No environment config file found at /Users/forteleaf/Library/Application Support/nushell/env.nu
Would you like to create one with defaults (Y/n):

Config file created at: /Users/forteleaf/Library/Application Support/nushell/env.nu
No config file found at /Users/forteleaf/Library/Application Support/nushell/config.nu
Would you like to create one with defaults (Y/n):

Config file created at: /Users/forteleaf/Library/Application Support/nushell/config.nu
     __  ,
 .--()°'.' Welcome to Nushell,
'|, . ,'   based on the nu language,
 !_-(_\    where all data is structured!

Please join our Discord community at https://discord.gg/NtAbbGn
Our GitHub repository is at https://github.com/nushell/nushell
Our Documentation is located at http://nushell.sh
Tweet us at @nu_shell

Nushell has been around for:
3yr 4month 3wk 1day 23hr 37min 48sec 769ms 498µs

You can disable this banner using the config nu command
to modify the config.nu file and setting show_banner to false.

let-env config {
    show_banner: false
    ...
}

장점

  1. Linux, macOS, Window 에서 작동
  2. nu pipeline은 구조화 된 데이터를 사용.
    안전하게 선택, 필터링, 정렬을 사용
  3. 강력한 플러그인

단점

  1. command 학습이 필요함.
    shell이름을 확인하려고 했는데....

    `echo $SHELL`
    Error: nu::parser::variable_not_found (link)
    
    × Variable not found.
    ╭─[entry #12:1:1]
    1 │ echo $SHELL
    ·      ───┬──
    ·         ╰── variable not found
    ╰────

nushell는 데이터 표현을 테이블 저장합니다. 마치 DB의 쿼리를 이용해서 물러오는 것 같습니다. 기본 zsh, fish, bash 등에 익숙해 있다면 생소한`cui
화면 처럼 보이기도 합니다.

/Users/forteleaf/vagrant〉ls                                                                                                                             09/28/2022 11:43:33 PM
╭───┬──────────┬──────┬───────┬──────────────╮
│ # │   name   │ type │ size  │   modified   │
├───┼──────────┼──────┼───────┼──────────────┤
│ 0 │ kubeset  │ dir  │ 320 B │ 2 months ago │
│ 1 │ window10 │ dir  │ 128 B │ 2 months ago │
╰───┴──────────┴──────┴───────┴──────────────╯
  1. 미완성적인 부분
    기존에 것들과 차이가 나고 PATH등 설정을 새로 해야함

starship

  • 호환성 우선
  • RUST 를 이용한 최고의 퍼포먼스와 안정성
  • 세심한 CUSTOM

Starship은 간단한 yaml 파일에 매우 다양한 구성 옵션을 제공 합니다. 그러나 nushell과 마찬가지로 처음에는 구성이 필요하지 않습니다. 모든 기본값이 이미 설정되어 있습니다! 바로 사용할 수 있는 멋진 구성 사전 설정을 제공합니다.

Starship은 다른 여러 도구 및 기술과 자동으로 통합됩니다. 패키지 버전을 표시하고(예: Poetry 프로젝트에 있을 때) AWS 리전, kubernetes 컨텍스트 및 네임스페이스를 표시할 수 있습니다. 간단히 말해서 모든 것과 작동합니다. 여기에서 전체 통합 목록을 살펴보십시오.


현재는 pastel Powerline으로 설정해서 사용하고 있습니다.

rust 로 작성되어 있어서 빠르다고 생각했지만, 별 차이를 느낄 수 없었습니다.

zellij

tmux와 매우 유사한 기능을 가지고 있습니다. 이쁜 라인이 생겨서 화면을 구분할 수 있는 큰 장점이 있습니다. 그런데 이로 인해서 볼 수 있는 text가 조금은 줄어듭니다. 한줄 정도 차이지만, 신경이 쓰입니다.

설치

$ brew install # Zellij
or
# zsh
bash <(curl -L zellij.dev/launch)

# fish
bash (curl -L zellij.dev/launch | psub)

장점

  • 기존에 사용하던 tmux의 단축키가 호환
  • 이쁨
  • layout 저장 (tmuxinator를 사용하고 있어서 ....)
  • pane 이동 기능이 직관적이고 편리

단점

  • tmuxinator 를 사용하고 있는데 이와 같은게 안 보임
  • ctrl+p 로 과거 실행했던 메뉴를 찾는데, 단축키가 설정되어 있음(pane 설정용)
    별도의 세팅을 해야되는 아쉬움
  • synchronize-pane 기능이 없다. 2021년에 기능이 추가 됨

기존에 사용하던 tmux의 단축키의 익숙함 때문에 zellij 에 적응이 안됩니다. 그리고, 기본 repository 에서 tmux 를 실행할 수 있는데, 굳이 zellij 를 설치해서 사용할 필요가 있을까 싶습니다.

반응형

'개발 > 리눅스' 카테고리의 다른 글

arch ctrl <-> caps lock swap  (0) 2023.01.02
freebsd | minidlna 구축  (0) 2022.10.13
ansible - 자동화의 시작  (0) 2022.07.14
Gitlab-CE 업그레이드  (0) 2022.07.05
LINUX | Linus Torvalds 가 우분투, 데비안을 싫어하는 이유  (0) 2020.07.13

+ Recent posts