Chapter 1. Go와의 첫 만남

Go를 이용해 최신 컴퓨팅 환경이 당면한 과제 해결하기 Go가 제공하는 기본 도구들 살펴보기

컴퓨터는 계속해서 발전해왔지만, 프로그래밍 언어는 하드웨어의 발전 속도를 따라잡지 못하고 있다. 가령, 사용하는 CPU 코어 수는 계속 늘어나지만 여전히 하나의 코어를 사용하던 시절의 기법을 바탕으로 프로그래밍 하고 있다.

그러나 프로그래밍 기법 역시 나름대로 혁신을 거듭하고 있다. 더 이상 한 명의 개발자가 전부 개발하지 않고, 다른 지역과 나라에서 서로 쪼개어서 개발한 후 합치기도 한다. 오늘날의 프로그래머들은 오픈 소스 소프트웨어의 힘에 의존하고 있으며, Go는 코드를 쉽게 공유할 수 있는 프로그래밍 언어이다.

1.1 Go 언어로 최신 컴퓨팅 환경이 당면한 과제 해결하기

개발자들은 프로젝트를 수행할 언어를 선택하는 과정에서 개발 속도성능을 두고 항상 갈등한다. 예를 들어, C나 C++ 같은 언어들은 빠른 성능을 보장하지만, Ruby나 Python은 개발 기간이 짧다. Go 언어는 이 둘 사이에 균형을 잘 맞추고 있다.

Go 언어의 장점

  • Go는 잘 설계된 기능과 간결한 문법을 가지고 있다.
  • 개발 언어의 관점에서 반드시 지원해야 할 사항 + 지원하지 말아야 할 사항까지 고려해서 만들어졌다.
  • 몇 가지 키워드로만 구성도니 간경한 문법이 특징이다. 컴파일 속도 또한 매우 빠르다.
  • Go에 내장된 동시성 기능으로 별도의 라이브러리 없이 시스템 자원의 효율적 사용이 가능하다.
  • 객체지향 개발의 오버헤드를 줄이고, 코드의 재사용에만 집중하여 간결하면서 효과적인 타입 시스템을 제공한다.
  • 메모리를 직접 관리할 필요가 없도록 Gargbage Collection도 지원한다.

1.1.1 개발 속도

C나 C++ 언어로 대형 애플리케이션을 개발하는 시간은 너무 많이 소요된다. Go는 똑똑한 컴파일러와 간결한 의존성 해석 알고리즘을 통해 매우 빠른 컴파일러를 제공한다. Java, C 또는 C++ 컴파일러들이 전체 라이브러리의 의존성을 탐생하는 것과는 달리, Go 컴파일러는 직접적으로 참조하는 라이브러리의 의존성만을 해석한다. 그 결과 대부분의 Go 애플리케이션들은 컴파일 시간이 1초도 걸리지 않는다.

동적 언어로 애플리케이션을 작성하는 경우 높은 생산성을 기대할 수 있는데, 그 이유는 코드를 작성한 후 별다른 중간 과정을 거치지 않고도 작성된 코드를 실행할 수 있기 때문이다. 반면, 동적 언어는 정적 언어들이 제공하는 타입 안정성을 제공하지 않으며, 런타임에 잘못된 타입 때문에 발생할 수 있는 버그를 방지하기 어렵다.

Go 언어에서는 개발자가 다른 타입의 값을 전달하면 컴파일러가 이를 자동으로 잡아내서 알려준다.

1.1.2 동시성

프로그래머들에게 가장 어려운 일 중 하나는 하드웨어의 자원을 효과적으로 사용하는 것이다. 최신 하드웨어는 여러 개의 코어를 가지고 있지만 대부분의 프로그래밍 언어들은 이런 추가적인 자원을 쉽게 활용하기 어렵고, 코드양도 많아지고, 에러도 쉽게 발생한다.

반면, Go의 고루틴(goroutine)은 스레드와 유사하지만 더 적은 메모리를 소비하며 더 적은 양의 코드로 구현할 수 있다. 채널(channel)은 내장된 동기화 기능을 이용해 고루틴 간에 형식화된(typed) 메시지를 공유할 수 있는 데이터 구조다. 고루틴들이 필요한 데이터를 먼저 사용하기 위해 경쟁하게 하는 것이 아니라 고루틴 간에 데이터를 서로 전송할 수 있기 때문에 프로그래밍 모델이 더 간편해진다.

고루틴(goroutine)

고루틴은 프로그램의 진입점(entry point) 함수를 비롯하여 다른 고루틴과 동시에 실행되는 함수다. 다른 프로그래밍 언어의 경우 이런 일을 가능하게 하려면 스레드를 사용해야 하지만, Go에서는 여러 개의 고루틴이 하나의 스레드에서 동작한다. 예를 들어, C나 Java로 웹 서버를 작성하면서 여러 개의 웹 요청을 동시에 처리하고자 한다면 스레드를 사용하기 위해 많은 양의 코드를 작성해야 한다. 반면 Go는 고루틴을 이용한 동시성 기능을 자체적으로 지원하는 net/http 라이브러리를 사용한다. 그러면 서버로 유입된(inbound) 각각의 요청들이 자동적으로 각자의 고루틴에서 동작하게 된다. 고루틴은 스레드보다 적은 메모리를 사용하며 Go 런타임이 설정된 논리 프로세서의 개수에 따라 자동적으로 고루틴을 실행하기 위한 스케줄링을 처리한다. 그리고 각각의 논리 프로세서는 하나의 OS 스레드에 연결된다. 이런 기법을 통해 상대적으로 적은 노력으로 훨씬 효율적인 애플리케이션을 작성할 수 있다.

만일, 본연의 목적을 달성하기 위한 코드를 실행하는 동안 다른 코드를 동시에 실행하고자 한다면 고루틴을 사용하는 것이 적절하다.

func log(msg string) {
		... 로그를 기록하는 코드
}

// 애플리케이션 코드에서 오류가 발견된 부분
go log("심각한 오류가 발생했습니다.")

위의 예제처럼 go 키워드를 사용하면 log 함수가 고루틴으로 동작하도록 스케줄링할 수 있다. 그러면 이 고루틴은 다른 고루틴과 동시에 실행될 수 있다. 즉, 로깅이 처리되는 동안 애플리케이션의 나머지 코드를 계속해서 실행할 수 있고, 이로 인해 최종 사용자들은 획기적인 성능의 향상을 피부로 느끼게 될 것이다. 고루틴의 오버헤드는 그다지 크지 않으므로 빈번하게 사용해도 전혀 문제되지 않는다.

채널(channel)

채널은 고루틴 간에 안전한 데이터 전송을 가능하게 하는 데이터 구조다. 채널을 이용하면 공유 메모리 접근을 허용하는 프로그래밍 언어에서 흔히 발생하는 문제들을 손쉽게 피할 수 있다.

동시성 프로그래밍에 있어 가장 어려운 부분은 동시에 실행 중인 프로세스나 스레드 혹은 고루틴에 의해 의도치 않게 데이터가 변경되는 일을 방지하는 것이다. 여러 개의 스레드가 잠금(lock)이나 동기화 처리 없이 공유되는 데이터를 변경하게 되면 그때부터 골치가 아파진다. 다른 언어에서는 전역 변수와 공유 메모리를 사용하는 경우, 같은 변수가 여러 스레드에 의해 동기화 처리 없이 변경된느 것을 방지하기 위해 복잡한 잠금 처리에 익숙해져야 한다.

채널은 동시에 발생하는 수정 요청으로부터 데이터를 안전하게 보호하기 위한 패턴을 제공함으로써 이 문제를 해결하고 있다. 채널을 통해 어느 한 시점에 하나의 고루틴만이 데이터를 수정할 수 있는 패턴을 적용할 수 있게 된다.

1.1.3 Go의 타입 시스템

Go는 계층구조가 없는 유연한 타입 시스템을 제공하기 때문에 리팩토링에 대한 부담을 최소화하면서 코드를 재사용할 수 있다. 즉, 전통적인 객체지향에 비하면 훨씬 간편하게 객체지향 프로그래밍이 가능하다. Go 개발자들은 합성(Composition)이라고 부르는 디자인 패턴과 마찬가지 방법으로, 기능을 재사용하기 위해 임베드(embed)한다. 다른 언어들도 합성패턴을 사용하지만 종종 상속(inheritance)과 너무 강하게 연결되어 결국에는 코드 재사용이 복잡하고 어려워지는 경향이 있다. 반면 Go는 전통적인 상속 기반의 모델에 비해 훨씬 작은 타입을 합성하여 타입을 정의한다.

게다가 Go는 모델의 타입을 모델링하는 것이 아니라 동작을 모델링할 수 있는 독특한 인터페이스를 구현하고 있다. Go에서는 어떤 타입이 인터페이스를 구현하고 있다는 것을 선언할 필요가 없다. 컴파일러는 현재 사용하고 있는 타입의 값이 사용하고자 하는 인터페이스를 만족하는지를 검사할 뿐이다. Go 표준 라이브러리에 정의된 대부분의 인터페이스들은 겨우 몇 가지 기능만을 노출하는 간단한 것들이다.

간결한 타입

Go는 int나 string 같은 내장 타입을 제공하는 것은 물론 사용자가 직접 타입을 정의하는 것도 허용한다. Go에서 사용자가 직접 정의한 타입은 데이터를 저장하기 위한 형식화된 필드를 가진다. Go에서 사용자가 직접 정의한 타입은 데이터를 저장하기 위한 형식화된 필드를 가진다. C에서 사용하는 구조체(struct)와 유사하게 동작한다. 그러나 Go의 타입에는 데이터를 조작하기 위한 메서드를 정의할 수도 있다는 점이 다르다.

작은 동작을 모델링하는 인터페이스

인터페이스(interface)는 타입의 동작을 표현하기 위해 사용한다. 타입의 값이 어떤 인터페이스를 구현한다는 것은 그 값이 일련의 특정한 행동을 수행할 수 있다는 것을 의미한다. 게다가 인터페이스를 구현하고 있다고 선언할 필요조차 없다. 단지 필요한 행위를 구현만하면 된다. 다른 언어들은 이런 기법을 덕 타이핑(duck typing)이라고 한다.

덕 타이핑이란 어떤 생물이 오리처럼 꽥꽥 소리를 낸다면 그 생물을 오리라고 간주할 수 있다는 개념이다.

Go에서는 어떤 타입이 인터페이스에 정의된 메서드를 구현하면 이 타입의 값에 해당 인터페이스 타입의 값을 저장할 수 있다. 그 외에 다른 것은 아무것도 선언할 필요가 없다.

interface User {
		public void login();
		public void logout();
}

Java에서 인터페이스를 구현하려면 User 인터페이스에 정의된 모든 약속들을 만족하는 클래스를 구현한 후, 그 클래스가 User 인터페이스를 구현하고 있음을 명시적으로 선언(implements)해야 한다.

type Reader interface {
		Read(p []byte) (n int, err error)
}

Go의 io.Reader 인터페이스를 구현하는 타입을 작성하려면 바이트의 슬라이스(slice)를 매개변수로 받아들여 정수와 에러를 리턴하는 Read 메서드를 구현하기만 하면 된다.

이런 방식은 전통적인 객체지향 프로그래밍 언어에서 사용하는 인터페이스 시스템과는 근본적으로 다른 것이다. Go의 인터페이스는 훨씬 간단하며 한 가지 동작만을 정의한다. 이를 바탕으로 코드의 재사용과 합성에 큰 이점을 얻을 수 있다. 데이터를 읽어야 하는 모든 타입에 io.Reader 인터페이스를 구현할 수 있고, 이런 타입들은 io.Reader 인터페이스를 통해 데이터를 읽는 동작을 수행하는 그 어떤 Go 함수에도 전달할 수 있다.

Go 네트워킹 라이브러리는 모드 io.Reader 인터페이스를 사용하여 작성되었다. 그렇게 함으로써 각기 다른 네트워크 작업을 수행하기 위해 구현해야 하는 네트워크 기능을 애플리케이션의 기능과 완전히 분리할 수 있기 때문이다. 하나의 인터페이스를 정의함으로써 데이터 소스의 종류와는 무관하게 데이터를 효과적으로 조작할 수 있게 된 것이다.

1.1.4 메모리 관리

Go는 메모리를 자동으로 관리하는 Garbage Collection을 제공한다. 따라서 약간의 오버헤드를 유발하지만 C나 C++처럼 메모리 누수를 방지하기 위한 별도의 노력을 하지 않아도 된다.

1.2 Hello, Go

package main		// Go 프로그램은 패키지 단위로 관리한다. 

import "fmt"		// import 구문을 이용해서 외부 코드를 참조할 수 있다.

func main() {		// C와 마찬가지로 애플리케이션을 실행하면 main 함수가 호출된다.
		fmt.Println("Hello, world!");
}

### 1.2.1 Go Playground

Go Playground에서는 웹 브라우저를 통해 간단하게 Go 코드를 편집하고 실행해볼 수 있다. 코드 공유하기 및 커뮤니티도 있어서 많은 도움을 받거나 줄 수 있다.

1.3. 요약

  • Go는 모던하고 빠르며 강력한 표준 라이브러리를 제공하는 프로그램 언어다.
  • Go는 동시성 기능을 자체적으로 내장한다.
  • Go는 코드 재사용을 위한 빌딩 블록(building block)으로서 인터페이스를 활용한다.