구조체 정의와 인스턴스화

구조체 정의와 인스턴스화 (Defining and Instantiating Structs)

구조체는 "튜플 타입" 절에서 다뤘던 튜플과 비슷해요. 둘 다 여러 관련 값을 담는다는 점이 공통점이죠. 튜플처럼 구조체의 조각들도 서로 다른 타입일 수 있어요. 하지만 튜플과 달리 구조체에서는 각 데이터 조각에 이름을 붙이기 때문에 값이 무엇을 뜻하는지 분명해요. 이 이름 덕분에 구조체는 튜플보다 더 유연해요. 인스턴스의 값을 지정하거나 접근할 때 데이터의 순서에 의존하지 않아도 되거든요.

출처: The Rust Book

구조체를 정의하려면 키워드 struct를 쓰고 구조체 전체의 이름을 지어요. 구조체의 이름은 함께 묶인 데이터 조각들의 의미를 잘 드러내야 해요. 그런 다음 중괄호 안에서 데이터 조각들의 이름과 타입을 정의하는데, 이 조각들을 **필드(field)**라고 불러요. 예를 들어 Listing 5-1은 사용자 계정에 대한 정보를 저장하는 구조체를 보여줘요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

Listing 5-1: User 구조체 정의

구조체를 정의한 뒤에 사용하려면, 각 필드에 구체적인 값을 지정해서 그 구조체의 인스턴스를 만들어야 해요. 인스턴스를 만들려면 구조체의 이름을 쓰고 중괄호 안에 *key: value* 쌍을 넣으면 돼요. 여기서 키는 필드의 이름이고, 값은 그 필드에 저장하고 싶은 데이터예요. 필드의 순서는 구조체에서 선언한 순서와 같을 필요는 없어요. 다시 말해 구조체 정의는 그 타입에 대한 일반적인 템플릿 같은 것이고, 인스턴스는 그 템플릿에 특정 데이터를 채워서 그 타입의 값을 만들어내는 거죠. 예를 들어 Listing 5-2처럼 특정 사용자를 선언할 수 있어요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };
}

Listing 5-2: User 구조체의 인스턴스 만들기

구조체에서 특정 값을 가져오려면 점 표기법(dot notation)을 사용해요. 예를 들어 이 사용자의 이메일 주소에 접근하려면 user1.email을 쓰면 돼요. 인스턴스가 가변(mutable)이라면 점 표기법으로 특정 필드에 값을 할당해서 그 값을 바꿀 수 있어요. Listing 5-3은 가변 User 인스턴스의 email 필드 값을 바꾸는 방법을 보여줘요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };

    user1.email = String::from("[email protected]");
}

Listing 5-3: User 인스턴스의 email 필드 값 바꾸기

인스턴스 전체가 가변이어야 한다는 점에 주의하세요. Rust는 특정 필드만 가변으로 표시하는 것을 허용하지 않아요. 다른 표현식과 마찬가지로, 함수 본문의 마지막 표현식으로 구조체의 새 인스턴스를 구성하면 그 새 인스턴스를 암시적으로 반환할 수 있어요.

Listing 5-4는 주어진 이메일과 사용자 이름으로 User 인스턴스를 반환하는 build_user 함수를 보여줘요. active 필드는 값 true를, sign_in_count는 값 1을 받아요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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

fn main() {
    let user1 = build_user(
        String::from("[email protected]"),
        String::from("someusername123"),
    );
}

Listing 5-4: 이메일과 사용자 이름을 받아 User 인스턴스를 반환하는 build_user 함수

함수 매개변수의 이름을 구조체 필드와 같은 이름으로 짓는 건 합리적이지만, emailusername 필드 이름과 변수를 반복해서 적어야 한다는 게 조금 번거로워요. 구조체에 필드가 더 많다면 각 이름을 반복하는 일은 더욱 귀찮아지겠죠. 다행히 편리한 축약 문법이 있어요!

필드 초기화 축약(field init shorthand) 사용하기

Listing 5-4에서 매개변수 이름과 구조체 필드 이름이 정확히 같으므로, 필드 초기화 축약 문법을 사용해 build_user를 다시 작성할 수 있어요. 동작은 완전히 같되 usernameemail의 반복이 없어져요. Listing 5-5를 보세요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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

fn main() {
    let user1 = build_user(
        String::from("[email protected]"),
        String::from("someusername123"),
    );
}

Listing 5-5: usernameemail 매개변수가 구조체 필드와 이름이 같아서 필드 초기화 축약을 사용한 build_user 함수

여기서 우리는 email이라는 필드를 가진 User 구조체의 새 인스턴스를 만드는 중이에요. email 필드의 값을 build_user 함수의 email 매개변수 값으로 설정하고 싶어요. email 필드와 email 매개변수는 이름이 같으므로 email: email 대신 그냥 email만 쓰면 돼요.

구조체 업데이트 문법으로 인스턴스 만들기

같은 타입의 다른 인스턴스가 가진 값들을 대부분 그대로 쓰되 일부만 바꿔서 새 인스턴스를 만들어야 할 때가 자주 있어요. 그럴 때 구조체 업데이트 문법(struct update syntax)을 사용할 수 있어요.

먼저 Listing 5-6에서 업데이트 문법 없이 일반적인 방식으로 user2에 새 User 인스턴스를 만드는 방법을 보여드릴게요. email에는 새 값을 넣지만, 그 외에는 Listing 5-2에서 만든 user1과 같은 값을 사용해요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("[email protected]"),
        sign_in_count: user1.sign_in_count,
    };
}

Listing 5-6: user1의 값 하나를 제외하고 모두 사용해 새 User 인스턴스 만들기

구조체 업데이트 문법을 사용하면 Listing 5-7처럼 더 적은 코드로 같은 효과를 낼 수 있어요. .. 문법은 명시적으로 설정하지 않은 나머지 필드들이 주어진 인스턴스의 필드와 같은 값을 가져야 한다는 것을 지정해요.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("[email protected]"),
        ..user1
    };
}

Listing 5-7: 구조체 업데이트 문법으로 User 인스턴스의 email 값은 새로 설정하고 나머지 값은 user1에서 가져오기

Listing 5-7의 코드도 user2email 값은 다르고 username, active, sign_in_count 필드는 user1에서 가져온 인스턴스를 만들어요. ..user1은 반드시 맨 마지막에 와야 해요. 남은 필드들의 값을 user1의 대응하는 필드에서 가져오겠다는 뜻이니까요. 다만 원하는 필드들을 어떤 순서로든 몇 개든 지정할 수 있고, 구조체 정의의 필드 순서와는 상관없어요.

구조체 업데이트 문법이 =를 할당처럼 사용한다는 점에 주목하세요. "변수와 데이터의 상호작용: 이동(Move)" 절에서 봤듯이 데이터를 이동시키기 때문이에요. 이 예시에서는 user2를 만든 뒤에 user1을 더 이상 사용할 수 없어요. user1username 필드에 있던 Stringuser2로 이동됐기 때문이죠. 만약 emailusername 양쪽에 새 String 값을 넣어서 user1activesign_in_count 값만 사용했다면, user2를 만든 뒤에도 user1은 여전히 유효했을 거예요. activesign_in_count는 둘 다 Copy 트레이트를 구현하는 타입이라, "스택 전용 데이터: Copy" 절에서 다룬 동작이 적용되거든요. 이 예시에서 user1.email을 여전히 사용할 수 있는 것도, 그 값이 user1에서 이동되지 않았기 때문이에요.

튜플 구조체로 여러 타입 만들기

Rust는 튜플과 비슷하게 생긴 구조체도 지원하는데, 이를 *튜플 구조체(tuple struct)*라고 불러요. 튜플 구조체는 구조체 이름이 주는 추가적인 의미는 있지만, 필드에 이름이 연결되어 있지는 않아요. 필드의 타입만 있을 뿐이죠. 튜플 전체에 이름을 붙여서 다른 튜플과 다른 타입으로 만들고 싶을 때, 그리고 일반 구조체처럼 각 필드에 이름을 붙이는 게 장황하거나 중복적일 때 튜플 구조체가 유용해요.

튜플 구조체를 정의하려면 struct 키워드와 구조체 이름을 쓰고 뒤에 튜플의 타입들을 나열하면 돼요. 예를 들어 여기서는 ColorPoint라는 이름의 튜플 구조체 두 개를 정의하고 사용해요.

Filename: src/main.rs

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

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

blackorigin 값은 서로 다른 튜플 구조체의 인스턴스이므로 서로 다른 타입이라는 점에 유의하세요. 여러분이 정의하는 각 구조체는 필드가 같은 타입일지라도 각자 고유한 타입이에요. 예를 들어 Color 타입의 매개변수를 받는 함수는, Point 타입이 세 개의 i32 값으로 이루어졌다 하더라도 Point를 인자로 받을 수 없어요. 그 외에는 튜플 구조체 인스턴스가 튜플과 비슷해서, 각 조각으로 분해(destructure)할 수 있고 인덱스와 함께 .을 사용해 개별 값에 접근할 수 있어요. 튜플과 달리 튜플 구조체는 분해할 때 구조체의 타입도 함께 이름을 적어야 해요. 예를 들어 origin 지점의 값을 x, y, z라는 변수로 분해하려면 let Point(x, y, z) = origin;이라고 작성해요.

유닛-라이크 구조체(Unit-Like Struct) 정의하기

필드가 아예 없는 구조체를 정의할 수도 있어요! 이런 구조체를 *유닛-라이크 구조체(unit-like struct)*라고 불러요. "튜플 타입" 절에서 언급했던 유닛 타입 ()과 비슷하게 동작하기 때문이죠. 유닛-라이크 구조체는 어떤 타입에 트레이트를 구현해야 하는데 그 타입 자체에 저장하고 싶은 데이터가 없을 때 유용해요. 트레이트에 대한 자세한 내용은 10장에서 다룰게요. 여기서는 AlwaysEqual이라는 이름의 유닛 구조체를 선언하고 인스턴스화하는 예시를 볼게요.

Filename: src/main.rs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

AlwaysEqual을 정의하려면 struct 키워드와 원하는 이름을 쓰고 세미콜론을 붙이면 돼요. 중괄호나 괄호는 필요 없어요! 그러면 subject 변수에 AlwaysEqual의 인스턴스를 비슷한 방식으로 만들 수 있어요. 정의한 이름을 중괄호나 괄호 없이 그대로 쓰면 돼요. 나중에 이 타입이 테스트 목적의 알려진 결과를 위해, 모든 AlwaysEqual 인스턴스가 어떤 다른 타입의 모든 인스턴스와 항상 같아지는 동작을 구현한다고 상상해 보세요. 그 동작을 구현하는 데 데이터는 전혀 필요 없을 거예요! 10장에서 트레이트를 정의하고 유닛-라이크 구조체를 포함한 어떤 타입에든 구현하는 방법을 보게 될 거예요.

구조체 데이터의 소유권

Listing 5-1의 User 구조체 정의에서 우리는 &str 문자열 슬라이스 타입 대신 소유된 String 타입을 사용했어요. 이것은 의도적인 선택이에요. 각 인스턴스가 자신의 모든 데이터를 소유하고, 그 데이터가 구조체 전체가 유효한 동안 내내 유효하기를 원하니까요.

구조체가 다른 무언가가 소유한 데이터에 대한 참조를 저장하는 것도 가능해요. 하지만 그러려면 10장에서 다룰 Rust 기능인 *수명(lifetime)*을 사용해야 해요. 수명은 구조체가 참조하는 데이터가 구조체만큼 오래 유효하다는 것을 보장해 줘요. 수명을 지정하지 않고 구조체에 참조를 저장하려고 하면, src/main.rs의 다음 코드처럼 동작하지 않아요.

Filename: src/main.rs

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "[email protected]",
        sign_in_count: 1,
    };
}

컴파일러는 수명 지정자가 필요하다고 불평할 거예요.

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

10장에서는 구조체에 참조를 저장할 수 있도록 이런 에러를 고치는 방법을 다룰 거예요. 하지만 지금은 &str 같은 참조 대신 String 같은 소유된 타입으로 이런 에러를 해결할 거예요.

더 알아보기 (Learn more)