메이븐은 빌드 시 pom.xml에 기술된 의존관계를 분석하여, 빌드에 필요한 jar 파일을 자동으로 아티팩트 레포지토리에서 다운로드 받아, 사용자의 로컬 레포지토리에 설치해줍니다. 그런데 가끔은 jar 파일을 수동으로(커맨드라인에서) 다운받고 싶을 때가 있습니다.

당연히 이런 기능을 제공하는 메이븐 플러그인이 있을 것으로 짐작하고 찾아보지만 잘 안보입니다. 좀더 집요하게 검색해보니 2개 정도 나옵니다.

1. http://www.agaetis.fr/public/maven-download-plugin/

2. http://maven.apache.org/shared/maven-downloader/

1번 플러그인은  mvn download:download -Dartifact=group:artifact:version:jar 식으로 실행하면 된다고합니다. 하지만 이 플러그인은 메이븐 메인레포지토리에 아직 등록되지 않았고, http://mvnrepository.com/ 같은 플러그인 검색사이트에서 검색해도 안나오는걸 보면 다른 공공(?) 레포지토리에도 등록하지 않은 듯합니다. 게다가 JDK6을 요구하기 때문에 그 이하 버전의 JDK 환경(저처럼)에서는 실행되지 않는 문제가 있습니다.

2번 플러그인은 메이븐 공식 플러그인 같기도합니다만 실행가능한 goal 등 어떻게 사용하면 되는지 설명이 전혀 없습니다. 그리고 실제로 mvn org.apache.maven.shared:maven-downloader:download 이렇게 실행해보면 일단 플러그인은 다운로드가 되지만 PluginDescriptor 에러가 납니다.

java.lang.IllegalStateException: The PluginDescriptor for the plugin Plugin [org.apache.maven.shared:maven-downloader] was not found.

에혀 이런저런 삽질이 모두 물거품이 되었네요. 구관이 명관인가요. 결국 아티팩트 다운로드 전용 pom.xml을 만드는 것으로 합의(?)를 봤습니다.

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>download</groupId>
     <artifactId>download</artifactId>
     <version>1.0</version>
     <packaging>jar</packaging>
     <name>DOWNLOAD ARTIFACTS</name>
     <description>for artifact downloads(다운로드 받을 라이브러리 지정후 mvn dependency:resolve 실행)</description>

     

  2.  <dependencies>
      <dependency>
       <groupId>다운로드 받을 라이브러리의 groupId</groupId>
       <artifactId>다운로드 받을 라이브러리의 artifactId</artifactId>
       <version>다운로드 받을 라이브러리의 version</version>
      </dependency>
     </dependencies>
  3. </project>

이렇게 pom.xml을 만들어두고 필요시마다 goupId, artifactId 등을 수정하여 mvn dependency:resolve 를 실행하면 됩니다. 테스트해보니 잘되네요. 이정도에서 만족해야겠습니다. ^^;

이 글은 스프링노트에서 작성되었습니다.

신고
Posted by 에코지오

댓글을 달아 주세요

Maven은 의존성 관리나 내재화된 빌드프로세스 같은 특징말고도 간단한 설정으로 여러가지 리포트를 생성해주는 좋은 기능을 가집니다. 뿐만아니라 pmd, findbugs, junit-report, cobertura 등의 코드분석 라이브러리가 생성하는 리포트들을 프로젝트 정보와 함께 하나의 사이트로 합쳐서 만들어줍니다. 아래는 메이븐 리포트 설정 예제입니다. 사실 default 설정을 따른다면 더 간단해질 수 있습니다.

 <reporting>
  <plugins>
   <!-- FindBugs 리포트 생성 플러그인 -->
   <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>findbugs-maven-plugin</artifactId>
    <version>1.2</version>
    <configuration>
     <threshold>Low</threshold
     <xmlOutput>true</xmlOutput>
    </configuration>
   </plugin>
   <!-- PMD 리포트 생성 플러그인 -->
   <plugin>
    <artifactId>maven-pmd-plugin</artifactId>
    <configuration>
     <rulesets>
      <ruleset>/rulesets/basic.xml</ruleset>
      <ruleset>/rulesets/unusedcode.xml</ruleset>
     </rulesets>
     <sourceEncoding>UTF-8</sourceEncoding>
     <targetJdk>1.5</targetJdk>
    </configuration>
   </plugin>
  ... ....
 </reporting>

Ant에서 이와 동일한 일을 처리하려면 각각의 코드분석 라이브러리에 대한 복잡한 XML 설정에다가, 결과 리포트들을 하나로 합치기까지 하고 싶다면 별도의 라이브러리(Glean, QALabXRadar 등)를 사용해야 하는 수고가 따릅니다.

Buildr도 여러가지 코드분석 리포트를 생성할 수 있습니다만, 아직은 좀 약합니다. 사실 내부적으로 리포트 생성은 AntWrap을 통해서 Ant 타스크를 호출하는 식으로 처리하고 있습니다. Ant 위에 얹혀살고 있는 셈이죠.

일단 Buildr 1.3.3은 junit, jdepend, cobertura, emma에 대한 리포트 생성 능력을 보유하고 있습니다.

- junit:report
- cobertura:xml, cobertura:html
- emma:xml, emma:html
- jdepend:xml, jdepend:text, jdepend:swing

1. JUnit / TestNG
Buildr 1.3.3에서는 기본적으로 junit 4.4 버전으로 테스트를 수행하고 리포트를 생성합니다. 테스트케이스 소스는 src/test/java에 있다고 가정합니다. 리포트는 ./reports/junit 아래에 생성됩니다. 경로는 Layout을 이용하면 바꿀 수 있습니다.(그러나 리포트 생성 디렉토리가 변경되지 않는 버그가 있네요).
테스트를 JUnit이 아니라 TestNG로 하고 싶으면 빌드스크립트에 test.using :testng 라고 한줄 써주면 됩니다. 쉽죠.
그외 다양한 테스트 옵션에 대한 내용은 여기여기를 참조하세요.

2.Cobertura / Emma
우선 buildfile에 require 'buildr/cobertura'(또는 require 'buildr/emma')를 선언합니다. 그러면 이제부터 빌드시 instrument 작업이 자동으로 함께 수행됩니다. 커버리지 리포트는 reports/cobertura(또는 reports/emma)에 만들어집니다.

3. JDepend
require 'buildr/jdepend'를 선언하고 jdepend:xml을 실행하면 ./jdepend.xml 파일이 생성됩니다. 생성된 xml을 jdepend.xsl 스타일을 적용하여 html로 바꾸는게 어려워보이지는 않은데 아직 html 변환까지는 지원하지 않네요.

그외 pmd와 findbugs는 제가 직접 만들어봤습니다. 자세한 실행 옵션은 코드를 보면 응용할 수 있을 겁니다.

4. PMD

task :pmd do
  pmd_classpath = transitive('pmd:pmd:jar:4.2.1').each(&:invoke).map(&:to_s).join(File::PATH_SEPARATOR)
  ant("pmd-report") do |ant|
    ant.taskdef :name=> 'pmd',
                :classpath=>pmd_classpath,
                :classname=>'net.sourceforge.pmd.ant.PMDTask'
    ant.pmd :rulesetfiles => 'basic,imports,unusedcode' do
      ant.formatter :type=>'xml', :toFile=> _(:reports, 'pmd.xml')
      #ant.formatter :type=>'html', :toFile=> _(:reports, 'pmd.html')
      compile.sources.each do |src|
        ant.fileset :dir=> src, :includes=>'**/*.java'
      end
    end
  end
end

5. FindBugs

  task :findbugs do
    findbugs_classpath = transitive('net.sourceforge.findbugs:findbugs-ant:jar:1.3.2').each(&:invoke).map(&:to_s).join(File::PATH_SEPARATOR)
    ant("findbugs-report") do |ant|
      ant.taskdef :name=> 'findbugs',
                  :classpath=>findbugs_classpath,
                  :classname=>'edu.umd.cs.findbugs.anttask.FindBugsTask'
      ant.findbugs :home => 'C:/findbugs-1.3.6', # 파인드버그 설치 경로
                   :reportLevel=>'high',
                   :output=>'xml',
                   :outputFile=>_(:reports, 'findbugs.xml') do
        ant.method_missing(:class, {:location=> compile.target})
      end
    end
  end

메이븐은 maven site 명령으로 한큐에 모든 코드분석작업 및 리포트 생성을 처리할 수 있습니다. Buildr은 이런 타스크가 없기 때문에 하나 만들어줍니다.

task :reports =>[:pmd, :findbugs, 'jdepend:xml', 'cobertura:xml', 'junit:report']

이제 'buildr 프로젝트명:reports' 명령으로 완벽하진 않지만 우리도 한큐에 코드분석을 처리할 수 있게 됐습니다. 떨궈진 xml 파일들은 Hudson 같은 CI툴과 연동하면 메이븐처럼 예쁘게 볼 수 있겠죠.

신고
Posted by 에코지오

댓글을 달아 주세요

Maven은 빌드에 필요한 라이브러리를 아래와 같이 설정합니다.

 <dependencies>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>3.8.2</version>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>commons-fileupload</groupId>
   <artifactId>commons-fileupload</artifactId>
   <version>1.1.1</version>
  </dependency>
  <dependency>
   <groupId>commons-lang</groupId>
   <artifactId>commons-lang</artifactId>
   <version>2.1</version>
  </dependency>
  ... ...
 </dependencies>

Buildr에서는 compile.with 메소드에 라이브러리 목록을 넘겨주어 의존성을 추가할 수 있습니다.

compile.with "org.apache.axis2:axis2:jar:1.2", "commons-fileupload:commons-fileupload:jar:1.1.1"

compile.with 메소드가 받아들이는 의존 라이브러리의 몇가지의 타입이 존재합니다.

1. 아티팩트(artifact) 스펙
위의 메이븐 pom에서 xml 형식으로 지정한 coordinates를 다음의 형식으로 지정합니다.

groupId:artifactId:packaging:version
groupId:artifactId:packaging:classifier:version

* 위의 형식 뿐 아니라 다양한 포맷으로 스펙을 지정할 수 있습니다. 여기를 참조하세요.

이렇게 아티팩트 스펙 형식으로 의존성을 추가하면 Buildr은 메이븐 레포지토리에서 해당 라이브러리를 다운받아 로컬에 설치합니다.

2. 다른 프로젝트
만일 다른 프로젝트의 패키징 결과물에 의존하고 있다면 그 프로젝트를 넘겨주면 됩니다.

compile.with projects('another-project', 'other-project')

3. 파일/디렉토리 경로
로컬에 존재하는 파일/디렉토리의 경로를 지정하여 의존성을 추가할 수 있습니다.
메이븐에서는 로컬 파일을 의존성에 추가하기 위해서 스코프를 system으로 잡고 파일의 절대경로를 지정하였습니다.

  <dependency>
   <groupId>weblogic</groupId>
   <artifactId>weblogic-api</artifactId>
   <version>10.0</version>
   <scope>system</scope>
   <systemPath>C:/bea/weblogic/xxx/wls-api.jar</systemPath>
  </dependency>

Buildr에서는 프로젝트에 대한 상대경로 또는 절대경로 모두 가능합니다.

compile.with 'C:/bea/weblogic/xxx/wls-api.jar' , 'lib/my-api.jar',

경로 지정시 path_to 메소드를 활용하는 것이 좋습니다.

compile.with path_to(:source,:main,:webapp,'WEB-INF/lib/my-api.jar')

또한 glob 패턴을 적용하여 파일을 지정할 수 있습니다. 메이븐도 이런게 되면 참 좋겠습니다.

compile.with FileList[path_to(:source,:main,:webapp,'WEB-INF/lib/*.jar'), 'lib/**/*.jar']

여기서 Rake::FileList를 사용했습니다. FileList 없이 그냥 패턴 문자열을 그대로 compile.with 메소드에 넘겨줄 수도 있습니다. 그러나 그렇게 되면 패턴 문자열이 해석없이 그대로 compile.dependencies에 할당되기 때문에 (단위테스트 등에서) 예상치못한 문제가 발생할 수 있습니다. 그래서 FileList를 통해 패턴을 해석한 뒤에 compile.with에 넘겨주는 것을 권장합니다.

Buildr에서는 놀랍게도 파일뿐 아니라 디렉토리도 의존성에 추가하는게 가능합니다.

compile.with '../AAA/classes'

다만, 이렇게 디렉토리를 의존성에 추가하고 war로 패키징하게 되면 war에 그 디렉토리가 포함이 됩니다. 이때는 그 디렉토리를 아래처럼 패키지 libs에서 빼주면 됩니다.

DEPEND_CLASSES =  '../AAA/classes'
compile.with DEPEND_CLASSES
package(:war).libs -= artifacts(DEPEND_CLASSES)

신고
Posted by 에코지오

댓글을 달아 주세요

  1. BlogIcon [짱가™] 2008.11.20 11:31 신고  댓글주소  수정/삭제  댓글쓰기

    멋진 강좌들이에요.. ^^
    나중에 좋은 참고가 될것 같아요..
    루비부터 공부해야 하나... ( 한 일주일 공부하다가 놔버렸네요.. ㅡㅡ;; )

    그루비가 더 땡기는데..
    이것참.. 맘만 굴뚝이고

    • BlogIcon 에코지오 2008.11.24 12:46 신고  댓글주소  수정/삭제

      buildr 공부하면서 제일 어려운게 루비네요. -.-;
      그래두 buildr 공부 핑계로 억지로 루비로 코딩을 하니까 재미는 있어요.
      저도 루비냐 그루비냐 고민 많이 했는데, 만약 한다면 일단은 루비 공부에 집중하기로 했습니다. ^^

  2. BlogIcon groovy 2008.11.28 18:16 신고  댓글주소  수정/삭제  댓글쓰기

    역시 멋지셈;;
    쿨럭;
    도망가기;;

메이븐에서는 기본적으로 자바 소스 폴더를 한 개만 지정할 수 있습니다.

 <build>
    <sourceDirectory>src/main/java</sourceDirectory>
 ....
 </build>

하지만 때로는 자바 소스가 여러 폴더에 흩어져 있는 경우도 있습니다.
메이븐에서는 그럴 경우 프로젝트를 분리하라고 권장합니다.
그러나 프로젝트를 분리할 형편(?)이 안되거나 분리하기 싫다면 어떻게 할까요?

Build Helper Maven 플러그인을 이용하면 됩니다.
build-helper 플러그인의 add-source 모조는 POM에 소스 디렉토리를 추가해줍니다. 아래처럼 사용할 있습니다.

  <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.2</version>
    <executions>
     <execution>
      <id>add-source-dir</id>
      <phase>generate-sources</phase>
      <goals>
       <goal>add-source</goal>
      </goals>
      <configuration>
       <sources>
         <source>another/src/main/java</source>
         <source>others/src</source>
       </sources>
      </configuration>
     </execution>
    </executions>
  </plugin>

 

신고
Posted by 에코지오

댓글을 달아 주세요

메이븐에서 이게 가능한가요? 

Ant에서는 classpath 엘리먼트를 통해서 클래스가 포함된 디렉토리를 컴파일 패스에 추가할 수 있습니다.

    <classpath>
      <pathelement location="mydir/classes"/>
    </classpath>

이것과 동일한 기능을하는 메이븐 설정/플러그인이 있나요?

아무리 구글링하고 찾아봐도 방법을 모르겠습니다.

아시는 분은 좀 알려주시길...

신고
Posted by 에코지오

댓글을 달아 주세요

메이븐의 특징 중 하나는 메이븐이 참조 라이브러리 의존관계를 체계적으로 관리해준다는 것이다. 메이븐이 의존성을 다루는 의존성 관리 메커니즘에는 의존성 전이(Transitive Dependencies)라는 게 있는데,  
A가 B에 의존하고 B가 C에 의존한다고 할 때, 즉 A -> B -> C 관계에서 A는 B에 대한 의존관계만 설정하면 메이븐이 알아서 C까지도 가져와서 적절한 스코프에 포함시켜 준다는 것이다.

그러나, B의 type이 war 이면 의존성 전이 메커니즘은 작동하지 않는다.

 <dependency>
   <groupId>mycom</groupId>
   <artifactId>B</artifactId>
   <version>1.0</version>
   <type>war</type>
 </dependency>

그러니까 메이븐이 자동으로 B.war가 의존하는 C.jar를 포함시켜주지 않는다는 얘기다.
게다가 B.war에 포함된 클래스들(WEB-INF/classes) 또한 A의 의존성에 포함되지 않는다. A의 소스가 B.war의 클래스를 참조한다면 컴파일시 에러가 발생할 것이다.

이클립스에서 A프로젝트의 빌드패스에 B 웹프로젝트를 추가하면 이클립스는 B 웹프로젝트의 빌드패스에 포함된 C.jar와 B 웹프로젝트의 클래스들을 A프로젝트에 자동으로 추가해준다.

아쉽지만 메이븐에서는 이게 자동으로 안된다.

신고
Posted by 에코지오

댓글을 달아 주세요

  1. BlogIcon 에코지오 2008.10.27 14:32 신고  댓글주소  수정/삭제  댓글쓰기

    http://jira.codehaus.org/browse/MNG-1991

메이븐에서 ear 패키지를 만들어 주는 플러그인은 maven-ear-plugin이다.
ear 파일에 포함되어 함께 패키징될 아카이브 모듈들은 <modules> 엘리먼트 하위에 <ejbModule>, <jarModule>, <webModule> 식으로 설정해서 끼워 넣는다.

그런데 xxxModule 설정 옵션을 보자면, 모두가 groupId와 artifactId를 통해서 끼워넣을 모듈을 지정하고 있다. 아무리 찾아봐도 모듈 파일의 경로를 설정하는 건 없다 ! 내가 원하는 건 임의의 경로에 있는 아카이브를 ear에 포함하고 싶은건데.... ㅜ.ㅜ

여기까지만 보면, 메이븐 저장소에 등록된 모듈만 ear에 포함시킬 수 있다는 결론이 나온다. 내가 만든 모듈을 ear에 포함시키고 싶으면 먼저 모듈을 로컬이나 리모트 저장소에 등록해주어야 한다는 거다. 이 무슨 시츄에이션인가?

그러나 저장소에 등록하지 않고 포함시킬수 있는 방법이 있다. 포함되는 모듈의 의존성 scope를 system으로 잡고, 파일의 절대경로를 지정하면 된다.

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-ear-plugin</artifactId>
 <version>2.3.1</version>
 <configuration>
  <modules>
   <ejbModule>
    <groupId>mycom</groupId>
    <artifactId>myapp-ejb</artifactId>
   </ejbModule>
   <webModule>
    <groupId>mycom</groupId>
    <artifactId>myapp-web</artifactId>
   </webModule>
  </modules>
 </configuration>
</plugin>
.....
......
<dependencies>
 <dependency>
  <groupId>mycom</groupId>
  <artifactId>myapp-web</artifactId>
  <version>1.0</version>
  <type>war</type>
  <scope>system</scope>
  <systemPath>${project.basedir}/myapp-web.war</systemPath>
 </dependency>
 <dependency>
  <groupId>mycom</groupId>
  <artifactId>myapp-ejb</artifactId>
  <version>1.0</version>
  <type>ejb</type>
  <scope>system</scope>
  <systemPath>${project.basedir}/myapp-ejb.jar</systemPath>

 </dependency>
 .....
</dependencies>

이렇게 잡아놓고 mvn package를 날리면 저장소에 등록되지 않은 myapp-ejb.jar, myapp-web.war 파일이 ear에 함께 패키징되는 것을 확인할 수 있다.

신고
Posted by 에코지오

댓글을 달아 주세요

이전 글에서 리소스 설정치환 방법에 대해 얘기했습니다. 그럼 메이븐 환경에서 리소스 설정치환 작업은 어떤 phase에서, 어떤 리소스를 대상으로 이루어져야 할까요?

1. 클래스패스에 포함되는 리소스

(1) 리소스 필터링 방식
메이븐이 process-resource 단계에서 타겟 디렉토리로 복사된 리소스를 대상으로 필터링을 해줍니다. 이때 원본 리소스는 변경이 없습니다. 그러니까 /src/jdbc.properties 파일에 password=${jdbc.password} 처럼값을 설정해놓으면 /target/classes/jdbc.properties 파일에는 그 부분이 password=my_password 식으로 바뀌어 있다는 말입니다.

(2) 리소스 교체 방식
타겟 디렉토리에 복사된 리소스 파일에 대해서 교체 처리해줍니다. 어차피 process-resource 단계에서는 resources:resources 골이 가장 먼저 실행되기 때문에 교체 작업(ant의 copy 타스크 이용)은 그냥 process-resources 단계에 바인딩하면 됩니다. 역시 원본 리소스 파일은 변경이 없습니다. 예를 들어 /src 밑의 jdbc.properties와 jdbc.properties.dev가 /target/classes 밑에 복사된 후 /target/classes/jdbc.properties.dev 파일이 /target/classes/jdbc.properties 파일을 덮어쓰게 됩니다.

 

2. WEB-INF 밑에 있는 리소스(web.xml, struts-config.xml, applicationContext.xml 등등)

WEB-INF 밑의 리소스에 대한 설정치환은 좀 지저분합니다. 
메이븐은 package 단계에서 war:war를 실행하는데, war:war는 무조건 war:exploded 를 먼저 실행하고 나서 exploded된 웹어플리케이션을 war 파일로 묶어냅니다. 우리가 원하는 것은 exploded 된 웹어플리케이션의 리소스 파일에 대해 리소스설정치환 작업을 하고나서 war 파일로 패키징하는 것인데, 중간에 끼어들 여지가 원천 봉쇄된 것입니다.

뭐 package 단계나 war:war를 실행하지 않으면 되긴합니다. war:exploded를 명시적으로 실행한 뒤에 설정치환을 하면 됩니다. 그런데 설정치환하는 execution을 어느 phase에 바인딩하는게 좋을지... 하여간 원본 리소스 파일을 건드리지 않은 상태로 리소스 치환을 하려니 지저분해지는군요.

만약 원본 리소스를 건드려도 무방하다면 package 전 단계(prepare-package 단계는 maven 2.1부터 지원하기 때문에 2.1 이전 버전이라면 process-resources 단계가 괜찮을까요)에서 원본 리소스를 대상으로 설정치환 작업을 처리하면 됩니다. 
신고
Posted by 에코지오

댓글을 달아 주세요

- 메이븐에 내가 만든 phase를 추가할 수 있는가?

- skip 옵션을 지원하지 않는 goal이 어떤 phase에 이미 바인딩된 경우에, 과연 런타임시에 그 goal의 실행을 막을 수는 없는 것인가? 예를 들어 package phase에서 war:war가 실행되지 않길 바란다면?

- 어떤 플러그인에 대해서 execution을 여러개 정의한 경우에 (바인딩된 phase를 통하지 않고) 특정 execution만 골라서 실행 가능한가? 반대로 특정 execution만 실행에서 제외할 수 있는가? execution의 id를 어떻게 활용할 방법은 없나?

신고
Posted by 에코지오

댓글을 달아 주세요

보안을 위해 서버 패스워드 같은 민감한 데이터를 pom.xml에서 감추는 방법.

1. 사용되는 부분을 프로퍼티로 처리하고 

<sshexec host="myserver" username="admin" password="${wasserver.password}"  command="xxxx" />

 mvn 실행시 -D옵션으로 값을 넘겨준다.

mvn deploy -Dwasserver.password=xxx

(그러나 허드슨 사용 환경에서 허드슨 빌드 console 화면에 데이터가 노출되는 문제가 있다)

2. 감추려는 데이터가 서버 접속 정보라면 settings.xml에 서버정보를 설정하여 pom.xml에서 데이터를 감출수 있다.

<settings> 
  <servers>
    <server>
      <id>server001</id>
      <username>my_login</username>
      <password>my_password</password>
      <privateKey>${user.home}/.ssh/id_dsa</privateKey>
      <passphrase>some_passphrase</passphrase>
      <filePermissions>664</filePermissions>
      <directoryPermissions>775</directoryPermissions>
      <configuration></configuration>
    </server>
  </servers>
  ...
</settings>
신고
Posted by 에코지오

댓글을 달아 주세요



티스토리 툴바