Go 언어 웹 프로그래밍 철저 입문

Page 1


PERFECT INTRODUCTION TO GO WEB PROGRAMMING

장재휴 지음

고언어_01.indd 1

2016-03-21 오전 10:48:25


목 차

1장 Go 시작하기

013

1.1  Go는 어떤 언어인가?  014 1.2  Go 설치하기  028 1.2.1  윈도에 설치하기  029 1.2.2  OS X에 설치하기  036 1.2.3  리눅스와 FreeBSD에 설치하기  042 1.2.4  설치 확인  044 1.2.5  OS X에 설치된 이전 버전 업그레이드  046

1.3  Go 개발 환경  047 1.3.1  작업 공간 구성  047 1.3.2  에디터  049

1.4  첫 번째 Go 프로그램  049 1.4.1  코드 실행  050 1.4.2  코드 분석  052

1.5  Go 참고 문서  053

2장 기본 문법

059

2.1  Go 문법의 특징  060 2.1.1  간결함과 유연함  060 2.1.2  정적 타입 언어, 동적 프로그래밍  062 2.1.3  모호한 요소 제거  063 2.1.4  세미콜론 생략 가능  064 2.1.5  주석  065 2.1.6  gofmt로 코드 서식 지정  065

2.2  변수와 상수  068 2.2.1  변수 선언  068 2.2.2  변수 이름  070 2.2.3  상수  072 2.2.4  열거형  073

고언어_01.indd 5

2016-03-21 오전 10:48:25


2.3  프로그램의 기본 흐름 제어  076 2.3.1  if  076 2.3.2  switch  079 2.3.3  for  082

2.4  함수  087 2.4.1  함수 정의  087 2.4.2  매개변수 전달 방식  091 2.4.3  defer  092 2.4.4  내장 함수  096 2.4.5  클로저  096 2.4.6  함수를 매개변수로 전달하기  098

2.5  패키지  099 2.5.1  패키지 종류  101 2.5.2  접근 제어  103 2.5.3  별칭  104 2.5.4  init() 함수  105

3장 데이터 타입

109

3.1  불  110 3.2  숫자  111 3.2.1  정수  111 3.2.2  실수(부동소수점)  113 3.2.3  복소수  114 3.2.4  숫자 연산  116

3.3  문자열  119 3.3.1  문자열과 문자  120 3.3.2  문자열 연산  124

3.4  배열과 슬라이스  128 3.4.1  생성과 초기화  129 3.4.2  내부 요소에 순차적으로 접근  131 3.4.3  부분 슬라이스 추출  132 3.4.4  슬라이스 변경  133

고언어_01.indd 6

2016-03-21 오전 10:48:25


3.5  맵  136 3.5.1  생성과 초기화  136 3.5.2  값 찾기  138 3.5.3  요소 추가, 수정, 삭제  139

3.6  포인터와 참조 타입  140 3.6.1  포인터 생성과 초기화  140 3.6.2  값 전달  143

4장 객체 지향 프로그래밍

145

4.1  객체 표현 방식  146 4.2  사용자 정의 타입  147 4.2.1  사용자 정의 타입의 종류  148 4.2.2  메서드  151

4.3  구조체  155 4.3.1  생성과 초기화  156 4.3.2  내부 필드 접근  158 4.3.3  구조체 임베딩  160 4.3.4  정보 은닉  164

4.4  인터페이스  167 4.4.1  인터페이스 정의  168 4.4.2  다형성  171 4.4.3  인터페이스 임베딩  178 4.4.4  타입 변환  180

5장 병행 처리

183

5.1  고루틴  184 5.2  채널  186 5.2.1  채널 방향  189 5.2.2  버퍼드 채널  190

고언어_01.indd 7

2016-03-21 오전 10:48:25


5.2.3  close & range  191 5.2.4  select  191

5.3  저수준 제어  193 5.3.1  sync.Mutex  194 5.3.2  sync.RWMutex  197 5.3.3  sync.Once  197 5.3.4  sync.WaitGroup  199 5.3.5  원자성을 보장하는 연산  201

5.4  활용  203 5.4.1  타임아웃  203 5.4.2  공유 메모리  205 5.4.3  파이프라인  211 5.4.4  맵리듀스  226

6장 에러 처리

233

6.1  에러 타입  234 6.2  에러 생성  235 6.2.1  errors.New() 사용  235 6.2.2  fmt 패키지 사용  237 6.2.3  사용자 정의 에러 타입  238

6.3  panic & recover  240 6.3.1  런타임 에러와 패닉  240 6.3.2  recover  243

6.4  에러 처리  245 6.4.1  에러 확인 함수 사용  246 6.4.2  클로저로 에러 처리  247

고언어_01.indd 8

2016-03-21 오전 10:48:25


7장 패키지

251

7.1  커스텀 패키지  252 7.1.1  패키지 만들기  252 7.1.2  별칭  254 7.1.3  운영체제에 종속적인 코드 처리  256 7.1.4  문서화  258

7.2  서드 파티 패키지  259 7.3  Go의 기본 라이브러리  260 7.3.1  문자열 다루기  260 7.3.2  컬렉션  262 7.3.3  파일, 디렉터리, 운영체제 환경 다루기  265 7.3.4  숫자 연산  267 7.3.5  네트워크  268 7.3.6  리플렉션  268 7.3.7  테스트  272 7.3.8  기타  276

8장 나만의 웹 프레임워크 만들기

279

8.1  나만의 웹 프레임워크 만들기  280 8.2  첫 번째 웹 애플리케이션  284 8.3  라우터  286 8.4  컨텍스트  293 8.5  미들웨어  298 8.6  추상화  306 8.7  렌더러  313 8.8  커스텀 미들웨어   318

고언어_01.indd 9

2016-03-21 오전 10:48:25


9장 다양한 패키지를 조합하여 마이크로 프레임워크 구성하기

323

9.1  채팅 애플리케이션 만들기  324 9.2  웹 서버 구동하기  327 9.3  인증 처리하기  331 9.3.1  세션  332 9.3.2  로그인  335 9.3.3  인증  339

9.4  채팅방과 메시지 처리하기  340 9.4.1  몽고DB 환경 구성  341 9.4.2  채팅방 관리 기능 구현  343 9.4.3  메시지 조회 기능 구현  345

9.5  HTML과 자바스크립트로 클라이언트 화면 만들기  347 9.6  웹소켓 기능 구현하기  353

10장 Revel 프레임워크로 블로그 만들기

363

10.1  Revel 프로젝트 만들기  364 10.1.1  Revel 설치하기  364 10.1.2  블로그 애플리케이션 만들기  364 10.1.3  데이터베이스 설정  365

10.2  Hello World!  365 10.2.1  웹 서버 시작하기  365 10.2.2  Hello Revel!  366

10.3  포스트 기능 만들기  368 10.3.1  포스트 모델 만들기  368 10.3.2  데이터베이스 초기화  369 10.3.3  포스트 컨트롤러 만들기  370 10.3.4  포스트 목록 보기  370 10.3.5  새 포스트 만들기  374 10.3.6  각 포스트 보여주기  379

고언어_01.indd 10

2016-03-21 오전 10:48:26


10.3.7  포스트 수정하기  382 10.3.8  포스트 삭제하기  386

10.4  댓글 기능 만들기  388 10.4.1  코멘트 모델 만들기  388 10.4.2  코멘트 컨트롤러 작성하기  389 10.4.3  댓글 작성을 위한 라우팅 규칙 추가하기  391 10.4.4  포스트의 Show 페이지에서 댓글 보여주기  392

10.5  리팩토링  394 10.5.1  댓글 목록을 별도의 템플릿으로 만들기  394 10.5.2  댓글 작성 폼을 별도의 템플릿으로 만들기  395 10.5.3  포스트 작성 폼을 별도의 템플릿으로 만들기  397 10.5.4  새로 분리한 템플릿에 댓글 삭제 기능 추가하기  398

10.6  데이터 처리에 ORM 프레임워크 적용  400 10.6.1  gorm 초기 설정  401 10.6.2  gorm으로 데이터 처리  404

10.7  로그인과 보안  407 10.7.1  사용자 모델 추가  407 10.7.2  로그인 기능 구현  408 10.7.3  기본 계정 생성  411 10.7.4  인증 인터셉터 추가  412 10.7.5  권한이 있는 사용자만 해당 기능에 접근  417

찾아보기  422

고언어_01.indd 11

2016-03-21 오전 10:48:26


1

Go 시작하기 Go는 아주 실용적인 언어이다. 빠른 성능, 안정성, 편의성, 쉬운 프로그래밍 을 목표로 개발되었고, 시스템 프로그래밍부터 웹 애플리케이션 개발까지 여 러 분야에서 여러 용도로 사용된다. 1장에서는 Go 언어의 특징을 살펴보고, Go를 설치하여 첫 번째 Go 프로그램을 실행해 본다. 마지막으로 Go 참고 문서도 소개한다.

1.1  Go는 어떤 언어인가? 1.2  Go 설치하기 1.3  Go 개발 환경 1.4  첫 번째 Go 프로그램 1.5  Go 참고 문서

고언어_01.indd 13

2016-03-21 오전 10:48:26


1.1

Go는 어떤 언어인가?

Go는 아주 실용적인 언어이다. 빠른 성능, 안정성, 편의성, 쉬운 프로그래밍을 목표로 개발되었 고, 시스템 프로그래밍부터 웹 애플리케이션 개발까지 여러 분야에서 여러 용도로 사용된다. Go의 특징은 다음과 같다. ●●

간결하고 유연한 문법

●●

병행 프로그래밍

●●

정적 타입 언어

●●

동적 프로그래밍

●●

쉬운 협업

●●

가비지 컬렉션 제공

●●

빠른 컴파일과 실행 속도

지금부터 Go의 특징을 하나씩 살펴보자.

간결하고 유연한 문법 Go는 간결하고 명확한 문법에 중점을 두고 설계됐다. 그래서 코드가 단순해졌고 가독성이 높아 졌다. 프로그래밍 언어에서 간결함은 아주 중요한 요소이다. 프로그래머가 언어의 모든 기능을 충분히 숙지하고 있다면 문제가 발생했을 때 가장 효과적인 선택을 할 수 있다. 다시 말해 프로그래머는 복잡한 문제를 해결하는 것에만 집중할 수 있게 된다. 언어의 기능이 많고 복잡하면 문제를 해결 하는 것보다 기능을 이해하고 익히는 것에 더 많은 시간을 할애해야 한다.

014

고언어_01.indd 14

2016-03-21 오전 10:48:26


1

Go를 사용하면 기억하기 쉬운 키워드 몇몇만으로도 규모가 큰 애플리케이션을 구현할 수 있다.

Go 시작하기

그림 1-1을 보면 다른 언어에 비해 Go의 키워드 수가 얼마나 적은지 알 수 있다. Go는 키워드를 이해하고 익히는 데 많은 시간을 할애하지 않아도 된다. 그림 1-1  언어별 키워드 수 200

188

180 160

150

140 120 100 79

80

65

60

50

40

41

37

34

29

25

20 0 VB.NET (11)

C++ (14)

C# (4)

PHP (5.6.12)

자바 (SE 8)

루비 (2.2.2)

C 자바스크립트 파이썬 (ANSI C) (ECMAScript 6) (2.3.5)

Go (1.5)

Go는 간결하고 유연한 타입 시스템으로 객체 지향을 표현한다. 다른 대중적인 객체 지향 언어 (C++, 자바, 파이썬 등)는 클래스 안에 상태와 동작을 정의하고, 추상 데이터 타입인 클래스를 인 스턴스화해서 객체로 사용한다. 이렇게 만들어진 객체는 상태와 동작을 갖는다. Go는 상태를 표현하는 ‘타입’과 동작을 표현하는 ‘메서드’를 분리하여 정의한다. 타입은 어떤 값을 표 현하는 수단이고, 메서드는 특정 타입의 동작을 표현한다. 이렇게 각각 따로 정의된 타입과 메서 드를 바인딩하여 객체를 표현한다. 이처럼 객체의 상태와 동작이 느슨하게 결합된 방식이라 더욱 자유롭게 객체 기반의 프로그래밍을 할 수 있다. Go의 내장 타입이나 다른 패키지에 정의된 타입 에도 원하는 메서드를 바인딩할 수 있어서 좀 더 자연스럽게 해당 타입을 사용할 수 있다. 또 한 가지 특이한 점이 있다. 전통적인 객체 지향 언어는 상속으로 코드를 재사용하는데 Go의 구 조체(struct)는 상속을 할 수 없다는 점이다. 대신 구조체가 다른 구조체를 포함하는 임베디드 타입 형태로 구조체를 정의할 수 있다. 이는 클래스 간의 과도한 상속 관계로 프로그램이 복잡해지는 것을 방지한다. Go는 상속이 아니라 조합으로 코드를 재사용한다.

015

고언어_01.indd 15

2016-03-21 오전 10:48:26


그림 1-2  상속과 조합 동물

청둥오리

Display()

Displayer Display()

오리

Quacker

Quack() Swim()

Quack() Swimmer

청둥오리

상속(Inheritance)

Swim() 조합(Composition)

물론 다른 언어에서도 조합으로 타입을 정의할 수 있다. 하지만 이는 상속을 구현하는 것보다 복 잡하고 어려워서 실제로 많은 개발자가 고민 없이 상속을 사용한다. 앞서 말했듯이 Go의 구조체 는 조합으로만 정의할 수 있다. 따라서 이를 기반으로 코드를 작성하다 보면 조합으로 문제를 해 결하는 것에 자연스레 익숙해진다. 기본 문법은 2장에서, Go의 객체 지향 표현 방식은 4장에서 자세히 설명한다.

병행 프로그래밍 지난 30여 년간 CPU의 성능이 무어의 법칙대로 18개월마다 2배씩 빨라졌다. 하지만 어느 순간 한계에 부딪혔고, CPU를 제작하는 회사들은 CPU 여러 개를 묶어 하나의 칩으로 사용하는 식으 로 발전 방향을 우회했다. 이제는 컴퓨터 하나에 CPU 여러 개를 장착해서 CPU 여러 개가 동시 에 계산을 수행하게 한다. 결과적으로는 더 빠른 계산이 가능해졌다. 필연적으로 소프트웨어도 이 러한 멀티 코어 전략에 영향을 받아 병행 처리나 병렬 처리를 지원하는 것이 필수 조건이 되었다.

Note

무어의 법칙

무어의 법칙은 반도체 집적회로의 성능이 18개월마다 2배로 증가한다는 법칙이다(http://bit.ly/1Qh95zU). 인텔의 공동 설립자인 고든 무어가 1965년에 내놓은 것으로 다음 세 가지를 이야기했다. 1. 메모리의 용량이나 CPU의 속도는 18~24개월마다 2배씩 향상된다. 2. 컴퓨팅 성능은 18개월마다 2배씩 향상된다. 3. 컴퓨팅 가격은 18개월마다 반으로 떨어진다.

016

고언어_01.indd 16

2016-03-21 오전 10:48:26


1

그림 1-3  인텔 프로세서의 트랜지스터 집적수 성장과 무어의 법칙

Go 시작하기

트랜지스터 집적수

1,000,000,000 코어 2 듀오

100,000,000 펜티엄 4 펜티엄 3

10,000,000

펜티엄 2 펜티엄

1,000,000

486 386

100,000 286 10,000 1,000

8086 4004

8080 8008

1970

1975

1980

1985

1990

2010

2000

2005

2010

병행 프로그래밍(concurrent programming)은 코드를 병렬로 동작하게 하여 멀티 코어의 이점을 충분 히 활용할 수 있게 해준다. 하지만 대부분의 메인스트림 언어(C, C++, 자바 등)로는 병행 프로그 램을 작성하고 테스트하기가 매우 어렵다. 스레드 간에 메모리를 공유하여 동기화하는 코드는 구 현하기가 어렵고, 코드를 안전하게 잘 작성했다 하더라도 스레드 자체의 부하 때문에 기대한 만큼 의 성능이 나오지 않는다. 대안으로는 단일 스레드 기반으로 프로그램을 만들고 이를 프로세스 여러 개로 동작시키는 멀티 프로세싱 방식도 있다. 실제로 이 방식도 많이 사용한다(주로 Node.js 애플리케이션에서 많이 사 용한다). 하지만 프로세스 간의 정보 공유나 동기화 작업을 해야 할 때 또 다른 어려움이 발생한 다. 프로세스 간의 통신은 멀티 스레드 환경에서 공유 메모리를 사용하는 것보다 훨씬 더 어렵다. 그런 이유로 최근에 함수형 언어가 주목받고 있다. 함수형 언어에서는 한 번 정해진 값은 바뀌지 않는다. 그래서 병행 처리를 좀 더 안전하게 할 수 있다. 하지만 객체 지향 프로그래밍에 익숙해진 사고를 함수형 언어에 맞게 바꾸는 것 또한 부담된다. Go 언어를 설계할 때 특별히 중점을 둔 부분이 바로 병행 처리다. Go는 병행 처리 코드를 안전하 고 작성하기 쉽게 설계했다. Go의 병행 처리 방식은 통신 순차 프로세스(Communcating Sequential Processes, CSP)

방식에 근간을 둔 것으로, 메모리를 공유하는 것이 아니라 메시지를 전달하는 방식으

로 동기화한다. 017

고언어_01.indd 17

2016-03-21 오전 10:48:26


그림 1-4  언어별 병행 프로그래밍의 난이도 Go

C#

얼랭

자바

C++

C

쉬움

어려움

Note

통신 순차 프로세스

1978년 영국의 컴퓨터 과학자인 토니 호아(C. A. R. Hoare)가 CACM 논문에서 처음으로 통신 순차 프로세스 (Communicating Sequential Processes, CSP)를 소개했다. CSP는 동시성을 지원하는 시스템에서 상호 작용 패턴을 표현하는 언어이다. CSP에서는 동시성을 커뮤니케이션의 입출력으로 본다. 그리고 메모리 공유 방식이 아니 라 동기화 방식으로 통신한다. 이후 CSP는 동시성을 지원하는 개발 언어에 지대한 영향을 끼쳤고 지금까지도 많은 연구가 진행되고 있다.

그림 1-5  고루틴과 채널

고루틴1

receive

채널

send

data

고루틴2 채널

send

receive

data 고루틴3

Go에서 동시에 처리해야 하는 작업은 고루틴으로 실행하고, 고루틴끼리 메시지를 주고받는 작업 은 채널이라는 통로를 사용한다. 고루틴은 Go 프로그램 안에서 동시에 독립적으로 실행되는 흐름 의 단위로, 스레드와 비슷한 개념이다. 하지만 스레드와 달리 고루틴은 수 킬로바이트 정도의 아 주 적은 리소스에서 동작하므로 한 프로세스에 수천, 수만 개의 고루틴을 동작시킬 수 있다. 고루 틴은 정보를 공유하는 방식이 아니라 서로 메시지를 주고받는 방식으로 동작한다. 그래서 Lock으 로 공유 메모리를 관리할 필요가 없고 구현도 어렵지 않다. Go의 병행 프로그래밍은 5장에서 더 자세히 설명한다.

018

고언어_01.indd 18

2016-03-21 오전 10:48:26


4

객체 지향 프로그래밍 4장에서는 Go에서 객체 지향 프로그래밍을 어떻게 표현하는지 소개한다. Go는 객체 지향을 표현하는 방식이 다른 대중적인 객체 지향 언어와는 조금 달라, 클래스를 기반으로 하는 객체 지향 프로그래밍에 익숙한 프로그래머라 면 Go의 방식이 조금 생소할 수도 있다. 먼저 Go의 객체 표현 방식을 알아 보고, 사용자 정의 타입, 구조체, 인터페이스를 설명한 후 객체 지향 프로그래 밍이 어떻게 녹아있는지 소개한다.

4.1  객체 표현 방식 4.2  사용자 정의 타입 4.3  구조체 4.4  인터페이스

고언어_01.indd 145

2016-03-21 오전 10:48:35


4.1

객체 표현 방식

객체 기반 언어에서는 상태와 동작을 표현하는 것을 객체라 한다. 보통 하나의 클래스에 상태와 동작을 정의하고, 추상 데이터 타입인 클래스를 인스턴스화해서 객체로 사용한다. 이렇게 만들어 진 객체는 상태와 동작을 갖는다. 객체 기반 언어 대부분은 하나의 클래스에 상태와 동작을 모두 표현하는 반면, Go는 상태를 표현 하는 ‘타입’과 동작을 표현하는 ‘메서드’를 분리하여 정의한다. 타입은 어떤 값을 표현하는 수단이고

메서드는 특정 타입의 동작을 표현한다. 타입과 메서드를 이어주는 명확한 연결 고리는 없다.

사용자 정의 타입과 메서드 Go에는 기본 타입 외에 사용자가 타입을 직접 정의할 수 있는 사용자 정의 타입이 있다. 일반적 으로는 구조체와 인터페이스를 사용자 정의 타입으로 지정해서 쓴다. 기본 타입이나 함수 서명 (signature)을 사용자 정의 타입으로 지정해서 쓰기도 한다.

메서드는 사용자 정의 타입과 함수를 바인딩시키는 방식으로 정의한다. 메서드를 정의할 때는 어

떤 타입과 연결할지 리시버(receiver)를 지정해준다. 리시버를 지정해주면 타입과 메서드가 연결되 어 특정 사용자 정의 타입에 대한 동작을 표현할 수 있게 된다. func (리시버명 리시버타입) 메서드명(매개변수) (반환타입 또는 반환값) { // body }

func과 메서드명 사이에 리시버를 지정해주는 것만 제외하면 함수를 정의하는 방식과 같다. 메서

드를 호출하면 리시버 값이 메서드 내부로 전달되며, 메서드 안에서 리시버 값을 사용할 수 있게 된다. 다음 코드는 Go에서 객체를 어떻게 표현하는지 보여준다. Item 타입은 name, price, quantity 필드로 현재 상태를 나타내고, Cost() 메서드로 동작을 표현한다. package main import "fmt" // 사용자 정의 타입 정의(구조체)

146

고언어_01.indd 146

2016-03-21 오전 10:48:35


type Item struct { name

string

price

float64

quantity int }

4 객체 지향 프로그래밍

// Item 타입에 Cost() 메서드 정의 func (t Item) Cost() float64 { return t.price * float64(t.quantity) } func main() { // 아이템 값 생성 shirt := Item{name: "Men's Slim-Fit Shirt", price: 25000, quantity: 3} // Cost() 메서드로 가격 출력 fmt.Println(shirt.Cost()) // 75000 }

Item 타입을 구조체로 정의했고 안에 name, price, quantity 필드를 정의했다. 다음 블록에는 Cost() 메서드를 정의했다. Item 타입을 함수의 리시버로 지정하여 Cost()는 Item의 메서드가

되었다. main 함수에서 Item 타입 값으로 shirt를 생성했고, shirt 아이템의 가격을 Cost() 메서 드로 출력했다. 코드의 각 부분은 다음 절에서 자세히 설명한다.

4.2

사용자 정의 타입

사용자 정의 타입(custom type)은 Go의 기본 타입 외에 추가로 타입을 정의할 때 사용하고 type 키 워드로 생성한다. type 타입명 타입명세 타입명은

패키지나 함수 내에서 유일해야 한다. 타입명세로는 Go의 기본 타입(string, int, slice,

map, channel 등)과 구조체, 인터페이스, 함수 서명을 사용할 수 있다. 147

고언어_01.indd 147

2016-03-21 오전 10:48:35


4.2.1  사용자 정의 타입의 종류 주로 구조체와 인터페이스를 정의할 때 사용자 정의 타입을 사용하지만, 기본 타입이나 함수 서명 을 사용자 정의 타입으로 지정해서 쓰기도 한다

기본 타입을 사용자 정의 타입으로 사용 현재 코드 문맥에 맞는 의미를 부여하기 위해 기본 타입을 사용자 정의 타입으로 정의한다. ▼ 기본 타입을 사용자 정의 타입으로 정의

type quantity int type dozen []quantity var d dozen for i := quantity(1); i <= 12; i++ { d = append(d, i) } fmt.Println(d)

실행 결과 [1 2 3 4 5 6 7 8 9 10 11 12]

quantity와 dozen처럼 기본 타입을 기반으로 하여 만들어진 커스텀 타입은 기본 타입과 같은

방식으로 사용할 수 있다. 이를테면 슬라이스에 새로운 요소를 추가하는 내장 함수 append()를 type dozen []quantity로 정의한 사용자 정의 타입 dozen에 사용할 수 있다.

Note

사용자 정의 타입과 기본 타입 간의 타입 변환

기본 타입을 매개변수로 받는 함수에 사용자 정의 타입을 매개변수로 전달하려면 사용자 정의 타입을 기본 타입으로 변환해야 한다. 그리고 사용자 정의 타입을 매개변수로 받는 함수에 기본 타입을 매개변수로 전달하려면 기본 타입을 사용자 정의 타입으로 변환해야 한다. type quantity int func main() {

var q quantity = 3

display(int(q)) }

148

고언어_01.indd 148

2016-03-21 오전 10:48:35


func display(i int){

fmt.Print(i) // 3

}

4 객체 지향 프로그래밍

함수 서명을 사용자 정의 타입으로 사용 마찬가지로 현재 코드 문맥에 맞는 의미를 부여하기 위해 함수 서명을 사용자 정의 타입으로 정의 하는 것도 유용하다. type quantity int type costCalculator func(quantity, float64) float64 func describe(q quantity, price float64, c costCalculator) { fmt.Printf("quantity: %d, price: %0.0f, cost: %0.0f\n", q, price, c(q, price)) } func main() { var offBy10Percent, offBy1000Won costCalculator offBy10Percent = func(q quantity, price float64) float64 { return float64(q) * price * 0.9 } offBy1000Won = func(q quantity, price float64) float64 { return float64(q)*price - 1000 } describe(3, 10000, offBy10Percent) describe(3, 10000, offBy1000Won) }

실행 결과 quantity: 3, price: 10000, cost: 27000 quantity: 3, price: 10000, cost: 29000

costCalculator와 서명이 일치하는 함수를 만들었고 이를 describe() 함수의 매개변수로 전달

했다. 기본 서명인 func(quantity, float64) float64를 사용하는 것보다 costCalculator 타 입을 사용하는 것이 더 가독성이 높고 의미도 분명하게 전달된다.

149

고언어_01.indd 149

2016-03-21 오전 10:48:35


8

나만의 웹 프레임워크 만들기 8장에서는 Go 기본 라이브러리를 사용하여 웹 프레임워크를 직접 만들어 본 다. net/http 패키지로 간단한 웹 애플리케이션을 만든 다음 조금씩 리팩토 링해서 프레임워크를 완성해 보자.

8.1  나만의 웹 프레임워크 만들기 8.2  첫 번째 웹 애플리케이션 8.3  라우터 8.4  컨텍스트 8.5  미들웨어 8.6  추상화 8.7  렌더러 8.8  커스텀 미들웨어

고언어_01.indd 279

2016-03-21 오전 10:48:42


8.1

나만의 웹 프레임워크 만들기

Go의 net/http 패키지는 웹 애플리케이션을 개발할 때 필요한 다양한 기능을 제공한다. net/ http 패키지가 제공하는 기능이 워낙 강력해서 Go의 기본 라이브러리만으로도 웹 애플리케이션

을 만들 수 있다. 하지만 웹 애플리케이션의 규모가 커지고 기능이 다양해지면 반복되는 코드가 생기고 시스템은 점점 복잡해진다. 반복되는 코드를 줄이고 좋은 아키텍처 패턴을 유지하기 위해 보통은 검증된 프레임워크를 사용한다. 다른 언어는 대부분 메인 프레임워크 한두 개가 웹 애플리케이션 영역을 주도한다. 자바의 대표 프레임워크로는 스프링(Spring)과 플레이(Play)가 있고, 루비에는 루비 온 레일즈(Ruby on Rails)와 시 나트라(Sinatra)가 있다. 파이썬은 보통 장고(Django)나 플라스크(Flask)로 웹 애플리케이션을 개발한 다. 이처럼 메인 프레임워크 한두 개가 웹 애플리케이션 영역을 주도하는 다른 언어와는 달리, Go 에는 저마다 다른 장점을 가진 프레임워크와 툴킷이 많은데 좀처럼 메인 프레임워크 하나로 모일 조짐이 보이지 않는다. 그림 8-1  언어별 주요 웹 프레임워크

자바

루비

Go

파이썬

Go는 풀스택 프레임워크 하나를 사용하기보다는 각 상황에 맞게 다양한 패키지를 조합해서 마 이크로 서비스 형태로 자신만의 프레임워크를 구성해 사용하는 것을 권장한다. 그래서 Go의 웹 프레임워크와 툴킷은 미들웨어 형태로 서로 연결될 수 있게 만들어져 있다. 초기에는 Revel이나

280

고언어_01.indd 280

2016-03-21 오전 10:48:42


Beego 등 풀스택 프레임워크를 겨냥한 프레임워크가 등장했었으나 이러한 이유로 크게 성공하지 못했다. Go로 웹 애플리케이션을 개발하려면 먼저 다양한 프레임워크와 툴킷의 구성 및 동작 방식을 파악 해야 한다. 그러고 나서 내가 만들려는 애플리케이션에 필요한 프레임워크와 툴킷을 선정하고 목 적에 맞게 조합해서 사용한다. 동작이 비슷한 프레임워크가 지금도 계속 만들어지고 있어서 이 와 중에 베스트 프랙티스를 만들어내는 것은 아주 어려운 일이다. 하지만 여기에 정답은 없다. 내가 구현하려는 시스템에 맞는 최적의 구성을 스스로 만들어내면 된다. 물론 Go를 처음 시작하는 사

8 나만의 웹 프레임워크 만들기

람에게는 여간 어려운 일이 아니다. 지금부터 Go 기본 라이브러리로 웹 프레임워크를 직접 만들어 보자. net/http 패키지로 간단한 웹 애플리케이션을 만든 다음 조금씩 리팩토링해서 프레임워크를 완성해 본다. 실제로 규모 있는 애플리케이션에 사용하기에는 기능이 다소 부족할 수 있다. 기능이 부족하다면 필요한 기능을 직 접 추가해서 사용해 보자. 이러한 경험이 나중에 다른 프레임워크를 선택하거나 여러 프레임워크 와 툴킷을 조합해서 사용할 때 많은 도움이 될 것이다.

프레임워크의 종류 먼저 Go의 주요 웹 프레임워크를 살펴보자. 자바의 스프링, 루비의 루비 온 레일즈, 파이썬의 장고처럼 Go의 풀스택 프레임워크로는 Revel(http://revel.github.io/)과 Beego(http://beego.me/)가 대표적이다. 이들은 웹 개발에 필요 한 기능 대부분을 제공하므로 프로젝트를 아주 빠르게 구성하여 동작해 볼 수 있고, 이미 구성된 프로젝트에 쉽게 기능을 추가할 수 있다. 풀스택 프레임워크와 달리 마이크로 프레임워크는 웹 개발에 필요한 기능을 최소한만 제공하 고, 나머지 기능은 다른 라이브러리나 프레임워크를 확장하여 사용하게 한다. 루비의 시나트라 나 파이썬의 플라스크가 대표적인 마이크로 프레임워크이다. Go의 마이크로 프레임워크로는 Martini(http://martini.codegangsta.io/), Goji(https://goji.io/), Gin(http://gin-gonic.github.io/ gin/), gocraft/web(https://github.com/gocraft/web) 등이 있다. 그 외에 용도별로 사용할 수 있는 다양한 라이브러리/툴킷이 있다. Gorilla web toolkit(http:// www.gorillatoolkit.org/)은 대표적인 웹 개발 툴킷이다. 웹 개발에 사용되는 주요 프레임워크와 라이브러리/툴킷을 다음과 같이 정리했다.

281

고언어_01.indd 281

2016-03-21 오전 10:48:42


●●

풀스택 프레임워크: Revel, Beego

●●

마이크로 프레임워크: Martini, Goji, Gin, gocraft/web

●●

라이브러리/툴킷 - 라우터: github.com/gorilla/mux, github.com/julienschmidt/httprouter, github.com/ bmizerany/pat - 컨텍스트: github.com/gorilla/context, golang.org/x/net/context - 미들웨어: github.com/justinas/alice - 렌더러: github.com/unrolled/render

만들어 볼 기능 웹 요청이 들어왔을 때 웹 애플리케이션이 어떤 과정으로 요청을 처리하는지 살펴보며 개발을 시 작해 보자. 그림 8-2  웹 요청 처리 과정 라우터 요청

응답

핸들러

핸들러

핸들러

1. 특정 URL이 호출되면 호출된 URL에 매핑된 핸들러가 실행된다. 2. 핸들러에서는 요청한 내용을 분석해서 비즈니스 로직을 실행한다. 3. 비즈니스 로직 수행이 끝나면 그 결과를 응답으로 전송한다.

언어나 플랫폼의 특성에 따라 다를 수 있지만, 웹 애플리케이션 대부분은 이와 같은 방식으로 웹 요청을 처리한다. 간단하게 설명했지만 실제로 웹을 구현할 때는 다음과 같은 복잡한 기능이 요구 된다. ●●

URL 패턴 매핑 기능

●●

로그 처리

●●

에러 처리

●●

정적 파일 처리

282

고언어_01.indd 282

2016-03-21 오전 10:48:42


●●

사용자 인증과 권한 관리

●●

보안 처리

●●

세션 상태 관리

●●

데이터베이스 접근 기능

●●

웹 요청/응답 추상화

●●

기타

8 나만의 웹 프레임워크 만들기

이러한 기능을 쉽게 구현하기 위해 그림 8-3처럼 동작하는 프레임워크를 만들어 볼 것이다. 그림 8-3  웹 프레임워크 동작 방식 라우터 요청 미들웨어1

미들웨어2 컨 텍 스 트

로그 처리 에러 처리 정적 파일 처리 웹 요청 정보 파싱 ⋮

미들웨어3

핸들러

핸들러

핸들러

응답

렌더러(JSON, XML, Html Template)

1. 라우터: 웹 요청을 들어오면 URL 기반으로 특정 핸들러에 전달한다. 2. 컨텍스트: 웹 요청의 처리 상태를 저장하는 공간이다. 3. 미들웨어: 핸들러 로직을 수행하기 전에 공통으로 수행할 코드 조각이고 재사용이 가능하

다. 미들웨어는 주로 다음과 같은 기능을 처리한다. ●●

로그 처리

●●

에러 처리

●●

정적 파일 처리

283

고언어_01.indd 283

2016-03-21 오전 10:48:42


●●

사용자 인증과 권한 관리

●●

보안 처리

●●

세션 상태 관리

●●

웹 요청 정보 파싱

4. 렌더러: 핸들러 로직 수행 결과를 다양한 형태(JSON, XML, Html Template 등)로 응답

한다.

8.2

첫 번째 웹 애플리케이션

다음은 net/http 패키지가 제공하는 함수이다. net/http 패키지에 관한 자세한 내용은 Go 공식 문서를 참고하기 바란다. ●●

func ListenAndServe(addr string, handler http.Handler) error

웹 서버를 구동하고 웹 요청을 받아 처리한다. 웹 요청을 처리할 핸들러를 전달하지 않으면 http.DefaultServeMux가 동작한다. ●●

func HandleFunc(pattern string, handler func(http.ResponseWriter, *http. Request))

URL별로 요청을 처리할 핸들러 함수를 등록한다. ●●

func Handle(pattern string, handler http.Handler)

URL별로 요청을 처리할 핸들러를 등록한다. ●●

func ServeFile(w http.ResponseWriter, r *http.Request, name string)

웹 요청에 대한 응답으로 특정 경로의 파일 내용을 전달한다. 이 함수들을 사용하여 웹 애플리케이션을 만들어 볼 것이다. 먼저 / 경로로 접속했을 때 welcome! 을 출력하는 첫 번째 웹 애플리케이션을 작성해 보자.

284

고언어_01.indd 284

2016-03-21 오전 10:48:42


▼ main.go

package main import ( "fmt" "net/http" ) func main() { // "/" 경로로 접속했을 때 처리할 핸들러 함수 지정

8

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

나만의 웹 프레임워크 만들기

// "welcome!" 문자열을 화면에 출력 fmt.Fprintln(w, "welcome!") }) // 8080 포트로 웹 서버 구동 http.ListenAndServe(":8080", nil) }

/ 경로로 웹 요청이 들어왔을 때 처리할 함수를 http.HandleFunc 함수에 지정했다. 핸들러 함

수 내에서는 fmt.Fprintln 함수의 첫 번째 매개변수로 http.ResponseWriter 를 전달하여 welcome! 문자열이 응답되게 했다. http.ListenAndServe 함수에 포트 번호를 지정해주면 해당 포트로 웹 서버가 구동된다. http. ListenAndServe 함수의 두 번째 매개변수로 nil을 전달했으므로 http.DefaultServeMux가 동작

한다. go run main.go 명령으로 웹 애플리케이션을 구동한 후 브라우저를 통해 핸들러 함수가 정상적

으로 동작하는지 확인해 보자. 그림 8-4  welcome! 메시지 출력

285

고언어_01.indd 285

2016-03-21 오전 10:48:42


추가로 /about url로 접속했을 때 about 메시지를 보여주는 about 핸들러도 추가해 보자. http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "about") }) 그림 8-5  about 메시지 출력

8.3

라우터

http.HandleFunc 함수의 첫 번째 매개변수로 전달하는 URL은 정적 문자열만 사용할 수 있다.

즉, /users/:id나 /users/:user_id/addresses/:address_id처럼 URL에 매개변수를 적용한 동적 URL은 사용할 수 없다. 또한, 같은 URL에 http 메서드별로 다른 핸들러 함수를 지정하는 것도 불가능하다. 이번에는 http 메서드와 URL 패턴별로 핸들러를 등록하고, 웹 요청이 들어왔을 때 적절한 핸들 러로 연결해주는 라우터(Router)를 만들어 보자. http.ListenAndServe 함수로 웹 서버를 구동할 때 두 번째 매개변수(http.Handler 인터페이스)로 라우터를 전달하면 모든 웹 요청을 라우터가 받아서 처리한다.

286

고언어_01.indd 286

2016-03-21 오전 10:48:42


그림 8-6  웹 프레임워크 - 라우터 구현 라우터 요청 미들웨어1

미들웨어2 컨 텍 스 트

로그 처리 에러 처리 정적 파일 처리 웹 요청 정보 파싱 ⋮

8

미들웨어3

핸들러

나만의 웹 프레임워크 만들기

핸들러

핸들러

응답

렌더러(JSON, XML, Html Template)

라우터 타입 정의 먼저 router.go 파일을 생성하고 router 타입을 정의해 보자. ▼ router.go

type router struct { // 키: http 메서드 // 값: URL 패턴별로 실행할 HandlerFunc handlers map[string]map[string]http.HandlerFunc }

router 구조체 내부에 handlers 필드를 정의했다. handlers는 또 다른 맵을 값(value)으로 사용한

2차원 맵이다. URL 패턴별로 실행할 핸들러들을 http 메서드별로 정의했다. router의 handlers 필드 내부의 값을 표현해 보면 표 8-1과 같다.

287

고언어_01.indd 287

2016-03-21 오전 10:48:42


표 8-1  router의 handlers 필드 내부의 값 예시 GET

POST

PUT

DELETE

/

indexHandler

/about

aboutHandler

/users

retrieveUserCollection

/users/:id

retrieveUser

/users/:user_id/addresses

retrieveUserAddressCollection

/users/:user_id/addresses/:address_id

retrieveUserAddress

/users

createUser

/users/:user_id/addresses

createUserAddress

/users/:id

updateUser

/users/:user_id/addresses/:address_id

updateUserAddress

/users/:id

deleteUser

/users/:user_id/addresses/:address_id

deleteUserAddress

라우터에 핸들러를 등록하기 위한 메서드 정의 이제 라우터에 핸들러를 등록하기 위한 메서드인 HandleFunc를 만들어 보자. ▼ router.go

func (r *router) HandleFunc(method, pattern string, h http.HandlerFunc) { // http 메서드로 등록된 맵이 있는지 확인 m, ok := r.handlers[method] if !ok { // 등록된 맵이 없으면 새 맵을 생성 m = make(map[string]http.HandlerFunc) r.handlers[method] = m } // http 메서드로 등록된 맵에 URL 패턴과 핸들러 함수 등록 m[pattern] = h }

288

고언어_01.indd 288

2016-03-21 오전 10:48:42


router의 HandleFunc 메서드 내부에서는 매개변수로 전달된 http 메서드, URL 패턴, 핸들러 함

수를 2차원 맵인 handlers 필드에 등록한다.

라우터에 http.Handler 인터페이스의 ServeHTTP 메서드 정의 http.ListenAndServe(addr, handler) 함수의 두 번째 매개변수로 전달하는 http.Handler 인

터페이스의 형태는 다음과 같다.

8

▼ http.Handler 인터페이스

나만의 웹 프레임워크 만들기

type Handler interface { ServeHTTP(http.ResponseWriter, *http.Request) }

특정 타입에 ServeHTTP(http.ResponseWriter, *http.Request) 메서드를 추가하면 웹 요청을 받아서 처리하는 http.Handler 인터페이스로 사용할 수 있다. 이제 router에 ServeHTTP 메서드를 만들어 보자. ServeHTTP 메서드는 웹 요청의 http 메서드와 URL 경로를 분석해서 그에 맞는 핸들러를 찾아 동작시킨다. 만약 웹 요청에 일치하는 핸들러가 등록되어 있지 않으면 NotFound 에러를 반환한다. ▼ router.go

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { if m, ok := r.handlers[req.Method]; ok { if h, ok := m[req.URL.Path]; ok { // 요청 URL에 해당하는 핸들러 수행 h(w, req) return } } http.NotFound(w, req) }

이 메서드에서는 라우터의 handlers에서 req.Method와 req.URL.Path에 일치하는 핸들러를 찾 아 수행한다. /users/:id이나 /users/:user_id/addresses/:address_id처럼 매개변수를 적용 한 동적 URL 패턴에 핸들러를 연결하는 기능은 아직 구현하지 않았다.

289

고언어_01.indd 289

2016-03-21 오전 10:48:43


매개변수가 적용된 동적 URL 패턴도 라우팅할 수 있게 ServeHTTP 메서드를 수정해 보자. 라우터 에 등록된 동적 URL 패턴과 실제 URL 경로가 일치하는지 확인하는 match 함수를 만들고, 라우 터의 ServeHTTP 메서드에서 match 함수를 사용하게 한다. ▼ router.go

func match(pattern, path string) (bool, map[string]string) { // 패턴과 패스가 정확히 일치하면 바로 true를 반환 if pattern == path { return true, nil } // 패턴과 패스를 "/" 단위로 구분 patterns := strings.Split(pattern, "/") paths := strings.Split(path, "/") // 패턴과 패스를 "/"로 구분한 후 부분 문자열 집합의 개수가 다르면 false를 반환 if len(patterns) != len(paths) { return false, nil } // 패턴에 일치하는 URL 매개변수를 담기 위한 params 맵 생성 params := make(map[string]string) // "/"로 구분된 패턴/패스의 각 문자열을 하나씩 비교 for i := 0; i < len(patterns); i++ { switch { case patterns[i] == paths[i]: // 패턴과 패스의 부분 문자열이 일치하면 바로 다음 루프 수행 case len(patterns[i]) > 0 && patterns[i][0] == ':': // 패턴이 ':' 문자로 시작하면 params에 URL params를 담은 후 다음 루프 수행 params[patterns[i][1:]] = paths[i] default: // 일치하는 경우가 없으면 false를 반환 return false, nil } } // true와 params를 반환 return true, params } func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // http 메서드에 맞는 모든 handers를 반복하여 요청 URL에 해당하는 handler를 찾음

290

고언어_01.indd 290

2016-03-21 오전 10:48:43


for pattern, handler := range r.handlers[req.Method] { if ok, _ := match(pattern, req.URL.Path); ok { // 요청 URL에 해당하는 handler 수행 handler(w, req) return } } // 요청 URL에 해당하는 handler를 찾지 못하면 NotFound 에러 처리 http.NotFound(w, req) return

8

}

나만의 웹 프레임워크 만들기

main.go에서 모든 웹 요청을 라우터가 처리하도록 수정 모든 웹 요청을 라우터가 받아 처리하도록 웹 애플리케이션을 구동하는 소스를 수정해 보자. ▼ main.go

package main import ( "fmt" "net/http" ) func main() { r := &router{make(map[string]map[string]http.HandlerFunc)} r.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "welcome!") }) r.HandleFunc("GET", "/about", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "about") }) r.HandleFunc("GET", "/users/:id", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "retrieve user") }) r.HandleFunc("GET", "/users/:user_id/addresses/:address_id", func(w http. ResponseWriter, r *http.Request) { fmt.Fprintln(w, "retrieve user's address")

291

고언어_01.indd 291

2016-03-21 오전 10:48:43


}) r.HandleFunc("POST", "/users", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "create user") }) r.HandleFunc("POST", "/users/:user_id/addresses", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "create user's address") }) http.ListenAndServe(":8080", r) }

URL 패턴에 매개변수를 적용하여 웹 요청을 전달해 보자. /users/1에 GET 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users/1 retrieve user

/users/1/addresses/2에 GET 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users/1/addresses/2 retrieve user's address

/users에 GET 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users 404 page not found

/users에 POST 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users -d ''

292

고언어_01.indd 292

2016-03-21 오전 10:48:43


/users/1/addresses에 POST 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users/1/addresses -d '' create user's address

나만의 웹 프레임워크 만들기

8.4

8

컨텍스트

지금까지 만든 것에 보완해야 할 점이 하나 있다. URL 패턴에 해당하는 매개변수를 핸들러 함수 내부로 전달할 수 있어야 한다. 예를 들어 URL 패턴을 /users/:id/addressses/:addresss_id 로 지정하여 핸들러를 등록했을 때는 핸들러 내부에서는 user_id와 address_id의 값을 알 수 있 어야 한다. 이번에는 웹 요청의 처리 상태를 저장하는 컨텍스트(Context)를 만들고, 컨텍스트로 URL 매개변수 의 값을 핸들러 내부에 전달해 보자. 그림 8-7  웹 프레임워크 - 컨텍스트 구현 라우터 요청 미들웨어1

미들웨어2 컨 텍 스 트

로그 처리 에러 처리 정적 파일 처리 웹 요청 정보 파싱 ⋮

미들웨어3

핸들러

핸들러

핸들러

응답

렌더러(JSON, XML, Html Template)

293

고언어_01.indd 293

2016-03-21 오전 10:48:43


컨텍스트 타입 정의 context.go 파일을 생성하고 Context 타입을 정의해 보자. ▼ context.go

type Context struct { Params map[string]interface{} ResponseWriter http.ResponseWriter Request

*http.Request

}

Context 타입 Parmas 필드에 라우터에서 해석한 URL 매개변수를 담고, 핸들러 내부에는 Context

값이 전달되게 한다.

핸들러 타입 정의 *Context를 매개변수로 받는 새 핸들러 함수를 정의해 보자. 앞으로는 핸들러 함수 타입으로 http.HandlerFunc를 사용하지 않고, 패키지 내에서 정의한 HandlerFunc를 사용한다. ▼ context.go

type HandlerFunc func(*Context)

라우터 수정 라우터에서도 패키지 내에서 정의한 HandlerFunc를 사용하도록 다음과 같이 수정하자. ▼ router.go

package main import ( "net/http" "strings" ) type router struct { // 키: http 메서드 // 값: URL 패턴별로 실행할 HandlerFunc

294

고언어_01.indd 294

2016-03-21 오전 10:48:43


handlers map[string]map[string]HandlerFunc } func (r *router) HandleFunc(method, pattern string, h HandlerFunc) { // http 메서드로 등록된 맵이 있는지 확인 m, ok := r.handlers[method] if !ok { // 등록된 맵이 없으면 새 맵을 생성 m = make(map[string]HandlerFunc) r.handlers[method] = m

8

}

나만의 웹 프레임워크 만들기

// http 메서드로 등록된 맵에 URL 패턴과 핸들러 함수 등록 m[pattern] = h } func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // http 메서드에 맞는 모든 handers를 반복하면서 요청 URL에 해당하는 handler를 찾음 for pattern, handler := range r.handlers[req.Method] { if ok, params := match(pattern, req.URL.Path); ok { // Context 생성 c := Context{ Params:

make(map[string]interface{}),

ResponseWriter: w, Request:

req,

} for k, v := range params { c.Params[k] = v } // 요청 url에 해당하는 handler 수행 handler(&c) return } } // 요청 URL에 해당하는 handler를 찾지 못하면 NotFound 에러 처리 http.NotFound(w, req) return } func match(pattern, path string) (bool, map[string]string) { // ... }

295

고언어_01.indd 295

2016-03-21 오전 10:48:43


router의 내부 필드인 handlers 타입을 map[string]map[string]HandlerFunc로 변경했다. 라

우터의 ServeHTTP() 메서드에서는 요청 URL에 해당하는 핸들러를 수행하기 전에 Context를 생성해서 매개변수로 전달한다. 이때 match() 함수에서 전달된 동적 URL의 매개변수 정보는 Context에 담는다.

main.go의 핸들러 함수에서 컨텍스트를 사용하도록 수정 마지막으로 핸들러 함수에서 컨텍스트를 사용하도록 수정해 보자. main.go 파일을 다음과 같이 수정한다. ▼ main.go

package main import ( "fmt" "net/http" ) func main() { r := &router{make(map[string]map[string]HandlerFunc)} r.HandleFunc("GET", "/", func(c *Context) { fmt.Fprintln(c.ResponseWriter, "welcome!") }) r.HandleFunc("GET", "/about", func(c *Context) { fmt.Fprintln(c.ResponseWriter, "about") }) r.HandleFunc("GET", "/users/:id", func(c *Context) { fmt.Fprintf(c.ResponseWriter, "retrieve user %v\n", c.Params["id"]) }) r.HandleFunc("GET", "/users/:user_id/addresses/:address_id", func(c *Context) { fmt.Fprintf(c.ResponseWriter, "retrieve user %v's address %v\n", c.Params["user_id"], c.Params["address_id"]) }) r.HandleFunc("POST", "/users", func(c *Context) {

296

고언어_01.indd 296

2016-03-21 오전 10:48:43


fmt.Fprintf(c.ResponseWriter, "create user\n") }) r.HandleFunc("POST", "/users/:user_id/addresses", func(c *Context) { fmt.Fprintf(c.ResponseWriter, "create user %v's address\n", c.Params["user_id"]) }) http.ListenAndServe(":8080", r) }

8 나만의 웹 프레임워크 만들기

URL 매개변수가 컨텍스트를 통해 잘 전달되는지 확인해 보자. /users/1에 GET 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users/1 retrieve user 1

/users/1/addresses/2에 GET 요청을 전송한다 명령 프롬프트

$ curl http://localhost:8080/users/1/addresses/2 retrieve user 1's address 2

/users/1/addresses에 POST 요청을 전송한다. 명령 프롬프트

$ curl http://localhost:8080/users/1/addresses -d '' create user 1's address

297

고언어_01.indd 297

2016-03-21 오전 10:48:43



Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.