관리 메뉴

A seeker after truth

<할 일 목록> 웹앱 작성해보기(1) 본문

Programming Language/Golang(Go)

<할 일 목록> 웹앱 작성해보기(1)

dr.meteor 2019. 10. 11. 01:42

*본문은 <Go 디스커버리>(염재현 저, 한빛미디어 2016)의 5장, 6장을 공부한 뒤 정리한 내용입니다.

 

1. Task 타입을 구조체로 정의하기

type Task = struct {
  title string
  status status
  due *time.Time
}

- due 필드가 포인터 타입으로 작성된 이유?: 기한이란 값은 없을 수도 있기 때문에(즉, '기한 없음' 자체가 due에 대한 one of the type이 될 수 있다는 뜻)

- time은 go에서 지원하는 패키지?라이브러리? 중 하나, 그 중 Time은 구조체 타입. 이를 이용해 Time 구조체를 받는..? 이용하는...? 메서드? 함수? 등등을 호출할 수 있다

- status는 "done"이란 bool 타입 필드를 대신하여 구조체로 작성된 필드이다.

type status int

이렇게 작성한 이유는 "소프트웨어"의 관점에서 비롯된다... 이것이 하나의 소프트웨어이고, 이 앱에 필요한 개념들은 추상화, 그리고 그것들끼리 일종의 관계를 맺고 있으며 모두 모여 구조화 되어 있기 때문. 책에는 완료했다/안했다 두 가지 정보보다 더 많은 종류의 상태가 추가될 수 있게 하기 위해서라고 말한다. 대신 '현재 상황을 알지 못한다'는 값만 추가하고 싶다면 포인터 자료형으로 만들 수도 있다.

또한 확장성을 위해서라고 한다(웹 앱의 잦은 업데이트와 패치 대비법). go언어가 아닌 다른 언어의 경우엔 bool형을 쓸 곳에 enum 타입을 쓰라고 말한다.

 

go에는 enum 대신 const와 iota를 이용해서 비슷한 역할을 하도록 만들 수 있다. status들을 정의하기 위해 아래와 같이 작성할 수 있다.

const (
  UNKNOWN status = 0
  TODO status = 1
  DONE status = 2
 )

var와 import처럼 const 역시 위와 같이 관련이 있는 것들끼리 묶어서 작성할 수 있다.

UNKNOWN은 굳이 필요 없을 수도 있지만, 기본값인 0을 이렇게 dummy값으로 두는 것이 여러모로 도움이 되는 경우가 많다고 한다.

위 경우처럼 0,1,2 순서대로 값을 부여하고자 할 때 iota를 이용한다.

const (
  UNKNOWN status = iota
  TODO
  DONE
)

0이 아닌 1 혹은 다른 값부터 부여하고 싶을 땐 어떻게 할까? 아래 예시 코드를 보자.

type ByteSize float64
const (
  _ = iota //ignore first value
  KB ByteSize = 1 << (10*iota)
  MB
  GB
)

위 코드에서 볼 수 있듯 밑줄로 무시하면 된다. 첫번째 값인 0을 버린다는 뜻이 된다. 또 하나 볼 점은, 처음에 쓴 iota 이후 다음 값은 1이 되는데 단순히 iota를 쓰지 않고 있는데, 이렇게 작성하는 것도 가능하다. 여기서 KB의 값은 1<<(10*1)이며, 비트를 왼쪽으로 10번 이동시킨다는 의미이며 이진수로 읽어야 한다. 즉 2의 10승(1024)가 된다. 그 다음 MB는 2의 20승이 된다. 위 예시의 경우  "const ("의 아래줄을 "KB Bytesize = 1"로 작성하는 것으로 시작하는 것이 좀 더 효율적인 방법이다.

 

Go의 구조체는 다른 언어들에 비해 단순하며, 여려 자료형의 필드들을 가질 수 있다는 점이 핵심이다. 구조체를 재사용하는 방법은 go언어에서 지원되는 포함관계를 이용해 더 쉽게 사용하는 방법이 있으며 8장에 나온다.

 

 

 

2. Deadline 자료형(타입)과 Overdue 메서드

*메서드는 구조체가 아니더라도 작성할 수 있다. 근데 이게 무슨 말이지...?(165쪽)

이번에는 Deadline 자료형을 하나 만들어보자. 다른 언어였다면 이에 대해 클래스나 구조체를 만들 것이다. 하지만 마감 시간 필드 하나만 있는 구조체를 만들 필요는 없다. Go 언어에서는 그냥 (명명된) 자료형에 이름을 붙이기만 하면 된다. 그리고 이에 대해 OverDue라는 메서드를 정의해보자.

type Deadline time.Time

//OverDue returns TRUE if the deadline is BEFORE the current time.
//Time()은 형변환, Before 메서드는 그 자체가 bool 타입 반환 메서드
func (d Deadline) OverDue() bool {
    return time.Time(d).Before(time.Now())
}

func ExampleDeadline_OverDue() {
    //d1,d2는 최종적으로 Time 구조체를 반환하는데 이를 Deadline 타입으로 형변환
    d1 := Deadline(time.Now().Add(-4*time.Hour))
    d2 := Deadline(time.Now().Add(4*time.Hour))
    fmt.Println(d1.OverDue()) //overdue 메서드 호출 방식 주목, 주의
    fmt.Println(d2.OverDue())
}

d1은 현재 시각에서 4시간 전, d2는 현재 시각에서 4시간 후를 마감 시각으로 갖는 변수다. 현재 시각에 대해 4시간 전 시각을 갖는 d1은 현재 시각을 기준으로 before가 맞으므로 OverDue 입장에선 true를 반환하며, 코드에서 의도한 의미는 '마감 시간 넘김'이 된다. d2는 그 반대다.

데드라인이 없는 경우까지 취급하려면 OverDue 메서드의 리시버를 포인터형으로 바꾸면 된다.

func (d *Deadline) OverDue() bool {
    return d!=nil && time.Time(*d).Before(time.Now())
}

Task 구조체도 다음과 같이 수정한다.

type Task = struct {
  Title string
  Status status
  Deadline *Deadline
}

그리고 Task에 대한 OverDue 메서드를 만들면 아래와 같다. 이 메서드를 통해 Deadline에 Task가 해야할 일을 위임한다.

func (t Task) OverDue() bool {
	return t.Deadline.OverDue()
}

 

테스트 코드를 첨부하면 아래와 같다.

func Example_taskTestAll() {
  d1 := Deadline(time.Now().Add(-4 * time.Hour))
  d2 := Deadline(time.Now().Add(4 * time.Hour))
  t1 := Task{"4h ago", TODO, &d1}
  t2 := Task{"4h later", TODO, &d2}
  t3 := Task{"no due", TODO, nil}
  fmt.Println(t1.OverDue())//true
  fmt.Println(t2.OverDue())//false
  fmt.Println(t3.OverDue())//false
}

 

 

 

3. 구조체 내장 개념으로 코드 리팩토링

Task 구조체가 Deadline 자료형의 필드를 갖고 있어서 메서드도 이용을 할 수 있었다. 그러나 메서드(=OverDue 메서드 이외의 메서드를 말하는 듯)마다 모두 같은 이름의 메서드(=OverDue메서드를 말하는 듯)를 호출하는 코드를 작성해야 했다. 이런 귀찮은 일을 덜어주는 것이 내장 기능이다.

type Task = struct {
  Title string
  Status status
  *Deadline
}

이렇게 Deadline 필드의 이름을 생략하면, Task 구조체를 리시버로 받는 Overdue 메서드를 작성할 필요가 없다. 실제로 이 메서드는 별 의미 없는 메서드에 해당한다.

하지만 필드가 내장되어 있으면 내장된 필드가 구조체 전체의 직렬화 결과를 바꿔버리는 문제가 생기기 때문에 Deadline을 내장하는 방식을 이용하면 곤란하다. 따라서 Deadline 자료형을 구조체로 만들어서 time.Time 구조체를 내장하는 방식을 이용한다.

type Deadline struct {
  time.Time
}

func NewDeadline(t time.Time) *Deadline {
  return &Deadline{t}
}

type Task struct {
  Title string
  Status status
  Deadline *Deadline
}

구조체를 내장하게 되면 내장된 구조체에 들어 있는 필드들도 바로 접근이 가능하게 된다는 점이 장점이다!

아래 코드가 좋은 예시다.

type Telephone struct {
  Mobile string
  Direct string
}

type Contact struct {
  Telephone
}

func ExampleContact() {
  var c Contact
  c.Mobile = "123-456-789"
  //이하 생략
}

여기서 c.Mobile에 접근했지만 실제론 c.Telephone.Mobile에 접근한 것과 마찬가지의 효과가 나타났다. 실제론 편의만을 제공할 뿐, 상속과 같은 개념과는 차이가 있고 다른 개념이라는 점을 헷갈려선 안된다.