사수님이 다른 사람의 코드도 많이 봐야 된다고 말씀해 주셨는데

백엔드 개발자가 팀에 2명 밖에 없기도 하고, 뭔가 동기부여도 잘 안되고 해서

거의 혼자만의 개발 세계에서 코드를 써내려왔던 것 같다. 

그러다가 이제 또 새로운 개발 프로젝트에 착수하게 되었는데 

뭔가... 이렇게 가다간 발전도 없이 계속 같은 코드만, 같은 기술만 사용할 것 같단 위기감이 들었다.

 

그래서 요즘 컨퍼런스도 좀 챙겨 보고 핫 하다는 기술도 좀 기웃기웃 거리고 있다.

Reactive Kafka가 무엇인지 알기 위해 NHN FORWARD 2019의 세션 영상을 보다가 샘플 코드를 얻게 되었는데

Kafka는 제쳐두고 일단 코드가 굉장히 깔끔하고 구조도 내가 써먹을게 많겠다 싶었다.

이참에 다른 사람의 소스 코드도 좀 봐야지란 생각에 우선 코드 구조부터 파악하기로 했다.

 

카프카에 대한 개념은 이전 포스팅에서 다뤘습니다 (https://modimodi.tistory.com/72)

 

 

https://github.com/EleganceLESS/nhn-forward-2019

 

GitHub - EleganceLESS/nhn-forward-2019

Contribute to EleganceLESS/nhn-forward-2019 development by creating an account on GitHub.

github.com

 

아래 이미지는 Class Diagram이 아니라는 것을 명확히 하고 싶다 ^^;

소스코드 이해를 위해서 파워포인트로 나름 클래스 구조를 그려봤는데 이게 나의 최선이라는 것에 좌절감이ㅋㅋ

 

Kafka는 제쳐두고 우선 참고할 만한 코드 부분에서 인상적이었던 부분을 좀 정리해보도록 하겠다. 

  • 우선 abstract 클래스를 많이 사용한 것이 눈여겨볼 부분이다. 
    • AS-IS 관계를 abstract 클래스를 활용해 더 세밀하게 컨트롤했다.
  • DemoController에는 각 자식 Controller에서 공통적으로 사용하는 /start, /stop EndPoint가 존재한다.
    • DemoController는 DemoService를 주입받고, DemoService의 start(), stop() 메서드를 활용한다.
    • 각 Controller에서는 메시지를 소비하고 정지하는 방법은 같지만 메시지를 생성하는 방법은 다른 Service들을 Injection 시킨다. 
  • DemoService는 추상클래스로 consume()이라는 추상 메서드를 가진다. 
    • DemoService의 다른 메서드는 자식 Service에서 사용되지만 consume() 추상 메서드는 서비스별로 다르기에 이 구조를 만든 것으로 보인다. 
  • DemoService의 consume()을 구현한 클래스 또한 추상 클래스이다. DemoService의 자식 클래스는 OperatorDemoService, SubscriberDemoService이고 각각 consumer(), getSubscriber() 추상 메서드를 포함하고 있다.
    • 메서드 구현을 자식 클래스에 맡기는 데 사용은 부모 클래스에서 한다.
      • 예를 들어 SubscriberDemoService의 getSubscriber() 메서드는 추상 메서드이고, 해당 클래스의 cosume() 메서드에서 이 getSubscriber() 메서드가 사용되는데 getSubscriber() 메서드 구현은 자식이 담당한다.

 

각 클래스 별로 공통적으로 사용하는 코드가 있을 때는 AS-IS 관계인지에 따라 상속을 이용했고, AS-IS 관계가 아닌 경우 interface의 default 기능을 활용했다. 그리고 부모-자식 관계를 1 depth 이상으로 가져가려면 그 관계에 대해 치밀하게 생각해봤다는 것이겠지..? 일단 코드 작성 전에 클래스 간의 관계를 고려해 설계부터 잘 하고 개발에 착수해야 겠단 생각이 많이 들었던 코드이다. 

 

테스트 코드도 눈여겨 볼게 많다. 

 

Test Code

 

Controller 단

@Test
    public void startAndStopTest() {
        Step0Service service = mock(Step0Service.class);
        when(service.start()).thenReturn(Mono.just("START"));
        when(service.stop()).thenReturn(Mono.just("STOP"));

        Step0Controller controller = new Step0Controller(service);

        WebTestClient testClient = WebTestClient.bindToController(controller)
                .build();

        testClient.get().uri("/step0/start")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("START");

        testClient.get().uri("/step0/start")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("Already Running");

        testClient.get().uri("/step0/stop")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("STOP");

        testClient.get().uri("/step0/stop")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("Not Running Now");
    }

Repository단

@Test
public void notifyTest() {
    StepVerifier.withVirtualTime(() -> repository.notify(Tuples.of(1, "홍길동")))
            .thenAwait(Duration.ofSeconds(3))
            .expectNext(Tuples.of("홍길동", true))
            .verifyComplete();
}
@Test
public void getReceiversTest() {

    StepVerifier.create(repository.getReceivers(1))
            .verifyComplete();

    StepVerifier.create(repository.getReceivers(2))
            .expectNext(Tuples.of(2, "조조"))
            .verifyComplete();

    StepVerifier.create(repository.getReceivers(3))
            .expectNext(Tuples.of(3, "유비"))
            .verifyComplete();

    StepVerifier.create(repository.getReceivers(4))
            .expectNext(Tuples.of(4, "조조"))
            .expectNext(Tuples.of(4, "손권"))
            .verifyComplete();
}

Service단

@Test
public void step2ConsumerTest() {
    ReceiverOffset offset = mock(ReceiverOffset.class);
    doNothing().when(offset).acknowledge();

    ReceiverRecord<String, String> record1 = mock(ReceiverRecord.class);
    when(record1.key()).thenReturn("1");
    when(record1.value()).thenReturn("1");
    when(record1.receiverOffset()).thenReturn(offset);

    ReceiverRecord<String, String> record2 = mock(ReceiverRecord.class);
    when(record2.key()).thenReturn("2");
    when(record2.value()).thenReturn("2");
    when(record2.receiverOffset()).thenReturn(offset);
}
 @Test
public void samplingTest() {
    Step2Service service = new Step2Service(null, null);

    StepVerifier.withVirtualTime(() -> Flux.just(1, 1, 1, 1, 1)
            .groupBy(Function.identity())
            .flatMap(service::sampling))
            .thenAwait(Duration.ofSeconds(5))
            .expectNext(1)
            .verifyComplete();
}

 

이 프로젝트 코드를 분석하기로 마음 먹은 것이 테스트 코드를 보고 난 다음이다.

mock을 활용해 단위 테스트를 진행하고, StepVerifier나 doNothing() ..? 이건 내가 한 번도 사용해보지 않은;

이미 작성해 놓은 테스트 코드를 다 뜯어 고치고 싶어서 시간 될 때 마다 조금씩 뜯어 고치고는 있는데

이게 한 두개 건드리다 보니 정말 끝도 없다^^;;

사수님이 말씀해주셨듯 이미 작성된 코드 건들다 보면 한도 끝도 없으니

새로 개발할 것들에 새로 익힌 기술들을 사용해보라는 말씀을 명심해야겠다... 

 

 

 

+ Recent posts