Today I Learned. by Rio

Stop all containers and remove all none tag docker images

|

서비스를 제공하는 서버와 빌드서버가 분리되어 있고, 빌드서버에서 도커 이미지 빌드를 계속 하게 되면 태그가 없는 이미지가 많이 쌓이게 된다. 이런 경우 수작업으로 이미지 목록 내에서 <none> 태그로 표시된 이미지들을 지워줘도 되는데 이미지가 많이 쌓일수록 굉장히 번거로워진다. 이럴 때 간단한 스크립트로 사용하지 않는 <none> 태그 이미지를 한번에 삭제할 수 있다.


리눅스 서버의 경우

# stop all containers for linux
docker rm $(docker ps -a -q)

# remove all none tag images for linux
docker rmi $(docker images -q)

윈도우즈 서버의 경우

# stop all containers for windows
FOR /f "tokens=*" %i IN ('docker ps -a -q') DO docker rm %i

# remove all none tag images for windows
FOR /f "tokens=*" %i IN ('docker images -q -f "dangling=true"') DO docker rmi %i

Kill specific process in windows batch

|

CI 환경을 구성하다 보면, 실행되고 있는 어플리케이션을 내리고 새로 올리는 세팅을 빈번하게 하게 된다. 이 경우 특정 프로세스를 찾아서 종료시켜야 하는데, Windows 환경에서 간단한 스크립트로 특정 프로세스를 찾고 죽이는 방법에 대한 커맨드를 소개한다.


아래는 특정 포트로 서비스되는 프로세스를 찾아 종료시키는 커맨드이다.

FOR /F "tokens=5 delims= " %%P IN ('netstat -ano ^| findstr :8080 ^| findstr LISTENING') DO @echo taskkill /PID /F %%P

위의 스크립트에서 @echo를 지워주면 taskkill명령이 실제로 실행되니 주의. 설명하자면 netstat 명령으로 LISTENING 상태의 포트 8080 사용하는 프로세스 아이디를 얻어내고 taskkill /PID 명령으로 종료시킨다는 뜻이다.


아래와 같이 특정 프로세스 이름을 통해 종료시킬 수도 있다.

FOR /F "tokens=2 skip=3 delims= " %P IN ('tasklist /fi "imagename eq java*"') DO @echo taskkill /PID /F %P

위 커맨드는 프로세스 이름이 java로 시작하는 모든 프로세스ID를 찾아서 taskkill 명령으로 종료시킨다는 뜻이다.

커맨드에 대한 테스트는 bat 파일을 만들어서 cmd창에서 실행시켜 볼 수 있고, jenkins의 경우는 Execute Windows batch command 창에 바로 붙여넣으면 실행된다. 검색조건을 변경하여 활용하면 필요한 형태로 프로세스ID를 찾아내어 종료시킬 수 있다.

Set not to kill processes after success job in Jenkins

|

jenkins로 ci환경을 구성한 후 서버를 올리는 job을 만들고 Build Now로 수행시키면 서버는 정상적으로 올라가나 job이 끝나지 않는다. 예를들어

java -jar app.jar

위의 명령이 젠킨스 job에서 수행이 된다면 app.jar는 정상적으로 올라가나 수행된 job은 사용자가 강제 종료하기 전까지 끝나지 않는다.

이 경우 linux 계열 위에서 올라간 jenkins에서는 nohup 명령어를 통해 간단히 job을 종료하면서 프로세스를 계속 살려둘 수 있다.

윈도우에서도 비슷한 역할을 하는 명령어가 있다.

start /min java -jar app.jar

start 명령만 사용하면 cmd창이 새로 뜨는데 /min 옵션을 추가하면 cmd창 없이 프로세스만 계속 지속시킬 수 있다.

만약 이렇게 설정해도 job 종료와 동시에 프로세스가 죽는다 하면 jenkins 서비스를 띄울때 -Dhudson.util.ProcessTree.disable=true 아규먼트를 추가해서 실행해보자.

java -Dhudson.util.ProcessTree.disable=true -jar jenkins.war

만일 윈도우 설치 패키지(msi)로 jenkins를 설치하여 윈도우 서비스에 올라가 있는 상황이라면 jenkins 설치 폴더를 찾아 폴더 내에 jenkins.xml 파일을 수정한다. 수정할 위치는 xml 파일 내에서 service > arguments 부분을 찾아서 적당한 위치에 위의 옵션을 넣어준다.

<service>
  <arguments>이곳에 java -jar로 젠킨스 서비스 올리는 명령이 들어가 있다</arguments>
</service>

xml 파일을 수정한 후 윈도우의 서비스 관리 창을 열어 jenkins 서비스를 찾아 다시 시작한다.

참고한 사이트

Make easy java collection code

|

자바 코드를 작성하다보면 간단한 자료구조들을 만들어 사용하는 경우들이 있다. List, Set, Map 등이 그런 간단한 자료구조들인데, 보통 new로 만들어서 사용하곤 한다. 테스트코드를 작성할 때도 역시 큰 의미없이 형태를 구성하는 list나 map등을 만드는 경우가 많이 생긴다. 이런 경우에 코드량을 줄이고 가독성을 높이기 위해서 일반적으로 new로 생성하는 list 또는 map 형태를 피하고 아래와 같은 형태를 사용하곤 했다.

1. List 생성시

자바 기본 라이브러리 중 Arrays 클래스의 asList를 사용해서 코드의 가독성을 높인다.

List<String> fruits = Arrays.asList("apple", "banana", "coconut");

static import를 활용할 수 있다면 아래와 같이 쓸 수 있다.

import static java.util.Arrays.*;

List<String> fruits = asList("apple", "banana", "coconut");

위 코드는 아래의 코드와 완전히 같은 역할을 한다.

List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("coconut");

List 내부의 값들을 명시적으로 보여주며 코드량을 획기적으로 줄여주어 굉장히 많이 사용했다.

2. 단 하나의 객체만 들어간 List 생성시
List<String> fruits = Collections.singletonList("apple");

static import를 활용할 수 있다면 아래와 같이 쓸 수 있다.

import static java.util.Collections.singletonList;

List<String> fruits = singletonList("apple");

그냥 asList 내에 파라미터 하나만 넘기는 방법으로 사용해도 결과는 같다.

3. ImmutableList 활용

google guava 라이브러리를 활용한 경우 ImmutableList를 활용하여 작성하기도 한다. 특히 테스트 데이터를 만드는 경우 중간에 값 변경을 하는 경우가 거의 없기 때문에 사용할 수 있다. 하지만 asList라는 함수를 활용하는 빈도가 높아 거의 쓰지는 않았다.

List<String> fruits = ImmutableList.of("apple", "banana");
4. Map 생성시

google guava 라이브러리 내의 ImmutableMap을 자주 사용했다. 특히 api 테스트시 간단하게 request body를 만들어 보낼 때 활용하면 좋다. 객체를 따로 만들지 않아도 IDE 툴의 code indent만 잘 맞추어 작성하면 key-value쌍이 가독성 있게 읽혀서 많이 사용하곤 했다.

Map<String, Object> requestMap = ImmutableMap.of(
                "name", "rio",
                "age", 33,
                "gender", "male"
        );

아래에 new HashMap으로 생성하는 코드를 위의 코드로 대체해서 쓸 수 있다.

Map<String, Object> requestMap = new HashMap<>();
requestMap.put("name", "rio");
requestMap.put("age", 33);
requestMap.put("gender", "male");

key-value쌍이 단 하나인 Map을 만들어야 하는 경우 자바 기본 라이브러리 중 Collections 클래스의 singletonMap을 static import를 활용해 사용하는 일이 많았다.

import static java.util.Collections.singletonMap;

Map<String, Object> singletonMap = singletonMap("key", "value");
5. 비어 있는 Collection 생성시

역시 자바 기본 라이브러리 중 Collections 클래스 내에서 제공되는 empty~ 메서드를 이용하곤 했다.

import static java.util.Collections.*;

List<String> emptyList = emptyList();
Map<String, Object> emptyMap = emptyMap();
Set<String> emptySet = emptySet();

위에 소개한 기법 외에도 간단한 자료구조를 만들어내는 방법들은 많을 것이다. 다만 프로젝트에서 자동화 테스트 지원을 하면서 여러가지 방법 중에 가독성 측면에서 이득을 보고 습관적으로 잘 사용했던 몇 가지 메서드만 잊어버리지 않기 위해 기록해 둔다.

보통은 생성한 Map, List 등을 특정 변수로 받아서 활용하기보다는 파라미터로 바로 inline으로 전달하는 목적으로 많이 쓰이기 때문에 실제 프로젝트 코드에 적용하는 경우 예시로 든 것보다 가독성 측면에서 더 좋은 효과를 볼 수 있다.

JUnit rerun failed test

|

프로젝트 진행중에 자동화 테스트를 수행하다 보면, 테스트 코드 또는 프로덕트 코드상의 로직 문제 외에도 실패하는 경우들이 발생하곤 한다. 불안정한 네트워크 환경, 테스트에 종속된 서비스가 내려가 있는 경우 등등 여러가지 이유가 있다. 이런 실패는 특히 개발 단계의 테스트 수행시 더욱 빈번하다. 또 GUI 자동화 테스트의 경우에는 더 다양한 경우들이 발생한다. 브라우저의 문제 또는 수행하는 Local PC에서 예상치 못한 화면 전환이 있다거나 더욱 다양한 이유로 인해 코드의 문제와는 상관없이 외부 요인으로 테스트가 깨지는 경우들이 발생한다.

이유야 어쨌든 테스트가 실패하면 실패한 테스트만 모아서 다시 재수행해보는 경우들이 생기고, 그런 상황에서 junit rule을 활용하는 방법이 있다. 테스트 클래스 내에 Rule 어노테이션을 활용하여 다음과 같이 Retry Rule을 적용한다.

@Rule
public RetryRule retryRule = new RetryRule(RETRY_COUNT);

RetryRule이라는 클래스를 만들어 준다. 생성자 안에 파라미터로 count를 받는 형태로 만들었다. 형태는 아래와 같으며 필요에 따라 조금씩 변형해서 사용하면 될 듯 하다.

public class RetryRule implements TestRule {

    private int retryCount;

    RetryRule(int retryCount) {
        this.retryCount = retryCount;
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return statement(base, description);
    }

    private Statement statement(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Throwable caughtThrowable = null;

                for (int i = 0; i < retryCount; i++) {
                    try {
                        base.evaluate();
                        return;
                    } catch (Throwable t) {
                        t.printStackTrace();
                        caughtThrowable = t;
                        System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed.");
                    }
                }
                System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures.");
                throw caughtThrowable;
            }
        };
    }
}

이렇게 세팅 후 JUnit 테스트를 수행하면 실패가 있는 경우 설정한 count만큼 재수행을 시도한다. 그리고 재수행 하면서 성공한 경우에는 테스트를 성공한 것으로 간주한다. junit 리포트에서도 성공한 테스트로 간주하니 염두해 두고 사용하면 된다.

테스트가 코드 외의 환경에 영향을 받는 경우이면서 외부 환경의 일시적인 문제로 실패한 경우 대부분 재수행하여 성공을 원하는 경우엔 이런 룰을 적용하며 테스트를 수행한다면 많은 도움을 받을 수 있다. 하지만 사이드 이펙트가 있을 수 있는데 통제되어야 하는 외부 환경의 영향으로 간헐적 실패가 있는 경우에는 이런식으로 해결하는 접근 보다는 외부 환경을 mocking 하거나 독립적인 테스트 환경을 구성하는 쪽으로 고려하는 편이 옳은 방법이라 볼 수 있겠다.