import psycopg2.extras
self.conn = psycopg2.connect(host='127.0.0.1', dbname='postgres', user='postgres', password='postgres', port=5432)
self.cur = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)


python 3.6 이하 버전에서는 datetime을 생성하면 timezone 정보가 없어 astimezone 같은 메소드를 실행하면 에러가 발생합니다. 지금부터 삽질해서 찾은 방법을 설명합니다. 


python 3.6은 datetime을 생성할 때 timezone을 지정하지 않아도 기본으로 내장으로 설정이 되어 있어 astimezone()를 실행해도 에러가 발생하지 않습니다. 하지만 3.6이하 버전은 datetime을 생성할 때 timezone을 지정하지 않으면 timezone이 None이므로 astimezone()을 실행하면 에러가 발생합니다. 따라서 결론은 datetime을 생성할 때 timezone을 지정하는 것입니다.


하지만 이미 생성된 datetime이라면 어떨까..

여기서 방법은 2가지로 나뉩니다.

datetime → time → datetime 으로 변환

먼저 datetime을 time타입으로 변환 시킨 다음, 이 값으로 timezone을 지정하여 datetime객체를 새로 생성하는 방법입니다.

이 방법은 아래에서 설명할 replace()함수가 지원되지 않는 python 버전에서 사용할 수 있습니다.

import datetime
import time

now_dt = datetime.datetime.now()
print(now_dt)

now_t = time.mktime(now_dt.timetuple()) + now_dt.microsecond / 1E6

print(now_t)

c_now_dt = datetime.datetime.fromtimestamp(now_t, datetime.timezone.utc)
print(c_now_dt)

# 결과
# 2017-10-15 18:45:32.357988
# 1508060732.357988
# 2017-10-15 09:45:32.357988+00:00

replace() 사용 (pyhton 3.3 이상)

위 방법은 상당히 귀찮으므로 python 버전이 3.3 이상이면 replace()함수를 사용합니다.

import datetime


now_dt = datetime.datetime.now()
print(now_dt)

c_now_dt = now_dt.replace(tzinfo=datetime.timezone.utc)
print(c_now_dt)

# 결과
# 2017-10-15 18:48:32.402106
# 2017-10-15 18:48:32.402106+00:00

주의점

위 두 결과를 보고 확인이 되듯이 replace()함수를 사용해 timezone을 입력하면 timezone은 변경이 되지만 utc 시간대로 변경되지 않습니다(!!)

아래는 python 3.6대에서 astimezone()도 추가하여 비교한 코드입니다.

import datetime
import time

now_dt = datetime.datetime.now()
print('현재 시간: ', now_dt)

now_t = time.mktime(now_dt.timetuple()) + now_dt.microsecond / 1E6
c_now_dt = datetime.datetime.fromtimestamp(now_t, datetime.timezone.utc)

print('datetime-> time -> datetime 변환시간: ', c_now_dt)

print('replace() 변환시간: ',now_dt.replace(tzinfo=datetime.timezone.utc))
print('astimezone() 변환시간: ',now_dt.astimezone(datetime.timezone.utc))


# 현재 시간:  2017-10-16 19:09:44.162545
# datetime-> time -> datetime 변환시간:  2017-10-16 10:09:44.162545+00:00
# replace() 변환시간:  2017-10-16 19:09:44.162545+00:00
# astimezone() 변환시간:  2017-10-16 10:09:44.162545+00:00


이것처럼 replace()를 사용할 때, utc 시간대로 변경되지 않는 다는 점을 숙지하고 사용해야 합니다.

결론

datetime 생성할떄 timezone과 timedeltal를 이용해서 필요한 타임존 설정을 해주면 모든게 해결됨. (근데 귀찮다)

파이썬 프로그래밍의 대부분은 데이터를 담은 클래스들을 정의하고 이 객체들이 연계되는 방법을 명시하는 일입니다. 모든 파이썬 클래스는 일종의 컨테이너로, 속성과 기능을 함께 캡슐화합니다. 파이썬은 데이터 관리용 내장 컨테이너 타입(리스트, 튜플, 세트, 딕셔너리)도 제공합니다.

시퀀스처럼 쓰임새가 간단한 클래스를 설계할 때는 파이썬의 내장 list 타입에서 상속받으려고 하는 게 당연합니다. 예를 들어 멤버의 빈도를 세는 메서드를 추가로 갖춘 커스텀 리스트 타입을 생성한다고 가정합니다.

class FrequencyList(list):
    def __init__(self, members):
        super().__init__(members)

    def frequency(self):
        counts = {}
        for item in self:
            counts.setdefault(item, 0)
            counts[item] += 1
        return counts


# list에서 상속받아 서브클래스를 만들어서 list의 표준 기능을 모두 갖춰 파이썬 프로그래머에게 시맨틱을 유지
# 또한 추가한 메서드로 필요한 커스텀 동작을 더할 수 있음


foo = FrequencyList(['a', 'b', 'a', 'c', 'b', 'a', 'd'])
print('Length is ', len(foo))
foo.pop()
print('After pop: ', repr(foo))
print('Frequency: ', foo.frequency())

# 결과
# Length is  7
# After pop:  ['a', 'b', 'a', 'c', 'b', 'a']
# Frequency:  {'a': 3, 'b': 2, 'c': 1}


이제 list의 서브클래스는 아니지만 인덱스로 접근할 수 있게 해서  list처럼 보이는 객체를 제공한다고 합니다. 예를 들어 바이너리 트리 클래스에 (list나 tuple같은) 시퀀스 시맨틱을 제공한다고 가정합니다.

class BinaryNode:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right


# 이 클래스가 시퀀스 타입처럼 동작하기 위해 어떻게 해야 할까?
#  파이썬은 특별한 이름을 붙인 인스턴스 메서드로 컨테이너 동작을 구현

bar = [1, 2, 3]
bar[0]

# 위와 같이 시퀀스의 아이템을 인덱스로 접근하면 다음과 같이 해석
bar.__getitem__(0)


# BinaryNode클래스가 시퀀스처럼 동작하게 하려면 객체의 트리를 깊이 우선으로 탐색하는 __getitem__을 구현

class IndexableNode(BinaryNode):
    def _search(self, count, index):
        # ...
        # (found, count) 반환
        pass

    def __getitem__(self, index):
        found, _ = self._search(0, index)

        if not found:
            raise IndexError('Index out of range')

        return found.value

tree = IndexableNode(10,
                     left=IndexableNode(5,
                                        left=IndexableNode(2),
                                        right=IndexableNode(6,
                                                            right=IndexableNode(7)
                                                            )
                                        ),
                     right=IndexableNode(15, left=IndexableNode(11)))

# 트리탐색은 물론 list처럼 접근 가능

print(tree.left.right.right.value)
print(tree[0])


# 결과
7
2


문제는 __getitem__을 구현한 것만드로 기대하는 시퀀스 시맨틱을 모두 제공하지 못합니다.

len(tree)# 에러

이러한 문제처럼 count, index 메서드 등 프로그래머들이 사용하는 기본 메서드를 전부 정의하기는 생각보다 어렵습니다. 


이런 어려움을 피하기 위해 내장 collections.abc 모듈은 각 컨테이너 타입에 필요한 일반적인 메서드를 모두 제공하는 추상 기반 클래스들을 정의합니다. 이 추상 기반 클래스들에서 상속받아 서브클래스를 만들다가 깜빡 잊고 필수 메서드를 구현하지 않으면, 모듈이 에러를 뱉어냅니다.

from collections.abc import Sequence

class BadType(Sequence):
    pass

foo = BadType()

# 결과
# TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__


앞에서 다룬 SequenceNode처럼 추상 기반 클래스가 요구하는 메서드를 모두 구혀ㅑㄴ하면 별도로 작업하지 않아도 클래스가 index와 count 같은 부가적인 메서드를 모두 제공합니다.

요약

쓰임새가 간단할 때는 list나 dict같은 파이썬의 컨테이너 타입에서 직접 상속

커스텀 컨테이너 타입을 올바르게 구현하는 데 필요한 많은 메서드에 주의

커스텀 컨테이너 타입이 collections.abc에 정의된 인터페이스에서 상속받게 만들어서 클래스가 필요한 인터페이스, 동작과 일치도록


사전작업

virtualenv로 django를 설치한 경우

Pycharm 내의 터미널을 사용하실 경우 선택된 interpreter에 따라 자동으로 변경되므로 아래와 같은 작업을 할 필요가 없습니다.

예시) djangoDjango라는 virtualenv를 생성한 경우

$ cd virtualenv
$ source bin/activate
(virtualDjango) $         # virtualDjango라는 virtualenv 터미널에 접속한 상태
# virtualenv 접속해제는 deactivate
$ 프로젝트로 이동

터미널

settings 파일이 쪼개져 있지 않은 경우

$ python3 manage.py inspectdb > models.py (아무이름이나 가능)

settings 파일이 쪼개져 있는 경우

$ python3 manage.py inspectdb --settings=djangotest(프로젝트이름).settings.dev > models.py (아무이름이나 가능)

결과

# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey has `on_delete` set to the desired behavior.
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from __future__ import unicode_literals

from django.db import models



class TCart(models.Model):
    cart_no = models.BigAutoField(primary_key=True)
    user_no = models.BigIntegerField()
    guest_session_key = models.CharField(max_length=-1)
    save_mileage_amount = models.BigIntegerField()
    delivery_amount = models.DecimalField(max_digits=16, decimal_places=2)
    coupon_sale_amount = models.DecimalField(max_digits=16, decimal_places=2)
    coupon_save_amount = models.DecimalField(max_digits=16, decimal_places=2)
    pay_mileage = models.BigIntegerField()
    pay_deposit = models.BigIntegerField()
    device_type = models.IntegerField()
    insert_timestamp = models.DateTimeField()
    updated_timestamp = models.DateTimeField()
    is_deleted = models.CharField(max_length=1)

    class Meta:
        managed = False
        db_table = 't_cart'
        unique_together = (('user_no', 'guest_session_key'),)

....

주의사항

Django로 가져온 테이블의 컬럼 필드와 postgreSQL 내 테이블의 컬럼 필드가 다를 경우가 존재합니다. (예를 들어, varchar(20)인데 text필드로 migrate가 되었다던지..) 따라서 생성한 후 확인 하는 것을 권장합니다.

일단 WSGI는 파이썬에만 해당이 됩니다. Go에는 3가지 옵션이 있습니다. (실제로는 4가지지만 일반적으로 CGI는 높은 부하로 인해 뺐습니다.)

Go의 표준 라이브러리에 내장 된 HTTP 서비스 기능.

이 경우 앱은 독립 실행형 서버입니다. 가장 간단한 설정 일 수도 있지만 다음과 같은 문제가 있을 수 있습니다. 

  • 다운그레이드 권한으로 권한이 부여된 포트(1024 아래의 포트, 예를들어 80포트)를 가진 앱을 실행하려면, 특별한 wrapper나 POSIX 기능을 사용해야합니다.
  • 연결을 끊지 않고 재배포를 하려면 goagain과 같은 다른 wrapper가 필요합니다.

웹 서버 형태의 reverse HTTP proxy 문제가 존재

대부분 독립 실행형의 다양한 문제를 해결하지만, 그래도 전체 HTTP 트래픽을 이리저리 전달하는 오버 헤드가 존재합니다.

FastCGI는 적절한 웹 서버를 통해 제공

FastCGI는  Nginx와 Apache와 같은 웹서버도 연결이 가능합니다. FCGI 클라이언트 구현은 Go 표준 라이브러리에서 사용할 수 있습니다. 


독립 실행형 서버 실행의 문제가 없는 것 이외로 효율적인 데이터 교환 프로토콜을 구현합니다. 또 다른 보너스는 Go 서버가 reverse HTTP proxy과 관련된 TCP 소켓보다 전송 비용이 낮은 Unix 파이프를 사용하여 front-end 웹 서버와 통신 할 수 있다는 것입니다. 결론적으로 WSGI나 CGI를 생각하지 말고 FCGI를 구현하는 것이 좋습니다.



len()
은 컨테이너에 포함된 항목의 수를 계산합니다. 다시 말해 문자열일 경우 문자의 길이를 반환하고 컨테이너타입인 튜플, 딕셔너리, 리스트의 경우는 속해있는 값의 개수를 반환합니다.

반면 sys.getsizeof()는 객체의 메모리 사이즈를 바이트 단위로 반환합니다. 객체는 모든 유형이 될 수 있습니다.

파이썬 문자열 객체는 문자 당 1바이트의 간단한 문자 시퀀스가 아닙니다. 특히, sys.getsizeof()함수에는 가비지 컬렉터의 오버헤드(아마 1바이트)가 포함되어 출력됩니다. 

getsizeof()는 object의 __sizeof__ 메서드를 호출하고 object가 가비지 컬렉터에서 관리되는 경우에는 가비지 컬렉터의 오버 헤드를 추가합니다.

예시

# python 3.4
import sys

en = 'a'
ko = 'ㅁ'

print('len()')
print(len(en))
print(len(ko))

# 결과
# 1
# 1

print('encode - utf8')
print(en.encode('utf-8'))
print(ko.encode('utf-8'))


# 결과
# b'a'
# b'\xe3\x85\x81'

print('sys.getsizeof()')
print(sys.getsizeof(''))
print(sys.getsizeof(en))
print(sys.getsizeof(ko))
 
# 결과
# 49
# 50
# 76


len()함수로 알파벳과 한글을 비교하면 길이가 1이 나옵니다. 파이썬3.x는 기본 유니코드를 utf-8로 사용합니다. 따라서 한글이나 다른 문자가 있으면 utf8로 해석을 합니다. 

이 문자열들을 utf-8로 encode하면 바이트 타입으로 변환이되고 알파벳은 1바이트, 한글은 3바이트로 표현이 됩니다.

마지막으로 sys.getsizeof()는 메모리에 실제로 올라가는 크기입니다. ''와 같이 빈 문자열도 49바이트가 차지하게 됩니다.


유니 코드 문자열의 경우 문자 별 크기는 최대 2 또는 4가됩니다 (컴파일 옵션에 따라 다름). Python 3.3 및 이후 버전에서 유니 코드 문자열은 문자열의 내용에 따라 문자 당 1에서 4 바이트를 차지합니다.

많이 사용하는 라이브러리, ORM, 웹 프레임워크 등 간단한 설명과 github 주소가 링크되어 있는 사이트

https://awesome-go.com/

CORS 정책상 Access-Control-Allow-Origin은 1개만 등록이 가능합니다. Origin을 *로 할 경우 전 URL에 대해 허용이 가능하지만 아래와 같은 경우면 특정 URL을 지정해야 합니다.

c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", "*") // 에러 - Credentials이 true일 경우 특정 URL 1개만 허용


필자가 사용한 방법은 허용할 URL 리스트를 만든 다음, 요청된 URL이 해당 리스트에 맞는지 체크하는 형식으로 문제를 해결했습니다.

allowUrlList := []string{"http://foo.com", "http://bar.com", "http://zoo.com"}
var allowUrl string
for _, url := range allowUrlList {
if c.Request.Header.Get("Origin") == url {
allowUrl = url
break
}
}

c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", allowUrl)


간단한 문젠대..

GET

 gin-gonic 웹 프레임워크에서 GET은 .Query()함수로 query parameter로 넘어온 데이터를 받습니다.

http://foo.com?id=1234
 
...
a := c.Query(id)

POST

POST의 경우 body로 넘어온 데이터를 받습니다.

http://foo.com
# body 부분
{id:1234}
 
...
a := c.PostForm("id")

DELETE

gin-gonic에서 DELETE 메소드는 무조건 query parameter로 데이터를 넘겨야합니다. body로 데이터를 넘겨줄 경우 이를 인식하지 못합니다.

http://foo.com
# body 부분
{id:1234}


...
a := c.PostForm("id") // nil 


http://foo.com?id=1234


...
a := c.Query(id) // 1234


'언어 > GO' 카테고리의 다른 글

[GO] 유용 사이트  (0) 2017.03.22
[GO] gin-gonic 헤더 Access-Control-Allow-Origin 여러개 추가 방법  (0) 2017.03.20
[GO] gin-gonic DELETE 메소드 데이터 호출방법  (0) 2017.03.17
[GO] gin-gonic CORS 문제 해결방법  (0) 2017.03.17
[GO] 채널  (0) 2017.03.02
[GO] 고루틴  (0) 2017.02.22

먼저 CORS에 대한 개념이 어느정도 존재한다는 가정하에 진행합니다.

TEST 환경

자바스크립트 -> Go Gin-gonic API를 요청하는 상황입니다.

Request Headers

var req = new XMLHttpRequest();
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.withCredentials = true;
req.setRequestHeader("Authorization", "Bearer "+$.cookie("AUTH_A_TOKEN"));

Response Headers

c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")

주의사항

위 Response Headers의 환경은 OPTIONS에만 나열하면 절대 안됩니다. OPTIONS에만 작성하고 GET, POST 등 메인 메소드에 작성하지 않을 경우 CORS문제가 발생했다고 에러가 계속나오게 됩니다. (몇 시간헤맴...) 

자세하게 Simple Request의 경우에는 서버에서 요청을 1번만 하고 요청에 대한 응답을 1번만 하게되어 OPTIONS 메서드를 호출하지 않습니다. (즉, GET을 요청했을 경우 GET에 대한 요청과 응답이 1번씩만 출력됩니다.) 하지만 Simple Request가 아닌 모든 경우에는 Preflight Request로 요청이 되는데 이때는 예비요청, 예비응답, 본 요청, 본 응답 으로 요청 2번, 응답 2번을 하게 됩니다. 예비 요청은 서버에서 구현하지 않아도 자동적으로 OPTIONS 메서드를 호출하게되고 해당 응답이 정상일 경우 본 요청을 하게 됩니다. (즉, GET을 요청하면 먼저 OPTIONS 요청, OPTIONS 응답 한 다음 정상적인 응답일 경우에 GET 요청, GET 응답이 됩니다.) 따라서 OPTIONS와 본 요청 메서드에 response header환경을 전부 적용해야 합니다.

해결법

위 header옵션들을 사용할 메소드마다 전부 나열해줍니다.

func main() {
	router = gin.Default()
	router.GET("/test", func(c *gin.Context) {
	    c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
		c.Header("Access-Control-Allow-Credentials", "true")
		c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
		c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
		c.Next()
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "status":  http.StatusOK,
	         "message": "ㅁ",
	         "data":    nil,
	      })
	   }
	})

	router.POST("/test", func(c *gin.Context) {
		c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
		c.Header("Access-Control-Allow-Credentials", "true")
		c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
		c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
		c.Next()
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "message":"success",
	         "status":http.StatusOK,
	         "data":nil,
	      })
	   }
	})
	router.OPTIONS("/test", func(c *gin.Context) {
	   c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
	   c.Header("Access-Control-Allow-Credentials", "true")
	   c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
	   c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
	   c.AbortWithStatus(204)
	})
	router.Run(":8081")
}


위 코드는 잘 동작하지만 중복되는 코드가 계속해서 포함됩니다. 아래와 같이 깔끔하게 변경할 수 있습니다.

func main() {
	router = gin.Default()
	router.Use(CORSMiddleware())
	router.GET("/test", func(c *gin.Context) {
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "status":  http.StatusOK,
	         "message": "ㅁ",
	         "data":    nil,
	      })
	   }
	})

	router.POST("/test", func(c *gin.Context) {
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "message":"success",
	         "status":http.StatusOK,
	         "data":nil,
	      })
	   }
	})
	router.Run(":8081")
}
 

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
      c.Header("Access-Control-Allow-Credentials", "true")
      c.Header("Access-Control-Allow-Origin", "http://foo.com:3000")
      c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}


+ Recent posts