저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

JUnit 5 소개

TDD 또는 Test 2017.10.10 21:24

Junit 5 정식 버전이 나왔다. 테스트 코드를 작성하는 개발자 입장에서 JUnit 5를 보면 JUnit 4에 비해 중요한 차이점은 다음과 같다.

  • JUnit 4가 단일 jar였던 것에 비해, JUnit 5는 크게 JUnit Platform, JUnit Jupiter, JUnit Vintage 모듈로 구성되어 있다.
  • 테스트 작성자를 위한 API 모듈과 테스트 실행을 위한 API가 분리되어 있다.
    • 예를 들어, JUnit Jupiter는 테스트 코드 작성에 필요한 junit-jupiter-api 모듈과 테스트 실행을 위한 junit-jupiter-engine 모듈로 분리되어 있다.
  • 자바 8 또는 그 이상 버전을 요구한다.

모듈 개요

JUnit 5를 사용해서 테스트를 작성하고 실행하려면 모듈의 개략적인 구성 정도는 알아야 설정을 이해하는데 도움이 된다. 모듈은 크게 다음과 같은 구조를 갖는다. (세부적인 모듈 목록은 JUnit 5 User Guide에서 확인할 수 있다.)



JUnit Platform은 테스트를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스를 정의하고 있다. Platform은 TestEngine을 통해서 테스트를 발견하고, 실행하고, 결과를 보고한다.


TestEngine의 실제 구현체는 별도 모듈로 존재한다. 이 모듈 중 하나가 jupiter-engine이다. 이 모듈은 jupiter-api를 사용해서 작성한 테스트 코드를 발견하고 실행한다. Jupiter API는 JUnit 5에 새롭게 추가된 테스트 코드용 API로서, 개발자는 Jupiter API를 사용해서 테스트 코드를 작성할 수 있다.


기존에 JUnit 4 버전으로 작성한 테스트 코드를 실행할 때에는 vintage-engine 모듈을 사용한다.


만약 테스트 코드를 작성하기 위한 새로운 API를 창안하다면, 그 API에 알맞은 엔진 모듈을 함께 구현해서 제공하면 JUnit Platform 수정없이 새로 창안한 테스트 API를 실행하고 결과를 리포팅할 수 있게 된다.


프로젝트 의존 설정


메이븐 프로젝트 설정


메이븐 프로젝트를 사용한다면 다음과 같이 설정한다.


<?xml version="1.0" encoding="UTF-8"?>

<project ...생략>

    ...생략


    <dependencies>

        <dependency>

            <groupId>org.junit.jupiter</groupId>

            <artifactId>junit-jupiter-api</artifactId>

            <version>5.0.1</version>

            <scope>test</scope>

        </dependency>


        <!-- 컴파일 경고 메시지 제거 목적, junit-jupiter-api 5.0.3 버전부터는 포함할 필요 없음 -->

        <dependency>

            <groupId>org.apiguardian</groupId>

            <artifactId>apiguardian-api</artifactId>

            <version>1.0.0</version>

            <scope>test</scope>

        </dependency>

    </dependencies>


    <build>

        <plugins>

            <plugin>

                <artifactId>maven-compiler-plugin</artifactId>

                <version>3.1</version>

                <configuration>

                    <source>1.8</source>

                    <target>1.8</target>

                    <encoding>utf-8</encoding>

                </configuration>

            </plugin>


            <plugin>

                <artifactId>maven-surefire-plugin</artifactId>

                <version>2.19.1</version>

                <dependencies>

                    <dependency>

                        <groupId>org.junit.platform</groupId>

                        <artifactId>junit-platform-surefire-provider</artifactId>

                        <version>1.0.1</version>

                    </dependency>

                    <dependency>

                        <groupId>org.junit.jupiter</groupId>

                        <artifactId>junit-jupiter-engine</artifactId>

                        <version>5.0.1</version>

                    </dependency>

                </dependencies>

            </plugin>

        </plugins>

    </build>


</project>


의존 설정:

  • org.junit.jupiter:junit-jupiter-api :테스트 코드 작성에 필요한 API 제공
  • org.apiguardian:apiguardian-api: 컴파일 경고 메시지를 제거하고 싶으면 추가 

maven-surefire-plugin 설정

  • 2.19.1 버전 사용: 2.20이 메모리릭 문제가 있고, 2.20.1 버전은 JUnit 5.0과 호환되지 않아 mvn test 시 에러 발생함(이슈 참고)
  • JUnit 5의 플랫폼을 사용해서 테스트를 실행하기 위해 플러그인 의존 추가
    • junit-platform-surefire-provider: 메이븐 Surefire에서 JUnit Platform을 실행하기 위한 모듈
    • junit-jupiter-engine: Jupiter API로 작성한 테스트를 위한 엔진 모듈
이렇게 설정하면 mvn test 명령어를 사용해서 Jupiter API를 사용해서 작성한 테스트를 메이븐에서 실행할 수 있다.

그레이들 프로젝트 설정


그레이들 프로젝트에서 JUnit Jupiter 테스트를 사용하기 위한 설정은 아래와 같다.


group 'junit5-prac'

version '1.0-SNAPSHOT'


buildscript {

    repositories {

        mavenCentral()

    }

    dependencies {

        classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.1'

    }

}


apply plugin: 'org.junit.platform.gradle.plugin'

apply plugin: 'java'


sourceCompatibility = 1.8


repositories {

    mavenCentral()

}


dependencies {

    testCompile("org.junit.jupiter:junit-jupiter-api:5.0.1")

    testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.1")

}


buildscript로 빌드 과정에서 그레이들이 JUnit Platform을 사용하도록 설정하고, apply plugin으로 플러그인을 적용한다. dependencies에는 Jupiter API를 사용해서 테스트를 작성하기 위한 API와 런타임에 실행하기 위한 엔진 의존을 추가한다. Jupiter Engine은 테스트를 실행할 때 필요하므로 testRuntime으로 설정한다.


이제 gradle test 명령어로 Jupiter API를 사용해서 작성한 테스트를 실행할 수 있다.


Jupiter API: 테스트 작성과 실행


테스트 코드 작성하기


Jupiter API를 이용해서 테스트 코드를 작성하는 방법은 JUnit 4 버전과 크게 다르지 않다. 다음은 Jupiter API를 이용한 테스트 코드 작성예이다.


import org.junit.jupiter.api.Test;


import static org.junit.jupiter.api.Assertions.assertEquals;


public class PasswordMeterTest {


    @Test

    void weak() {

        PasswordMeter meter = new PasswordMeter();

        assertEquals(PasswordLevel.WEAK, meter.meter("1234"));

    }

}


JUnit 4를 이용한 테스트 코드와 거의 동일한 것을 알 수 있다. @Test 애노테이션을 이용해서 테스트로 사용할 메서드를 지정하고, assertEquals() 메서드를 이용해서 기대하는 값과 실제 결과 값이 같은지 비교한다. 차이점은 @Test 애노테이션과 assertEquals() 메서드가 속한 패키지이다. Jupiter API의 패키지는 org.junit.jupiter.api로 시작한다.


인텔리J에서 실행하기


인텔리J 최신 버전(2017.2.5)은 JUnit 5를 지원한다. 메이븐이나 그레이들 프로젝트를 임포트했다면 해당 테스트 클래스나 메서드를 실행하면 된다.


[참고]

인텔리J 버전마다 내장하고 있는 JUnit 버전이 다르다. 2017.2.5의 경우는 Jupiter 5.0.0 버전을 내장하고 있다.([인텔리j]\plugins\junit\lib에 위치) JUnit Platform Launcher와 Jupiter Engine을 내장된 버전 대신 다른 버전을 사용하고 싶다면 User Guide의 Ide 지원을 참고한다.


이클립스에서 실행하기 1: JUnit 4를 이용한 실행 


현재 사용중인 이클립스 Oxygen(4.7) 버전은 아직 JUnit 5의 Platform과 Jupiter API를 지원하지 않는다. 이런 환경에서 Jupiter API로 작성한 테스트를 실행하려면 다음과 같은 의존을 추가해 주어야 한다.


       <!-- JUnit5를 지원하지 않는 환경(eclipse oxygen 등)에서 실행하기 위한 설정 -->

        <dependency>

            <groupId>junit</groupId>

            <artifactId>junit</artifactId>

            <version>4.12</version>

        </dependency>

        <dependency>

            <groupId>org.junit.platform</groupId>

            <artifactId>junit-platform-runner</artifactId>

            <version>1.0.1</version>

            <scope>test</scope>

        </dependency>

        <dependency>

            <groupId>org.junit.jupiter</groupId>

            <artifactId>junit-jupiter-engine</artifactId>

            <version>5.0.1</version>

            <scope>test</scope>

        </dependency>


JUnit 4 버전에 대한 의존을 추가했는데, 그 이유는 JUnit 4의 @RunWith를 사용해서 Jupiter API 기반 실행하기 위함이다. junit-platform-runner 모듈은 Jupiter 기반 테스트를 실행할 수 있는 Runner인 JUnitPlatform을 제공한다. 이 Runner를 사용하면 JUnit 4만 지원하는 개발 환경에서 Jupiter API로 작성한 테스트를 실행할 수 있다. 다음은 사용 예이다. 아래 코드에서 @Test 애노테이션의 JUnit 4의 @Test가 아니라 Jupiter API의 @Test 임을 알 수 있다.


import org.junit.jupiter.api.Test;

import org.junit.platform.runner.JUnitPlatform;

import org.junit.runner.RunWith;


import static org.junit.jupiter.api.Assertions.assertEquals;


@RunWith(JUnitPlatform.class)

public class PasswordMeterTest4Eclipse {


    @Test

    void weak() {

        PasswordMeter meter = new PasswordMeter();

        assertEquals(PasswordLevel.WEAK, meter.meter("1234"));

    }

}



이클립스에서 실행하기 2: 플러그인 사용


Oxygen에서 JUnit 5를 실행할 수 있는 또 다른 방법은 아직은 베타 버전인 플러그인을 설치하는 것이다. 이에 대한 설치 방법은 https://wiki.eclipse.org/JDT_UI/JUnit_5 문서에서 확인할 수 있다.



Jupiter API: assert 메서드


기본 assert 메서드, fail() 메서드


Jupiter API의 org.junit.jupiter.api.Assertions 클래스는 값 검증을 위한 assert로 시작하는 static 메서드를 제공하고 있다. 주요 메서드는 다음과 같다.

  • assertEquals(expected, actual) : int, long 등 기본타입과 Object에 대한 assertEquals 메서드가 존재한다.
  • assertNotEquals(Object unexpected, Object actual)
  • assertTrue(boolean condition)
  • assertFalse(boolean condition)
  • assertNull(Object actual)
  • assertNotNull(Object actual)
  • fail()

assertAll()


아래 코드를 보자.


@Test

void assertEach() {

    Game game = new Game(123);

    Score score = game.guess(134);

    assertEquals(1, score.getStrikes());

    assertEquals(1, score.getBalls());

}


이 코드에서 만약 첫 번째 assertEquals()가 실패하면 그 시점에서 테스트 실패하므로 두 번째 assertEquals()를 실행하지 않는다. 이 두 assertEquals()는 실제로는 하나의 score를 검증하는 것이므로 개념적으로 하나를 검증하는 것이다. 이럴 때 유용하게 사용할 수 있는 것이 assertAll()이다. assertAll()은 여러 검증을 하나로 묶어서 테스트 할 수 있게 해준다. 다음은 assertAll()의 예를 보여주고 있다.


public class AssertAllTest {

    @Test

    void assertAllSample() {

        Game game = new Game(123);

        Score score = game.guess(145);

        assertAll(

                () -> assertEquals(2, score.getStrikes()),

                () -> assertEquals(1, score.getBalls())

        );

    }

}


assertAll()은 함수형인터페이스인 Executable 목록을 파라미터로 갖는다.(Executable 인터페이스는 파라미터가 없고 리턴 타입이 void인 execute() 메서드를 정의하고 있다.) 함수형 인터페이스이므로 위 코드와 같이 람다식을 사용해서 여러개의 검증을 목록으로 전덜할 수 있다.


assertAll()의 특징은 목록으로 받은 모두 Executable을 실행한다는 점이다. 그리고 그 중에서 실패한 검증에 대해서만 리포트를 한다. 예를 들어, 위 코드에서 assertAll()로 전달한 모든 Executable이 검증에 실패했다고 하자. 이를 인텔리J에서 실행해보면 다음과 같이 assertAll()에서 실패한 모든 검증 결과가 콘솔에 출력되는 것을 알 수 있다.


xpected :2

Actual   :3

 <Click to see difference>


Expected :1

Actual   :0

 <Click to see difference>


assertThrows()


실행한 코드에서 특정 익셉션이 발생하는지 확인할 때에는 assertThrows() 메서드를 사용한다. 다음은 사용예이다.


@Test

void simple() {

    assertThrows(ArithmeticException.class, () -> divide(100, 0));

}


private int divide(int op1, int op2) {

    return op1 / op2;

}



Jupiter API: 테스트 라이프사이클


Jupiter API도 JUnit 4와 동일한 라이프사이클을 갖는다. Jupiter API의 애노테이션이 좀 더 의미를 살리는 이름을 사용한다.


public class LifecycleTest {


    @BeforeAll // JUnit 4의 @BeforeClass

    static void initAll() {

        System.out.println("initAll");

    }


    @BeforeEach // JUnit 4의 @Before

    void init() {

        System.out.println("init");

    }


    @Test

    void someTest() {

        System.out.println("someTest");

    }


    @Test

    void anyTest() {

        System.out.println("anyTest");

    }


    @AfterEach // JUnit 4의 @After

    void tearDown() {

        System.out.println("tearDown");

    }


    @AfterAll // JUnit 4의 @AfterClass

    static void tearDownAll() {

        System.out.println("tearDownAll");

    }


}



보조 애노테이션: @DisplayName과 @Disabled


@DisplayName은 테스트 클래스나 메서드의 표시 이름을 지정한다.


@DisplayName("라이프사이클 테스트")

public class LifecycleTest {


    @DisplayName("어떤 테스트")

    @Test

    void someTest() {

        System.out.println("someTest");

    }


    @Disabled("테스트 환경 구성 필요")

    @Test

    void anyTest() {

        System.out.println("anyTest");

    }


위 코드를 실행하면 아래 그림과 같이 테스트 결과에서 @DisplayName에 지정한 이름을 사용하는 것을 알 수 있다.


위 코드에서 @Disabled 애노테이션을 사용했는데, 이 애노테이션은 테스트를 실행 대상에서 제외한다. 위 그에서도 테스트를 하지 않고 생략한 것을 알 수 있다.


다음에는


이어지는 글(http://javacan.tistory.com/464)에서는 계속해서 Jupiter API에 추가된 기능을 추가로 살펴볼 예정이다.








Posted by 최범균 madvirus

댓글을 달아 주세요

최근에 시작한 프로젝트는 모사의 기간계 시스템의 기능을 개선하는 작업이다. 이 기간계 시스템은 다음의 기술들을 사용한다.

  • 클라이언트 영역
    • 마이플랫폼
  • 서버 영역
    • 글루(glue) 프레임워크 (포스데이터에서 만든 프레임워크)
      • 스프링 프레임워크 기반 (DB 연동, 트랜잭션 처리 등을 글루 프레임워크에서 확장)
    • 글루를 위한 마이플랫폼용 어댑터
개발과정은 주로 다음과 같다.
  • 서버사이드
    • 사용할 SQL 쿼리를 XML 파일에 추가한다.
    • 글루의 AD(ActivityDiagram)에서 Activity 및 Activity 흐름을 정의한다.
      • 이 Activity에서 DAO를 이용해서 쿼리를 실행한다.
  • 클라이언트 사이드
    • 화면을 만든다.
    • 화면에서 특정 이벤트(조회 버튼 클릭 등)가 발생하면 서버에 특정 Activity 흐름 실행을 요청하는 코드를 작성한다.
    • 응답을 받아 화면에 반영하는 코드를 작성한다.
스프링 설정 파일에는 다음과 같은 것들이 정의되어 있다.
  • DataSource / 모든 Activity에서 사용할 DAO / 트랜잭션 매니저

테스트하는 과정


코드를 만들면 개발자가 만든 코드가 잘 동작하는지 확인을 하게 되는데, 이 과정은 아래와 같다.

  1. 로컬에 웹로직을 실행환다.
  2. 서버 어플리케이션을 war로 묶어 웹로직에 배포한다. (평균 20~30초 정도 소요)
  3. 마이플랫폼 화면을 실행한다.
  4. 화면 조작을 통해 서버와 통신을 하고, 이 과정에서 눈으로 확인한다. (5초에서 심하면 1분 이상 소요)

그리고, 서버 영역에서 쿼리 변경, Glue AD 변경, 직접 구현한 Activity 클래스 변경 등이 발생하면 2-4 과정을 다시한다. 문제는 아주 사소한 쿼리 변경 조차도 이게 정상적으로 동작하는지 확인하려면 보통 1분 이상 소요된다는 점이다. 웹로직에 배포하고, 클라이언트를 실행해서 화면 조작을 통해 눈으로 확인해야 한다. 이거 참,, 참을 수 없는 상황이다.


원하는 것


원하는 건 이거다.

  • 서버 영역 코드가 잘 동작하는 지 확인하기 위해서 클라이언트를 최대한 사용하지 않는 것
  • 웹로직에 배포 없이 서버 영역 코드를 확인하는 것.
즉, 마이플랫폼을 최대한 사용하지 않고, 웹로직 서버를 실행하지 않으면서 서버 코드를 테스트하고 싶은 것이다. 서버 영역의 코드를 테스트하기 위해 마이플랫폼 코드까지 작성해야 하고, 마임플랫폼에서 서버를 호출해 주어야 하다보니, 뭔가 잘 안되면 이게 서버 문제인지 마이플랫폼 코드를 잘못 작성한 건지 확인하는데에도 시간이 많이 소요된다.

테스트 대상

대부분 DB 데이터 중심으로 작업이 이루어지기 때문에, 쿼리 자체는 오렌지나 SQLDeveloper와 같은 오라클 클라이언트 도구를 통해서 거의 검증이 된다. 마이플랫폼을 통해서 확인하고 싶은 것은 서버쪽 글루 AD가 올바르게 동작하는지 확인하는 것이다.

글루를 이용해서 프로그래밍 해 본 경험이 있다면 다음과 같은 Activity Diagram을 그려봤을 것이다. (난 처음 그려봤다.) 클라이언트(마이플랫폼이나 웹브라우저)에서 요청을 보내면, 글루 프레임워크는 요청에서 지정한 Activity Diagram(AD) 코드를 선택한 뒤에 그 코드에 정의된 순서에 맞춰 차례대로 Activity를 실행한다.


위 그림에서 한 개의 타원이 각각 1개의 Activity를 의미하며, 클라이언트의 요청이 들어올 경우 시작부터 끝에 이르기까지 순차적으로 연결된 Activity 들이 실행된다. 클라이언트가 요청을 보내면 위 그림에서 굵은 선으로 표시한 것과 비슷하게 시작 지점부터 끝 지점까지 실행이되는데, 바로 이 하나의 흐름이 정상적으로 동작하는 지 확인하기 위해 클라이언트를 실행하는 것이다.


JUnit을 테스트 실행의 어려움


약간(?)의 분석 과정을 거쳐 글루 AD의 한 흐름을 테스트하려면 다음의 코드를 실행하면 된다는 것을 알아내었다. (글루 프레임워크 소스는 없었기에 디컴파일러의 힘도 좀 빌렸다.)


// AD를 선택하고 차례대로 실행해주는 컨트롤러

PosBizControlIF controller = PosBizController.getController();


PosContext posContext = new PosContext();


// 클라이언트의 요청 정보를 생성

posContext.put("ServiceName", "A1101010010F-service");

posContext.put("cobSysList", "1");

posContext.put("CODE_ID", "C51010");


// AD 실행

controller.doAction(posContext);


// 결과 확인

PosRowSet rowSet = (PosRowSet) posContext.get("ds_sysList");


그런데, 문제는 PosBizControllIF의 구현 클래스인 PosBizController가 사용하는  초기화 코드에 있다. 이 초기화 코드는 최종적으로 스프링의 XmlBeanFactory를 생성하는데, 이 때 사용하는 스프링 설정 경로가 아래와 같이 하드코딩되어 있다.


// PosBizController의 생성자 부분의 코드

((PosServiceLoader)PosContext.getBeanFactory().getBeanObject("serviceLoader"));  


// PosContext.getBeanFactory()

public static PosBeanFactory getBeanFactory() {

    ...

    PosBeanFactory factory = new PosBeanFactoryImpl();

    ...

}


// 하드 코딩 되어 있는 PosBeanFactoryImpl

public class PosBeanFactoryImpl implements PosBeanFactory {

    protected XmlBeanFactory ctx;


    public PosBeanFactoryImpl() {

        ctx = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));

    }


위 클래스들은 제공되는 jar 파일에 포함되어 있고 소스도 없기 때문에 (위 내용은 디컴파일해서 알아냈다), 소스를 수정할 수 없다. (디컴파일한 코드를 수정해서 다시 밀어넣고 싶지 않았고, 이 jar 파일을 사용하는 다른 프로젝트가 수십개에 달한다.)


제품용 코드와 테스트용 코드를 별도 소스폴더로 구분하고 각각 다른 바이너리 디렉토리에 생성되도록 한 뒤에 테스트용 클래스패스의 우선순위를 높여주면, 일단 같은 파일 이름을 사용하면서 테스트 코드에서 테스트용 스프링 설정을 사용할 수 있다. 하지만, 여전히 다른 이름을 가진 스프링 설정 파일을 사용할 수는 없다.


javassit를 이용한 클래스 변경


그래서, 런타임에 클래스를 조작해서 설정 파일을 변경하는 방법을 도전해봤다. 클래스 변경 도구로는 여러 프로젝트에서 사용되는 javassit를 선택했다. 약간의 테스트를 거쳐 변경에 성공했다. 다행히 내가 하려는 건 간단하게 할 수 있었다. 실제 코드는 다음과 같다.


public abstract class AbstractPosBizControllerTest {


    protected PosBizControlIF controller;


    @Before

    public void init() throws Exception {

        ClassPool pool = ClassPool.getDefault();

        CtClass cc = pool.get("com.posdata.glue.bean.PosBeanFactoryImpl");

        CtConstructor c = cc.getDeclaredConstructor(null);

        c.insertAfter("this.ctx = new org.springframework.beans.factory.xml.XmlBeanFactory("+

            "new org.springframework.core.io.ClassPathResource("+

            "\"applicationContextForUnitTest.xml\"));");

        cc.toClass();


        controller = PosBizController.getController();

    }

}


위 코드에서 c.insertAfter()는 해당 생성자에 지정한 코드를 추가하는 것으로 결과적으로 PosBeanFactoryImpl 클래스의 생성자를 다음과 같이 변경해준다.


    public PosBeanFactoryImpl() {

        ctx = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));

        ctx = new XmlBeanFactory(new ClassPathResource("applicationContextForUnitTest.xml"));

    }


따라서, ctx는 최종적으로 applicationContext.xml 파일이 아닌 applicationContextForUnitTest.xml 파일을 사용해서 BeanFactory를 생성하게 된다. 각 테스트 코드는 앞서 작성한 추상 클래스를 상속받아 로컬에서 서버배포 없이 AD 테스트를 수행할 수 있게 되었다.


public class SomeADTest extends AbstractPosBizControllerTest {


    @Test

    public void shouldHaveResultRows() {

        PosContext posContext = new PosContext();

        posContext.put("ServiceName", "SomeAD-service");

        posContext.put("cobSysList", "1");

        posContext.put("CODE_ID", "C51010");

        controller.doAction(posContext);


        PosRowSet rowSet = (PosRowSet) posContext.get("ds_sysList");

        assertTrue(rowSet.count() > 0);

    }


}


이제 쿼리 설정 XML 파일의 사소한 변경, 글루 AD의 사소한 변경 등 서버의 사소한 변경 이후 제대로 동작하는 지 테스트하기 위해 낭비되는 시간(war 묶기->웹로직 배포->컨텍스트재시작, 마이플랫폼 클라이언트 UI 클릭질, 눈으로 확인: 보통 1분 이상)을 줄일 수 있게 되었다. 이는 곧 개발 속도의 향상이고, 이는 다시 빠른 퇴근과 잉여시간 발생으로 이어지리라!


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 박병상 2013.06.07 15:51 신고  댓글주소  수정/삭제  댓글쓰기

    ㅋ. 최차장님 안녕하세요.
    전에 유지보수하던 박병상입니다.
    GLUE 쪽을 건드리시고 계시네요.. ㅎㅎ

    저때 저걸 할수 있었다면 좋았을텐데.. ㅎ
    건강하세요.. ㅎ