주제
패스워드 콘솔 만들기
개요
애초에 sqlite를 이용해 ORM 적용해 두었었던 작품입니다.
(참고 : https://pypi.org/project/python-sqlite-orm/)
그런데 sqlite 사용하는 관점과 mysql(mariaDB) 사용적 관점에서 migration 할 타이밍이 온 것같습니다.
(사실 DBMS까지 다 비교하진 않았습니다. 다만, Console개념을 살려서 Client-Server구조의 Storage 프로그램이 필요했을뿐)
간단히 알아봅시다.
세대별 DBMS의 구조
구분 |
모델 | 설명 |
DBMS |
1 세대 |
파일시스템 | : 데이터가 계층적이며 상하 종속적인 관계로 구성 - 장점 : 데이터의 엑세스 속도가 빠르고, 데이터의 사용량을 쉽게 예측 할 수 있다. |
-ISAM -VSAM |
2 세대 |
계층형 (Hierachical DBMS) | : 데이터가 계층적이며 상하 종속적인 관계로 구성 - 장점 : 데이터의 엑세스 속도가 빠르고, 데이터의 사용량을 쉽게 예측 할 수 있다. |
-IMS -System2000 |
3 세대 |
네트워크형 (Network DBMS) | : 데이터 구조를 네트워크상의 노드 형태로 논리적이게 표현한 데이터 모델로서 각각의 노드를 서로 대등한 관계로 구성한 시스템(여기서 노드란 시스템을 의미하는 것이 아니라 자료를 말한다.) - 장점 : 계층형 데이터베이스 관리시스템의 문제점인 상하 종속적인 관계는 해결되었다. - 단점 : 구성과 설계가 복잡하고 궁극적으로 데이터의 종속성을 해결하지 못하였다. |
-IDS -IDMS -TOTAL |
4 세대 |
관계형 (Relational DBMS) | : 수학적 논리 관계를 테이블의 형태로 구성한 구조로서 테이블 내의 컬럼 중 일부를 다른 테이블과 중복해 각 테이블간의 상관관계를 정의 - 장점 : 업무 변화에 대한 적응력 높아 변화하는 업무에 쉽게 활용하며 유지보수 편리하다. 따라서 생산성도 향상된다. - 단점 : 다른 DBMS 보다 더 많은 자원이 활용되어 시스템의 부하가 높다. |
-Oracle -Mysql -DB2 -SQL Server -Sybase |
5 세대 |
객체지향 (Object Oriented DBMS) | : 멀티미디어 데이터의 원활한 처리와 RDBMS의 비지니스형 데이터 타입만 처리되는 기본적인 제한점을 극복하고자 고안 | -Object Store -UniSQL |
1 2 3 | mysql> create database manage_console default CHARACTER SET UTF8; Query OK, 1 row affected (0.00 sec) | cs |
일단 DB만들고, 확인
1 2 3 4 5 6 7 8 9 10 11 12 | mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | manage_console | | mysql | | performance_schema | | project | | sys | +--------------------+ 6 rows in set (0.00 sec) | cs |
테이블 생성
1 2 3 4 5 6 | mysql> CREATE TABLE managesite(idx int primary key auto_increment, -> sitename varchar(30) not null, -> siteurl varchar(100), -> snsauth varchar(20) default 'null' -> ) ENGINE=INNODB; Query OK, 0 rows affected (0.01 sec) | cs |
확인
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | mysql> show tables; +--------------------------+ | Tables_in_manage_console | +--------------------------+ | managesite | +--------------------------+ 1 row in set (0.00 sec) mysql> desc managesite; +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | idx | int(11) | NO | PRI | NULL | auto_increment | | sitename | varchar(30) | NO | | NULL | | | siteurl | varchar(100) | YES | | NULL | | | snsauth | varchar(20) | YES | | null | | +----------+--------------+------+-----+---------+----------------+ 4 rows in set (0.01 sec) | cs |
그리고 DB사용자를 추가
1 | create user 'namki'@'localhost' identified by '%password%'; | cs |
mysql ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
라는 에러가 발생하면 Mysql password policy requirements 에러 validation 제거하여 해결하기 을 참고하자
내 계정 만들고 권한 설정 끝
1 2 | mysql> grant all privileges on *.* to 'namki'@localhost; Query OK, 0 rows affected (0.00 sec) | cs |
이제 새 테이블을 만드려고 했는데, 에러발생.
1 | ERROR 1215 (HY000): Cannot add foreign key constraint | cs |
이전 테이블에서 키가 더 필요하다 여기서 키를 좀 조사해보면은
키(key)의 종류
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | * 키(Key) - 열쇠는 무언가를 열거나 잠글 때 사용하는 것으로, 같은 것이 하나도 없다. 우리집 열쇠가 옆집의 열쇠랑 다르듯이 말이다. - 이와 같이 키라는 것은 무언가를 식별하는 고유한 식별자(identifier) 기능을 한다. - 즉, 키는 데이터베이스에서 조건에 만족하는 관계의 행을 찾거나 순서대로 정렬할 때 다른 행과 구별할 수 있는 유리한 기준이 되는 속성의 집합이다. - 키의 종류로는 기본키, 슈퍼키, 후보키, 대체키, 외래키 등이 있다. 1. 슈퍼키(Super Key) - 테이블에서 각 행을 유일하게 식별할 수 있는 하나 또는 그 이상의 속성들의 집합이다. 슈퍼키는 유일성만 만족하면 슈퍼키가 될 수 있다. - 유일성이란 하나의 키로 특정 행을 바로 찾아낼수 있는 고유한 데이터 속성을 말한다. 예를 들면 전국에서 나를 구별할 수 있는 유일하고 고유한 속성은 주민번호이듯이 말이다. 주민번호는 전국민이 모두 겹치지 않아 유일하고 고유한 구별 방법으로 쓰인다. - 아래 사진을 보자. 7조라는 팀에 팀원은 4명이 있다. 이 4명을 구분할 수 있는 것은 절대 겹치지 않는 학번 일수도 있고, 주민번호일 수도 있다. - 이름과 나이를 묶어서 하나의 속성으로 만드는 것도 가능하다. 이름과 나이를 합쳐서 7조안에서 중복만 되지 않으면 가능하기 때문이다. 이름과 나이를 합쳐서 4명을 구분할 수 있으면 슈퍼키가 될 수 있다. - 학번과 주민번호를 묶어서 슈퍼키로 만들수도 있고, 학번과 주민번호과 이름을 합쳐서 슈퍼키로도 만들수 있고, 학번과 주민번호과 이름과 나이를 합쳐서 슈퍼키를 만들수도 있다. 어떤 속성끼리 묶던 중복값이 안나오고 서로 구별만 할 수 있으면 된다. 2. 후보키(Candidate Key) - 테이블에서 각 행을 유일하게 식별할 수 있는 최소한의 속성들의 집합이다. 후보키는 기본키가 될 수 있는 후보들이며 유일성과 최소성을 동시에 만족해야한다. - 아래 사진을 보자. 7조라는 팀에 팀원은 4명이 있다. 이 4명을 구분하는 슈퍼키들이 모여 있는데, 슈퍼키들 중에서 속성은 최소한의 갯수로 4명을 구분할 수 있어야 후보키가 될 수 있다. - 학번 슈퍼키와 주민번호 슈퍼키는 속성들이 각 1개씩 이루어져 있다. 하지만 이름+나이 슈퍼키는 이름과 나이를 묶어서 2개의 속성으로 되어 있다. 이름+나이 슈퍼키는 2개 이므로 각 1개의 속성인 주민번호와 학번 슈퍼키가 최소성을 만족한다고 할 수 있다. - 따라서 이름+나이 슈퍼키는 갯수가 다른 것보다 많기 때문에 최소성을 만족하지 못해서 후보키가 될 수 없다. 3. 기본키(Primary Key) - 후보키들 중에서 하나를 선택한 키로 최소성과 유일성을 만족하는 속성이다. - 테이블에서 기본키는 오직 1개만 지정할 수 있다. - 기본키는 테이블 안에서 유일하게 각 행들을 구별할 수 있도록 쓰인다. - 기본키는 NULL 값을 절대 가질수 없고, 중복된 값을 가질 수 없다. - 각 행들을 구별하려면 값이 없어선 안되고, 중복되어서도 안되기 때문이다. 4. 대체키(Alternate Key) - 후보키가 두개 이상일 경우 그 중에서 어느 하나를 기본키로 지정하고 남은 후보키들을 대체키라한다. - 대체키는 기본키로 선정되지 않은 후보키이다. - 아래 사진을 보자. 7조라는 팀에 팀원은 4명이다. 후보키로 학번과 주민번호가 뽑혔고, 둘 중에서 기본키는 학번이 되었다. 학번이 기본키가 되고 남은 후보키인 주민번호는 대체키가 될 수 있다. 학번 기본키가 없어지게 되면 주민번호는 없어진 기본키를 대체할 수 있게된다. 5. 외래키(Foreign Key) - 테이블이 다른 테이블의 데이터를 참조하여 테이블간의 관계를 연결하는 것이다. 데이터를 좀더 조회하기 쉽다. - 다른 테이블의 데이터를 참조할 때 없는 값을 참조할 수 없도록 제약을 주는 것이다. - 참조 될 테이블(A)이 먼저 만들어지고 참조하는 테이블(B)에 값이 입력되어야 한다. - 이때, 참조될(A) 열의 값은 참조될(A) 테이블에서 기본키(Primary Key)로 설정되어 있어야한다. - 외래키는 참조되는 테이블의 기본키와 동일한 키 속성을 가진다. - 참조되는 부모테이블이 먼저 생성된 뒤 데이터를 넣고, 참조하는 자식 테이블이 다음에 생겨야된다. - 부모 테이블 먼저 삭제될 수 없다. 왜냐하면 부모테이블을 참조하는데 부모테이블이 삭제되면 자식테이블은 참조하는 것이 없어지기 때문에 외래키 오류가 생긴다. - 외래키 관계에서 부모테이블을 삭제하려면 자식테이블 먼저 삭제한 후 부모테이블을 삭제해야한다. - 아래 사진을 보자. 부모 테이블은 학생 테이블이고, 자식 테이블은 수강 테이블이다. - 학생테이블은 학번이 기본키이자 참조되는 참조키이다. - 수강테이블은 학번이 참조하는 키이자 외래키이다. - 기본 형태 : 컬럼명 컬럼타입 FOREIGN KEY (외래키) REFERENCES 부모테이블명(참조키); | cs |
그래서 기존 테이블에 유니크한 키를 추가했다.
1 2 3 | mysql> alter table managesite add unique key `primary key` (sitename); Query OK, 0 rows affected (0.01 sec) Records: 0 Duplicates: 0 Warnings: 0 | cs |
그리고 나서 다시 테이블(완전 식벽자의 데이터를 품을) 생성.
1 2 3 4 5 6 7 8 9 | mysql> CREATE TABLE pwd_identity( -> idx int primary key auto_increment, -> userid varchar(30) not null, -> userpw varchar(30) not null, -> rdate date, -> sitename varchar(30) not null, -> CONSTRAINT foreign key (sitename) references managesite (sitename) -> ) ENGINE=InnoDB; Query OK, 0 rows affected (0.01 sec) | cs |
그리고 내용을 추가해야하는데, 아무래도 민감한데이터가 존재하니 데이터를 암호화 하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | mysql> select hex(AES_ENCRYPT('abc', 'a')); +----------------------------------+ | hex(AES_ENCRYPT('abc', 'a')) | +----------------------------------+ | 31314BA898CCDE00814E2520B02B646B | +----------------------------------+ 1 row in set (0.00 sec) mysql> select AES_DECRYPT(UNHEX("AE1A17EC25E92DF81AA67584D51D5741"),'b'); +------------------------------------------------------------+ | AES_DECRYPT(UNHEX("AE1A17EC25E92DF81AA67584D51D5741"),'b') | +------------------------------------------------------------+ | abc | +------------------------------------------------------------+ 1 row in set (0.00 sec) ''' # "암호화 키"는 임의의 값이 올 수 있으며, "문자열"은 암호화하고자 하는 값이 됩니다. # AES_ENCRYPT 암호화 INSERT INTO 테이블명 VALUES (HEX(AES_ENCRYPT('문자열', '암호화 키'))); # AES_DECRYPT 복호화 SELECT AES_DECRYPT(UNHEX(필드명), '암호화 키') FROM 테이블명; 예제 (ex #1 # AES_ENCRYPT 암호화 INSERT INTO tbname VALUE (HEX(AES_ENCRYPT('123456','가나다라'))); // 결과: 5A33E11DC0B638E4E5E74EBD52F55E3D # AES_DECRYPT 복호화 SELECT AES_DECRYPT(UNHEX(필드명), '가나다라') FROM tbname; ''' | cs |
(주의 Hash값 16자리 이상인 경우 길이는 64, 그 이하의 길이는 32)
Okay이제는 python 메서드를 사용해서 mysql를 제어하겠다.
필요한 라이브러리는 pip search로 검색할 수 있다.(로컬쪽 모듈 한계) 그래서 pypi 문서에서 하나를 찾게 되었다. 그이름은 바로
이 모듈은 MySQL C 클라이언트 라이브러리에 의존하지 않고, Python으로 작성된 MySQL 드라이버이다.
그 다음으로 python을 이용해 mysql 커넥션 pool을 만드는 코드를 짰다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # -*- coding: utf-8 -*- import mysql.connector #DB 모듈 from mysql.connector import Error #create Connection #C:\Bitnami\wampstack-7.1.24-1\mysql\ #127.0.0.1 def connect(): try: conn = mysql.connector.connect( host="127.0.0.1", user="{userid}", passwd="{password}" ) if conn.is_connected(): #연결에 대한 로직 print("Connected to mysql DB") except Error as e: print("error name : {}".format(e)) finally: conn.close() return conn if __name__=="__main__": db = connect() print("good") print(db) | cs |
수정 (전)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Connector(Singleton): conn = None def __init__(self, connect_info=None): global engine if engine is None: engine = sqlalchemy.create_engine( connect_info, pool_size=5, max_overflow=5, pool_recycle=505) def __enter__(self): self.conn = engine.connect() return self.conn def __exit__(self, exe_type, exc_val, exc_tb): self.conn.close() with Connector(config['MARIADB']['MARIADB_CONNECT_INFO']) as con: onion_count = con.execute("select count(*) from onions_info").fetchall()[0] return onion_count | cs |
수정 (후)
(참고 : http://www.mikusa.com/python-mysql-docs/index.html)
크게 두 가지 이유로 현재 코드는 동작은 하지만 뭔가 굉장히 허전하다.
1. 리소스 낭비(weak resource)
2. 보안에 취약(dynamic of qry vuln & structure)
그 이외 몇가지 부수적인것들로 인해 세션과 ORM 모듈을 적용하기로 생각했고, DB커넥션 풀을 객체로 사용하기로 마음먹음.
(오후에 vue.js랑 Node.js 코딩테스트(18시~23시29분) 때문에 멘탈 정상아님...하....테스트 포기함유 ㅠㅠ )
DB커넥션 풀을 객체로 반환할 때는 with grammer를 사용할 것이다. 그러면 미리 커넥션이 생성하고 DB이벤트마다 트랜잭션 사이클을 돌릴 수 있어서
자원을 조금 더 효율적으로 사용가능하다. -> 이 기법자체가 사실 Connection Pool 이랍니다.(블로그찾음)
DB 구현 함수 |
|||||||
DB 생성 | 테이블 생성 | ||||||
|
| ||||||
데이터 삽입 | 데이터 조회 | ||||||
|
| ||||||
데이터 수정 | 데이터 삭제 | ||||||
|
|
ORM
SQLAlchemy 객체 관계형 매퍼는 데이터베이스 테이블을 이용해 사용자가 정의한 파이썬 클래스의 메소드와 각각의 행을 나타내는 인스턴스로 표현된다. 객체와 각 연관된 행들의 모든 변경점들이 자동으로 동기되어 인스턴스에 반영되며, 그와 동시에 사용자가 정의한 클래스와 각 클래스 사이에 정의된 관계에 대해 쿼리할 수 있는 (Unit of work이라 하는)시스템을 포함하고 있다.
이 ORM에서 사용하는 SQLAlchemy 표현 언어는 ORM의 구성 방식과도 같다. SQL언어 튜토리얼에서는 직접적인 의견을 배제한 채 데이터베이스들의 초기에 어떻게 구성해 나가야 하는지에 대해 설명하는 반면 ORM은 고수준의, 추상적인 패턴의 사용 방식과 그에 따른 표현 언어를 사용하는 방법을 예로 보여준다.
사용 패턴과 각 표현 언어가 겹쳐지는 동안, 초기와 달리 공통적으로 나타나는 사항에 대해 표면적으로 접근한다. 먼저 사용자가 정의한 도메인 모델서부터 기본적인 저장 모델을 새로 갱신하는 것까지의 모든 과정을 일련의 구조와 데이터로 접근하게 해야한다. 또 다른 접근 방식으로는 문자로 된 스키마와 SQL 표현식이 나타내는 투시도로부터 명쾌하게 구성해, 각 개별적인 데이터베이스를 메시지로 사용할 수 있게 해야 한다.
가장 성공적인 어플리케이션은 각각 독자적인 객체 관계형 매퍼로 구성되야 한다. 특별한 상황에서는, 어플리케이션은 더 특정한 데이터베이스의 상호작용을 필요로 하고 따라서 더 직접적인 표현 언어를 사용할 수 있어야 한다.
(제 실력이 미천해 깔끔하게 번역이 안되네요. 공통된 부분에만 집중하고 각 데이터베이스의 특징을 몰개성화 하며 단순히 저장공간으로 치부하는 다른 ORM과 달리 SQLAlchemy는 각 데이터베이스의 특징도 잘 살려내 만든 ORM이다, 대충 이런 내용입니다. 원문 보세요. ㅠㅠ)
근데 sqlalchemy는 DB 세션을 이용가능한 engine을 생성하여 편함.
1 | pip3 install sqlalchemy | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #import mysql.connector #DB 모듈 from mysql.connector import Error from sqlalchemy import create_engine from sqlalchemy import scoped_session, sessionmaker from sqlalchemy.ext.declarative import declarative_base engine = create_engine('mysql://namki@localhost/?charset=utf8', convert_unicode=False) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) #refer: https://docs.sqlalchemy.org/en/rel_0_9/core/engines.html Base = declarative_base() Base.query = db_session.query_property() def init_db(): import models Base.metadata.create_all(engine) | cs |
mysql_control.py
접속이 끝나더라도 계속 해서 연결 상태를 유지시키기 위해 session_maker를 통해 세션을 만들어 준다.
scoped_session은 Thread_safe를 유지하기 위해 주로 웹 어플리케이션에서 사용한다.
새로운 Table과 mapper를 만들기 위해 declarative_base()를 호출하여 Base를 생성하고, 생성된 Base의 메타데이터를
해당하는 model의 값으로 생성한다.
DB 연결과 세션 생성에 대한 스크립트가 완료되었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | # -*- coding: utf-8 -*- from sqlalchemy import create_engine from sqlalchemy import scoped_session, sessionmaker from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, DateTime from database import Base import datetime engine = create_engine('mysql://namki:dd4351@localhost/?charset=utf8', convert_unicode=False) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) #refer: https://docs.sqlalchemy.org/en/rel_0_9/core/engines.html Base = declarative_base() Base.query = db_session.query_property() #insert into managesite (sitename, siteurl, snsauth) values('facebook', 'https://www.facebook.com/', 'false') #insert into pwd_identity (userid, userpw, rdate, sitename) values ('01094065290', hex(AES_ENCRYPT('pw', md5('key'))), now(), 'facebook'); def init_db(): import models Base.metadata.create_all(engine) def todaydate(): d=datetime.date.today() return d.isoformat() class User(Base): _tablename_ = 'managesite' idx = Column(Integer, primary_key=True, not null, auto_increment) #{Field:idx, Type:int(11), Null:NO, Key:PRI, Extra:auto_increment} sitename = Column(varchar(30), not null, unique_key=True) #{Field:sitename, Type:varchar(30), Null:NO, Key:UNI} siteurl = Column(varchar(100)) #{Field:siteurl, Type:varchar(100), Null:YES} snsauth = Column(varchar(20)) #{Field:snsauth, Type:varchar(20), Null:YES} def __init__(self, sitename, siteurl, snsauth): self.sitename = sitename self.siteurl = siteurl self.snsauth = snsauth def __repr__(self): return "<managesite(%s', '%s', '%s'>" %(self.sitename, self.siteurl, self.snsauth) class Pwd(Base): __tablename__="pwd_identity" idx = Column(Integer, primary_key=True, not null, auto_increment) #{Field:idx, Type:int(11), Null:NO, Key:PRI, Extra:auto_increment} userid = Column(varchar(30), not null) #{Field:userid, Type:varchar(30), Null:NO} userpw = Column(varchar(100), not null) #{Field:userpw, Type:varchar(100), Null:NO} rdate = Column(varchar(30)) #{Field:rdate, Type:varchar(50), Null:NO} sitename = Column(varchar(30), not null, forenign_key=True) #{Field:sitename, Type:varchar(30), Null:NO} def __init__(self, userid, userpw, rdate=todaydate(), sitename): self.userid = userid self.userpw = userpw # required hash self.sitename = sitename def __repr__(self): return "<pwd_identity(%s', '%s', '%s'>" %(self.userid, self.userpw, self.sitename) if __name__ == "__main__" : main() init_db() pri_user = User() db_session.add(pri_user) db_session.commit() pri_user_pwd = Pwd() db_session.add(pri_user) db_session.commit() | cs |
이것으로 마무으리~다음편을 기대하시라....사실 이번에는 밑도 끝도 없는 포스팅임과 동시에
공부하는데 의미를 두게 되었다.(까먹은 DB야 오랜만:D)
참고
ref : sqlite 내부 조인 -http://thinking-jmini.tistory.com/14
ref : 생활코딩 내부 조인 - https://www.youtube.com/watch?v=U8FWvjaQBDs
ref : 미래학자의 MYSQL - http://futurists.tistory.com/11
ref : 흔한 컴공의 개발기 - http://simsimjae.tistory.com/75
ref : 진화중 - https://m.blog.naver.com/PostView.nhn?blogId=imf4&logNo=220779816879&proxyReferer=https%3A%2F%2Fwww.google.com%2F
ref : TCP스쿨 - http://tcpschool.com/mysql/mysql_constraint_foreignKey
ref : Oracle Advanced SQL 강좌 - http://www.gurubee.net/lecture/2688
ref : mysql내장함수 암복호화 - http://blog.habonyphp.com/entry/mysql-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%95%94%ED%98%B8%ED%99%94-%EB%B3%B5%ED%98%B8%ED%99%94%ED%95%98%EB%8A%94-AES-%ED%95%A8%EC%88%98#.XCMrKmgzZPY
ref : 농사짓는 개발자 - https://openlife.tistory.com/350
ref : 데이터 암복호화 : http://jeonjin.tistory.com/673
'프로그램언어+' 카테고리의 다른 글
우분투에 LAMP(apache2, mysql, php7) 환경구축 (0) | 2019.04.20 |
---|---|
데이터베이스 조인 정리(JOIN) - (작업중) (0) | 2018.12.19 |
DB에서 JOIN의 종류(데이터 결합) (0) | 2018.12.18 |
VCS (Version Control System) GIT(포스팅중) (0) | 2018.12.16 |