SpringBoot 1.4 기반의 Integration Test 작성하기

기본적인 내용은 Spring 블로그에 포스팅된 내용을 바탕으로 한다.   한글로 읽기 귀찮다면 링크된 본문을 참고하자!!

 

스프링 부트는 복잡한 설정없이 손쉽게 웹 어플리케이션 서버를 실행할 수 있다는 점때문에 자바 언어 세계에서 널리 사용되어오고 있다.  부트 역시 스프링 프레임워크에서 지원했던 방식과 유사한 형식의 테스트 방식을 지원하고 있었다.  하지만 부트의 테스트는 부트 자체가 웹 어플리케이션 개발을 쉽게 했던 만큼은 더 나아가지 못했다.

가장 간단한 테스트를 작성하는 방법은 물론 스프링을 배제한 형태다.  스프링의 각종 @(어노테이션 – Annotation)을 벗어난 코드라면 이 방식의 테스트가 가장 적절하다.  사람들이 오해하는 것 가운데 하나는 @Component 혹은 @Service라고 어노테이션이 붙은 클래스를 테스트할 때 꼭 스프링을 끼고 해야한다고 생각한다는 점이다.

굳이 그럴 필요가 없다.

  • 객체는 그냥 new 를 이용해서 만들면 된다.
  • @Value 어노테이트된 값은 그냥 값을 셋팅하면 된다.
  • 테스트 대상 메소드는 대부분 public 키워드를 갖는다. 테스트 메소드에서 접근을 걱정할 일이 거의 없다.
  • 외부에 공개되지 않은 메소드를 테스트해야하거나 아니면 @Value 값을 강제로 설정해야하는 경우라면 아예 테스트 대상 클래스를 상속받는 클래스를 클래스를 테스트 패키지에 하나 만든다.  그럼 원형을 해칠 필요도 없고, 딱 테스트에 적합한 형태로 맘대로 가지고 놀 수 있다.

 

하지만 스프링과 엮인 부분들이 많다면 테스트에 스프링을 끼지 않을 수 없다. 난감하다. 스프링과 함께 테스트를 돌릴 때 가장 난감한 점은 테스트 실행 시간이 꽤 든다는 점이다.  특히 매 테스트마다 어플리케이션 전체가 올라갔다가 내려갔다를 반복된다.  단위 테스트라는 말을 쓸 수 있을까?  그래서 그런지 통합 테스트(Integration Test)라고 이야기하는 경우가 많다.  이런 것이 싫었던 것도 있어서 정말 Integration Test가 아니면 웬만하니 테스트를 잘 작성하지 않았다.

이러던 것이 스프링부트 1.4 버전을 기점으로 좀 더 쉬운 형태로 테스트 작성 방법이 바뀐 사실을 알게 됐다.  예전게 못먹을 거라고 생각이 들었던 반면에 이제는 좀 씹을만한 음식이 된 것 같다.  사실 부트 버전을 1.4로 올린 다음에 테스트를 돌려볼려고 하니 Deprecated warning이 이전 테스트에서 뜨길래 알았다.  그냥 쌩까고 해도 별 문제가 없을 것을 왜 이걸 굳이 봤을까…

이미 봐버렸으니 역시나 제대로 쓸 수 있도록 해야하지 않을까?  이제부터 언급하는 스프링 테스트는 부트 1.4 바탕의 테스트 이야기다.

@SpringBootApplication
public class Application {
   public static void main(String[] args) throws Exception {
      SpringApplication.run(IPDetectionApplication.class, args);
   }
}

테스트를 작성할려면 가장 먼저 어플리케이션 자체의 선언이 @SpringBootApplication 어노테이션으로 정의되야만 한다.  부트 어플리케이션이 되는 방법은 이 방법말고 여러 방법이 있지만 1.4의 테스트는 꼭 이걸 요구한다. 안달아주면 RuntimeException을 낸다.  강압적이지만 어쩔 수 없다.

환경을 갖췄으니 이제 테스트를 이야기하자.  가장 간단히 테스트를 돌리는 방법은 테스트 클래스에 다음 두개의 어노테이션을 붙혀주면 된다.

@RunWith(SpringRunner.class)

@SpringBootTest

이 두 줄이면 스프링 관련 속성이 있는 어떤 클래스라도 아래 코드처럼 테스트할 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
@Log4j
public class SummonerCoreApiTest {
    @Autowired
    private SummonerCoreApi summonerApi;

    @Test
    public void shouldSummonerApiLookUpAnAccountWithGivenAccountId() {

다 좋은데 이 방식의 문제점은 실제 스프링 어플리케이션이 실행된다는 점이다.  물론 테스트 메소드(should…)가 끝나면 어플리케이션도 종료된다.  @SpringBootTest 라는 어노테이션이 주는 마력(!!)이다.  만약 Stage 빌드를 따르고 있다면 테스트를 위한 환경을 별도로 가질 수 있다.  메이븐을 이용해서 환경을 구분하는 경우에는 별 문제가 없지만 만약 스프링 프로파일(Spring profile)을 이용하고 있다면 별도로 지정을 해줘야 한다.  @ActiveProfiles(“local“) 어노테이션을 활용하면 쉽게 해당 환경을 지정할 수 있으니 쫄지 말자.

OMG

어떤 클래스라도 다 테스트를 할 수 있다!!  몇 번에 걸쳐 이야기하지만 어플리케이션 실행에 관련된 모든게 다 올라와야하기 때문에 시간이 오래 걸린다.  좀 더 현실적인 타협안이 있을까?

일반적으로 Integration Test는 RESTful 기반 어플리케이션의 특정 URL을 테스트한다.  따라서 테스트 대상이 아닌 다른 요소들이 메모리에 올라와서 실행 시간을 굳이 느리게 할 필요는 없다.  타협안으로 제공되는 기능이 @WebMvcTest 어노테이션이다.  어노테이션의 인자로 테스트 대상 Controller/Service/Component 클래스를 명시한다.  그럼 해당 클래스로부터 참조되는 클래스들 및 스프링의 의존성 부분들이 반영되어 아래 테스트처럼 실행된다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";

        GIVEN: {
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            assertThat(actualResult.getPlayerId(), is(...));
        }
    }
    ...

이 테스트는 Integration Test이다.  역시나 Integration Test는 비용이 많이 들고, 테스트 자체가 제대로 돌도록 만드는 것 자체가 힘들다.  DB를 연동하면 DB에 대한 값도 설정을 맞춰야하고, 외부 시스템이랑 연동을 한다면 것도 또 챙겨야한다.

포기할 수 없다. 이걸 단위 테스트할려면 어떻게 해야하는거지?  Mocking을 이용하면 된다!!!  테스트 대상 코드에서 “스프링 관련성이 있다.“는 것의 대표는 바로 @Autowire에 의해 클래스 내부에 Injection되는 요소들이다.   이런 요소들이 DB가 되고, 위부 시스템이 된다.  해당 부분을 아래 코드처럼 Mocking 방법을 알아보자.

  • 테스트 대상 코드의 내부 Injecting 요소를 @MockBean이라는 요소로 선언한다.  이러면 대상에서 Autowired 될 객체들이 Mocking 객체로 만들어져 테스트 대상 클래스에 반영된다.
  • 이 요소들에 대한 호출 부위를 BDDMockito 클래스에서 제공하는 정적 메소드를 활용해서 Mocking을 해준다.
  • given/willReturn/willThrow 등 과 같이 일반적인 Mockito 수준에서 활용했던 코드들을 모두 활용할 수 있다.

그럼 명확하게 클래스의 동작 상황을 원하는 수준까지 시뮬레이션할 수 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    private JacksonTester<AccountInfo> responseJson;
    private JacksonTester<AccountInfo[]> responseJsonByPlayerId;

    @Autowired
    private MockMvc mvc;

    @MockBean
    private AccountInfoService accountInfoService;

    @Before
    public void setup() {
        JacksonTester.initFields(this, new ObjectMapper());
    }

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";
        final AccountInfo expectedAccountInfo;

        GIVEN: {
            expectedAccountInfo = new AccountInfo("dontCareAccountId", givenUsername, "dontCarePlayerId");
            given(accountInfoService.queryByUsername(givenUsername)).willReturn(expectedAccountInfo);
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            actualResult.andExpect(status().isOk())
                    .andExpect(content().string(responseJson.write(expectedAccountInfo).getJson()));
        }
    }
    ...

특정 서비스를 테스트하는데 전체를 다 로딩하지 않고, 관련된 부분 모듈들만 테스트 하고 싶은 경우에는 아래 코드를 참고한다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { SomeService.class, InterfacingApi.class, CoreApi.class})
@Log4j
public class SomeServiceTest {
    @Autowired
    private SomeService service;

   @Test
    public void shouldServiceQueryAccount() {
        final long accountIdAsRiotPlatform = 1000l;
        GIVEN: {}

        final Account account;
        WHEN: {
            account = service.lookup(accountIdAsRiotPlatform);
        }

        THEN: {
            assertThat(account.getAccountId(), is(accountIdAsRiotPlatform));
        }
    }
}

만약 Controller를 메소드를 직접 호출하는게 아니라 MockMvc를 사용해 호출하는 경우라면 다음 2개의 어노테이션을 추가하면 된다. 이러면 테스트할 대상 하위 클래스를 지정할 수 있을뿐만 아니라 MockMvc를 활용해서 Controller를 통해 값을 제대로 받아올 수 있는지 여부도 명시적으로 확인할 수 있다.

@AutoConfigureMockMvc
@AutoConfigureWebMvc

Swagger 관련 오류에 대응하는 방법

JPA 관련된 테스트 혹은 WebMvcTest를 작성하는 과정에서 아래와 같은 Exception을 만나는 경우가 있다.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.util.List<org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1466)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1097)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1059)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
	... 58 more

별거 한거 없이 정석대로 진행을 했는데 이런 뜬금없는 오류가 발생하는 상황에서는 프로젝트에 Swagger 관련된 설정이 있는지 확인해봐야한다. Swagger에 대한 Configuration에서 다른 설정을 해주지 않았다면 스프링부트 테스트는 Swagger와 관련된 Bean 객체들도 실행시킬려고 한다. 이걸 회피하기 위해서는 다음의 두가지 설정을 Swagger쪽과 문제가 되는 테스트쪽에 설정해줘야 한다.

Swagger 설정

@Configuration
@EnableSwagger2
@Profile({"default", "local", "dev", "qa", "prod"})
public class SwaggerConfiguration {
...
}

스프링부트가 기본으로 스프링 프로파일을 사용한다. 본인이 스프링 프로파일을 사용하든 안하든… 위의 케이스처럼 스프링이 적용될 환경을 명시적으로 정의해둔다. 대부분의 환경이 위의 5가지 환경으로 분류되고, 별도로 명시하지 않으면 default가 기본 프로파일로 잡힌다. 물론 테스트 케이스를 실행하는 경우에도 명시적으로 해주지 않으면 default 프로파일이 적용된다. 때문에 이제 테스트가 이를 회피하기 위한 프로파일을 명시해준다.

테스트 클래스 설정

@RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles("test")
@Log4j
public class AccountInfoServiceTest {
...
}

이렇게 실행되면 테스트가 실행될 때 test라는 프로파일을 가지기 때문에 Swagger 관련 모듈들을 로딩하지 않고, 테스트가 실행된다.

일단 여기까지 정리해봤다.

Kafka를 이용한 메시징 시스템 구성하기

최근의 개발 경향은 확실히 마이크로서비스를 지향한다.  가능하면 작은 어플리케이션을 만든다.  그리고 이 어플리케이션들의 소위 콜라보(Collaboration)로 하나의 시스템이 만들어진다.  혹은 만들어지게 구성을 한다.  이와 같은 마이크로서비스 모델이 주는 이점은 나도 몇 번 이야기를 했고, 많은 사람들이 장점에 대해서 구구절절하게 이야기하기 때문에 말을 더 하지는 않겠다.

????여기에서 급질문!!  작은 어플리케이션… 근데 작은 어플리케이션을 지향하는 마이크로서비스의 문제점은 없을까?

먼저 생각나는 첫번째 문제는 작게 만든다고 했으니까 반대급부로 “어플리케이션의 수가 많아진다.” 아닐까 싶다.  지극히 당연한 지적이다. 여기서부터 문제들의 가지를 뻣어나가보자.  뭔가 새로운 걸 만든다면 그럼 이 많은 것들 가운데 연동해야 할 것들이 또한 많다는 것을 의미한다.  잘 외우고 있던지 아니면 잘 적어놔야한다.

많은 대상들과 서로 정보를 주고 받으면 새로운 시스템이 동작해야한다.  대상이 많아지면서 우리는 돌이켜봐야한다.  어울려 일하는 대상과 어떤 방식으로 일을 해야하는지.  일부 처리는 인과 관계가 존재하기 때문에 순서에 맞게 일이 되야 한다.  하지만 특정 작업은 다른 일과 아무 관련성이 없는 경우에는?  개발자의 세상에서 이 상황은 “관련이 없는 이 일과 저 일을 한꺼번에 해도 된다는 것“을 의미한다.  어플리케이션 수준에서 동시에 뭔가를 함께 실행되도록 만드는 건… 생각보다 어렵다.

아래와 같은 통상적인 A, B, C, D, E라는 어플리케이션에 의해 구성된 마이크로서비스를 보자.  A 어플리케이션은 “B → C → E” 순서로 다른 어플리케이션과 연동한다.  물론 B, C, E 어플리케이션들도 나름 열심히 연동한다.  이 와중에 D 어플리케이션이 고장되면 어떻게 될까?  A 어플리케이션은 과연 문제가 D 때문에 본인의 인생이 꼬인 걸 알 수 있을까?

MicroserviceModel

 

A는 B 혹은 C 혹은 E의 문제라고 알 것이다.  당연히 A가 직접 연동하는 대상에 D는 포함되어 있지 않기 때문이다.  책임은 전가되고 긴가민가 하는 상황이 길어지면 해결은 안되고 서로 비난하는 상황만 연출된다.  D 어플리케이션은 그 와중에 자신이 문제의 핵심이라는 사실을 알지 못할 수도 있다.  이 혼돈은 “문제의 본질을 알지 못한다.“가 최종적인 결론이다.

마이크로서비스 구조가 활발해질수록 당면하게 되는 이슈를 다음의 3가지 어려운 말로 정리해본다.

  • 소규모 어플리케이션의 증가로 인한 연동 구조의 복잡성이 증가하고 이로 인한 관리 비용이 증가한다.
  • 어플리케이션의 구현 로직이 필연적으로 동기화될 수 밖에 없으며, 상호 독립적인 어플리케이션들의 직렬 실행이 전반적인 성능 저하를 유발시킨다.
  • 다중 계층에 의한 연동 구조는 문제 요인에 대한 즉각적인 발견을 어렵게 하며, 핵심 요소의 장애 발생시 시스템 수준의 재앙을 초래할 수 있다.

이렇게 열거해놓고 보니 아무리 마이크로서비스 구조가 좋다고 이야기를 해봐도 연동 계층이 많아질수록 벗어나고 싶던 Monolythic 시스템과 별반 차이없는 괴물이 되어버린다.  Monolythic 구조의 문제점을 해결하는데 적절한 대안으로 마이크로서비스 구조가 딱이긴 하다.  하지만 과하면 탈이 나긴 마련인데…  이 상황을 다른 각도에서 볼 여지는 없을까?

지금까지의 논의에서 작은 시스템이 많아졌을 때 이런 문제 저런 문제라고 이야기를 했지만 그 본질은 “연동”에 있다.  연동을 위한 관리 포인트, 직렬화된 순차적 연동, 연동의 깊이에 따른 문제 진단의 어려움!  그렇다 문제는 바로 연동이다!!

그럼 이 시점에서 연동을 왜 하는지를 생각해보자.  연동의 목적의 기능(Function)에 있는지 아니면 데이터 흐름(Flow)에 목적이 있는지?  후자의 경우라면 데이터 흐름을 제공하는 시스템이 있다면 문제를 좀 더 손쉽게 풀 수 있지 않을까?  서론이 무지 길었는데 이것이 하고 싶은 이야기다. (정말 말이 많은 편이다. ㅠㅠ)

System as a data perspective

넓은 의미에서 우리가 만드는 시스템은 데이터의 흐름이다.  외부 입력은 시스템에 내재된 로직의 계산 과정을 거친다.  시스템의 한 어플리케이션에서 처리된 데이터는 다른 어플리케이션으로 흐른다.  데이터의 흐름은 큰 맥락상 순차적이거나 혹은 확산적인 형태일 수 있다.

Consecutive data flow

Data pipelining

순차적 데이터 전달 모델은 시스템은 입력을 이용해 결과물을 만든다.  한 어플리케이션의 출력은 다른 어플리케이션의 입력으로 이어진다.  크게 색다를 것도 없다.  대부분의 마이크로서비스가 가지는 전형적인 모델이다.  시스템의 전체 형상에서 필요한 로직을 코드로 반영했고, 안타깝게도 흐름 관련 부분도 개발자가 코드로 추가 제어를 해야만 한다.

우리의 업무 로직은 반드시 존재해야한다.  하지만 전체 데이터 흐름을 제어하는 것까지 꼭!!! 제어해야만하는 걸까?  그림에서 보이듯이 우리의 입력이  D API Service에 도달하면 그만이다.  흐름의 제어해줄 수 있는 존재가 있다면, 우리는 이 흐름만을 정의하면 된다.  그리고 흐름 사이에 새롭게 필요해진 업무 로직을 살짝 끼워넣는다.  기존에 잘 돌아가던 기능들을 변경하지 않는다.

  • 새로운 로직을 Input/Output Data Format에 근거해서 만들면 된다.
  • 그리고 이 로직을 흐름 사이에 반영하면 된다.

기능들을 조합하기 위해 별도의 시스템을 새로 만들 필요도 없고, 기존 시스템들을 변경시키지도 않는다.

Data proliferation

쓸모가 많은 데이터의 경우 많은 사람들/시스템들이 해당 데이터를 원한다.  데이터를 얻는 대부분의 방법은 구하는(Get) 것이다.  원하는 사람이 데이터를 구하는 것이지 원한다고 데이터를 가진 사람이 떠먹여주진 않는다.  그런데… 떠먹여주면 안될까???

data proliferation

쓸모가 많은 데이터의 경우라면 관심 시스템들이 요청로직을 개별적으로 만드는 것보다는 아예 전달받는 데이터를 가진쪽에서 전달해주는 것이 훨씬 더 효율적일 수 있다.  특히 관심 데이터가 생성(Available)되었을 때 이를 전달자가 즉시 구하는 쪽에 바로 전달할 수 있다.  이는 시스템간의 데이터 동시성을 보장한다는 큰 매력이 된다.  이외에 데이터를 가진 쪽에서 바로 데이터를 넣어주기 때문에 구하는 쪽에서 일일히 Get(or Query) 기능을 구하지 않더라도 바로 들어오기 때문에 구현적 관점에서도 일부 이점을 제공한다. (하지만 크지는 않다 ㅠㅠ)

Kafka

Kafka는 오픈 소스 분산형 메시징 시스템이다.  주절이주절이 설명하는 것보다는 공식 홈페이지에 들어가보면 관련 내용을 잘 찾아볼 수 있다.  관련 책을 한권 사서 읽어도 봤지만 책보다는 그냥 홈페이지랑 구글링 통해서 얻는 자료가 더 유익하다.  그만큼 오픈 소스의 힘은 위대한 거겠지?

만든 사람들이 이야기하는 Kafka의 주요 능력이라고 개요 페이지에 적어둔걸 옮겨본다.

  1. It lets you publish and subscribe to streams of records. In this respect it is similar to a message queue or enterprise messaging system.
  2. It lets you store streams of records in a fault-tolerant way.
  3. It lets you process streams of records as they occur.

정리해보면 Publish & Subscribe 모델을 지원하는 메시징 시스템이고, 분산형 시스템들이 가지는 대부분의 특징처럼 한 두개 장비가 망가지더라도 잘 돌아간다라는 것, 그리고 Queue를 기본한 메시징 시스템으로 데이터 순서를 보장한다라는 것이다.  개인적인 매력을 느끼는 부분들을 열거해보면…

  • 시스템이 별도의 DB등 번거로운 외부 시스템을 필요로 하지 않는다.   DB. 정말 싫다.
  • 설치할게 별로 없다. 압축풀고 실행시키면 바로 돈다.
  • 특이한 프로토콜을 사용하도록 강요하지 않는다.  Producing/Consuming 과정에서 String 혹은 Byte Array를 바로 사용할 수 있다.  정 특이한걸 만들고 싶으면 역시나 만들어서 쓸 수 있다.

가장 잘 적용되리라고 생각되는 분야는 real-time streaming data pipeline 이다. 실시간 데이터의 시스템/어플리케이션간 손실없는 전달이 필요한 환경에 최적이다라고 개발자들이 이야기하고 많이 써보지는 않았지만 동감한다.

Why Kafka?

왜 그럼 이 시스템을 구현하는데 Kafka를 선택했을까?

  • 빠르다 – 분산형 메시지 처리와 관련된 시스템들이 Kafka 이전에도 Active MQ와 같이 다양하게 존재해왔다.  이런 분산형 시스템들에 비해서 Kafka 시스템이 갖는 강점은 빠른 데이터 처리에 있다.  여러 이유가 있을 수 있겠지만, Kafka를 최초로 개발한 LinkedIn 팀에서 기존 Messaging system의 문제점을 분석하고 이를 개선하기 위해 어떤 부분이 시스템의 핵심이 되어야하는지, 그 맥락을 잘 집었다고 볼 수 있겠다.
  • Publish & Subscribe 모델 지원 – 앞서 언급한 데이터 활용 모델을 구현하기 위해서는 메시징 시스템이 Publish & Subscribe 모델을 지원해주는게 최선이다.  지원하지 않는다면 따로 이 부분을 구현해야하는데 데이터의 연속성 보장등을 어플리케이션에서 단독으로 보장하기에는 많은 무리수가 있다.  Topic을 기반으로 Kafka가 이 부분에 대한 강력한 뒷배경이 되어준다.
  • 잠깐 다운되도 된다 – 데이터를 처리하는 Consuming 시스템의 입장에서 다운타임은 운영적 관점에서 치명적인 시간이된다.  하지만 Kafka 역시 분산형 메시지 시스템들이 공통적으로 지원하는 메시지 보관 기능을 마찬가지로 지원한다.  다만 언제까지고 데이터를 보관해주지는 않는다.  시스템 단위에서 설정된 보관 시간이 존재하고, 해당 시간 이내의 데이터는 분산환경내에 보관된다.

Topic and Partitions

본격적인 이야기를 진행하기전에 Kafka 내부에서 데이터가 다뤄지는 Topic과 Partition에 대한 개념에 대해 짧게 알아보고 가자.

Topic은 Kafka에서 데이터에 대한 논리적인 이름이다.  Kafka를 통해 유통되는 모든 데이터는 Topic이라는 데이터 유형으로 만들어지며 Producer/Consumer들은 자신이 생성 혹은 소비할 Topic의 이름을 알고 있어야만 한다.

Partition은 Topic을 물리적으로 몇 개의 분할된 Queue 형태로 나눠 관리할지를 지정한다.  특정 Topic이 1개의 Partition으로 되어 있다는 것은 데이터 처리를 위한 Queue가 한개 존재한다는 것이다.  3개의 Partition으로 구성되어 있다면 3개의 Queue가 존재하고, 각각의 Queue에는 독립적으로 데이터가 들어갈 수 있다.   이론적으로 Partition의 개수가 많을수록 동시에 클러스터에 데이터를 더 빠르게 받아들일 수 있음을 의미한다. 하지만 과하면 망한다는거… 따라서 클러스터를 구성하는 서버, CPU/Core, Memory 등 Resource의 제약을 고려해서 최적의 Partition 정책을 세우는게 필요하다.

우리가 다루는 Kafka내의 메시지/데이터는 모두 Topic이라는 이름을 통해 유통된다.  그리고 이 유통 과정에서 성능이 제대로 나게 할려면 시스템에 걸맞는 Topic별 Partition 정책을 어떻게 가져가야할지를 잘 판단해서 결정하는게 필요하다!

Messaging system with Kafka

자 여기까지를 바탕으로 Kafka를 이용한 메시징 시스템 구현 내용을 본격적으로 탐구 생활해본다.

지금까지 Kafka 시스템에 대해 쭉~ 이야기했다.  계속 이야기를 했지만 Kafka 자체가 메시징 시스템이다.  그런데 여기다가 뭘 더할게 있다고 이야기를 더할려고 할까?   메시지 시스템은 다른 시스템이 쓰라고 존재한다.  그렇다면 관건은 이 시스템을 어떻게 하면 손쉽게 사용할 수 있을지다.  아무리 좋다고 하더라도 사용하기 불편하다면 말짱 도루묵이다. 결국 시스템이 사용하는 시스템이더라도 그 시스템을 사용하도록 만드는건 사람이고, 복잡한 절차나 기술적인 난해함이 있다면 환영받지 못한다.

환영받는 메시지 시스템이 될려면 어떤게 필요할까?

  • 접근성이 좋아야 한다.  개발자라면 누구나 아는 기술로 접근이 가능해야 한다.
  • 표현의 자유를 보장해야 한다.  메시지에 어떤 데이터가 담길지 나도 모르고 당신도 모른다.  그런고로 다양한 데이터 형식을 기술할 수 있는 자유도를 가지고 있어야 한다.

사실 Kafka가 좋긴 하지만 이걸 사용하는 사람이 굳이 Kafka를 이해하면서까지 이 시스템을 쓰라고 하는건 어불성설이다.  따라서 메시지 시스템의 내부는 감추면서 개발자들이 보편적으로 활용할 수 있는 기술들을 가지고 접근할 수 있도록 해주는게 가장 좋다고 생각한다.

이런 특성들을 고민해봤을 때 가장 좋은 것은 Publish & Subscribe 구조에 대응하는 Producer / Consumer를 연동하는 개별 어플리케이션이 Kafka 수준에서 연동하도록 할게 아니라 이를 중계해주는 Delegating Layer를 지원해주는게 좀 더 효과적이다.  Delegating Layer를 통해 Kafka에 대한 의존성 대신 흔히 어플리케이션 연동을 위해 사용하는 HTTP 프로토콜을 사용할 수 있도록 한다면, 연동하는 어플리케이션들이 보다 손쉽게 이를 이용할 수 있다.  또한 JSON 메시지를 활용하면 앞서 이야기했던 표현의 자유를 적극적으로 챙길 수 있다.  물론 개발자들의 익숙함은 덤일 것 이고 말이다.  설명한 전체적인 윤곽을 간단히 스케치해보면 아래와 같은 형상이 될 수 있다.

 

MessagingSystemOverview

 

  • Producer – HTTP POST 방식으로 연동 어플리케이션으로부터 Payload 값을 받는다.  Payload가 Kafka 시스템을 통해 유통될 Topic의 이름은 URI Path Variable을 통해 전달받는다.  RESTful의 개념상 Path Variable이 자원의 이름을 내포하기 때문에 이 형태가 가장 적절한 표현 방법일 것 같다.  POST 메소드를 사용하는 것도 메시지를 생성(Create)한다는 의미이기 때문에 맥락상 어울린다.
  • Consumer – Kafka의 특정 Topic을 Subscribe하고 있으며, 생성된 데이터를 수신하면 이를 요청한 어플리케이션에 마찬가지로 POST로 JSON Document를 POST 메소드의 Payload로 전달한다.  여기에서는 POST 메소드로 전달 메소드를 전체적인 일관성을 위해 고정해서 사용한다.  PUT 혹은 PATCH 등과 같은 다른 메소드를 사용할 수 있도록 허용할 수도 있으나 되려 이용에 혼선을 줄 수 있기 때문에 하나만 지원한다.

이와 같은 구조는 데이터를 생산/소비하는 쪽 모두에게 웹서비스의 기본적인 개념만 있다면 누구나 손쉽게 데이터를 접근할 수 있는 길을 열어준다.  생산자 측에서는 Producer의 endpoint로 JSON Payload로 데이터를 전달하면 된다.  소비자도 마찬가지로 JSON Payload를 받을 수 있는 endpoint를 웹어플리케이션의 형태로 만들어두면, 자신이 관심을 둔 메시지가 생성될 때마다 이를 수신하게 된다.

한가지 눈여겨 볼 부분은 메시지 시스템의 Consumer와 실제 Consumer Application의 관계다.  RESTful 기반의 웹 어플리케이션은 입력된 Payload를 이용해 일련의 처리를 수행하고, 그 결과를 요청자에게 반환한다.  그리고 앞서 Consecutive Data flow의 그림에서 설명했던 것처럼 반환된 Payload는 연속된 처리를 위한 입력으로 사용되는 경우가 많다.  이를 개발자적인 관점에서 살펴보자.  일종의 함수(Procedure가 아닌 Function) 호출과 아주 비슷한 모델 아닌가?

topicedMsg1 returnedMsg1 = consumer1.function1(initialTopicedMsg);
topicedMsg2 returnedMsg2 = consumer2.function1(returnedMsg1);
topicedMsg3 returnedMsg3 = consumer2.function1(returnedMsg2);
...

약간 억지로 끼워맞추기식이긴 하지만 Method Chain 비슷해보이지 않나?? -_-;;  중요한 점은 특정 Topic이 있다면 해당 Topic을 입력으로 받는 Consumer를 찾을 수 있다는 것이다.  그리고 그 Consumer에게 Topic의 메시지를 전달하면 그 Response Payload를 얻을 수 있다.  하지만 Response로부터 Payload가 어떤 Topic인지를 명시하는 것이 좀 까리하다.

물론 Response Header에 Custom Header를 정의하는 것도 방법일 수 있겠다.  하지만 이렇게 하는 경우에 이를 암묵적인 규약으로 개발자들에게 강요하는 것이고 더구나 명시성이 떨어진다.  Consumer Application의 코드를 알기 전에는 Topic 값으로 어떤 것을 반환하는지 알기 어렵다.  이런 경우에 명시적으로 아예 Consumer Application의 정의에 반환되는 Payload에 대응하는 Topic의 이름이 뭔지를 명시하는 것이 오히려 바람직하다고 개인적으로 생각한다.

이 방식으로 Consumer와 Consumer Application간에 상호작용하는 예시를 순서도로 그려보면 아래와 같은 형상이 될 수 있다.

ConsumerInteractions

 

  • 일반적인 경우는 Consumer 1과 같이 입력 메시지를 처리 후 Response Payload를 반환한다.  Consumer 1이 반환한 Payload의 타입은 설정을 통해 어떤 Topic에 해당하는지 메시지 시스템은 알고 있다.  반환 메시지를 시스템에 지정된 Topic 이름으로 Publish함으로써, 자동적으로 다음 순서에 해당하는 Consumer 2를 찾아 들어가게 된다.
  • 특정 Topic에 대해서는 여러 Consumer들이 관심이 있을 수 있고, 당연히 여러 Consumer에게 동시에 전달된다.
  • Consumer Application에서 반환된 값이 무시되어야 할 경우도 있다. (물론 반환 Payload는 존재할 수 있다.)  이 경우를 위해 명시적으로 해당 Response Payload를 무시하기 위한 장치가 필요하다.  이미 우리는 프로그래밍 과정에서 void 라는 단어와 많이 익숙하다.  Topic의 이름 가운데 void 라는 reserved keyword를 둔다.  그리고 Topic의 이름이 void인 경우에는 반환된 payload를 무시한다.
  • 일괄적으로 메시지를 무시하는 경우가 아니라 특정 케이스에 한해서만 시스템이 이를 무시하도록 만들고 싶은 경우도 발생한다.  이건 코딩에서 if 문과 비슷한 역할이다.  이에 대한 처를 위해 HTTP Status code 가운데 특정 값을 Response 값으로 반환하는 경우에 Response Payload를 무시하도록 하자.  HTTP Status code는 개발자들의 공통 사항이기, 별도의 장치를 두는 것보다는 훨씬 설득력이 있다. (이 경우가 필요할까 싶기도 하지만 실제로 활용하다보면 정말 많은 도움이 된다.  이게 있으면 메시지 시스템을 가지고 for loop도 만들 수 있다!!!)

시스템에 대한 이야기 와중에 이 부분의 이야기가 가장 길었다.  그만큼 메시징 시스템 가운데 개발을 해야하는 측면에서 가장 많은 시간 투자가 필요한 부분이다.  하지만 이 시나리오에 맞춰서 잘 만들어두면 정말 편리한 세상을 당신이 만들고 있다는걸 알게 될 것이다.

Pros

메시징 시스템을 중심으로 마이크로서비스 방식의 어플리케이션 시스템을 구성한다고 했을 때 다음과 같은 이점을 기대할 수 있다.

  • 서로 의존성이 없는 작업들을 동시에 처리할 수 있다.  시스템 수준의 상당한 성능 향상을 기대해 볼 수 있다.
  • 단위 작업에 집중할 수 있다.  작업/배포의 단위를 더욱 더 작게 만들 수 있다. 노드 혹은 go 언어가 더 득세할 수 있지 않을까?
  • 설정 변경만으로 새로운 업무 처리가 가능하던가 혹은 기존 업무 흐름을 손쉽게 조작할 수 있다.
  • 데이터를 주고 받아야 할 상대방의 endpoint를 굳이 알 필요도 없고, 상대방을 직접 호출할 필요도 없다.

Cons

메시징 시스템이 많은 장점을 주는 것처럼 보이지만, 이것도 실제로 쓸려고 했을 때 이런 저런 문제점이 있다.

  • 시스템 자체가 문제다.  구축을 위해서는 Kafka 및 Producer/Consumer 클러스터를 구축하는데 상당히 시간이 걸린다.  장기적이 아니라면 그냥 일반적인 마이크로서비스 체계를 구축해서 사용하는게 훨 낫다.
  • 트랙잭션 이런걸 생각하지말자.  SOA로부터 현재의 마이크로서비스 세상까지 웹 도메인에서 트랜잭션을 완성시키고자하는 많은 노력이 있었다.  하지만 제대로 된 놈을 못봤다.  마찬가지로 여기에서도 이걸 실현시킬 생각을 했다면 번지수를 한참 잘못 찾았다.  되려 트랜잭션이 없어도 돌아가도록 본인의 어플리케이션을 설계하는게 훨씬 더 재미있을 것 같다.
  • 비동기 방식이다.  성능적으로는 아주 좋은 이점이지만 실제 운영될 때 상상을 초월하는 모습을 보여주는 경우가 있다.  특히 타이밍이 맞아야 하는, 즉 동기성이 필요한 부분에 이 시스템을 이용한다면 낭패를 볼 수 있다.  “내가 확인했을 때 항상 그랬다!!” 라는 말을 비동기 세상에서는 하지 말자.  항상 그렇게 동작하다 중요한 순간에 당신을 배반하는게 비동기 세상이다.

 

여기까지다.  이 과정을 통해 만들었던 시스템이 궁극의 마이크로화를 향해나가는 서비스 세상에서 서로 얽히고 설킨 거미줄 같은 실타래를 큰 파이프를 꼽아서 정리하는데 도움이 됐으면 좋겠다.  또한 동기적 세상에서만 머무리던 사고를 비동기를 활용할 수 있는 다른 기회의 세상이 되기를 또한 바란다.

이제부터는 Kafka 클러스터를 실제로 설치하는 과정에서 참고했던 정보와 주의해야할 몇가지 경험들을 공유하기로 하겠다.

Tips in building a Kafka cluster

기본 설치 방법: http://kafka.apache.org/quickstart 로컬 환경에서 간단히 돌려보기 위한 방법은 이걸로 충분하다.

AWS EC2 setup

물리 장비를 사용한다면 각 장비를 셋업해야하지만… 요즘 다 AWS 쓰는거 아닌가???  그러니 AWS를 기반으로 설명하도록 하겠다.

  • Kafka를 1개 instance로 셋업한다.
  • 셋업된 instance를 AMI로 만들고, instance를 재생성할 때 이를 활용하면 좋다.
  • 생성시킬 전체 클러스터의 각 EC2 instance의 IP를 DNS 작업을 해준다. 설마 IP로 관리할 생각을 가지고 있지는 않다고 생각하는데.

Configuring cluster

  • 클러스터링: http://epicdevs.com/20  해당 사이트에서 기본적인 설치 방법에 대해 잘 설명해뒀다.
  • Cluster 노드 장비들은 클러스터 외부에서 인식 가능한 이름을 부여하고 이를 DNS에 등록해서 활용하는게 좋다.  만약 DNS 등록이 여의치않다면 각 노드들의 이름을 개별 노드의 hosts 파일에 모두 등록하는 작업을 최소한 해둬야한다.

Possible issues

진행을 하다가 이런 문제점을 만날 수 있다.

Partition offset exception

kafka.common.KafkaException: Should not set log end offset on partition [test,0]'s local replica 2
at kafka.cluster.Replica.logEndOffset_$eq(Replica.scala:66)
at kafka.cluster.Replica.updateLogReadResult(Replica.scala:53)
at kafka.cluster.Partition.updateReplicaLogReadResult(Partition.scala:239)
at kafka.server.ReplicaManager$$anonfun$updateFollowerLogReadResults$2.apply(ReplicaManager.scala:905)

여러 이유가 있을 수 있지만, FAQ를 읽어보면 좋은 Insight를 얻을 수 있다. 내 경우에는 topic을 생성할 때 쉽게 하자고 현재 zookeeper 호스트의 이름을 localhost로 줬다. 이 때문에 cluster내의 다른 instance에서 해당 호스트를 제대로 찾아내지 못했던 것 같다. 이 경우에 topic자체는 생성된 것이기 때문에 전체 instance를 내렸다가 올리면 문제를 해결할 수 있다. 할려면 제대로 된 호스트 이름을 주고 작업해주는게 좋다.

Metadata error

기본 설정이라면 클러스터 환경에서Producer/Consumer client에서 Metadata를 확인할 수 없는 경우 아래와 같은 오류를 만날 수 있다.

2016-11-23 09:12:18.231  WARN 3694 --- [ad | producer-1] org.apache.kafka.clients.NetworkClient   : Error while fetching metadata with correlation id 168 : {test=LEADER_NOT_AVAILABLE}
2016-11-23 09:12:18.334  WARN 3694 --- [ad | producer-1] org.apache.kafka.clients.NetworkClient   : Error while fetching metadata with correlation id 169 : {test=LEADER_NOT_AVAILABLE}

오류가 발생하는 이유는 클러스터 외부의 장비에서 접속한 클러스터 장비 가운데 하나를 찾질 못해서 발생한다. 문제를 해결할려면 다음의 설정을 server.properties 파일에 반영해줘야 한다.

advertised.host.name = known.host.name

advertised.host.name 필드는 Broker에 접근한 클라이언트에게 “이 서버로 접근하세요” 라는 정보를 알려주는 역할을 담당한다. 별도로 설정을 잡지 않으면 localhost 혹은 장비의 이름이 반환된다. Producer 혹은 Consumer쪽에서 반환된 hostname을 인식할 수 없으면 추가적인 작업을 할 수 없으므로 이 오류가 발생된다. 따라서 해당 속성의 값에 DNS에 등록된 호스트의 이름을 넣어주거나 클러스터내에 존재하는 장비들을 클라이언트 환경의 /etc/hosts 파일에 반영해줘야 한다.

Amazon Kinesis

Kafka를 가지고 작업을 하다보니 아마존에서 재미있는 걸 만들었다라는걸 알았다. 키네시스(Kinesis)?  대강 살펴보니 하는 짓이 내가 지금까지 이야기했던 것들과 상당히 유사한 일을 해준다.  물론 아마존 클라우드에 있는 여타 기능들과의 연계성도 아주 좋다.  만약 장기적인 관점에서 Kafka 클러스터를 구축할게 아니라면 Kinesis도 다른 대안이 될 수 있을 것 같다.

특히 이 글을 읽는 여러분들의 시스템이 AWS에서 동작하는 것들이 많다면 Kinesis가 상대적으로 더 좋은 선택이 될 수 있다. 가끔 생각지도 않은 요금 폭탄에 대해 쓰기전에 고민은 반드시 필수다!!

 

자주 사용하는 Shell script 모음

이것도 정리를 해두지 않으니 매번 찾게된다.

때 될때마다 정리해두자.

특정 디렉토리의 하위 디렉토리를 Pretty printing 하기

#!/bin/sh

depth=$2

space=" "
count=1
while [ $count -le $depth ];
do
	count=$((count+1))
	space="$space    "
done;

for dir in `ls $1`
do
	if [ -d $1/$dir ]; then
# pretty printing version
		echo "$space$dir"
# full path version
#		echo "$1/$dir"
		nextdepth=$((depth+1))
		$PWD/list.sh $1/$dir $nextdepth
	fi
done

다음 명령으로 실행시킴

./list.sh <aboulte-path] 1

.svn 디렉토리 지우기

find . -name .svn -exec ls '{}' \;

for-loop 돌리기

for i in array
do 
... looping commands here ...
done

특정 문자열을 공백으로 치환해서 이를 배열로 뽑아내기

email=abc@def.xyz
tokens=(${$email//@/ })
id="${tokens[0]}"
doimain="${tokens[1]}"
  • 주의할 점은 배열로 나눌려면 각각의 토큰이 공백( )으로 구분되어야 한다는 점이다.

간단한 사칙연산하기

index=$((index+1))

이 예제는 index++과 동일한 기능을 수행한다.

가능하면 까먹지 말아야 하는데 쓰는 경우가 자주 있질않다보니…


curl을 이용해서 반복적으로 웹 API 호출하기

간단한 배치로 웹 API를 호출하는 경우가 필요한데 이런 경우에 대표적으로 이용할 수 있는 것이 curl이다.  웹 API를 호출할려면 기본적으로 이런저런 헤더 정보를 추가로 줘야하는데 그런 경우에 문자열을 표시하기 위해 따옴표(“)를 써야한다.  그런데 따옴표로 스크립트상의 파라미터를 감싸버리면 이게 명령상에서 제대로 인식하지 못하는 경우가 발생한다.

우회적인 방법으로 생각할 수 있는게 스크립트 상에서 스크립트를 만들고, 그 스크립트를 실행하는 방법으로 이를 구현할 수 있다.

#!/bin/sh
for ip in `cat ip`
do
  data=" '{ \"clientIp\": \"$ip\" }' "
  out="curl -s -X POST http://localhost:8080/api/v1/ip -H \"Content-Type: application/json; charset=utf-8\" -d $data | awk -F, '{ print \$3 };'"
  echo $out > run.sh
  chmod +x run.sh
  result=`./run.sh`
  echo $ip " " $result
done

주의할 점은 출력할 결과에 마찬가지로 따옴표 혹은 쌍따옴표가 포함되야 하는 경우에는 Backslash(\)를 이용해서 이를 별도로 표시해줘야 한다는 것! 특히나 생성되는 스크립트 파일에서 참조되는 스크립트 변수와 같은 경우에도 마찬가지로 backslash를 넣어줘야한다.


awk 사용해서 마지막 필드 출력하기

awk '{ print $NF }' filename.txt

카라마조프 형제들 – 고전은 힘들다.

“죄와벌” 이후에 정말 오래간만에 도스토예스키의 작품을 난데없이 읽기 시작해서 이제사 마쳤다.  책갈피 기록을 찾아보니 올해 2월 17일이니까 다 읽는데까지 무려 10달이 넘게 걸려버렸다.  하기야 대학 2학년때 읽기 시작했던 죄와벌을 대학 4학년이나 되서야 다 읽었으니, 그 시절의 독서 속도에 비해서는 그나마 읽기가 좀 더 나아졌다고 해야할까?

난데없이 얽기를 시작했다고 이야기했다.  다니던 성당의 좌파 성향 한가득이시던 신부님의 강론중에 나온 “카라마조프”의 이야기를 들고, 참 재미있겠다는 생각이 들어 덜컥 이북을 구매했다.   신부님은 참 재미있다고 읽어보라고 권하긴 했지만… 생각에는 도스토예프스키의 책이 나한테는 참 어렵다는 생각이다.  읽다보면 간간히 재미있는 부분도 있긴 한데 너무 오래 읽어서 그런가 내용이 이어지지 않는 부분들이 많았던 것 같다.  아무래도 나중에 시간을 다시 내서 한 한달쯤안에 다시 한번 읽어봐야할 것 같다.

책에서는 카라마조프 가족의 4 남자가 나온다.  지독히도 세속적인 아버지 표도르, 순정파 장교 출신 드미트리, 시대의 지식인을 상징하는 이반, 마지막으로 믿음으로 순수한 막내 알료샤.  소설이 배경을 이루는 1800년대 말 즈음의 재정 러시아를 살아가는 인간 군상의 모습을 표현한다.  시대적 배경을 알려면 당시의 러시아 상황을 알아야 할 것 같지만 짧은 지식이 거기에 미치지는 못한다.  다만 물질적 탐욕, 성욕과 순정 그리고 이성을 빙자한 인간의 이기주의 등등을 소설에서 읽을 수는 있었던 것 같다.

과연 누가 아버지의 살해범인가? 장남 드미트리인가 아니면 사생아이지 인정받지 못하고 하인의 신분으로 머물렀던 스메르자코프인가?  그리고 스스로 인텔리임을 자임하며 스메르자코프에게 불만투성이 세상으로부터의 탈출구를 은연중에 내비쳤던 둘째 아들 이반인가?  수도원에서 성직자의 길을 걷다가 장로의 유언으로 스스로 그 길을 벗어난 얄료샤는?  온 집안이 풍비박산이 난 이후 먼 곳으로 떠나는 그의 모습은 뭘 의미하는거지?  그리고 이 막장 드라마에 등장하는 그루센카와 카체리나라는 두 여성이 의미하는 시대의 자화상은 뭘까?

러시아가 격변의 소용돌이에 있는 시점이고, 자본에 의해 사회적인 구조가 급변하는 시기였던 것 같다.  커다란 회오리 바람이 불어오는 과정에서 사생아를 포함한 이 일가족 각각이 그 시대를 살아가는 사람들을 형상하는 하나의 아이콘이지 않았을까 싶은 생각이다.  그 안에 결론은 없다.  자신의 추구만을 생각하다 그 욕망의 실타래가 얽혀버렸다.  풀려고 해도 풀기에는 이성적 사고보다는 포장된 이면 아래의 감정이 그 결과로 모두를 몰아가지 않았나 싶다.  옳고 그르다라는 결론은 의미가 없는 것만 같다.  남은 절망을 뒤로하고 막내 알료샤는 희망을 이야기하며 고향을 떠나려한다.

인형조종사에 의해서 대통령이 놀아나는 2016년 겨울의 대한민국을 관통하하고 있다.  기회를 내서 한번 더 읽어봐야겠다.

Springboot에서 Exception을 활용한 오류 처리

Java를 가지고 개발하는 오류 처리는 Exception을 활용하는 것이 정석이다.  개인적으로 값을 오류 체크하고 어떻게든 값을 만들어 반환하기보다는 오류가 발생하면 “오류다!” 라고 떳떳하게 선언하는 것이 좋은 방법이라고 생각한다.

RESTful API를 구현한 경우,  오류의 상태를 알려주는 가장 정석적인 방법은 HTTP Status Code를 활용하는 방법이다.  Exception을 통해서 이 응답 코드를 반환해주는 건 아주 쉽다.

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No candidate")
public class NotExistingCandidateException extends RuntimeException {
    public NotExistingCandidateException(String candidateEmail) {
        super(candidateEmail);
    }
}

이것과 관련해서 약간 말을 보태본다.  RESTful API를 개발하면서 응답 메시지의 Body에 상태 코드와 응답 메시지를 정의하는 경우를 왕왕본다.  RESTful 세상에서 이런 방식은 정말 안좋은 습관이다.   몇 가지 이유를 적어보면.

  1. 완전 서버 혹은 API를 만든 사람 중심적이다.  클라이언트에서 오류를 알기 위해서는 반드시 메시지를 까야한다.  호출한 쪽에서는 호출이 성공한 경우에만 처리하면 되는데 구태여 메시지를 까서 성공했는지 여부를 확인해야한다.
  2. API 클라이언트의 코드를 짜증나게 만들뿐만 아니라 일관성도 없다.  한 시스템을 만드는데 이런 자기중심적인 사람이 서넛되고, 상태 변수의 이름을 제각각 정의한다면 어떻게 되겠나?  헬이다.
  3. 표준을 따른다면 클라이언트 코드가 직관적이된다. Ajax 응답을 처리한다고 했을 때 성공은 success 루틴에서 구현하면 되고, 오류 처리는 error 구문에서 처리하면 된다.  프레임웍에서 지원해주기 때문에 코드를 작성하는 혹은 읽는 사람의 입장에서도 직관적이다.  더불어 성공/실패에 대한 또 다른 분기를 만들 필요도 없다.
  4. 이 경우이긴 하지만 Exception을 적극적으로 활용하는 좋은 습관을 가지게 된다.  코드를 작성하면서 굳이 Exception을 아끼시는 분들이 많다. 하지만 Exception은 오류 상황을 가장 명시적으로 설명해주는 좋은 도구이다.  문제 상황에서 코드의 실행을 중단시키고, 명확한 오류 복구 처리를 수행할 수 있는 일관성을 제공하기 때문에 코드의 품질을 높일 수 있다.

표준이 있으니 표준을 따르자.  이게 사려깊은 개발자의 태도다.

ResponseStatus라는 어노테이션을 사용하면 Exception이 발생했을 때, 어노테이션에 정의된 API Response Status Code로 반환된다.  이때 주의할 점은 정의한 Exception이 반드시 RuntimeException을 상속받아야 한다는 것이다.  Throwable을 상속받거나 implement 하는 경우에는 어노테이션에 의해 처리되지 않기 때문에 주의하자.

    @ExceptionHandler(UnknownAccessCodeException.class)
    public String handleUnknownAccessCodeException() {
        return "unknown";
    }

만약 Thymeleaf와 같은 UI framework을 사용해서 특정 오류 케이스가 발생했을 때 특정 뷰를 제공하고 싶다면, Controller 수준에서 ExceptionHandler 어노테이션을 활용할 수 있다.  위 예제는 Exception이 발생했을 때 unknown이라는 Thymeleaf의 뷰를 제공하라고 이야기한다.  이때도 주의할 점은 Exception은 반드시 RuntimeException을 상속해야한다는 점이다.

만약 어플리케이션 전반적으로 Exception에 대한 처리 로직을 부여하고자 한다면 ControllerAdvice 어노테이션을 활용한다.  어노테이션을 특정 클래스에 부여하면 해당 클래스에서 정의된 Exception 핸들러들이 전역으로 적용된다.

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

상세한 정보는 스프링 페이지에서 참고하면 된다.

외부 발표를 하다보면 좋은 것들?

올해 이런 저런 기회가 되서 몇 번 강의를 했었다.  학생 시절 과외 경험으로 가르치는건 내 적성은 아니었다.  한 두 사람도 아닌 수십명 앞에서 이야기를 한다는 것 자체가 사람을 긴장하게 만들기도 한다.

올해 이전까지 강의 경험 횟수를 합쳐보면 2~3번이 전부다.  할 때마다 몇 일 동안 시간을 가지고 준비를 했다.  전달할 내용을 충실히 전달할 수 있을지 이야기하는 강연 무대에서 심하게 떨지는 않을지…  걱정이 현실이 되지 않도록 준비하지만 항상 충분하지 못했던 것 같다.  걱정 가득히 준비하고 진행하지만 그럼에도 불구하고 강의는 할만 한 것 같다.

강의 준비를 위해 가장 필요한 건 시간이다.  자료 준비와 발표 준비를 위해 투자해야 할 시간이 만만치 않다.  자료는 단순한 텍스트 자료로 통하지 않는다.  PPT로 자료를 만드는 건 텍스트를 쓰는 것보다 더 시간이 걸린다.  부여 설명이 아닌 핵심이 되는 문장과 이해를 돕기 위한 이미지들이 있어야 한다.  맥락을 유지하기 위해 핵심 이외의 문장들을 말로 어떻게 풀어낼지도 함께 고려해야 한다.  그렇기 때문에 텍스트로 만드는 것보다 더 많은 시간이 필요하다.

발표는 과외가 아니다.  내가 앞에서 이야기하는 말을 들어주는 사람들이 한 두 명이 아닌 수십 명이다.  긴장이 될 수 밖에는 없고 말 떨리기가 일쑤다.  어떤 경우에는 주어진 시간보다 너무 빨리 혹은 너무 오래 동안 이야기를 한다.  이럴 때면 종종 “여긴 어디?, 나는 누구?“와 같은 경험을 느꼈다.

그런데 왜 이런 사서고생을 할까?  싫은 소리를 듣는 때가 태반인데??

먼저 준비 시간과 과정에서 많은 것들을 깨우치게 된다.  안다고 생각했던 것들 가운데 많은 것들이 아는 척을 했다는 것!  알고 있다고 생각을 했지만 사실 각론을 따져보다보면 제대로 알지 못했던 것들이 하나 하나씩 드러나게 된다.  시간이 길어지면 길어질수록 왜곡되고 자의적으로 해석된 지식들을 바로 잡을 수 있는 기회가 더 많아진다.  특히 발표는 자료를 읽는게 아니라 자신의 머리속에 그 내용을 정리해야  한다.  그래야지 내용을 흐름으로 이어지게 만들 수 있다.  그렇기 때문에 드문드문 알던 것들을 제대로 엮어서 온전한 지식으로 정리할 수 있게 된다.  올해 강의 혹은 발표했던 TDD, Git, TDD 내용에 대한 자료들을 만들면서 반성하고 제대로 공부할 수 있었다.

눈으로 이야기를 전달하는 글과 달리 발표(말)을 통해 전달하는 정보의 속도는 확연히 빠르다.  특히 한명이 아닌 여러 사람들에게 이야기를 전달을 위해서는 전달 방법을 계속 고민해야한다.  효과적인 방법은 하늘에서 뚝 떨어지는게 아니니까.  효과적으로 발표를 할 수 있는 사람은 그만큼 효과적인 소통을 할 수 있다.  조직화된 사회를 살아가는 인간이라는 동물의 특성상 “효과적인 소통“은 생존을 위한 중요한 수단이다.

발표는 한방향 소통이긴 하지만 세상에는 자신의 의견(주장)을 한마디도 못하는 사람들이 허다하다.  하고 싶은 것을 하지 못하는 것만큼 사람을 갑갑하게 만드는 건 없다.  자신의 의견을 적절히 말하고, 그 의견이 반영된 방향으로 사람들이 움직여준다? 사회적인 욕구가 있는 사람이라면 당연히 이걸 원한다.  그 사람의 이름값은 올라가고 다른 사람들의 지지를 받는다.  소위 말해서 “이름 값(Name Value)“를 얻는다.  열심히 노력한 결과로 얻어지는 이름 값이라면 당연한 성취라고 생각한다.  부끄러워하지 말고 되려 자랑스러워 해야한다.

마지막으로 한번 강의를 할 때마다 들어오는 금전적인 보상이 쏠쏠하다.  “돈”이라는 이야기를 꺼내니 앞서 이야기한 “사서고생”이라는 말이 영 안맞는 단어인 것처럼 보인다.  들인 시간과 대비해서 들어오는 “돈”을 생각하면 사서 고생이라는 단어가 그리 틀린 말은 아니다.  돈이 들어오면 감사한 마음으로 받아서 좋은 곳에 사용하면 된다.  시간을 만들어준 가족이나 동료와 좋은 시간을 함께 한다면 이 또한 좋지 않겠나? (개인적으로는 평소에는 나이가 패널티지만 이런 경우에는 어드밴티지로 작용한다. ^^  우리나라에서는 평가 기준이 나이순/경력순이라…)

 

Docker를 활용한 Singlepage 웹앱(WebApp) 구현 환경 구성하기

최근의 개발은 Single page 웹앱 형태로 웹 페이지를 개발하고 있다.  이전 회사에서는 웹앱이라는 개념도 제대로 몰랐는데… 장족의 발전이다.

웹앱 개발 방식이 개발자 관점에서 좋은 점은 Frontend와 Backend를 명백하게 구분할 수 있다는 점이다. 백엔드는 Business Logic을 중심으로 Restful API 방식으로 개발한다.  UI를 배제하고 로직에 집중할 수 있고, 테스트 케이스도 작성할 수 있기 때문에 제대로 개발한다라는 느낌을 준다.  근데 로직은 UI가 있어야지 표현되는 것이기 때문에 이것도 개발은 해야한다.

UI를 담당하는 Frontend는 HTML과 JS로만 구성된다.  예전처럼 PHP 혹은 JSP 같이 서버에서 상황에 따라 다른 컨텐츠를 내려줄 필요가 없다.  따라서 이런저런 복잡한 시스템은 필요없이 아파치 혹은 nginx 정도만으로도 충분히 개발할 수 있다.  딱봐도 쉬울 것 같은데 이렇게 썰을 길게 푸는건 개발 환경을 이야기하고 싶기 때문이다.  HTML과 JS로 코드를 짜면 되기 때문에 코딩을 위한 환경 자체는 쉽다.  하지만 작성한 코드를 눈으로 확인할려면 웹서버를 실행시켜야 한다.

로컬에 웹서버를 실행하는 방법이 가장 쉽게 떠오른다.  물론 가장 쉬운 방법이다.  하지만 여러 웹앱의 개발하는 경우를 생각하면 좀 귀찮아진다.

  • 테스트할 때마다 설치된 웹 서버의 Document Root를 바꿔줘야 하기 때문이다.
  • 물론 Configuration 파일 각 프로젝트별로 정의하면 된다.
  • 하지만 Configuration을 git repo에 함께 두기 애매하다. 개발하는 사람들별로 디렉토리 구성이나 이런 것들이 틀리기 때문이다.
  • 결국 이건 개발하는 사람들이 각자 잘 하는 수밖에는 없다.

이게 정답일까?  좀 더 쉽게 개발하고 배포할 수 있는 환경이 뭘까 싶어서 좀 고민을 해봤다.  최근에 Docker를 자주 사용하고 있기 때문에 이걸 활용하는 방안을 찾아봤다.  다음과 같은 접근 방법을 생각해봤다.

  • 로컬에 Docker 환경을 구성하고, Docker instance가 직접 로컬의 특정 디렉토리를 보도록 설정한다.  코딩을 하면서 변경하는 부분들은 Docker에서 바로 보고 이를 반영해줄 것이다.  HTML, CSS, JS는 따로 컴파일 할 필요가 없지 않은가?
  • 서버에 배포를 위해서는 Dockerfile을 이용해서 모든 리소스가 하나가 되도록 패키징한다.  그럼 이 안에 설정 및 컨텐츠 파일들이 모두 포함되기 때문에 이를 죽~ 배포하면 된다.

먼저 디렉토리 구조를 아래와 같이 잡았다고 가정해보자.

docker-directory-structure

이걸 바탕으로 로컬에서 간단히 Docker instance를 실행하는 방법은 아래와 같다.

$ docker run --name webapp -v $HOME/Workspace/projects/webapp/content:/usr/share/nginx/html:ro \
-v $HOME/Workspace/projects/webapp/conf:/etc/nginx:ro \
-v $HOME/Workspace/projects/webapp/logs:/var/log/webapp \
-p 5050:80 -d nginx

docker 실행에서 각 파라미터에 대해 간단히 부연한다.

  • –name : 실행할 docker instance의 이름
  • -v : docker에 마운트할 설정 정보. local-path:docker-path 형식이며 :ro를 덧붙히면 docker-path는 readonly 디렉토리임을 알리는 지시자다.
  • -p : port mapping. -v 옵션과 마찬가지로 local-port:docker-port를 나타낸다.
  • -d : docker image의 이름을 나타낸다.  nginx를 웹앱용 웹서버로 사용한다. (설정이 간단해서 아파치보다 더 좋은 것 같다. ^^;)

여기에서 가장 핵심은 -v 옵션에 따라붙은 마운트 정보이다.  로컬에서 작업하는 각 파일들을 Docker에서 잘 볼 수 있도록 해당 디렉토리를 바인딩한다.  이렇게 설정된 Docker를 통해 브라우저를 통해 확인해보자.

http://localhost:port 라고 입력하면 되겠지? 물론 포트는 앞서 설정한 local-port를 입력하면 되겠지?  라고 생각해서 입력하면 찾을 수 없다라는 어이없는 메시지만 본다.  이거 뭘까???  문제는 Docker의 nginx 서버가 특정 IP Address에 Binding되어 있다는 점이다.  Docker가 바인딩한 IP를 확인하는 방법은 다음 명령을 통해 확인할 수 있다.

$ docker-machine ip default

출력 결과로 알려준 IP와 포트로 접속해보자.  만약 문제가 없다면 짜잔~ 하고 개발하던 내용을 확인할 수 있다.

안된다고?  그럼 문제를 진단할 때다.  가장 먼저 이게 실행중인지 여부를 확인하는게 우선이다.  실행 확인은 다음 명령을 이용하면 된다.

$ docker ps

그런데 나오질 않는다고? docker ps 명령으로 목록이 나타나지 않는 이유는 docker에서 실행될 프로세스가 죽어버렸기 때문이다. nginx가 죽는 이유는 딱 하나. 바로 nginx configuration에 문제가 있기 때문이다. 이전에 설치가 제대로 되기나 한건지를 먼저 확인할려면 docker ps -a 명령을 이용하면 된다. 우리는 개발자이니까 먼저 로그를 확인해야겠지?

$ docker logs webapp

결과를 보면 시스템에서 출력해주는 웹 엑세스 로그 내용을 확인해볼 수 있다.  만약 nginx 설정에 오류가 있는 경우에는 로그 내용 잠깐 살펴보면 바로 문제를 파악할 수 있다.

개발을 다 마무리했다면 이제 배포를 준비할 때다.  배포를 위해서 Dockerfile을 하나 작성하면 된다.

FROM nginx
COPY content /usr/share/nginx/html
COPY conf /etc/nginx

설정의 구성은 간단하다.

  • nginx의 기본 설정은 Linux 기준으로 /usr/share/nginx/html을 Document Root로 지정한다. (물론 설정 파일에서 이를 변경할 수도 있지만.)  해당 디렉토리에 프로젝트에서 작업한 파일을 복사해넣으면 된다.
  • 별도의 설정 변경이 있는 경우에 해당 설정을 /etc/nginx 디렉토리에 넣으면 된다.

물론 엑세스 로그등을 별도로 봐야할 필요성이 있다면 이런 설정을 추가할 수 있다. (단순 웹앱이기 때문에 이럴 필요성이 있을까 싶긴 하지만…)

 

바쁘다.

회사를 옮겨와서 가장 바쁘게 일을 하는 시절이 아닐까 싶다.  한달 넘게 일과 삶의 균형이 무너진 상태였다.  제대로 된 개발을 포기하고 사용자를 위해 한번쯤(?) 고생하는 것이 주는 의미가 더 크다고 생각했다.  이런 결심으로 시작한 작업의 종착점이 이제 얼마 남지 않았다.  잃었던 삶을 되찾을 수 있을 것 같다는 생각도 들고, 반영되기 시작한 작업의 결과에 대한 반향도 나쁘지는 않은 듯 싶다.

동료들과 소주 한잔을 기울이면서 이번 작업에 대해 이야기를 해봤다. 작업 사이사이 지친 몸을 달래면서 소주 한잔 들이키며 나눈 이야기의 반응은 의외로 나쁘지 않다는 것이다.  대부분의 사람들이 평소와는 달리 모든 사람들이 종종 늦은 시간까지 작업을 했었다.  게임을 즐기는 시간도 상대적으로 크게 줄거나 의도적으로 아예 하지 않은 사람들도 있었다.  이런 상황을 나쁘지 않다고 평가하다니… ㅋㅋㅋ

의외다.  물론 이 “괜찮음”의 기본 전체는 “1년에 한두번”이라는 전체가 있긴했지만 의외다.

이 반응이 의외로 보는 이유는 현재의 작업 방식이 지극히 SI적(!!)이라는데 있다.  기능이 구현되어야 할 날짜는 정해져있다. 뭘 만들어야 하는 지에 대한 공감대는 있지만 세부 기능에 대한 정의는 없다.  언제든 작업하던게 뒤집어질 수도 있었다. (사실 다른 이유긴 했지만 아예 엎어질뻔하기도 했다.)  불명확한 것들 투성임에도 불구하고 일정이 정해졌으니 무조건 하라는건 개발하는 사람들이 가장 싫어하는 일 형태다.

그럼에도 불구하고 이 일에 다 같이 참여해서 해내는 의지가 있었다.  결과도 나쁘지 않았을 뿐더러 의지를 통해 함께 작업하면서 “팀” 이라는 개념이 모두에게 각인된 것처럼 보였다.  조직도에 그려진 팀이 아니라 함께 함께 논의하고 페어 프로그래밍하고 PR에 대해서 검토하는 “팀”으로써 움직였다.  개인적으로 다른 것보다 이 부분이 이번 작업의 가장 큰 결과물이라고 생각한다.

팀이 팀으로써 일을 한다는 것, 그리고 개발을 정말 제대로 된 개발 모습으로 열심히 한다는 것.  이 두가지에서 얻는 것이 개인적으로는 이번 작업의 큰 경험이었다.  물론 과정에서 들어난 잘못된 부분들과 협업에 대한 부분들도 조만간에 고쳐나가야 하긴 하지만.  바쁜 시절이 오래 가면 모둘에게 피곤함을 안겨주겠지만 그래도 좀 만 더 바빠야 할 것 같다.  그래야지 시스템의 체계도 제대로 구성시킬 수 있을 것 같고, 일의 틀도 제대로 맞출 것 같다.

미안하지만 좀만 더 고생하자.

 

logstash를 활용한 실시간 검출 시스템 구축

회사에서 DBA 분이 elastic 제품군을 가지고 나름 재미있는 기능을 개발하신 걸 공유받은 적이 있다.  그걸 보면서 WoW!!! 라는 감탄이 절로 나왔다.  주변의 오픈 소스 유틸리티들을 활용하면 쿨한 기능들을 설정만으로도 만들 수 있다라는 사실이 놀라웠다.

더욱 내가 반성했던 건 이 작품이 개발자가 아닌 DBA님의 도전이었다라는 점!  약간의 반성을 더 해보자면 뭔가를 집착적으로 코딩할 생각만 했다라는 생각이 훅~ 하고 머리를 때렸다.

언제가 기회가 된다면 elastic 제품을 함 써봐야겠다라는 다짐을 마음속에 뒀다.  근데 생각보다 일찍 기회가 찾아오는군. ㅋㅋ

뭘 해야하는가 하면…

뭔가 시스템을 만들어놓으면 씨잘데기없이 뭐 없나… 하면서 주변을 서성거리는 분들이 있다.  대부분의 일반 사용자를 위한 시스템들이 그렇지만 대용량 처리를 위해 아래와 같은 시스템 형상을 가진다.   시스템의 가장 앞단에는 방화벽이 존재해서 DDoS와 같은 공격을 우선 차단하고 정상적인 요청들만 실제 서비스에 흘려 보낸다.  방화벽을 통과한 요청 건들은 각 서비스 장비들로 분산된다.  전체에 비해 분산된 개별 장비에서 인지한 의심스러운 건수들은 미미하게 간주될 수 밖에 없다.

AsIsConfiguration

나뉘어진 실체를 파악하기 위해서는 이를 다시 모아야 한다. 데이터를 모아서 함께 평가하기 위한 좋은 툴이 바로 elastic 제품군이라는 걸 앞서 링크한 슬라이드를 통해 쏠쏠히 배웠다. 배웠다면 제대로 써먹어봐야지.

우선 각 서비스 장비들에는 처리한 요청의 요구자(소스)와 처리 결과를 로그 파일로 기록한다.  이건 당연히 개발자의 상식이다. 이걸 바탕으로 활용할 수 있는 제품들을 궁리해봤다.

  • filebeat – 로그 파일을 읽어들여서 이를 수집 시스템에 전달한다.  수집 시스템은 logstash 혹은 elastic search 혹은 bigdata 처리용 hive 등등을 지정할 수 있다.
  • logstash – 수집된 로그를 regular expression을 통해 특정 필드를 추출하거나 거르는 역할을 수행한다.  정제된 값들은 elastic search를 통해 전송하거나 hive 등으로 전송할 수 있다.
  • elastic search – 수집된 데이터를 다양한 검색 조건으로 빠르게 조회할 수 있는 기능을 제공한다.
  • watcher – elastic search에 존재하는 데이터 패턴을 조회하여 지정된 조건에 도달했을 때 이를 지정된 방식으로 알린다.  알림 방식은 email 혹은 http post 등을 사용할 수 있다.

근데 이렇게 하면 정말 되나?

이 정도의 제품 셋이면 원하는 나쁜 놈들을 찾을 수 있겠다 싶었다. 하지만 급하게 기술 검토를 하다보니 몇가지 문제점이 보였다.

  1. 만들려는 시스템의 궁극적인 목표는 비정상적인 요청자들을 찾아내 엉뚱한 짓을 못하도록 방화벽(Firewall)에 도움을 요청하는 것이다.  최근 N분 동안 비정상 요청들의 소스를 찾는 것이 기본 전제이다.  하지만 watcher의 동작은 배치 방식이다. 따라서 최근 N 분이라는 전제를 달성하기 어렵다.
  2. 물론 1분 정도 단위로 계속 돌리면 될 것 같기는 하다.  하지만 인위적인 Pull 방식으로 데이터를 elastic search를 통해 처리하면 전체적인 효율성의 문제가 나타날 수 있다. filebeat, logstash를 통해 들어오는 정보를 실시간으로 판단하면 되는데, 그걸 1분 단위로 Pulling하면 중간에 있는 elastic search가 부하를 발생시키고 이후에 시스템의 bottleneck이 될 수 있다.
  3. 이 시점에서 개발자의 역량이면 이 요구 사항을 대응할 수 있는 간단한 처리 기능을 직접 만드는 편이 오히려 효율적이다.  logstash를 통해 정제된 요청들을 받아서 각 소스 단위의 최근 N분 데이터를 평가하고 오류 발생시 이를 처리하는 기능이면 족하니까.

위 설명을 정리해보면 아래와 같은 구조가 개발자의 약간의 작업을 가미했을 때 가장 적합한 구조로 보였다.

System flows

이것저것 많이 쓰는 구조에서 간결해지긴 했다. 하지만 원래 의도했던 있는 것들을 잘 활용해보자에서는 약간 멀어진 느낌이다.  설명한 Architecture 구조는 내가 다루는 시스템의 상황에 맞는 구조이다. 상황은 각자에 따라 다르기 때문에 이것이 절대 정답이 될 수 없다. 사실 정답이란 없다.  하지만 한가지 중요한 이야기를 하나 더한다.  있는 것들에 너무 의존하지 말라는 것이다.  얽매이다보면 문제를 보는 바른 시각을 잃게 되고 제대로 보지 못한다.  되려 자신의 기술 수준에서 최선을 길을 찾으려고 노력해라.  설령 그 길이 잘못된 길이라도 시도함으로써 얻는 것들이 훨씬 많다.

정리가 됐으니 이제 만들어보자

시스템을 만드는 건 우선 elastic software를 설치하는 것부터 시작이다. 데이터를 제대로 받아올 수 있는지를 확인하는게 가장 먼저일테니까.

Elastic 제품군을 깔아보자

가장 먼저 할 일이 logstash를 설치하는 일이다. 설치 방법은 링크한 사이트에 충분히 자세히 설명이 나와있으니 그걸 참고하면 되겠다.  이미 들어 알고 있는 사람들은 다들 알겠지만 설정의 핵심은 filebeat을 통해 뿜어져 들어오는 로그에서 어떤 내용을 취할 건지를 설정하는 대목이다.  일반적인 Regular expression과 아주 약간 차이가 있기에 사용할 형식을 미리 테스트해보는게 좋다.

샘플 로그를 대상으로 데이터를 미리보기식으로 살펴볼 수 있는 웹앱이 있는데 쏠쏠하다.  포맷 오류 혹은 파싱 오류로 logstash를 몇번 내렸다올렸다하는 수고를 덜어줄 것 같다.  막무가내로 정의하는 것 말고도 logstash 설치 후 공통적으로 사용할 수 있는 로그 패턴들이 있으니까 다음 링크에서 참고하는 것도 좋다.

https://github.com/logstash-plugins/logstash-patterns-core/blob/master/patterns/grok-patterns

다음으로 filebeat을 설치한다. 설치 및 설정 방법 역시 친절한 설명이 사이트에 있다.  내려 받아서 그냥 설정을 잡으면 된다………………………  하지만 Segmentation fault를 내면서 죽어버네!  뭥미???

http://pds20.egloos.com/pds/201005/25/46/c0060146_4bfb18415c1f5.gif

확인해보니 filebeat이 GO 언어로 만들어졌군.  GO 플랫폼이 리눅스 커널 2.6.23 이상 버전부터 지원하기 땜시롱 안된다.  GO 플랫폼을 까는 건 벼룩잡다가 초가삼간 태우는 격이다.  ㅠㅠ

나는 개발자다

초가삼간을 태울 수는 없지만 역할을 되집어 생각해보면 로그 파일을 읽어다가 logstash 서버로 전송하면 된다.  다행이 logstash에서 TCP로 텍스트 전송하는 입력 타입을 지원한다.  그럼 내가 해야할 일은 tail -f 와 동일한 기능을 수행하는 모듈을 하나 만들면 된다.  와중에 JRE가 설치되어 있어서 자바 기반으로 하나 만들었다!!

https://github.com/tony-riot/logreader

사용법은 간단하다.  jar 파일을 만들거나 혹은 다운로드 받은 이후에 다음 명령으로 실행하면 된다.


java -jar logreader.jar /your/logs/absolute/path.log logstash-IP logstash-listening-port

각 실행 파라미터는 다음의 의미를 갖는다.

  • path.log – 읽어들일 파일의 경로를 지정한다. 가능하면 절대 경로를 지정한다.
  • logstash-IP – Logstash 서버의 IP 주소 혹은 도메인 이름을 입력한다.
  • logstash-listening-port – Logstash 서버에서 설정한 TCP 입력의 포트를 지정한다.

실행하면 아무런 Output도 출력하지 않는다. 출력되는게 없다고 놀라지 말자. 🙂

가장 메인이 되는 logstash 설정은 아래와 같다.

input {
    tcp {
        port => "5555"
        codec => line
    }
}

filter {
    grok {
        match => { "message" => "\AINFO\s{2}\|\s%{DATE_US:date} %{TIME:time}\s\|\s[a-zA-Z0-9._-]+\s\|\s[a-zA-Z0-9._\(\)]+ \|\stransaction_code=error\: (?[a-zA-Z\s]+),userName=%{USERNAME:username}\,ip=%{IP:sourceIp},source=%{USERNAME:source}" }
    }
    if "_grokparsefailure" in [tags] {
        drop { }
    }
}

output {
    http {
        url => "http://127.0.0.1:8080/api/v1/log-receiver"
        http_method => "post"
        format => "json"
        content_type => "application/json"
        mapping => ["date", "%{date}", "time", "%{time}", "cause", "%{cause}", "username", "%{username}", "sourceIp", "%{sourceIp}", "source", "%{source}" ]
    }
}

이런 설정으로 이제 수집에 관련된 부분은 마무리가 됐다. 

2017/08/01 첨언

logstash 5.5.1 버전으로 업하면서 이전과 설정이 변경된 사실을 완전 삽질 가운데 알았다.  이전 버전에서는 grok의 match 설정만으로도 매치되지 않는 다른 패턴의 경우에는 데이터가 output으로 전달되지 않았다.  근데 버전업을 해보니 매치가 되던 안되던 죄다 output으로 데이터를 전송해버려서 과도한 Stacktrace로 인해 시스템이 맛탱이가 가버렸다. ㅠㅠ

이런 현상을 막기 위해서는 성공 여부를 파악할 수 있는 _grokparsefailure 값을 tags에서 검색해서 존재하는 경우, 이를 drop filter를 사용해서 걸려내야한다. 버전업에 항상 좋은 일은 아니라는거… 덕분에 반나절 가까이를 날려버렸다.

 

자, 그럼 Analyzer라는 걸 이야기해볼까?

앞선 설정에서 볼 수 있는 것처럼 logstash에서 정제된 결과는 POST 방식으로 지정된 API 서버에 전달된다.  이때 POST의 Payload 값에는 match를 통해 추출된 값들이 JSON format을 통해 전달된다.

이제 Source 단위로 정리해서 추출하는 Business Logic을 구현하면 되겠다.  이건 뭐 누구나 할 수 있는 웹 어플리케이션 개발이다.  구질구질한 설명은 생략하겠다. 🙂

이렇게 만들어졌다.

지금까지의 설명을 한장의 그림으로 설명하면 이렇다.  원래는 많은 것들을 빌어다가 손쉽게 시스템을 만들 계획이었지만 생각만큼 녹록하지는 않았던 것 같다.  결국 1개 시스템만 활용하고 말았으니 말이다.

LogstashBasedSystem

의도한 전체 시스템을 검증된 오프 소스 제품들을 이용해서 구축해본 몇 가지 소감을 정리해본다.

  • 전체적인 시스템 구축 비용이 확실히 절감된다.  다른 개발자들을 통해 이미 검증되었고, 사용에 대한 다양한 레퍼런스들이 많아서 Troubleshooting이 쉽다.
  • logstash라는 중심축을 통해 필요한 부분만을 구현하기 때문에 개발에 낭비가 없다.  딱 그 부분 혹은 그 기능까지만 개발하면 된다.
  • 시스템적인 제약 사항이나 성능적인 부분은 쓰기 전에 충분히 검토해봐야 뒤늦은 후회를 안한다.
  • 이름에 현혹되지 말아야겠다. 명성이 자자하더라도 쓰고자 하는 현실에 맞질 않으면 폭망이다.

 

에필로그

이 작업을 하면서 30대의 젊음을 불사르면 만들었던 Open Manager 라는 제품 생각이 났다.  로그 처리 하나는 기막히게 했던 물건이었다.  과거의 추억이지만 간만에 로그 다루는 작업을 하다보니 예전 추억이 새록새록하다.

몇 날의 밤세움이후에도 지치지 않았던 건 그만한 열정이 가슴 안에 가득했기 때문이겠다.

아직은 그 열정의 불꽃을 꺼뜨리고 싶지 않다.

 

칼의 노래

어랍쇼!!!!

독후감은 어데로가고, 엉뚱한 워드프레스 PP 내용이 버틴거지?

서비스를 이전할 때 어데론가 날라간 모양이다.