상호 순환 참조가 꼭 필요한가?

상호 순환 참조

많은 언어에서 상호 참조는 안티 패턴으로 인식된다. 참조를 한다는 뜻은 클래스(구현체, 모듈, 기타 등등) 하나를 설명하기 위해서는 다른 클래스의 도움을 받아야 한다는 것이다.

  • Class A가 B를 의존하고, Class B가 A를 의존하는 경우.
  • Class A가 B를 의존하고, Class B가 C를 의존하고, Class C가 다시 A를 의존하는 경우.

즉 의존 관계도를 그릴 때, loop 가 이뤄진 경우를 말한다. 프로젝트가 작을 때는 쉽게 파악이 되지만, 이해 관계자들도 떠나고, 프로젝트가 커져 의존관계가 큰 원을 그리는 경우 (A -> B -> C -> D -> E -> A)는 논리적으로 발견하기 힘들어진다.

class A {
    B b;
}
class B {
    A a;
}
//

class A {
    B b;
}
class B {
    C c;
}
class C {
    A a;
}

왜 안 좋은가?

패키지 내부에서의 상호의존은 경우에 따라 허용해야 할 수도 있지만 범용적인 예를 다뤄보자. 상호 의존을 하게 되면 의존을 하는 클래스들끼리 강한 결합을 하게 된다. A가 변했을 때, 죄 없는 B도 같이 변해야 할 필요성이 있다는 것이다. 강한 결합은 의도하지 않은 부수효과를 발생하고 결국 전체적인 질이 하락하게 된다. 정원에 난 잡초 하나를 뽑지 않고 놔두면, 잡초는 무럭무럭 옆으로 자라난다. 개발자는 자신이 관리하는 코드를 깨끗하고 명료하게 관리해야 할 책임이 있다.

어떻게 파훼해야 하는가?

대부분의 문제는 파인만 알고리즘으로 풀 수 있다. feynman_algorithm

  1. 문제를 쓴다.
  2. 매우 깊게 생각한다.
  3. 답을 쓴다.

Class A가 B를 참조해야 하는 이유를 골똘히 생각해보자. 왜 참조하고 있는가? 그리고 Class B가 A를 참조해야 하는 이유를 골돌히 생각해보자. 왜 참조하고 있는가?

의존성의 방향을 한쪽 방향으로 바꿔줄 필요가 있다. 의존성의 방향을 한쪽으로만 통제하면 변경에 영향을 받는 부분을 명확하게 이해할 수 있어진다.

혹은 A와 B를 모두 알고 있는 Class C를 만들어서 A와 B에서 의존적으로 하는 일을 위임받아서 하는 방법도 있다.

간혹 언어의 특성을 이용해서 문제를 잠시 회피하는 방법도 있다. (C++ 에서 포인터를 사용한다거나, 모듈을 하나씩 빌드한다거나.)

더 읽어보면 좋을 것들

mesos marathon 1.4 버전 이후에서 헬스체크 변경사항

Marathon 에서 헬스 체크

ref : http://mesosphere.github.io/marathon/docs/health-checks.html#mesos-level-health-checks

마라톤에 떠 있는 application은 framework 레벨에서 헬스체크를 지원한다. 헬스 체크는 사실 두종류 인데, mesos level 헬스체크와 marathon level 헬스체크가 있다. 디폴트는 marathon 레벨의 헬스체크인 HTTP` 다.

  • marathon level 헬스체크
    • HTTP (default)
    • HTTPS
    • TCP
    • COMMAND
  • mesos level 헬스체크
    • MESOS_HTTP
    • MESOS_HTTPS
    • MESOS_TCP

marathon 1.4 버전부터 HTTP, HTTPS, TCP 헬스체크가 deprecated 되었다. 동일한 기능을 제공하는 MESOS_HTTP, MESOS_HTTPS, MESOS_TCP로 갈아타면 된다.

mesos level 헬스체크의 장단점

  • 장점
    • newtowking failures 에서 자유롭다
    • 헬스체크가 작업을 실행하는 agent 에 위임되므로, 작업량을 수평적으로 확장할 수 있다.
  • 단점
    • agent 에서 수행되므로 리소스가 더 소모된다.
    • Mesos-level health checks require tasks to listen on the container’s loopback interface in addition to whatever interface they require. If you run a service in production, you will want to make sure that the users can reach it.

marathon level 헬스체크에서 mesos level 헬스체크로 넘어가는 예

HTTP

{
  "path": "/api/health",
  "portIndex": 0,
  "protocol": "HTTP",
  "gracePeriodSeconds": 300,
  "intervalSeconds": 60,
  "timeoutSeconds": 20,
  "maxConsecutiveFailures": 3,
  "ignoreHttp1xx": false // 이건 MESOS_HTTP 에는 존재하지 않는다.
}

mesos HTTP

{
  "path": "/api/health",
  "portIndex": 0,
  "protocol": "MESOS_HTTP",
  "gracePeriodSeconds": 300,
  "intervalSeconds": 60,
  "timeoutSeconds": 20,
  "maxConsecutiveFailures": 3
}

Spring 5 with Kotlin

깔끔한 Spring 5 Webflux

Spring 5에 들어가면서 많은 변화가 있었습니다. 기본적으로 Webflux라는 비동기, 리엑티브 웹 프레임워크가 등장했고, Pivotal & Spring의 Reactive 구현체인 Reactor가 본격적으로 적용되기 시작했습니다. 게다가 Spring boot부터는 공식적으로 JSP를 지원하지 않더니, Spring 5를 사용하는 Spring boot 2부터는 기본 내장 웹서버가 톰캣에서 네티로 바뀌었습니다. 그렇기 때문에 기본적으로 서블릿도 사용하지 않죠. 그러기에 Webflux에서는 독자적으로 구현한 ServerRequest, ServerResponse 등을 사용합니다. (물론 Spring MVC가 완전히 없어진 것은 아니기 때문에 원한다면 추가적은 설정을 통해서 이전과 같이 JSP와 톰캣을 사용할 수는 있습니다.)

Webflux에서는 기존 Spring MVC에서 사용하던 방법과 동일하게 어노테이션(@Controller, @RequestMapping 등)을 통해서 컨트롤러를 구현할 수 있지만 새로운 RouterFunction이라는 것을 이용해서 구현할 수도 있습니다. SpringOne platform의 키노트에서는 ‘XML은 똥이다’라고 말하면서 @Configuration 어노테이션과 함께 빈 설정을 코드내에서 하도록 권장하더니, 이번에는 어노테이션도 마음에 안들기 시작했는지 컨트롤러를 어노테이션을 통해 구현하는 것이 아닌 함수형 페러다임을 따라서 구현하는 방법을 내놓았습니다.

XML is ddong

Phil Webb; Pivotal Spring Boot Lead

annotation to functional & KotlinDSL

Sébastien Deleuze; Pivotal as a Spring Framework and Reactor commiter

아래의 코드는 Spring Webflux에서 RouterFunction을 구현하는 예제입니다.

    @Bean
    public RouterFunction<ServerResponse> routerFunction(SomeHandler someHandler) {
        return nest(accept(TEXT_HTML), 
                    route(GET("/"), request -> ok().contentType(TEXT_HTML).render("index")))
                .andNest(path("/api/v1/some").and(accept(APPLICATION_JSON)),
                    route(GET("/"), someHandler::getDataAll)
                        .andRoute(GET("/{id}"), someHandler::getData)
                        .andRoute(POST("/{id}"), someHandler::postData)
                        .andRoute(DELETE("/{id}"), someHandler::deleteData)
                        .andRoute(PUT("/{id}"), someHandler::putData));
    }

웹서버에서 제공하는 API를 한곳에서 볼 수 있고 URL과 처리 로직간에 어노테이션이 아니라 함수를 지정함으로써 보다 명확하기는 하지만… 아직 저는 RouterFunction이 깔끔하다는 또는 가독성이 좋다는 말은 들어보지 못한 것 같습니다. 저도 그다지 깔끔하다는 생각이 들지는 않네요. 그런데 Spring 5에는 저 코드를 깔끔하고 보기 좋게 작성할 수 있는 방법을 제공합니다. 사실 위 Sébastien Deleuze의 PPT에도 힌트가 있습니다. 바로 개발에 Java가 아닌 Kotlin을 이용하는 것이지요!

Spring 5 with Kotlin

최근 여러 유명 프레임워크에서 Kotlin을 정식으로 지원하겠다는 소식이 들려왔습니다. 대표적으로 Spring Framework는 5 버전부터 Kotlin을 정식으로 지원하며, Spark, Vert.x도 Kotlin을 지원하기 시작했습니다. 여기에서는 그중 Spring Framework를 중심으로 Kotlin를 사용해 확장된 기능에 대해서 알아보고자 합니다.

kotlin in spring

spring 5에는 0.4%나 Kotlin으로 작성되었다. 이는 Spring에서 무려 2번째로 많이 사용된 언어라는 의미한다!

spring initializr

Kotlin을 위해 구성된 Spring 프로젝트 템플릿은 SPRING INITIALIZR에서 손쉽게 구성하여 받을 수 있다.

Spring Webflux

RouterFunction

물론 특별히 프레임워크나 라이브러리에서 Kotlin을 지원하지 않더라도 Java와 호환성이 좋기 때문에 별로 큰 문제없이 Kotlin을 사용할 수는 있습니다. Spring 5에서 일부 모듈이 Kotlin으로 개발되었다고는 하지만 해당 모듈들을 살펴보면 실제 핵심 로직을 구현한 것은 아닙니다. Kotlin으로 작성된 부분은 주로 기존 기능을 확장하여 Java로는 불가능하거나 매우 장황해지는 부분을 보다 편하고 깔끔한 코드로 작성할 수 있도록 돕는 역할을 하는 모듈이 대부분입니다. Java로는 표현하는데 한계가 있던 부분들을 Kotlin으로 개선한 것이죠.

이 확장 모듈 중에는 Webflux에서 사용할 수 있는 모듈이 있으며 그중 가장 대표적인 것이 RouterFunctionDSL입니다. RouterFunctionDSL을 사용하면 위에서 잠깐 살펴봤던 RouterFunction을 보다 깔끔하게 작성할 수 있습니다. 위에서 Java로 작성한 RouterFunction을 Kotlin으로 재작성해보겠습니다.

    @Bean
    fun routerFunction(someHandler: SomeHandler) = router {
        accept(TEXT_HTML).nest {
            GET("/") { ok().contentType(TEXT_HTML).render("index") }
        }

        ("/api/v1/some" and accept(APPLICATION_JSON)).nest {
            GET("/", someHandler::getDataAll)
            GET("/{id}", someHandler::getData)
            POST("/{id}", someHandler::postData)
            DELETE("/{id}", someHandler::deleteData)
            PUT("/{id}", someHandler::putData)
        }
    }

Kotlin의 확장 기능을 통해서 보다 보기 편하게 RouterFunction이 개선되었습니다! 코드를 장황하게 만들던 의미없는 코드들이 많이 줄어들고 정말 API를 작성하는데 필요한 부분만 남아서 한눈에 알아보기에도 좋네요. 개인적으로 기존에 사용하던 어노테이션을 통해서 컨트롤러를 구현하는 것보다 이 코드가 더 깔끔한 것 같습니다.

Model

RouterFunction을 사용하지 않고 기존의 어노테이션을 사용하더라도 작은 편한점이 하나 있습니다. 깨알 같은 변화이지만 가독성은 더 좋아지는 것 같네요.

Java

@RequestMapping("/someUrl")
public String getSome(Model model) {
    model.addAttribute("name", "kwseo");
    model.addAttribute("items", service.findItems());
    return "somePage";
}

Kotlin

@RequestMapping("/someUrl")
fun good(model: Model): String {
    model["name"] = "kwseo"
    model["items"] = service.findItems()
    return "somePage"
}

Bean

빈을 생성하기 위한 간단한 모듈도 제공합니다. 아래와 같이 간단한 표현으로 빈을 생성할 수 있지만, 지금까지 @Bean을 통해서 빈을 생성하여 ApplicationContext를 직접 다루지 않는 개발자에게는 별로 큰 메리트처럼 보이지는 않습니다.

val someContext = beans {
    bean<SomeClass1>()
    bean<SomeClass2>()
    bean {
        // code ...
        SomeClass(ref())
    }
    
    profile("test") {
        bean<SomeClassForTest>()
    }
}

하지만 Spring boot 2와 함께 사용한다면 run할때 등록해주는 방법이 있습니다. Spring boot 2에서는 더 유연하게 main 함수를 작성할 수 있는 확장 모듈을 제공합니다.

@SpringBootApplication
class SomeWebApplication

fun main(args: Array<String>) {
    runApplication<SomeWebApplication>(*args) {
        addInitializers(someContext)
    }
}

RestTemplate & WebClient

Spring에서 제공하는 웹 클라이언트 클래스인 RestTemplate을 사용한다면 응답 값의 데이터 타입이 조금이라도 복잡해지면 코드가 장황해집니다. getForObject 같은 간단한 메소드를 사용할 수 없고 아래의 코드와 같이 같은 exchange와 함께 ParameterizedTypeReference<...>() {}라는 정말 이름이 길어서 손가락을 아프게하는 클래스를 사용해야할 때가 있습니다. 이 역시 Kotlin으로 확장되어 손가락을 덜 아프게 해줍니다.

Java

 ResponseEntity<List<SomeData>> response = restTemplate.exchange("/someUrl", HttpMethod.GET, httpEntity, new ParameterizedTypeReference<List<SomeData>>(){});

Kotlin

val response = restTemplate.getForEntity<List<SomeData>>("/someUrl")

그리고 Spring5부터는 AsyncRestTemplate deprecated되고 WebClient가 새롭게 등장하였습니다. 이도 역시 Kotlin의 뛰어난 타입추론 기능으로 인해서 불필요한 코드를 깨알같이 줄일 수 있습니다.

Java

Flux<SomeData> users  = client.get().retrieve().bodyToFlux(SomeData.class)

Kotlin

val users : Flux<SomeData> = client.get().retrieve().bodyToFlux()

Test

참고로 코틀린의 특성 덕분에 테스트를 수행하는 메소드의 이름을 보다 자유롭게 작성할 수 있습니다. 물론 무엇을 테스트하는 코드인지 주석을 달아주는 방법도 좋지만 테스트 메소드들을 한 곳에 모아서 볼때(예를 들면, mvn test 또는 Intellij에서 전체 테스트 코드를 실행했을 때) 메소드명만 봐도 어떤 역할을 하는지 쉽게 알 수 있을 것입니다.

    @Test
    fun `Some function should be successful when called`() {
        // ...
    }

    @Test
    fun `이 테스트는 abc 대한 테스트이며 반드시 성공해야한다!`() {
        println("GOOD")
    }

이외에도 다른 Kotlin이 확장 모듈이 존재하지만 여기서는 이런 것도 있구나하고 간단히 소개하는 자리이기 때문에 전부 구구절절 설명하지는 않을 것입이다(사실, 아직 많지는 않아서 그냥 구글에서 몇 개 찾아보는 것이 더 효율적일 것입니다). 몇몇 Kotlin 확장 모듈 및 Kotlin이 가진 이점 덕분에 Spring 5에서는 보다 가독성이 좋은 깔끔한 코드를 작성할 수 있으며, Kotlin을 통해 보다 유연한 클래스 및 함수를 설계할 수 있을 것입니다. 또한, Kotlin의 가장 큰 장점 중 하나인 Null safety를 통해서 보다 견고한 애플리케이션을 개발하고, 불필요한 코드들을 제거한 간결한 코드 덕에 생산성도 향상될 것 입니다. 정말 깔끔하네요.

애자일 코리아 콘퍼런스 2017 후기

본 글은 브런치에 적은 글을 가져왔다.

애자일 코리아 2017. 2013년 열린 후 4년 만의 콘퍼런스라고 했다. 처음 가는 기대와 애자일에 대한 해답을 얻을 기대가 밍글밍글 섞여있었다.

AgileKorea2017 시작.

Lean Coffee

등록을 마치고 커피를 한잔 뽑아서 ‘Lean Coffee’를 하러 커피 마시는 공간으로 총총. 테이블마다 큰 주제가 있고, ‘특정한 이야기 주제가 없는 구조화된 대화 방법’이라고 한다. 가볍게 네트워킹. 내가 참여했던 테이블은 ‘Communication & imporv’였다. TDD 머시기 테이블에 잠깐 갔었는데, 가벼운 이야기를 주로 나누고 있어서, 차라리 커뮤니케이션에 대한 이야기를 하는 게 좋다고 판단했다.

직장동료들과의 관계 이야기, improv 이야기, 에자일에 대한 경험들을 이야기하다 보니 시간이 훌쩍 지났다. imporv라는 것을 새로이 알게 된 것도 엄청난 수확. 원래는 조승빈 아저씨의 ‘애자일, 한때의 유행인가’와 정우진 아저씨의 ‘DevOps’이야기를 들으러 갈 계획이었으나…. ‘개발자를 위한 imporv’를 들으러 가게 되었다. 엄청 잘한 선택! (강연은 동영상으로 다시 보면 되니까…) 생각보다 빠르게 커피타임이 지나고, 동료와 합류하여 첫 번째 세션인 ‘state of the art in agile’을 들으러 갔다.


timetable timetable - https://www.facebook.com/AgileKoreaConference

1. The State of the Art in Agile

소트웤스의 CTO인 마이크 아저씨의 강연 소프트웨어를 바라보는 관점에 대해 유명한 아저씨로부터 도덕책 같은 이야기를 많이 들었다. 새겨들을 것은 많지만 큰 감동은 없는.. 짧게나마 요약하면 이렇다.

  • CI, CD 쓰세요. 꼭.
  • tdd 하세요.
  • microservice 짱 좋아요.
  • microservice들로 api eco 시스템을 만들어요.
  • products, not projects
    프로젝트에 치고 빠지듯이 프로덕트를 만들면 안 되고, 장기적인 관점에서 프로덕트를 바라봐야 한다.

애자일은 혼자 하는 게 아니라, 같이 만들어가는 거라는 걸 위의 맥락에서도 많이 느꼈다.

2. 애자일 전파를 위한 혼자만의 싸움 전략

두 번째 세션은 SKP에서 애자일 전파를 위해 고군분투하는 신원 님의 세션.

본인이 회사에서 애자일을 전파하기 위해서 겪었던 경험들을 나눠주셨다. ‘애자일’이라는 키워드를 받아들이는 사람들의 뉘앙스, 분위기, 태도, 이해도… 들이 모두 다르고 이 모든 사람들을 한 번에 관통하는 은탄환은 존재하지 않음을 반증하고 계셨다. 5가지 싸움 전략과 함께, 마무리는 ‘애자일 코치 좀 더 뽑아주세요.’

짧은 경험에 비춰보면, 성공적인 애자일 사례에는 ‘좋은 애자일 코치가 있었다.’라는 말을 많이 듣게 된다. 두 번째로 많이 들리는 이야기는 상호 소통이 원활하고 팀 전체의 가치 공유가 명확하게 되어있어서, Goal 설정이 잘된 경우에 ‘이번에 우리 애자일 좀 된 것 같아.’ 라는 말이 나왔다고 한다. 여러 사람이 한마음 되는 것이 얼마나 어려운가…

여기까지 듣고 다시 밥 먹으러 총총. 아침부터 너무 배고팠던지라 빠르게 뚝딱.

3. Design for growth, A tactical toolkit for middle management: the leader of knowledge workers

트위터에 Enterprise Agile coach를 하시는 Luk lau 아저씨의 세션.

중간 관리자, 혹은 어느 정도 규모 있는 조직을 세팅할 때 많은 교훈을 줄만한 세션이었다.

  • Golden Hammer
  • 애자일을 움직이는 것은 사람이다.
    결국 소프트웨어를 만드는 것은 사람이다.
  • ‘팀’이 중요하다
    팀워크, 팀빌딩, 팀의 유대감이 그래서 필요한 거다.
  • 애자일 팀만 도입한다고 끝나는 게 아니다.
    애자일팀이 문화를 바꿀 수 있도록 지원해주고, 힘을 기를 수 있게 서포트 해줘야 한다.
  • game storming
  • facilitation
  • how to start movement - 3분짜리 동영상(한글로 자막도 나옴!), 운동이 시작되는 방법

4. 소 잡는 칼!(마이크로 서비스)

라이엇 게임즈의 지두현 님이 대략 1년 동안 마이크로 서비스를 개발하면서 느낀 후기

유명한 게임회사의 흥미로운 이야기 세션이었다. 가장 개발자 친화적인 세션이라 더욱 재미가 있었는지도… 소 잡는 칼에 대한 이야기를 먼저 하면, (싸게 만들려면 만들 수도 있을?) 새로운 기능을 만들어서 추가하는 데 있어서 생각보다 돈이 많이 들었다.(feat. AWS)

  • 데이터에 집중해서, 데이터 플로우에 맞게 기능을 구현 (with Apach Kafka)
  • (사족) 마이크로 서비스는 역시 개발자에게 정신건강에 좋다.
    스프린트는 항상 골을!
    스프린트의 마지막엔 항상 데모를!
  • 불필요한 부분을 없애고, 사람과 사람에 집중해서 개발하자.
  • 관습에 저항하라 여기 어디 관련 글이 있을 것 같은데… https://engineering.riotgames.com/

5. 애자일 조직에서 소프트웨어 아키텍트

NBT의 남상균 아저씨의 세션

NBT의 조직 히스토리와 함께, 소프트웨어 아키텍트로써 어떤 일들을 처리하고 있는지를 나누는 세션이었다. 드로이드 나이츠에서 뵀었는데, 반가웠다.

6. 개발자를 위한 Imporv

즉흥연기 임프로그 imfrog 에서 준비해주셨다. (애자일 콘퍼런스 후기의 주인공)

애자일은 ‘소통’이다.

상호 ‘소통’ 증진을 위한 imporv 세션. ‘신뢰’, ‘공감’, ‘팀빌딩’, ‘에너지’, ‘리더십’, ‘yes and’. 6가지 주제 리더들과 20분간 짧고 굵게 교감 + 회고. 하는 세션이었다. 아쉽게도 시간이 충분하지 못하여, ‘에너지’, ‘리더십’ 파티에는 가보지 못했다. (신뢰, 공감, 팀빌딩에서 행했던 활동들의 순서가 안 맞을 수 있다… 정정해주시면 감사합니다 :D )

‘신뢰’, ‘공감’, ‘팀빌딩’

상호 간의 신뢰를 구성하기 위해서 아이컨택을 하고 마임(행동?)을 진행했다. 액션을 진행하면서 서로 눈을 먼저 바라봐야 했다. ‘집 잽 잡’, ‘공던지기’, ‘연상 키워드 말하기’. 워밍업이 좀 끝나고 이제 슬슬 본 게임(시작하기 전에 탈출을 못하도록 출구가 봉쇄되었다.) ‘어깨동무를 둘이 한 몸인 것처럼 이야기하기’, 주거니 받거니 하면서 한 명인 것처럼 말을 이어나갔는데, 주거니 받거니가 진짜 어려웠다. 차라리 따라가는 건 쉬웠는데, 한 문장 내에서 주-부 를 전환하는 건.. 어렵다. 처음 하는데 쉬운 게 있을까. ‘내 앞사람이 한 말에 대한 감정을 다른 대화로 이어나가기’, ‘마임에 대한 상황을 말로 표현하기’, 내 앞사람의 행동, 말에 대해 매우 집중하고, 감정을 동조하거나 이어나가는 활동이었다. 짧은 시간 동안 타인에 대한 높은 집중력을 요구하는 활동. 사회생활하면서 이만큼 집중력을 발휘한 적이 몇 번이나 있었는가?

‘yes and’

그리고 대망의 ‘yes and’ 상대방의 말에 ‘맞아~ 그리고~’를 하고 대화를 이어가는 활동. 아무 말 대잔치지만 내 앞사람과의 유대감이 어마어마 해졌다. 나와 yes and 했던 두 분의 얼굴이 아직도 기억이 난다. ‘나와 함께하는 이 사람들은 안전하다’라는 유대감이 아주 깊이 각인되었다. 나중에 다시 만나도 엄청 친하게 인사할 것 같은..

생판 남인 사람들과 모여 ‘미친 짓도 함께 하니 재미있다.’를 체감했다. 안전한 곳을 설정하고, 안전한 곳에서 유대감을 생성하고, 사회적 가면을 벗어둔 채로 상호 유대를 격하게 함양. 콘퍼런스가 끝나고 이 사람들과의 유대감을 지속해보고자 얼굴책에도 다시 가입.

다소 비쌌지만(사장님 감사해요),

얻은 게 정말 많은 콘퍼런스였다. 고생하신 분들께 너무 감사드리고, 아쉬운점은 다음 콘퍼런스에서 해우하시기를!!

agile_photo 행사 앨범에서, https://www.facebook.com/AgileKoreaConference/posts/1137604536373266

Spring Boot에서 테스트를 - 2

Spring Boot에서 테스트를 - 2

이전 문서에서는 Spring Boot에 새롭게 추가된 spring-boot-test-starter에 포함되어있는 모듈들과 기본적인 Spring Boot 테스트 모듈에 대해서 알아보았습니다. 이번에는 좀 더 세부적으로 들어가서 JSON 테스트, WebMvc 테스트, JPA 테스트, JDBC 테스트, Mongo 테스트, RestClient 테스트에 대해서 알아볼 것 입니다.

@JsonTest

@JsonTest 어노테이션을 사용하면 보다 편하게 JSON serialization과 deserialization을 테스트해볼 수 있습니다. @JsonTest 어노테이션은 ObjectMapper@JsonComponent 빈을 포함한 Jackson의 테스트를 위한 모듈들을 자동으로 설정합니다. 테스트를 위한 빈으로 JacksonTester, GsonTester, BasicJsonTester 등이 있습니다. 이를 주입받아서 사용하면 보다 편리하게 JSON을 테스트해볼 수 있습니다. 그리고 Assertj는 JSON을 위한 기능들을 제공합니다(JSONassert, JsonPath를 기반으로한). 아래는 JSON serialize와 deserialize를 테스트하는 예제입니다.

@RunWith(SpringRunner.class)
@JsonTest
public class ArticleJsonTest {
    @Autowired
    private JacksonTester<Article> json;

    @Test
    public void testSerialize() throws IOException {
        Article article = new Article(
                1,
                "kwseo",
                "good",
                "good article",
                Timestamp.valueOf((LocalDateTime.now())));

        // assertThat(json.write(article)).isEqualToJson("expected.json");  직접 파일과 비교
        assertThat(json.write(article)).hasJsonPathStringValue("@.author");
        assertThat(json.write(article))
                .extractingJsonPathStringValue("@.title")
                .isEqualTo("good");
    }

    @Test
    public void testDeserialize() throws IOException {
        Article article = new Article(
                1,
                "kwseo",
                "good",
                "good article",
                new Timestamp(1499655600000L));
        String jsonString = "{\"id\": 1, \"author\": \"kwseo\", \"title\": \"good\", \"content\": \"good article\", \"createdDate\": 1499655600000}";

        assertThat(json.parse(jsonString)).isEqualTo(article);
        assertThat(json.parseObject(jsonString).getAuthor()).isEqualTo("kwseo");
    }
}

@WebMvcTest

이전 문서에서 client-side에서 API를 테스트하는 TestRestTemplate을 살펴봤다면, 이번에는 server-side에서 API를 테스트하는 @WebMvcTest 어노테이션에 대해서 알아볼 것 입니다. 해당 어노테이션은 기존에 spring-test에서 컨트롤러를 테스트할 때 많이 사용하던 MockMvc에 관한 설정을 자동으로 수행해주는 어노테이션입니다. @WebMvcTest 어노테이션을 사용하면 테스트에 사용할 @Controller 클래스와 @ControllerAdvice, @JsonComponent, @Filter, WebMvcConfigurer, HandlerMethodArgumentResolver 등을 스캔합니다. 그리고 MockMvc를 자동으로 설정하여 빈으로 등록합니다.

@RunWith(SpringRunner.class)
@WebMvcTest(ArticleApiController.class)
public class ArticleApiControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private ArticleService articleService;

    @Test
    public void testGetArticles() throws Exception {
        List<Article> articles = asList(
                new Article(1, "kwseo", "good", "good content", now()),
                new Article(2, "kwseo", "haha", "good haha", now()));

        given(articleService.findFromDB(eq("kwseo"))).willReturn(articles);

        mvc.perform(get("/api/articles?author=kwseo"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("@[*].author", containsInAnyOrder("kwseo", "kwseo")));
    }

    private Timestamp now() {
        return Timestamp.valueOf(LocalDateTime.now());
    }
}

Async Web Test

컨트롤러에서 FutureDeferredResult의 객체를 반환하면 HTTP 요청과 응답은 비동기로 동작합니다. 기존과 다른 방식으로 동작하기에 MockMvc로 테스트 방법도 약간의 변화가 필요합니다.

    ...
    @Test
    public void testGetArticle() throws Exception {
        Article expected = new Article(1, "kwseo", "good", "good content", now());

        given(articleService.findOneFromRemote(eq(1))).willReturn(expected);

        MvcResult result = mvc.perform(get("/api/articles/1")).andReturn();
        mvc.perform(asyncDispatch(result))      // asyncDispatch 필요
            .andExpect(status().isOk())
            .andExpect(jsonPath("@.id").value(1));
    }
    ...

위 코드처럼 MockMvc로 요청을 한 뒤 MvcResult로 받어서 asyncDispatch로 감싸줄 필요가 있습니다.

@DataJpaTest

Spring Data JPA를 테스트하고자 한다면 @DataJpaTest 기능을 사용해볼 수 있습니다. 이 어노테이션과 함께 테스트를 수행하면 기본적으로 in-memory embedded database를 생성하고 @Entity 클래스를 스캔합니다. 일반적인 다른 컴포넌트들은 스캔하지 않습니다. 참고로 @DataJpaTest@Transactional 어노테이션을 포함하고 있습니다. 그래서 테스트가 완료되면 자동으로 롤백하을 하기 위해서 직접 @Transactional 어노테이션을 달아줄 필요가 없습니다. 그런데 만약 @Transactional 기능이 필요하지 않다면 아래와 같이 줄 수 있습니다.

@RunWith(SpringRunner.class)
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class SomejpaTest {
    ...
}

@DataJpaTest 기능을 사용하면 @Entity를 스캔하고 repository를 설정하는 것 이외에도 테스트를 위한 TestEntityManager라는 빈이 생성됩니다. 이 빈을 사용해서 테스트에 이용한 데이터를 정의할 수 있습니다. 아래는 @DataJpaTest를 사용하여 테스트를 수행하는 예제입니다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class ArticleDaoTest {
    @Autowired
    private TestEntityManager entityManager;
    @Autowired
    private ArticleDao articleDao;

    @Test
    public void test() {
        Article articleByKwseo = new Article(1, "kwseo", "good", "hello", Timestamp.valueOf(LocalDateTime.now()));
        Article articleByKim = new Article(2, "kim", "good", "hello", Timestamp.valueOf(LocalDateTime.now()));
        entityManager.persist(articleByKwseo);
        entityManager.persist(articleByKim);


        List<Article> articles = articleDao.findByAuthor("kwseo");
        assertThat(articles)
                .isNotEmpty()
                .hasSize(1)
                .contains(articleByKwseo)
                .doesNotContain(articleByKim);
    }
}

만약 테스트에 in-memory embedded database를 사용하지 않고 real database를 사용하고자 하는 경우, @AutoConfigureTestDatabase 어노테이션을 사용하면 손쉽게 설정할 수 있습니다.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class SomeJpaTest {
    ...
}

@JdbcTest

Spring Data JPA를 사용하지 않더라도 데이터베이스 테스트를 해볼 수 있습니다. @JdbcTest@DataJpaTest와 비슷한 설정을 수행하지만 순수 JDBC 테스트를 준비합니다. @JdbcTest 어노테이션을 사용하면 마찬가지로 im-memory embedded database가 설정되며, 테스트를 위한 JdbcTemplate이 생성됩니다.

@DataMongoTest

최근 점점 많은 인기를 얻고 잇는 NoSQL DB인 MongoDB에 대해서도 편리한 테스트 기능을 제공합니다. @DataMongoTest 어노테이션이 이를 위한 기능을 제공하며 설정하는 내용은 @DatajpaTest와 유사합니다. 위에 다른 데이터 테스트 모듈과 유사하게 im-memory embedded MongoDB를 사용하지만, @DataMongoTest@Entity가 아닌 @Document를 스캔하며 MongoTemplate을 생성합니다.

@RunWith(SpringRunner.class)
@DataMongoTest
public class SomeMongoTest {
    @Autowired 
    private MongoTemplate mongoTemplate;
    ...
}

만일 in-memory embedded MongoDB를 사용하는 것은 원하지 않고 외부에 직접 구축한 MongoDB을 사용하고자 한다면 아래와 같이 속성을 추가하면 됩니다.

@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)

@RestClientTest

@RestClientTest 기능은 자신이 서버 입장이 아니라 클라이언트 입장이 되는 코드를 테스트할때 유용합니다. 예를 들면, Apache HttpClient나 Spring의 RestTemplate을 사용하여 외부 서버에 웹 요청을 보내는 경우가 있습니다. @RestClientTest는 요청에 반응하는 가상의 Mock 서버를 만든다고 생각하면 됩니다. 내부 코드에서 웹 요청이 발생할 경우 @RestClientTest로 인해서 생성된 가상의 서버가 응답을 해줍니다. 물론 그 가상의 서버가 어떤식으로 응답을 할지 정의할 수 있습니다. 이를 사용하면 보다 RestTemplate 같은 객체를 Mock 객체로 바꿔서 테스트하는 것보다 리얼 환경에 가깝게 단위 테스트를 수행할 수 있습니다. 이 기능을 사용하면 자동으로 MockRestServiceServer라는 빈이 생성되며 이를 이용하면 손쉽게 요청과 응답에 대한 설정을 할 수 있습니다.

@RunWith(SpringRunner.class)
@RestClientTest(ArticleServiceImpl.class)
public class ArticleServiceImplWithRestClientTest {
    @MockBean
    private ArticleDao dao;
    @Autowired
    private ArticleServiceImpl service;
    @Autowired
    private MockRestServiceServer server;

    @Test
    public void testGetFindOneFromRemote() throws Exception {
        String articleJson = "{ \"id\": 1, \"author\": \"kwseo\", \"title\": \"gogogo\", \"content\": \"good\", \"date\": 1502322765 }";

        server.expect(requestTo("http://sample.com/some/articles/1"))
            .andRespond(withSuccess(articleJson, MediaType.APPLICATION_JSON));

        Article article = service.findOneFromRemote(1);
        assertThat(article.getId()).isEqualTo(1);
    }
}