[묘공단] Tucker의 Go 언어 프로그래밍 - 구조체

힐링코더·2024년 2월 4일
0

묘공단-Go편

목록 보기
11/20
post-thumbnail

Tucker의 Go 언어 프로그래밍 구조체편에서 목차는
선언 및 기본 사용
구조체 변수 초기화
구조체를 포함하는 구조체
구조체 크기
프로그래밍에서 구조체의 역할
과 같다.
그런데 나는 조금 형식을 바꾸고 싶다.

1. 구조체란?

구조체의 원래 이름은 structure다.
그걸 그냥 struct이라 줄여 부르고 있다.
마치, 롯리(롯데리아), 맥날(맥도날드)처럼...

이는 서로 다른 타입의 데이터를 하나의 논리적 단위로 묶는 방법을 제공하는 복합 데이터 타입이다.
구조체는 여러 필드를 가지며, 각 필드는 구조체 내에서 데이터의 특정 부분을 나타낸다.
고랭에서 구조체는 강타입(strongly typed) 언어의 특성을 따르며, 사용자 정의 타입을 생성할 때 중요한 역할을 한다.

type Person struct {
    Name string
    Age  int
}

<잠깐!>
프로그래밍 언어는 타입 시스템의 엄격함에 따라 강타입 언어와 약타입(weekly typed) 언어로 구분할 수 있다.

강타입 언어: 변수의 타입이 명확히 정의된다. 한 번 정의되면 변경할 수 없다. 타입 간의 변환(캐스팅)이 자동으로 이루어지지 않거나, 엄격한 규칙을 따른다. 예를 들어, 정수형 변수에 문자열을 할당하려면 명시적인 타입 변환을 수행해야 한다.
대표 언어: Java, Go, C#

약타입 언어: 변수의 타입이 덜 엄격하다. 런타임에 변수의 타입이 변경될 수 있다. 암시적 타입 변환이 자주 일어나서 때로는 예상치 못 한 방식으로 작동할 수 있다.
대표 언어: Javascript, PHP


언어에 구조체가 있다고 다 같은 구조체가 아니다.
기본적인 개념은 유사하지만, 사용 방식과 기능에서 차이가 있다.

C/C++: 데이터 필드만 포함할 수 있다. 메서드는 포함 못 한다. C++에서는 클래스도 사용한다. C++에서 구조체는 기본적으로 public이고 클래스는 private이다(기본접근제어자 얘기다).

Python/JavaScript: 일반적으로 클래스 또는 객체를 사용해서 복합 데이터 타입을 표현한다. 딕셔너리나 객체 리터럴이 유사한 용도로 사용될 수 있다.

Go: 고랭에서 구조체는 데이터 필드와 함께 메서드도 가질 수 있다. 인터페이스와 함께 사용하면, 전통적인 객체지향프로그래밍 언어의 클래스와 유사한 기능도 구현할 수 있다. 고랭은 클래스와 상속을 지원하지 않지만, 구조체를 통해 메서드를 연결하고, 인터페이스를 통해 다형성을 제공한다.


여기까지 왔다면 왜 구조체를 써야 하나, 클래스만으로는 안 되나 생각할 수 있다.

고랭은 전통적인 클래스 기반의 객체지향프로그래밍 모델을 사용하지 않는다. 대신, 구조체와 인터페이스를 사용해 데이터와 그 데이터에 대한 동작을 캡슐화한다.

이렇게 하는 이유는...

  1. 데이터 캡슐화: 구조체는 관련 데이터를 논리적으로 그룹화하여 캡슐화한다.
  2. 타입 안정성: 구조체를 사용하면 명확한 타입 시스템 내에서 복잡한 데이터 구조를 안전하게 다룰 수 있다.
  3. 메서드 연결: 고랭의 구조체에서는 메서드를 담을 수 있다. 이는 데이터와 관련된 동작을 가능케 한다. 이 정도만 되어도 클래스 기반 언어의 클래스와 메서드 관계와 유사하다.
  4. 인터페이스와의 상호작용: 다형성과 유연성을 제공한다.

나는 여기까지 블로깅하면서 더는 개념만 블로깅하는 걸로는 부족하다, 실제 고 언어로 코딩을 하면서 언어의 강력함을 느껴봐야겠다라고 생각했다.

고랭에서는 클래스와 상속 대신 구조체, 인터페이스, 컴포지션(composition)을 사용해 코드를 구성한다고 한다.
그게 어떤 걸까? 이제 직접 코딩해야 되는 때가 온 것이다.


구조체: 필드의 모음. 서로 관련된 데이터를 그룹화해서 사용자 정의 타입을 만든 것. 각 필드는 이름과 타입을 가진다.
컴포지션: 상속되신 사용되는 개념. 한 구조체가 다른 구조체를 포함하는 방식인데 "HAS-A" 관계를 나타낸다.
인터페이스: 메서드 시그니처의 집합이다. 덕 타이핑을 통해 구현체가 인터페이스가 요구하는 메서드를 모두 구현하게 한다. 이렇게 하면 명시적인 구현 선언 없이도 인터페이스의 규약을 만족시킬 수 있다.


고랭은 왜 이런 방식으로 설계됐는가?

명확성: 구조체와 인터페이스를 사용하면 코드의 역할과 의도가 명확해진다.
간결성: 고랭은 "작은 인터페이스, 큰 구현"의 원칙을 따른다. 인터페이스를 작고 단순하게 구성해 필요한 기능만을 명시적으로 표현함으로써, 코드를 간결하게 유지한다.
확장성: 인터페이스와 컴포지션을 사용하면 기존 코드를 수정하지 않고도 새로운 기능을 추가하거나 확장할 수 있다. 이는 대규모 시스템의 확장성과 유연성에 큰 장점을 제공한다.
유지보수성: 명확하고 간결한 코드는 이해하기 쉬워 유지보수하기 좋다. 컴포지션을 통한 코드 재사용은 중복을 줄이고 변경에 대한 영향을 최소화한다.


그런데 나는

데이터 캡슐화, 타입 안정성, 메서드 연결, 인터페이스와의 상호작용은 다 일반적인 클래스에서도 가능한 거 아닌지

라는 의문이 들었는데 이에 대한 뾰족한 답변은 찾질 못 했다.
이런 부분은 자기가 코딩해 봐야 아는 것 같다.
다시 본론으로 돌아가자.


구조체 선언 방법은 위에서 소개한 바 있다.

type Person struct {
    Name string
    Age  int
}

2. 구조체 사용 방법은

var p Person
p.Name = "John Doe"
p.Age = 30
fmt.Println(p.Name, p.Age) // 출력: John Doe 30

구조체 타입의 변수를 선언하고
변수.필드 식으로 필드에 접근하여 값을 읽거나 쓸 수 있다.


3. 구조체 변수 초기화 방법은

다양하다.
가장 일반적인 건

p := Person{Name: "Alice", Age: 25}

이런 형식이고

모든 필드를 순서대로 초기화한다면 필드 이름을 생략할 수도 있다.

p := Person{"Bob", 28}

하지만 이런 방식은 필드 순서에 의존적이므로 권장되지 않는다.
딱 봐도 뭐가 뭔지 모르겠지 않는가.


4. 구조체를 포함하는 구조체

위에서 그렇게 컴포지션, 컴포지션 얘기했는데 드디어 나왔다.

고랭은 다른 OOP 언어에서의 상속과 유사한 효과를 얻기 위해 '구성(composition)'을 사용한다.

type ContactInfo struct {
    Email   string
    ZipCode int
}

type Person struct {
    Name    string
    Age     int
    Contact ContactInfo
}

var p Person
p.Name = "Charlie"
p.Age = 35
p.Contact.Email = "charlie@example.com"
p.Contact.ZipCode = 12345

읽어 보면 '이거구나!' 하면서 바로 이해가 될 것이다.


5. 구조체 크기

(매우 당연한 소리 같지만)
구조체의 크기는 그 안에 포함된 모든 필드의 크기의 합이다.

'unsafe' 패키지의 'Sizeof' 함수를 사용해 구조체의 크기를 바이트 단위로 알아낼 수 있다.

'unsafe' 패키지는 이름 그대로 프로그램의 안정성을 보장하지 않으므로 일반적인 애플리케이션 개발에서는 사용을 지양하는 게 좋다고 한다.
구조체 크기에 대한 정보는 주로 저수준 프로그래밍이나 성능 최적화가 필요한 경우에 사용된다고도 한다.


일반 Tucker의 Go 언어 프로그래밍 - 구조체편은 다 정리했는데
내가 여전히 이해가 덜 된 부분이 있어서
그 부분을 아래에 남긴다.

  1. 왜 굳이 고랭에서 상속을 버리고 구성을 택했는가?

상속과 구성은 OOP에서 코드 재사용성을 달성하는 두 가지 주요 방법이다.

상속은 한 클래스가 다른 클래스의 속성과 메서드를 확장(상속)할 수 있게 하는 메커니즘이다. 이를 "IS-A" 관계를 나타낸다. 예를 들어, "개는 동물이다"라는 관계는 상속을 통해 모델링할 수 있다. 상속의 단점은 높은 결합도와 때로는 불필요한 기능의 상속으로 인해 복잡성이 증가한다는 것이다.

구성은 하나의 객체가 다른 객체를 포함하거나 참조하는 방식이다. 이는 "HAS-A" 관계를 나타낸다. 예를 들어, "자동차는 엔진을 가지고 있다"는 식이다. 구성은 더 유연하고 낮은 결합도를 제공한다. 코드의 재사용성과 유지보수성도 향상시킨다.

고랭은 구성과 인터페이스를 통한 디자인을 채택하여, 상속의 복잡성과 제한사항을 피하고 더 단순하고 유연한 코드 구조를 장려한다. 이런 접근 방식은 코드를 더 모듈화하고 결합도를 낮추며, 특정 구현이 아닌 행위에 초점을 맞춘다.

  1. 왜 굳이 'unsafe' 패키지가 존재하는가?

unsafe 패키지는 고랭의 타입 안정성을 우회하여 포인터 연산과 같은 저수준 프로그래밍 작업을 가능케 한다(이 패키지는 이름에서 알 수 있듯이 사용에 주의가 필요하고 일반적인 애플리케이션 개발에서는 사용을 지양해야 한다.).

이 패키지는
1. 성능 최적화: 특정 경우, 저수준 메모리 접근을 통해 성능을 크게 향상시킬 수 있다.
2. 시스템 프로그래밍: 운영체제와의 직접적인 상호작용이나 하드웨어 수준의 프로그래밍이 필요한 경우 사용할 수 있다.
3. 기존 C 라이브러리와의 인터페이스: C로 작성된 라이브러리와의 상호운용성을 위해 저수준 메모리 접근이 필요할 수 있다.
를 위해 존재하는(개발된) 것 같다.

  1. 굳이 구조체 크기 알아내서 뭐 하나?

메모리 사용 최적화: 대규모 시스템이나 메모리가 제한적인 환경(예: 임베디드 시스템)에서는 메모리 사용량을 최적화하는 것이 중요하다. 구조체의 크기를 알아두면 메모리 사용량을 예측하고 관리하는 데 도움이 된다.

성능 최적화: 메모리 레이아웃을 이해하고 구조체를 효율적으로 구성함으로써 캐시 효율성을 높이고 성능을 개선할 수 있다. 캐시 미스를 줄이기 위해 자주 접근되는 데이터를 메모리상에서 서로 가까이 위치시키는 것이 중요할 수 있으며, 이를 위해서는 구조체의 크기와 메모리 레이아웃을 이해해야 한다.

상호 운용성: C와 같은 다른 언어로 작성된 코드와 상호 운용할 때, 데이터 구조의 메모리 레이아웃이 중요해질 수 있다. 특히 외부 라이브러리를 사용하거나 시스템 호출을 수행할 때, 구조체의 메모리 레이아웃을 정확히 알고 있어야 호환성을 보장할 수 있다.

디버깅 및 최적화: 개발 과정에서 메모리 문제를 디버깅하거나 시스템의 성능을 최적화하는 과정에서 구조체의 크기와 메모리 사용 패턴을 분석할 필요가 있을 수 있다. 이때 구조체의 크기를 알아내는 것이 유용하다.

구조체의 크기를 알아내는 것은 고급 프로그래밍 기법에 해당하며, 일반적인 애플리케이션 개발에서는 자주 사용되지 않는다. unsafe 패키지를 사용할 때는 프로그램의 안정성과 유지 보수성을 저해할 수 있으므로, 꼭 필요한 경우에만 신중하게 사용해야 한다. Go의 철학은 간결성과 안전성에 중점을 두고 있으므로, 일반적인 경우에는 Go의 안전한 기능과 패턴을 사용하는 것이 권장된다.

라고 한다...
고랭은 알면 알수록 내가 생각한 것 이상의 언어같다.

profile
여기는 일상 블로그, 기술 블로그는 https://yourhealingcoder.tistory.com/

0개의 댓글