Go Codelab (Unofficial, Inspired by Google)
5. 구조체와 인터페이스

앞에서 잠깐 살펴본 모델 패키지 models에는 클라이언트와 서버에서 사용할 센서들의 공용 메서드를 정의하는 Sensor interface와 센서들의 공통 필드를 갖는 SensorInfo struct를 정의해놓은 sensor.go 파일이 있습니다.

구조체와 인터페이스가 무엇인지 살펴보기 전에 실제 sensor.go의 코드의 일부를 봅시다.

// Sensor is common interface for any sensors
type Sensor interface {
    ...
}

// SensorInfo has common fields for any sensors
type SensorInfo struct {
    ...
}

위 코드에서 보이는 SensorInfo가 센서들의 공통 필드를 정의한 struct이며 Sensor가 센서들의 공용 메서드를 정의하는 interface입니다. 그럼 이 structinterface가 무엇인지 우리 코드를 보며 자세히 살펴봅시다.


구조체 (Struct)

구조체 struct는 여러 필드를 가질 수 있는 일종의 확장 타입입니다. C 언어에서의 struct와 거의 유사합니다.struct는 다음과 같이 구성됩니다.

type StructName struct {
    <embededStruct>
    <varname> <vartype> `<tag>`
    ...
}

StructName은 구조체의 이름을 뜻하며 varname은 필드명, vartype은 필드의 타입, tag는 잠시후 다시 설명할 해당 필드가 인코딩 되었을 때의 키값을 정의하는 태그입니다. 참고로 Go에서는 변수를 선언할 때 var name type처럼 타입을 변수명 뒤에 선언합니다. 필드의 타입은 그 어떤 타입도 가능합니다. embededStruct는 조금 이따 살펴보겠습니다.

우선 실제 구조체를 살펴보기 위해 SensorInfo 코드를 봅시다.

type SensorInfo struct {
	Name    string    `json:"name"`
	Type    string    `json:"type"`
	GenTime time.Time `json:"gen_time"`
}

StructInfo 구조체는 Name, Type, Gentime이라는 필드를 가지며 각각 string, string, time.Time의 타입을 갖습니다. 그렇다면 뒤에 json:"<tag>"는 무엇일까요? 위에서 이미 말했듯이 이는 인코딩 되었을 때의 키값을 정의합니다. 즉, 이 구조체를 JSON 타입으로 인코딩 했을 때 해당 필드의 키값이 tag로 설정된다는 뜻입니다. 위 구조체를 JSON으로 인코딩하기 되면 다음과 같이 인코딩 됩니다.

{
    'name': 'name value',
    'type': 'type value',
    'gen_time': 'gentime value'
}

이 태그는 필수는 아니며 필요에 따라 선택적으로 설정할 수 있습니다.

이제 아까 언급한 embededStruct를 다시 살펴봅시다. sensor.go에 있는 실제 코드를 보겠습니다.

// GyroSensor produces x-y-z axes angle velocity values
type GyroSensor struct {
	SensorInfo
	AngleVelocityX float64 `json:"x_axis_angle_velocity"`
	AngleVelocityY float64 `json:"y_axis_angle_velocity"`
	AngleVelocityZ float64 `json:"z_axis_angle_velocity"`
}

sensor.go에 정의된 센서 구조체중 하나입니다. 여기에 선언되어 있는 SensorInfoEmbedded struct이며 이는 Embedding을 뜻합니다. 이는 다른 구조체의 정보를 그대로 가져와 사용하겠다는 뜻이며, 따라서 해당 구조체는 임베딩한 구조체의 모든 필드를 그대로 가질 수 있게됩니다. 즉, GyroSensorSensorInfo에 선언되어있는 모든 필드를 갖게됩니다.

Embedding을 사용하게되면 코드 중복을 피할 수 있으며, 코드의 재사용성이 증가하게 됩니다. 얼핏보면 Java, C++, Python에서의 상속과 비슷해 보이지만 다릅니다. 상속은 하위 클래스가 상위 클래스의 모든걸 가져오는 구조인 반면 Embedding은 필요한 구조체의 필드들을 가져와 재사용하겠다는 의미를 가집니다. 즉, 일종의 구조체 모듈인셈입니다. Go 프로그래밍을 하게되면 앞으로 이러한 Embedding을 많이 볼 수 있을 것입니다.

구조체는 다음과 같이 사용할 수 있으며, 앞으로도 계속 보게될 것입니다.

gyroSensor := GyroSensor{
                // Embedded 구조체 필드의 경우 다음과 같이 초기화합니다.
		SensorInfo: SensorInfo{
			Name:    "GyroSensor",
			Type:    "VelocitySensor",
			GenTime: time.Now(),
		},
		// 자체 필드는 다음과 같이 초기화합니다.
		AngleVelocityX: faker.GenerateAngleVelocity(epsilon),
		AngleVelocityY: faker.GenerateAngleVelocity(epsilon),
		AngleVelocityZ: faker.GenerateAngleVelocity(epsilon),
	}


인터페이스 (Interface)

인터페이스 interface는 일종의 메서드 시그니쳐의 모음입니다. Java에서의 인터페이스와 유사하며, 어떤 타입이 특정 인터페이스의 메서드들을 모두 구현하고 있으면 그 타입은 해당 인터페이스 타입을 갖게됩니다. interface는 다음과 같이 구성됩니다.

type InterfaceName interface {
    <method signature>
    ...
}

InterfaceName은 인터페이스의 이름을 뜻하며, method signature는 메서드의 시그니쳐입니다.

실제 인터페이스를 살펴보기 위해 Sensor 코드를 봅시다.

type Sensor interface {
	SendingOutputString() string
	ReceivingOutputString() string
	GenerateSensorData(epsilon float64) Sensor
}

Sensor라는 인터페이스에 SendingOutputString() string, ReceivingOutputString string, GenerateSensorData(epsilon float64) Sensor라는 메서드 시그니쳐들이 있습니다. 만약 어떤 타입이 이 시그니쳐들을 갖는 메서드들을 모두 구현한다면 그 타입은 Sensor interface 타입을 갖게되며, Sensor를 인자로 받는 그 어떤 함수의 인자로도 들어갈 수 있습니다.

다음은 실제 GyroSensor 구조체에 구현된 메서드들입니다.

func (s GyroSensor) SendingOutputString() string {
    ...
}

func (s GyroSensor) ReceivingOutputString() string {
    ...
}

func (s GyroSensor) GenerateSensorData(epsilon float64) Sensor {
    ...
}

GyroSensorSensor 인터페이스의 모든 메서드들을 구현하고 있으므로 Sensor interface 타입이 됩니다. 즉, Sensor 타입으로 사용할 수 있게 됩니다. 참고로 Go에서는 특정 타입의 메서드를 다음과 같이 선언할 수 있습니다.

// StructType 타입의 st 변수를 '리시버 (receiver)'라고 하며, 리시버를 특정 구조체 타입으로 선언하면 그 타입의 메서드가 됩니다.  
func (st StructType) functionName(args) (returnTypes) {
    // procssing with 'st'
}

결론적으로, 그 어떤 구조체라도 Sensor 인터페이스의 세가지 메서드 시그니쳐들만 구현하면 Sensor타입으로 사용할 수 있게됩니다. Go에서 인터페이스는 타입 호환성 및 확장성 측면에서 굉장히 중요한 개념으로 Go 프로그래밍을 하게되면 인터페이스를 자주 접할 것입니다.


도전!

structinterface를 사용해 다른 센서들처럼 호환 가능한 여러분만의 센서를 만들어보세요! 새로운 센서의 GenerateSensorData 함수를 위한 여러분만의 랜덤 데이터 생성 함수를 faker/range.go에도 추가해보세요!