태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

'GarbageCollector'에 해당되는 글 2건

  1. 2007.09.04 [ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part7 (2)
  2. 2007.08.29 [ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part5 (3)

[ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part7

Enterprise Java 2007.09.04 15:32
[Enterprise Java는 거대한 동기화 머신이다]

IBM JVM

- Heap Management & Garbage Collection
IBM JVM의 Heap 메커니즘은 전통적으로 Sun HotSpot JVM과 매우 다른 방식을 사용해왔다. 간단하게 말하면, IBM JVM은 Generation 기법을 사용하지 않았다. 따라서 Young(New)과 Old(Tenured) Generation의 구분이 존재하지 않았다.

"않았다"라는 과거 시제를 사용한 것에 유의하자. IBM Java 5(JDK 1.5)부터는 Generation 알고리즘에 기반한 GC 기법을 제공한다. 이는 Sun HotSpot JVM 계열에서 보편적으로 검증된 방법론을 흡수한 것으로 이해할 수 있다.

Sun HotSpot JVM의 GC 기법을 다룬 블로그(http://blog.naver.com/ukja/120041944714)에서 성능을 보는 두가지 대표적인 관점인 [응답시간(Response Time)]과 [처리량(Throughput)]에 대해 언급한 바 있다. IBM JVM의 GC도 동일한 관점에서 분류된다. JDK 1.4 까지는 다음 세 개의 Garbage Collector가 제공되었다.

  • Throughput 최적화 Collector: -Xgcpolicy:optthruput 옵션으로 지정 (기본값).
  • Response Time 최적화 Collector: -Xgcpolicy:optavgpause 옵션으로 지정
  • SubPool Collector: -Xgcpolicy:subpool 옵션으로 지정. Heap을 여러 개의 Sub pool로 나누어 관리함으로써 성능 향상을 꾀한다. 16개 이상의 멀티 CPU 환경에서만 사용할 것을 권장한다.

JDK 1.5에서는 다음 Garbage Collector가 추가되었다.

  • Generational Concurrent Collector: -Xgcpolicy:gencon 옵션으로 지정. Sun JVM의 Generation 기법과 매우 유사하다.

- Mark and Sweep + Compaction
IBM JVM이 제공하는 기본 Collector는 Young/Old Generation의 구분을 사용하지 않기 때문에 Minor GC(copy), Major GC(mark and sweep)와 같은 구분 또한 존재하지 않는다. 대신 메모리를 정리하는 일련의 과정을 Mark and Sweep + Compaction으로 구분한다. Mark and Sweep 단계는 Sun JVM에서도 이미 언급된 바 있다. 이른바 Stop the World 작업으로 Application Thread를 멈춘 상태에서 Alive Object를 Mark하고(Dead Object를 찾고), 지우는 작업을 한다. IBM JVM은 Mark and Sweep 단계를 매우 "가벼운" 작업으로 간주한다. 비록 이 단계가 Sun JVM의 Major GC와 비슷한 속성을 지니고 있지만 Major GC에 비해서는 단순한 작업으로 구현되어 있다.

Sun JVM의 Major GC와 가장 비슷한 작업은 Compaction 단계에서 발생한다. Mark and Sweep으로 메모리 정리를 한 후에도 오브젝트 할당에 필요한 여유 메모리를 찾지 못하면 Compaction, 즉 압축이 발생한다. Mark and Sweep 작업이 단순히 Dead Object를 정리하는 작업인데 반해 Compaction 작업은 흩어진 프리 메모리를 합치는 작업을 하기 때문에 Mark and Sweep 단계에 비해 많은 시간을 필요로 한다. 다행히 일반적으로 Compaction 작업은 드물게 발생한다.

Mark and Sweep 단계를 거쳤음에도 불구하고 오브젝트 할당에 필요한 메모리를 찾지 못하는 이유는 단편화(Fragmentation)에 있다. IBM JVM의 Mark and Sweep은 Compaction 작업을 하지 않기 때문에 프리 메모리가 여기 저기에 흩어지는 현상이 생기게 된다. 이런 현상을 단편화라고 부른다. 가령 1M의 프리 메모리가 연속되지 않은 10K 청크 100개로 흩어져 있다고 가정해보자. 이 경우 비록 총 1M의 프리 메모리가 존재하지만 20K의 연속된 메모리는 할당될 수없다. 이런 경우에는 Compaction 작업이 추가로 발생하게 된다.

Sun HotSpot JVM에서는 Major GC를 최적화하는 것이 Heap 튜닝의 대표적인 기법이듯이, IBM JVM에서는 Compaction을 줄이는 것이 Heap 튜닝의 대표적인 기법이다.

---(참조)-----------------------------------------------------------------------------------
IBM이 제시하는 Heap 튜닝 기법중 하나가 길이가 긴 Array를 사용하지 말라는 것이다. 이 기법의 근거가 바로 Heap의 단편화에 있다. Application이 일정 시간 동작하고 나면 필연적으로 단편화가 발생한다. 이럴 때 길이가 긴 Array를 사용하면 연속된 메모리 공간을 할당받지 못해 Compaction 작업이 발생하게 된다. 따라서 효과적인 자료 구조를 사용해서 작은 크기의 Array를 여러 개 사용할 것을 권장하고 있다.
---------------------------------------------------------------------------------------------

- Mark and Sweep과 Garbage Collector
Throughput 최적화 Collector는 말 그대로 Mark and Sweep+Compaction 기법을 사용한다. 주기적으로 Mark and Sweep이 발생하고 필요한 경우 Compaction을 통해 메모리 병합을 시도한다.

이러한 일련의 GC 작업은 Application Thread를 완전히 멈춘 상태에서 진행되기 때문에 GC 작업 자체는 가장 최적화된다. 그 만큼 처리량은 높아지지만, Pause Time이 길어지면서 Response Time이 길어지는 단점이 생긴다.

Response Time 최적화 Collector는 Mark and Sweep+Compaction 기법에 약간의 변화를 가한다. Mark and Sweep 단계를 되도록이면 Application Thread를 멈추지 않는 상태에서 Concurrent 하게 진행한다. 즉 Concurrent Mark 단계와 Concurrent Sweep 단계를 추가로 두어서 Mark and Sweep에 의한 Pause Time을 최소화한다. Concurrent Mark와 Concurrent Sweep 단계는 Application Thread와 같이 동작하며, 그 만큼 Applicaiton Thread의 CPU 자원을 소모한다. 따라서 Throughput 최적화 Collector에 비해 Throughput이 다소 떨어질 수 있다.

---(참조)----------------------------------------------------------------------------------
IBM JVM의 Response Time 최적화 Collector의 GC 수행 기법이 Sun JVM의 Low Pause Collector(CMS Collector)와 매우 유사한 것은 우연이 아니다. 두 Vendor가 최신 Garbage Collection 기법을 서로 차용하고 개발하는 과정에서 자연스럽게 발생하는 공명 현상이다
--------------------------------------------------------------------------------------------

- Global GC와 Scavenger GC
IBM JVM에서는 Minor GC와 Major GC라는 분류법은 존재하지 않는다. 대신 Global GC와 Scavenger GC라는 분류법이 존재한다. Throughput 최적화 Collector(optthruput)와 Response Time 최적화 Collector(optavgpause)가 행하는 GC는 무조건 Global GC이다. 즉 Sun JVM의 관점에서 보면 항상 Major GC를 수행하고 있는 셈이다. 하지만 Compaction이 일어날 때만 진정한 의미에서 Major GC와 같다고 할 수 있다.

IBM JDK 1.5에서 추가된 Generational Concurrent Collector(gencon)에서는 Scavenger GC가 Minor GC의 역할을 하고 Global GC가 Major GC의 역할을 한다. Scavenger GC는 Mark and Sweep 방식이 아닌 Copy 방식(Alive Object를 Allocate Space에서 Survivor Space로 복사하는 것을 의미)을 사용하며 Global GC에서 Mark and Sweep+Compaction을 사용한다.

- IBM JVM의 장점
IBM JVM에 제공하는 기본 Collector(optthruput, optavgpause)의 장점은 안정된 GC 패턴이다. Sun JVM이 제공하는 Generation 기법이 훨씬 지능적이고 효과적인 것은 부인할 수 없는 사실이다. 특히 튜닝을 통해 Major GC를 최소화하고 Minor GC를 최적화했다면 GC에 의한 성능 저하 현상을 대부분 피할 수 있다. 하지만 Major GC에 의한 Spike 현상(갑자기 GC Pause Time이 급증하는 현상)은 항상 고질적인 문제로 남아 있다. Minor GC 시에는 안정된 패턴을 보이다가도 Major GC가 발생할 때 수초 ~ 수십초까지 GC Pause가 발생한다면 성능에 미치는 영향은 치명적일 수 있다.

반면 IBM JVM에서는 상대적으로 Compaction의 발생 빈도가 높지 않기 때문에 전반적으로 안정적인 패턴을 보인다. 이 말은 역설적으로 IBM JVM에서 Compaction이 최적화되고, Sun JVM에서 Major GC가 최적화되면 둘 사이에는 큰 성능 차이가 없을 것이라는 것을 암시하기도 한다.

(위의 말이 IBM JVM이 항상 안정적이라는 말은 아니며, Sun JVM에 비해 성능이 뛰어나다는 의미는 더더욱 아니다)

- IBM JVM의 단점
Sun JVM이 New/Old Generation의 크기, Survivor Ratio 등의 조정을 통해 세밀한 튜닝이 가능한 반면, IBM JVM에서 optthruput이나 optavgpause를 사용할 경우에는 튜닝에서의 세밀함이 부족한 편이다.

JDK 1.5에서 제공하는 gencon Collector를 쓸 경우에는 Sun JVM과 거의 비슷한 설정이 가능하다. 하지만 튜닝가능한 옵션 수는 비교적 작은 편이다.

(하지만 역설적으로 튜닝 옵션이 적다는 것은 더 자동화되어 있고 편리하다는 의미이기도 하다)


- IBM JVM의 GC 관련 중요 옵션들

-Xms, -Xmx: Heap의 최소(시작)크기, 최대 크기를 결정한다.

-Xgcpolicy: : optthruput|optavgpause|gencon|subpool. Garbage Collector의 종류(
따라서 Heap 관리의 종류)를 결정한다.

-Xdisableexplicitgc: System.gc()에 의한 GC를 비활성화한다.

-Xgcthreads: Parallel GC 작업을 할 Thread 개수를 지정한다. Default = CPU#-1. 만
일 여러 프로세스가 CPU를 나누어 쓰는 환경이라면 이 값을 낮출 필요가 있다.

-Xloa: LOA(Large Object Area)를 사용할 지의 여부를 결정한다. Default는 활성화상태이다.

-Xloainitial, -Xloamaximum, -Xloaminium: LOA의 초기 크기, 최대크기, 최소 크기를 지정한다. 0 ~ 1 사이의 값을 지정한다.

-Xmaxe, -Xmine: Heap expansion이 증가할 최대/최소 크기를 지정한다.

-Xmaxf, -Xminf: Heap 크기를 조정할 Free Memory의 비율을 결정한다. Default 값은 0.6(60%), 0.3(30%)이다. 즉, Free Memory가 전체 Memory의 60%이상이 되면 Heap Shrinkage가 발생하고, 전체 Memory의 30% 이하이면 Heap Expansion이 발생한다.

-Xmn, -Xmns, -Xmnx: Generational Concrreunt Collector를 사용할 경우 New(Nursery) Generation의 크기(최대/최소를 동일하게), 최소(시작)크기, 최대크기를 지정한다.

-Xmo, -Xmos, -Xmox: Generational Concurrent Collector를 사용할 경우 Old(Tenured) gEneration의 크기(최대/최소를 동일하게), 최소(시작)크기, 최대크기를 지정한다.

-Xpartialcompactgc: Incremental Compaction을 활성화한다. 만일 Compaction에 의한 성능 저하가 발생한다고 판단되면 이 옵션을 사용해본다.

-Xsoftrefthreshold: Soft Reference 객체를 몇 번째 GC Cycle에 해제할지의 여부를 결정한다. Default는 32이다. 즉, 32번째 GC Cycle까지 Soft Reference가 참조하고 있는 객체가 Mark되지 않았다면(즉 Live Object로 판단되지 않았다면) 메모리에서 해제된다. 이 값을 낮춤으로써 프리 메모리가 좀더 빨리 확보되도록 할 수 있다. 하지만 그 만큼 Soft Reference의 효율성은 떨어진다.

---(참조)--------------------------------------------------------------------------------
Reference 객체의 의미는 다음 문서에 잘 설명되어 있다. http://java.sun.com/developer/technicalArticles/ALT/RefObj/

Reference 객체는 Application 개발자가 Garbage Collector의 행동 양식에 영향을 줄 수 있는 유일한 방법이다. 대용량의 메모리를 사용하는 큰 Application 작성자라면 반드시 Reference 객체를 효과적으로 사용할 수 있어야 한다.
------------------------------------------------------------------------------------------
-verbosegc: GC 로그를 남긴다.

-Xverbosegclog: : GC 로그를 특정 파일명으로 남긴다.

간단한 성능 테스트
아래 결과는 세 가지 주요 Collector(optthruput, optavgpause, gencon)의 성능을 비교 테스트한 결과이다.

Case1: Heap Size = 512M, -Xgcpolicy:optthruput
23999893 allocated
Global GC = 22
Scavenger GC = 0
Total GC Time = 7.967951
Avg GC Time = 0.362180

Case2: Heap Size = 512M, -Xgcpolicy:optavgpause
21612553 allocated
Global GC = 23
Scavenger GC = 0
Total GC Time = 0.915822
Avg GC Time = 0.039818

Case3: Heap Size = 512M, -Xgcpolicy:gencon
14321989 allocated
Global GC = 4
Scavenger GC = 122
Total GC Time = 24.730480
Avg GC Time = 0.196274

위의 결과를 해석해보면
  • 처리량은 optthruput 옵션을 사용한 경우에 가장 뛰어나다. 이것은 예상된 결과이다.
  • GC에 의한 Pause Time은 optavgpause인 경우에 가장 낮다. 이것 역시 예상된 결과이다. IBM JVM의 GC가 매우 안정적이고 예상 가능한 방식으로 작동하는 것을 확인할 수 있다.
  • gencon을 사용한 경우 GC Pause Time은 optthruput에 비해 개선되지만 처리량이 지나치게 낮아지는 것을 알 수 있다. 그 이유는 첫째, gencon을 사용할 경우 약간의 옵션 튜닝이 필요할 수 있다는 것과 현재의 Application 패턴이 gencon에는 맞지 않다는 것이다.
J2EE 환경의 복잡한 Application에서는 gencon을 사용한 경우 더 우수한 성능을 나타낸다는 테스트 결과가 있다. 아래 테스트 결과는 복잡한 J2EE Application에서 optthruput과 gencon 옵션을 사용한 경우의 GC 패턴을 보여주고 있다.

파란색 = optthruput, 녹색 = gencon




위의 결과를 보면 처리량면에서도, Pause Time 면에서도 gencon을 사용한 경우가 더 우수한 성능을 보이는 것을 확인할 수 있다.(단 gencon을 사용한 경우 Global GC에 의한 Spike 현상이 나타난 것을 알 수 있다)


아래 결과는 optthruput을 사용하면서 Heap Size를 조정한 경우의 테스트 결과이다.

Case1: Heap Size = 256M, -Xgcpolicy:optthruput
13000597 allocated
Global GC = 32
Scavenger GC = 0
Total GC Time = 15.830381
Avg GC Time = 0.494699

Case2: Heap Size = 1024M, -Xgcpolicy:optthruput
24214091 allocated
Global GC = 23
Scavenger GC = 0
Total GC Time = 9.089497
Avg GC Time = 0.395196

위의 결과를 보면 Heap Size가 큰 경우가 그렇지 않은 경우에 비해 탁월한 성능 개선 효과가 있는 것을 확인할 수 있다. 또한 Sun JVM에서 볼 수 있었던 Heap Size의 크기와 New/Old Generation의 크기에 의한 미묘한 성능 차이 현상이 발생하지 않고 안정적인 패턴을 보이는 것을 알 수 있다. 이러한 안정적인 패턴이 IBM JVM의 가장 큰 장점이다.

---------------------------------------------------------------------------------------------------
다음 글에 계속...

(PS) 애초에 걱정한 것처럼 점점 용두사미로 전락하고 있다... ㅠㅠ







신고
Trackback 0 : Comments 2
  1. moncler 2013.01.04 15:01 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

  2. moncler españa 2013.01.05 16:32 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment


[ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part5

Enterprise Java 2007.08.29 11:22
Enterprise Java는 거대한 동기화 머신이다.

Garbage Collection과 Thread 동기화
Enterprise Java 환경에서 성능에 가장 많은 영향을 미치는 동기화(Synchronization) 문제가 바로 Garbage Collection(GC)이다. WAS 환경을 구축하거나 운영해본 경험이 있는 사람이라면 누구나 GC 문제에 민감할 것이다. 어쩌면 치를 떨지도...?

GC가 발생하는 동안 Application Thread는 어떤 식으로든 작동을 멈추거나 지연이 발생하게 된다. 이런 멈춤(Pause)과 지연이 과도하면 Application의 성능에 치명적인 영향을 미치게 된다. Web Server-->WAS-->DB 로 이루어지는 복잡한 시스템에서는 WAS에서의 GC로 인한 작업 지연이 연쇄 작용을 일으켜 시스템 전체의 성능 저하를 유발할 수 있다.

일반적으로 GC에 의한 성능 저하 문제를 "동기화 문제"로 인식하는 경우가 잘 없기 때문에 "동기화"라는 용어에 고개를 갸우뚱할 지 모르겠다. 필자는 "GC"를 메모리라는 자원을 둘러싼 Thread 동기화의 문제로 보는 것이 가장 정확한 해석이라고 생각한다. Thread가 특정 메모리 영역을 사용하려고 하는 시점에 해당 메모리 영역에 대해 GC 작업이 발생하게 되면 Thread가 블로킹(Blocking)되면서 성능 저하 현상이 발생하는 것이 우리 겪는 GC의 성능 문제이다.

Garbage Collection와 JVM
JVM은 Sun이 정하는 표준이다. 하지만 GC 자체는 표준이 아니다. 다만, Java와 같이 메모리를 핸들링하는 API를 제공하지 않는 어플리케이션에서는 자동화된 메모리 해제 작업이 필요하기 때문에 Garbage Collection 기능이 필수가 된 것 뿐이다.

GC가 표준이 아니기 때문에, GC를 구현하는데 있어서 어떤 알고리즘을 쓸지 또한 전혀 약속된 바가 없다. 각 JVM 벤더들이 자신들 고유의 알고리즘을 통해 GC를 구현한다. 앞으로 몇 차례의 글을 통해 Sun HotSpot JVM과 IBM JVM의 GC의 작동방식과 장단점에 대해 깊이있는 논의를 하게 될 것이다.

Java의 메모리(Non-Heap + Heap) 관리 기법과 GC 알고리즘은 실과 바늘같은 관계이다. GC를 어떻게 할지에 따라 메모리 관리 기법이 결정된다는 것이 정확한 표현일 것이다.

Sun HotSpot JVM과 IBM JVM은 전혀 다른 종류의 GC 기법을 구현해왔다. 최근(특히 Java 5부터)에 와서는 서로의 장점을 흡수해서 상당히 비슷한 개념들을 제공하지만, 여전히 뚜렷한 차이점들을 보인다.

각 JVM이 제공하는 GC의 작동 방식과 장단점을 정확하게 이해해야만 합리적인 성능 개선이 가능하다.

GC의 성능을 보는 두가지 관점
[성능을 개선시킨다]라는 말은 매우 추상적이고 기준이 애매모호하다. 따라서 좀 더 정확한 기준을 정할 필요가 있다. 여기에는 무수히 많은 방법론과 기준이 있겠지만, 보편적으로는 다음과 같은 두 가지의 기준을 사용한다.
  • Throughput 개선: Throughput은 흔히 "처리량"으로 해석된다. 즉 주어진 시간내에 얼마나 많은 일을 하느냐가 곧 Troughput이다.
  • Response Time 개선: Response Time은 "응답시간"으로 해석된다. 즉 특정 요청에 대해 얼마나 빨리 응답을 보내느냐가 Repsonse Time의 기준이다.
Throughput과 Response Time은 상호 배반적인 속성을 지니고 있다. Throughput을 개선시키려면, 즉 주어진 시간내에 최대한 일을 많이 하려면 되도록 통신량을 줄이고 일을 모아서 처리할 수 밖에 없다. 이런 이유로 Throughput을 개선시키기 위해서는 Batch 프로세싱을 사용할 수 밖에 없다.

반면 Response Time을 개선시키려면 작업을 처리하면서 결과가 나오는 즉시 결과를 보내주어야 한다. 이런 이유로 Response Time을 개선시키려면 Looping 프로세싱을 사용할 수 밖에 없다.

(참고) 오라클과 같은 DBMS에서도 동일한 원리가 적용된다. 오라클의 OPTIMIZER_GOAL ALL_ROWS라면 Throughput 기준의 최적화를, FIRST_ROWS라면 Response Time 기준의 최적화를 수행하게 된다. 오라클의 PL/SQL 블록은 항상 수행단위가 블록이기 때문에 예외없이 Throughput 기준의 최적화가 수행된다. 오라클에서도 이 원리를 잘 이해해야만 최적의 쿼리 튜닝이 가능하다.

두 가지 종류의 Garbage Collector
위에서 설명한 이유때문에 어떤 JVM의 Garbage Collector라도 항상 두 가지 종류 이상(Throughput 기준 하나+Response Time 기준 하나)를 제공한다.

Sun HotSpot JVM
IBM JVM
Througput 기준
-XX:+UseParallelGC-Xgcpolicy:optthruput
Response Time 기준
-XX:+UseConnMarkSweepGC
-Xgcpolicy:optavgpause
기타-XX:+UseSerialGC-Xgcpolicy:gencon

Throughput 기준의 Garbage Collector는 공격적으로 GC를 수행한다. 사용자가 체감하는 응답 시간을 다소 희생하더라도 GC 작업 자체를 최대한 효율적이고 집중적으로 수행할 수 있도록 한다. 따라서 GC 작업 시간이 다소 길 수 있고 이로 인해 Thread의 장시간 동기화 및 응답 시간의 지연을 초래하게 된다.

반면 Response Time 기준의 Garbage Collector는 다소 소극적인 GC를 수행한다. GC를 수행하는 과정에서 Thread가 동기화되고 이로 인해 사용자가 대기하는 시간을 최소화하기 위해서 GC 작업 자체에 대한 자원 소모를 줄이면서 오랜 시간에 걸쳐 나누어서 하게 된다. 하지만 그 만큼 비효율적이고 장기적으로는 사용자의 작업을 저해하는 결과를 낳을 수 있다.

각 GC가 제공하는 기능을 정확하게 이해해야만 Application의 요구 사항에 따라 적절한 GC를 선택할 수 있다.

-----------------------------------------------------------------------------------------

다음 글에 계속...




신고
Trackback 0 : Comments 3
  1. 멀더엄마 2007.08.30 17:21 신고 Modify/Delete Reply

    GC 알고리즘도 중요하지만, WAS 인스턴스에 할당하는 메모리 사이즈도 중요한듯.
    메모리 사이즈를 크게주면, Full GC하는 간격이 늘어나지만 그만큼 Full GC 시간이 많이 걸리니 그시간동안 Thread 가 대기하게되는 문제가 있고,
    메모리 사이즈를 작게하면, Full GC 발생 주기가 짧아지는대신 GC 하는 시간이 줄어들어 그만큼 짧은시간동안만 Thread가 대기하게됨.

    WAS 인스턴스 갯수나 request 수 등을 고려해서 적당하게 메모리를 잡으면 될듯. 보통 1~2기가로 잡아주는것같음(???)
    다 ... 아는 얘긴가?

  2. 욱짜 2007.08.30 17:44 신고 Modify/Delete Reply

    Heap Size와 함께 Heap를 구성하는 Young(Eden+Survivor), Tenured Space의 크기도 중요... Part6을 참조

  3. 카이스턴 2007.09.13 11:35 신고 Modify/Delete Reply

    공부중이라 좋은 자료 발견해서 비밀글로 퍼갑니다~

Write a comment

티스토리 툴바