[PostgreSQL] 아무도 자세히 알려주지 않았던 Autovacuum의 위험성

PostgreSQL을 사용함에 있어서 Vacuum의 동작 방식을 정확히 이해하는 것은 너무나도 중요한 일입니다. 만일, 업무 시간에 autovacuum 수행으로 인한 IO saturation 현상이 발생한다면 성능 상의 문제가 발생하기 때문입니다. 따라서, Vacuum, 특히 자동으로 수행되는 autovacuum의 원리 및 위험성을 정확히 파악하고 있어야 함은 PostgreSQL 운영에 있어서 가장 중요한 부분이라고 할 수 있습니다.

1. PostgreSQL의 age에 대한 이해


Vacuum을 쉽게 이해하기 위해서는 나이(age) 개념을 이해해야 합니다. 현재 XID가 1,969라고 가정하겠습니다. 이때 테이블 A를 생성하면 해당 테이블의 relfrozenxid=1,970이되고 age=1, 즉 1살이 됩니다. 그리고 테이블 A에 4개의 트랜잭션이 발생하면, 5살로 증가합니다. 그런데 여기서 주의해야할 사항이 있습니다. 이 시점에 테이블 B에 5개의 트랜잭션이 발생한다면 테이블 A의 나이는 어떻게 될까요? 테이블 A의 나이는 5살이 증가한 10살이 됩니다. 즉, 나 (테이블 A)는 아무것도 하지 않았지만 세월이 흘러감에 따라 (트랜잭션이 발생함에 따라) 자연스럽게 나이가 증가하는 개념입니다. 다소 철학적이죠? 즉, PostgreSQL의 나이는 사람의 나이와 동일한 개념이라고 이해하시면 됩니다. (그림-1 참조)

그림-1. 트랜잭션 발생에 따른 테이블 A의 나이 증가

pg2-1

2. Autovacuum의 동작 원리


Autovacuum은 autovacuum_freeze_max_age와 vacuum_freeze_min_age 파라미터에 의해서 제어됩니다.

  • autovacuum_freeze_max_age : autovacuum 대상이 되는 테이블의 나이(age)를 의미합니다. 기본 설정 값이 2억이므로 2억살 이상 되는 테이블은 autovacuum 대상이 됩니다.
  • vacuum_freeze_min_age : 매뉴얼의 설명은 이해하기가 다소 어렵습니다. 매뉴얼의 설명보다는 autovacuum 수행 직후에 설정되는 테이블의 나이(age)라고 이해하면 됩니다. 기본 설정 값이 5천만이므로, autovacuum 직후에 해당 테이블의 나이는 5천만살이 됩니다. (그림-2 참조) 만일, vacuum_freeze_min_age=0으로 설정하면 autovacuum 수행 직후에 해당 테이블의 나이는 0살이 됩니다.
그림-2. autovacuum 수행 후의 나이 변화 (vacuum_freeze_min_age=5천만인 경우)
pg2-2
pg2-3

즉, autovacuum 직후에 설정되는 테이블의 나이는 아래의 수식을 이용합니다.
테이블 RelfrozenXID = CURRENT XID – vacuum_freeze_min_age
테이블의 나이 (age) = CURRENT XID – 테이블 RelfrozenXID

3. Autovacuum의 위험성


눈치가 빠른 분들은 이미 2장의 설명을 통해 Autovacuum의 위험성을 간파했을 것입니다. 즉, 테이블에 어떠한 변경사항도 없고, Autovacuum (또는 Vacuum)이 수행됐다고 하더라도 일정 기간이 경과되면 해당 테이블은 또 다시 Autovacuum 대상이 됩니다.

autovacuum_freeze_max_age과 vacuum_freeze_min_age 파라미터가 기본 설정 값이라면, autovacuum이 발생한 이후에 매 1.5억 트랜잭션이 발생할 때 마다 autovacuum을 재 수행하게 됩니다. 특히, 9.6 이전 버전까지는 visibility map 내에 all_frozen 비트가 없으므로 테이블을 거의 full scan 해야하는 문제가 존재합니다. 이는 테이블의 크기가 클 수록, 보관주기가 길수록 더 심각한 문제일 수 밖에 없습니다.

4. 테스트 시나리오 및 결과 요약


테스트를 위한 파라미터 설정과 테스트 시나리오는 다음과 같습니다.

4-1. 파라미터 설정 내역

  • autovacuum_freeze_max_age : 20만으로 설정
  • vacuum_freeze_min_age : 4만으로 설정
  • vacuum_freeze_table_age : 14만으로 설정 (이번 테스트에는 영향을 미치지 않음)

4-2. 테스트 시나리오

  • 파티션 테이블 생성
  • 1월 파티션에 20만건 입력
    • 20만건을 입력했으므로 모든 파티션 테이블 나이가 20만으로 증가함
    • 따라서 모든 파티션 테이블에 대해서 Autovacuum이 수행됨
  • 2월 파티션에 16만건 입력
    • 16만건을 입력했으므로 모든 파티션 테이블 나이가 20만으로 증가함
    • 따라서 모든 파티션(1월 포함) 테이블에 대해서 Autovacuum이 수행됨
  • 3월 파티션에 16만건 입력
    • 16만건을 입력했으므로 모든 파티션 테이블 나이가 20만으로 증가함
    • 따라서 모든 파티션(1월, 2월 포함) 테이블에 대해서 Autovacuum이 수행됨

4-3. 결과 요약 및 해결 방안
테스트 결과를 보면 이전에 Autovacuum을 수행했더라도 재 수행되며, 기존과 거의 유사한 IO 리소스를 사용하는 것을 알 수 있습니다. (그림-3~그림 5 참조) 아쉽게도 9.5까지는 이 문제를 해결할 수 있는 방법은 없습니다. 다만, 시스템의 TPS와 보관주기에 따라 적절히 큰 값으로 autovacuum 관련된 파라미터 값을 설정하는 정도입니다. 그리고 IO 부하가 집중되는 현상을 완화시키기 위해 autovacuum_vacuum_cost_delay (단위: 밀리세컨) 파라미터를 큰 값을 설정함으로써 autovacuum 프로세스가 IO를 사용하는 빈도를 낮출 수는 있습니다. 하지만 반대급부로 autovacuum 처리 시간이 길어지게 됩니다. 근본적인 해결을 위해서는 9.6부터 (2016년 9월 현재 베타 버전) 제공되는 Visibility map의 향상된 기능을 이용해야 합니다. 해당 내용은 별도 포스팅 예정입니다.

그림-3. 1월 파티션에 20만건 입력 및 Autovacuum 수행 후

pg2-4

그림-4. 2월 파티션에 16만건 입력 및 Autovacuum 수행 후

pg2-5

그림-5. 3월 파티션에 16만건 입력 및 Autovacuum 수행 후

pg2-6

5. 테스트 세부 내역


테스트 세부 내역은 다음과 같습니다.

5-1. 파라미터 설정

-- 파라미터 수정 (/opt/PostgreSQL/9.4/data/postgresql.conf)
autovacuum_freeze_max_age=200000 # <-- (2억)
vacuum_freeze_min_age=40000      # <-- (5천만)
vacuum_freeze_table_age=140000   # <-- (1억5천만)
log_autovacuum_min_duration=0    # <--(-1)
log_rotation_size=200MB          # <--(10MB)

5-2. 마스터 테이블 및 파티션 테이블 생성

-- 마스터 테이블 생성
create table p1
(
    c1       integer ,
    logdate  date,
    dummy    char(2000)
);

-- 파티션 테이블 생성
create table p1_y201601 (CHECK ( logdate >= DATE '2016-01-01' AND logdate < DATE '2016-02-01' )) inherits (p1); create table p1_y201602 (CHECK ( logdate >= DATE '2016-02-01' AND logdate < DATE '2016-03-01' )) inherits (p1); create table p1_y201603 (CHECK ( logdate >= DATE '2016-03-01' AND logdate < DATE '2016-04-01' )) inherits (p1); create table p1_y201604 (CHECK ( logdate >= DATE '2016-04-01' AND logdate < DATE '2016-05-01' )) inherits (p1); create table p1_y201605 (CHECK ( logdate >= DATE '2016-05-01' AND logdate < DATE '2016-06-01' )) inherits (p1); create table p1_y201606 (CHECK ( logdate >= DATE '2016-06-01' AND logdate < DATE '2016-07-01' )) inherits (p1); create table p1_y201607 (CHECK ( logdate >= DATE '2016-07-01' AND logdate < DATE '2016-08-01' )) inherits (p1); create table p1_y201608 (CHECK ( logdate >= DATE '2016-08-01' AND logdate < DATE '2016-09-01' )) inherits (p1); create table p1_y201609 (CHECK ( logdate >= DATE '2016-09-01' AND logdate < DATE '2016-10-01' )) inherits (p1); create table p1_y201610 (CHECK ( logdate >= DATE '2016-10-01' AND logdate < DATE '2016-11-01' )) inherits (p1); create table p1_y201611 (CHECK ( logdate >= DATE '2016-11-01' AND logdate < DATE '2016-12-01' )) inherits (p1); create table p1_y201612 (CHECK ( logdate >= DATE '2016-12-01' AND logdate < DATE '2017-01-01' )) inherits (p1);

5-3. 트리거 함수 및 트리거 생성

-- 트리거 함수 생성
CREATE OR REPLACE FUNCTION p1_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
    IF    ( NEW.logdate >= DATE '2016-01-01' AND NEW.logdate <  DATE '2016-02-01') THEN INSERT INTO p1_y201601 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-02-01' AND NEW.logdate <  DATE '2016-03-01') THEN INSERT INTO p1_y201602 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-03-01' AND NEW.logdate <  DATE '2016-04-01') THEN INSERT INTO p1_y201603 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-04-01' AND NEW.logdate <  DATE '2016-05-01') THEN INSERT INTO p1_y201604 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-05-01' AND NEW.logdate <  DATE '2016-06-01') THEN INSERT INTO p1_y201605 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-06-01' AND NEW.logdate <  DATE '2016-07-01') THEN INSERT INTO p1_y201606 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-07-01' AND NEW.logdate <  DATE '2016-08-01') THEN INSERT INTO p1_y201607 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-08-01' AND NEW.logdate <  DATE '2016-09-01') THEN INSERT INTO p1_y201608 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-09-01' AND NEW.logdate <  DATE '2016-10-01') THEN INSERT INTO p1_y201609 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-10-01' AND NEW.logdate <  DATE '2016-11-01') THEN INSERT INTO p1_y201610 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-11-01' AND NEW.logdate <  DATE '2016-12-01') THEN INSERT INTO p1_y201611 VALUES (NEW.*);     ELSIF ( NEW.logdate >= DATE '2016-12-01' AND NEW.logdate <  DATE '2017-01-01') THEN INSERT INTO p1_y201612 VALUES (NEW.*);
    ELSE
        RAISE EXCEPTION 'Date out of range.  Fix the p1_insert_trigger() function!';
    END IF;
    RETURN NULL;
END;
$$
LANGUAGE plpgsql;

-- INSERT 트리거 생성
CREATE TRIGGER insert_p1_trigger
    BEFORE INSERT ON p1
    FOR EACH ROW EXECUTE PROCEDURE p1_insert_trigger();

5-4. XID 증가를 위해 DBLINK를 이용한 LOOP COMMIT 프로시저 생성

-- insert_p1 프로시저 생성
CREATE OR REPLACE FUNCTION insert_p1(v_c1 integer, v_logdate date, v_dummy char) RETURNS VOID AS $$
BEGIN
       PERFORM dblink('myconn','INSERT INTO P1 VALUES ('||''''||v_c1||''''||','||''''||v_logdate||''''||','||''''||v_dummy||''''||')');
       PERFORM dblink('myconn','COMMIT;');
END;
$$ LANGUAGE plpgsql;

-- loop_insert_p1 프로시저 생성
CREATE or replace FUNCTION loop_insert_p1(v_logdate date, v_end integer) RETURNS VOID AS $$
BEGIN
    -- SIZE 증가를 위해 500만건 Bulk Insert
    insert into p1 select generate_series(1,5000000), v_logdate, 'dummy';   

    -- TXID 증가를 위해 LOOP 커밋
    FOR i in 1..v_end LOOP
        PERFORM insert_p1(i, v_logdate, 'dummy');
    END LOOP;
END;
$$ LANGUAGE plpgsql;

5-5. 익스텐션 설치

-- DBLINK 익스텐션 설치
create extension dblink;

5-6. 1월 파티션에 20만건 입력

-- 현재 TXID_CURRENT 확인
$ get_time.sh
 txid_current
--------------
         1908

-- 현재 테이블 나이 확인
$ get_info.sh
  relname   | age | relfrozenxid | reltuples |  size   | last_autoanalze | last_autovacuum
------------+-----+--------------+-----------+---------+-----------------+-----------------
 p1_y201601 |  18 |        1,891 |         0 | 0 bytes |                 |
 p1_y201602 |  17 |        1,892 |         0 | 0 bytes |                 |
 p1_y201603 |  16 |        1,893 |         0 | 0 bytes |                 |
 p1_y201604 |  15 |        1,894 |         0 | 0 bytes |                 |
 p1_y201605 |  14 |        1,895 |         0 | 0 bytes |                 |
 p1_y201606 |  13 |        1,896 |         0 | 0 bytes |                 |
 p1_y201607 |  12 |        1,897 |         0 | 0 bytes |                 |
 p1_y201608 |  11 |        1,898 |         0 | 0 bytes |                 |
 p1_y201609 |  10 |        1,899 |         0 | 0 bytes |                 |
 p1_y201610 |   9 |        1,900 |         0 | 0 bytes |                 |
 p1_y201611 |   8 |        1,901 |         0 | 0 bytes |                 |
 p1_y201612 |   7 |        1,902 |         0 | 0 bytes |                 |

-- 20만건 입력
select dblink_connect('myconn','dbname=test port=5434 user=postgres password=oracle');
select loop_insert_p1('2016-01-02 00:00:00', 200000);

-- 20만건 입력 후 TXID_CURRENT 확인
$ get_time.sh
 txid_current
--------------
      201,913

-- Autovacuum 수행 종료 후 테이블 나이 및 TXID_CURRENT() 확인
$ get_info.sh
  relname   |  age   | relfrozenxid |  reltuples  |  size   |   last_autoanalze   |   last_autovacuum
------------+--------+--------------+-------------+---------+---------------------+---------------------
 p1_y201601 | 40,002 |      161,914 | 5.19996e+06 | 380 MB  | 2016-09-06 09:59:03 | 2016-09-06 09:58:41
 p1_y201602 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201603 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201604 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201605 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201606 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201607 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201608 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201609 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201610 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201611 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30
 p1_y201612 | 40,002 |      161,914 |           0 | 0 bytes |                     | 2016-09-06 09:56:30

$ get_time.sh
 txid_current
--------------
      201,916

-- Autovacuum 수행 시간 확인
2016-09-06 09:55:00 KST LOG:  automatic analyze of table "test.public.p1_y201601" system usage: CPU 0.06s/0.28u sec elapsed 21.81 sec
2016-09-06 09:58:41 KST LOG:  automatic vacuum of table "test.public.p1_y201601": index scans: 0
        pages: 0 removed, 48599 remain
        tuples: 0 removed, 5200000 remain, 0 are dead but not yet removable
        buffer usage: 63807 hits, 33426 misses, 48606 dirtied
        avg read rate: 1.719 MB/s, avg write rate: 2.499 MB/s
        system usage: CPU 0.92s/1.80u sec elapsed 151.93 sec
2016-09-06 09:59:03 KST LOG:  automatic analyze of table "test.public.p1_y201601" system usage: CPU 0.00s/0.25u sec elapsed 22.62 sec

5-7. 2월 파티션에 16만건 입력

-- 16만건 입력
select loop_insert_p1('2016-02-02 00:00:00', 160000);

-- 16만건 입력 후 TXID_CURRENT() 확인
$ get_time.sh
 txid_current
--------------
      361,920

-- Autovacuum 수행 종료 후 테이블 나이 및 TXID_CURRENT() 확인
$ get_info.sh
  relname   |  age   | relfrozenxid |  reltuples  |  size   |   last_autoanalze   |   last_autovacuum
------------+--------+--------------+-------------+---------+---------------------+---------------------
 p1_y201601 | 40,001 |      321,921 |     5.2e+06 | 380 MB  | 2016-09-06 09:59:03 | 2016-09-06 10:19:27
 p1_y201602 | 40,001 |      321,921 | 5.16005e+06 | 377 MB  | 2016-09-06 10:21:38 | 2016-09-06 10:21:12
 p1_y201603 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201604 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201605 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201606 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201607 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201608 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201609 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201610 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201611 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14
 p1_y201612 | 40,001 |      321,921 |           0 | 0 bytes |                     | 2016-09-06 10:18:14

$ get_time.sh
 txid_current
--------------
      361,922

-- Autovacuum 수행 시간 확인
2016-09-06 10:17:06 KST LOG:  automatic analyze of table "test.public.p1_y201602" system usage: CPU 0.06s/0.30u sec elapsed 21.79 sec
2016-09-06 10:19:27 KST LOG:  automatic vacuum of table "test.public.p1_y201601": index scans: 0
        pages: 0 removed, 48599 remain
        tuples: 0 removed, 5200000 remain, 0 are dead but not yet removable
        buffer usage: 48618 hits, 48615 misses, 376 dirtied
        avg read rate: 3.706 MB/s, avg write rate: 0.029 MB/s
        system usage: CPU 0.17s/0.43u sec elapsed 102.48 sec
2016-09-06 10:21:12 KST LOG:  automatic vacuum of table "test.public.p1_y201602": index scans: 0
        pages: 0 removed, 48225 remain
        tuples: 0 removed, 5160000 remain, 0 are dead but not yet removable
        buffer usage: 63844 hits, 32640 misses, 48232 dirtied
        avg read rate: 1.331 MB/s, avg write rate: 1.966 MB/s
        system usage: CPU 1.37s/1.42u sec elapsed 191.65 sec

2016-09-06 10:21:38 KST LOG:  automatic analyze of table "test.public.p1_y201602" system usage: CPU 0.00s/0.27u sec elapsed 25.84 sec

5-8. 3월 파티션에 16만건 입력

-- 16만건 입력
select loop_insert_p1('2016-03-02 00:00:00', 160000);

-- 16만건 입력 후 TXID_CURRENT() 확인
$ get_time.sh
 txid_current
--------------
      521,925

-- Autovacuum 수행 종료 후 테이블 나이 및 TXID_CURRENT() 확인
$ get_info.sh
  relname   |  age   | relfrozenxid |  reltuples  |  size   |   last_autoanalze   |   last_autovacuum
------------+--------+--------------+-------------+---------+---------------------+---------------------
 p1_y201601 | 40,001 |      481,926 |     5.2e+06 | 380 MB  | 2016-09-06 09:59:03 | 2016-09-06 10:39:11
 p1_y201602 | 40,001 |      481,926 |    5.16e+06 | 377 MB  | 2016-09-06 10:21:38 | 2016-09-06 10:39:47
 p1_y201603 | 40,001 |      481,926 | 5.15997e+06 | 377 MB  | 2016-09-06 10:41:46 | 2016-09-06 10:41:23
 p1_y201604 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201605 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201606 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201607 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201608 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201609 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201610 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201611 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12
 p1_y201612 | 40,001 |      481,926 |           0 | 0 bytes |                     | 2016-09-06 10:39:12

$ get_time.sh
 txid_current
--------------
      521,927

-- Autovacuum 수행 시간 확인
2016-09-06 10:39:11 KST LOG:  automatic vacuum of table "test.public.p1_y201601": index scans: 0
        pages: 0 removed, 48599 remain
        tuples: 0 removed, 5200000 remain, 0 are dead but not yet removable
        buffer usage: 48618 hits, 48615 misses, 0 dirtied
        avg read rate: 2.834 MB/s, avg write rate: 0.000 MB/s
        system usage: CPU 0.37s/0.25u sec elapsed 133.99 sec
2016-09-06 10:39:47 KST LOG:  automatic vacuum of table "test.public.p1_y201602": index scans: 0
        pages: 0 removed, 48225 remain
        tuples: 0 removed, 5160000 remain, 0 are dead but not yet removable
        buffer usage: 48244 hits, 48240 misses, 376 dirtied
        avg read rate: 2.459 MB/s, avg write rate: 0.019 MB/s
        system usage: CPU 0.40s/0.28u sec elapsed 153.25 sec
2016-09-06 10:41:23 KST LOG:  automatic vacuum of table "test.public.p1_y201603": index scans: 0
        pages: 0 removed, 48225 remain
        tuples: 0 removed, 5160000 remain, 0 are dead but not yet removable
        buffer usage: 63734 hits, 32751 misses, 48232 dirtied
        avg read rate: 1.085 MB/s, avg write rate: 1.597 MB/s
        system usage: CPU 1.00s/1.75u sec elapsed 235.91 sec

2016-09-06 10:41:46 KST LOG:  automatic analyze of table "test.public.p1_y201603" system usage: CPU 0.00s/0.28u sec elapsed 22.23 sec

다음 포스팅:[PostgreSQL 9.6] Visibility Map 내의 FROZEN 비트를 이용한 Vacuum 성능 향상에 대한 고찰

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s