러스트(Rust) 정리

박정호·2022년 6월 20일
0

깃허브에 각각의 목차를 두고 코드를 나누어 놨습니다.
벨로그 글도 조금 더 진행 후에 분기하여 다시 업로드 하겠습니다.
깃허브 링크

내가 러스트를 공부하는 이유

  1. 자바스크립트는 싱글 쓰레드(Single Thread)다.
  2. 러스트는 low level이다
  3. 나는 인터프리터 언어가 싫다.
  4. 리눅스를 사랑하자
    4-1. vim를 사랑하자
    4-2. IDE는 왜 쓰는 것인지?

Installation

https://www.rust-lang.org/tools/install

카고(Cargo)

카고란 ? 러스트의 빌드 시스템이자 패키지 관리자. 의존 라이브러리들을 관리한다.

cargo new hello_cargo
cargo build
cargo run
cargo check
cargo update : dependencies 등 업데이트 검색
cargo doc --open

간단한 프로젝트라면 카고와 rustc 컴파일은 큰 차이가 없지만,
여러개의 의존(Dependency)가 들어간다면 카고를 사용해 빌드를 조율하는 것이 훨씬 간편하다.

cargo - authors 등의 대한 정보는 환경변수를 가져온다. (깃에서 가져올 줄 알았는데)

println vs println!

매크로

숫자 맞히기 게임!

프렐류드(Prelude) : 모든 러스트 프로그램에 정의된 특별한 영역으로 해당 프로그램 또는 크레이트가 외부로 노출하는 타입을 명시한 모듈.

크레이트(crate)

크레이트란 : (바이너리) 소스 파일의 집합.

변수, 상수

프로그램 전체에서 하드코드 시킬 만한 값을 잘 파악해서 상수로 정의하자.

데이터 타입

  1. 스칼라(Scalar)
  • 정수(interger)
    부호 없음(u), 부호 있음(i) : 2의 보수로 표현
  • 부동 소수점 숫자(floating point numbers)
  • 불리언(Booleans)
  • 문자(characters)
    문자열 리터럴은 큰따옴표, char 리터럴은 작은따옴표이다.
  1. 컴파운드(Compound) : 하나의 타입으로 여러 개의 값을 그룹화한 타입.
  • 튜플(tuples)
  • 배열(arrays)

함수

구문과 표현식

소유권

모든 프로그램은 실행 중인 동안에 어떠한 방법으로든 컴퓨터의 메모리를 관리해야 한다.
가비지 콜렉터를 이용해 더 이상 사용되지 않는 메모리를 지속적으로 찾아 자동으로 해제하는 행위
등으로 말이다.
또 다른 언어는 프로그래머가 명시적으로 메모리를 할당하고 해제해 주어야 한다.
러스트는 제 3의 방법을 사용한다. 메모리는 컴파일러가 컴파일 시점에 검사하는 다양한 규칙으로
이루어진 소유권 시스템애 의해 관리된다.
따라서, 소유권과 관련된 기능은 프로그램의 실행 성능에 아무런 영향을 미치지 않는다.
소유권 시스템의 규칙에 익숙해질수록 더 안전하며 효율적인 코드를 작성할 수 있다.

스택(Stack) 메모리와 힙(Heap) 메모리

타 언어는 자주 신경쓰지 않으나, 시스템 프로그래밍 언어 환경에서는 값이 스택 메모리에 저장되었는지 힙 메모리에 저장되었는지에 따라 언어의 동작이나 의사결정에 큰 영향을 미친다.
스택과 힙은 모두 코드가 런타임에 활용하는 메모리의 일부이며 각자 다른 방법으로 데이터를 구성한다.
스택은 LIFO 구조이다. 마지막에 들어온 데이터가 제일 먼저 나간다.
스택에 저장하는 모든 데이터는 고정된 크기를 가져야 한다.
컴파일 시점에 크기를 알 수 없는 데이터나 런타임에 동적으로 크기가 변하는 데이터는 힙 메모리에 저장한다.
힙은 스택에 비해 조금 더 복잡하다.
힙에 데이터를 넣는 동작은 메모리의 일정 공간을 할애할 것을 요청하는 것이다.
따라서 운영체제는 힙 메모리에서 일정한 공간을 찾아 사용 중인 메모리로 표시한 후, 해당 메모리의 주소값인 포인터(pointer)를 넘겨준다. 이 과정을 '힙 메모리 할당(allocation on the heap)'이라고 한다.
스택에서는 발생하지 않는다.
포인터는 이미 크기가 고정된 값이므로 포인터는 스택에 저장할 수 있지만, 실제 포인터가 가리키는 데이터가 필요할 때는 포인터가 가리키는 메모리를 따라가야 한다.
스택에 데이터를 푸시하는 것이 힙에 할당하는 것보다 빠른 이유는 운영체제가 새 데이터를 저장할 공간을 찾을 필요가 없기 때문이다.
새 데이터는 항상 스택의 가장 위에 추가된다. 힙에 공간을 할당하는 것은 상대적으로 더 많은 작업이 필요하다.
운영체제가 먼저 데이터를 저장할 충분히 큰 공간을 찾은 후 다음 할당 작업을 위한 예약 작업을 수행해야 하는 이유다.
힙 메모리에 저장된 데이터에 대한 접근은 스택에 저장된 메모리에 접근하는 것보다 느리다.
그 이유는 포인터를 따라가야 하기 때문이다. 현대의 프로세서들은 메모리 사이를 왔다 갔다 하는 일이 더 적을수록 더 빠르게 동작한다.
코드에서 함수를 호출할 때 이 함수에는 힙 메모리에 저장된 데이터에 대한 포인터를 포함한 여러 값이 전달 되며, 이 값들은 함수의 로컬 변수에 할당되어 스택에 저장된다. 함수의 실행이 완료되면 이 값들은 스택에서 제거된다.
코드의 어느 부분이 힙 메모리에 저장된 데이터를 사용하는지 추적하고 힙에 저장되는 데이터의 중복을 최소화해 사용하지 않는 데이터를 힙 메모리에서 제거하면 메모리 부족 문제를 해소할 수 있다.
러스트의 소유권 기능은 바로 이 문제를 해결하려는 방법이다.

소유권 규칙

  • 러스트가 다루는 각각의 값은 소유자(owner)라고 부르는 변수를 가지고 있다.
  • 특정 시점에 값의 소유자는 단 하나 뿐이다.
  • 소유자가 범위를 벗어나면 그 값은 제거된다.

변수의 범위

범위 : 프로그램 안에서 어떤 아이템이 유효한 한도.

문자열 vs String

문자열 리터럴은 컴파일 시점에 문자열의 내용을 이미 알고 있으므로 텍스트를 최종 실행할 수 있는 형태로 직접 하드코딩할 수 있다. 그러므로 문자열 리터럴은 빠르고 효율적이다. 하지만 이러한 장점은 불변성에서 나온다.
안타깝게도 컴파일 시점에 그 길이를 미리 알 수 없거나 프로그램의 실행 중에 길이가 변경되는 문자열은 그 문자열이 사용할 메모리를 바이너리 형태로 미리 변환할 수가 없다.

가변 문자열을 지원하는 String 타입은 길이를 조절할 수 있는 텍스트이므로 컴파일 시점에 알 수 없는 내용을 저장하기 위해 힙 메모리에 일정 부분의 메모리를 할당해야 한다.
따라서 두 가지 절차를 거친다.

  • 해당 메모리는 반드시 런타임에 운영체제에 요청해야 한다.
  • String 타입의 사용이 완료된 후에는 이 메모리를 운영체제에 다시 돌려줄 방법이 필요하다.
    첫 번째 절차는 개발자가 처리해야 한다. String::from 함수를 호출하면 이 함수가 필요한 메모리를 요청한다.
    하지만 두 번째 절차는 다르다. 가비지 콜렉터를 사용한다면 개발자가 이를 직접 처리할 필요가 없지만,
    러스트는 직접해야 하지롱 ㅋ
    문제는 이 작업을 제대로 수행하는 것은 지금까지의 프로그래밍 역사상 쉽지 않은 일이었다.
    메모리를 너무 일찍 해제하면 유효하지 않은 변수 때문에 고생하게 되며, 메모리를 해제를 두 번 실행하는 것 또한 버그를 유발한다.
    정확히 한 번씩의 할당과 해제 작업이 이루어져야 한다.

러스트는 메모리의 할당과 해제를 다른 방식으로 수행한다.
변수에 할당된 메모리는 변수를 소유한 범위를 벗어나는 순간 자동으로 해제한다.

변수가 범위를 벗어나면 러스트는 drop 이름의 특별한 함수를 호출한다.
drop 함수는 String 타입을 구현한 개발자가 메모리를 해제하는 코드를 작성해둔 함수이다.
러스트는 중괄호를 만나면 자동으로 drop 함수를 호출한다.

소유권과 함수

값을 함수에 전달한다는 의미는 값을 변수에 대입하는 것과 유사하다.
변수를 함수에 전달하면 대입과 마찬가지로 값의 이동이나 복사가 이루어진다.

구조체

구조체(struct)는 서로 관련이 있는 여러 값을 의미 있는 하나로 모으고, 이름을 지정해 접근할 수 있는 사용자 정의 데이터 타입이다. 객체지향 언어에 익숙하다면 구조체는 객체의 데이터 속성과 같다고 생각해도 무방하다.

struct User {
    //각각을 필드라 한다.
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

구조체를 정의한 후 사용하려면 각 필드에 저장할 값을 명시해서 구조체의 인스턴스를 생성해야 한다.

fn main() {
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
}

구조체에서 원하는 값을 읽으려면 마침표(.)를 이용하면 된다.
또한 인스턴스가 만약 가변 인스턴스라면 특정 필드에 새로운 값을 대입할 수도 있다.

user1.email = String::from("anotheremail@example.com");

함수를 사용해 구조체의 인스턴스를 만드는 것도 가능하다. 이때 함수의 마지막 표현식은 묵시적으로 새 인스턴스를 리턴해야 한다.

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

기존 인스턴스로 새 인스턴스를 생성할 때 다음의 편의성을 제공한다

let user2 = User {
	email : String::from("another@example.com"),
	username : String::from("anotherusername567"),
	..user1
);

튜플 구조체

튜플과 유사하게 생긴 구조체를 선언할 수도 있다.
이러한 구조체를 튜플 구조체(tuple structs)라고 한다.
튜플 구조체는 구조체에는 이름을 부여하지만, 필드에는 이름을 부여하지 않고 타입만 지정한다.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

매개변수에 튜플을 넣거나 하는 행위도 모두 가능하다.

유사 유닛 구조체

심지어 필드가 하나도 없는 구조체를 선언할 수도 있다. 이를 유사 유닛 구조체(unit-like structs)라고 한다.
유닛 타입, 즉 ()과 유사하게 동작하기 때문이다.

&str : 문자열 슬라이스 타입
String : String 타입

튜플

구조체는 튜플과 유사하다. 튜플과 마찬가지로 구조체는 각기 다른 타입의 값으로 구성된다.
하지만 튜플과는 달리 각 데이터에 별개의 이름을 부여해서 값의 의미를 더 분명하게 표현할 수 있다.
또한, 각 데이터에 이름이 있으므로 구조체는 튜플보다 더욱 유연하다.
참조할 데이터를 가리키거나 인스턴스의 값을 읽을 때 데이터의 순서에 의존할 필요가 없기 때문이다.

println! 매크로는 다양한 형식으로 문자열을 출력할 수 있으며, 중괄호는 기본적으로 println! 매크로가 Display 형식을 출력하도록 한다.
Display 형식은 최종 사용자를 위한 출력 형식이라고 보면 된다.
기본 타입들은 Display 형식을 기본적으로 구현하고 있다.
왜냐하면 기본 타입들은 자신을 표현할 방법이 한가지 뿐이기 때문이다.
숫자 1은 1이라고 하는 거 말고는 딱히 표현 방법이 없다.
하지만 구조체는 다르다.

  • 값들을 쉼표로 구분할건지?
  • 중괄호도 출력할건지?
  • 어떤 필드를 보여줄지?
    등의 불명확성 때문에 러스트는 구조체는 Display 크레이트를 구현하지 않도록 남겨두었다.

메서드

메서드(method)는 함수와 유사하다. 함수와 마찬가지로 fn 키워드를 이용해 정의한다.
차이점은 메서드는 함수와 달리 구조체의 컨텍스트(context) 안에 정의하며, 첫 번째 매개변수는 항상 메서드를 호출할 구조체의 인스턴스를 표현하는 self여야 한다.

첫 번째 매개변수를 self로만 선언해서 인스턴스에 대한 소유권은 갖는 메서드는 드물다.
이 기법은 메서드가 self를 다른 인스턴스로 교체한 후 호출자가 더 이상 예전 인스턴스를 사용하지 못하도록 할 때 활용한다.

함수 대신 메서드를 사용할 때의 장점은 메서드를 호출할 때마다 매번 첫 번째 매개변수로 타입을 넘겨줄 필요가 없다는 점과 더불어 코드를 더 잘 정리할 수 있다는 점이다.
타입과 관련된 모든 것을 하나의 impl 블록에 모을 수 있어서 나중에 다른 개발자가 Rectangle 구조체의 기능들을 파악하기 위해 라이브러리의 여러 곳을 검색할 필요가 없어진다.

impl 블록의 또 다른 유용한 기능은 self 매개변수를 사용하지 않는 다른 함수도 정의할 수 있다는 점이다.
이런 함수를 연관 함수(associated functions)라고 한다.
이 함수들은 구조체의 인스턴스를 직접 전달받지 않기 때문에 메서드가 아니라 함수다.
String::from 함수가 좋은 예이다.
연관 함수는 구조체의 새로운 인스턴스를 리턴하는 생성자(constructors)를 구현할 때 자주 사용한다.
연관 함수를 호출하려면 구조체의 이름과 함께 :: 문법을 사용하면 된다.

let sq = Rectangle::square(3);

이 함수는 해당 구조체에 대해서만 사용할 수 있다.
:: 문법은 연관 함수의 호출은 물론 모듈이 생성하는 이름 공간(namespace)을 정의하는데 모두 사용된다.

impl은 여러 개로 나눠서 선언할 수도 있다.
딱히 그럴 필요성은 없겠지만?

구조체는 프로그램 안에서 특정한 의미가 있는 사용자 정의 타입을 선언하기 위한 개념이다.
구조체를 이용하면 관련된 데이터를 모아 개별적으로 이름을 부여할 수 있어 코드를 더욱 깔끔하게 작성할 수 있다.
메서드는 구조체의 인스턴스에 원하는 동작을 부여하며, 연관 함수는 구조체의 인스턴스가 없는 상황에서 구조체에 적용할 수 있는 기능을 구분짓는 데 활용할 수 있다.

열거자(enums)

열거자는 사용 가능한 값만 나열한 타입을 정의할 때 사용한다.
러스트의 열거자는 함수형 언어의 대수식 데이터 타입과 유사하다.
열거자에 나열한 값은 반드시 하나만 사용할 수 있다.

enum IpAddrKind {
	// 각각의 값들을 열것값이라 한다.
	v4,
	v6,
}

let four = IpAddrKind::v4;
let six = IpAddrKind::v6;

// 열거자 역시 함수의 매개변수로 사용 가능 하다.
fn route(ip_type : IpAddrKind) { }

route(IpAddrKind::v4)
route(IpAddrKind::v6)

I/O

개발 팁

  • 하나의 함수는 하나의 동작을 수행하는 것이 좋다.

  • 필요한 만큼의 에러 처리를 수행하자
    에러 처리 로직을 변경할 때 한 곳의 코드만 수정할 수 있도록 모든 에러 처리 코드를 한 곳으로 모으는 것이 유리하다.

  • 범위에 더 많은 변수를 선언할수록 각 변수의 목적을 명확히 하기가 어려워진다.
    따라서, 설정 변수를 하나의 구조체에 모아서 그 목적을 명확히 하는 편이 낫다.

  • 많은 러스트 개발자들이 런타임 비용 때문에 데이터 복제를 이용해 소유권 문제를 해결하는 것을 피하는 경향이 있다.

TDD 테스트 주도 개발 기법 (소프트웨어 개발 기법)

  1. 실패하는 테스트를 작성한 다음 의도한 이유 때문에 실패하는지 확인한다.
  2. 테스트에 성공하기에 충분한 코드를 작성하거나 수정한다.
  3. 추가했거나 수정한 코드를 리팩토링하면서 테스트가 계속 성공하는지 확인한다.
  4. 1~3단계를 계속 반복한다.

웹 서버 구현하기

단일 스레드 웹서버 구현하기

웹서버 구현에 있어 주축이 되는 두 개의 프로토콜은 HTTP, TCP 프로토콜이다.
두 프로토콜 모두 요청-응답 프로토콜이다. 즉, 클라이언트가 요청을 보내면 서버가 그 요청을 듣고 적절한 응답을 제공하는 프로토콜이다. 요청과 응답의 콘텐츠는 프로토콜 명세에 정의되어 있다.
TCP는 한 서버로부터의 정보를 다른 서버로 전송하는 방법을 정의한 저수준 프로토콜이다.
하지만 그 정보가 무엇인지는 정의하지 않는다.
HTTP는 TCP를 기반에 둔 프로토콜이며(HTTP/3 부터는 UDP 프로토콜 이용), 요청과 응답의 콘텐츠를 정의한다.
대부분의 HTTP는 TCP를 이용해 데이터를 전달한다.
HTTP는 텍스트 기반 프로토콜이며, 요청은 다음과 같은 형식을 따른다.

요청

Method Request-URI HTTP-Version CRLF : 요청 줄, 클라이언트가 요청하는 정보를 표현
ex) (GET/POST, 통합 자원 식별자, HTTP버전, CRLF: 요청 줄 마무리 뜻의 단어)
headers CRLF
message-body

응답

HTTP-Version Status_Code Reason-Phrase CRLF : 상태 줄
ex) (HTTP/1.1 200 OK\r\n\r\n)
headers CRLF
message-body

profile
개발하기

0개의 댓글