spring restdoc 을 만들어 보자

spring restdoc 을 만들어 보자

restdoc을 쓸수도 있을 거 같아서 정리 한다. spring-boot 기준으로 작성 하였다.
기본적인 spring-boot를 안다고 가정하고 작성한다.


... <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <version>1.0.1.RELEASE</version> </dependency> .... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <includes> <include>**/*Documentation.java</include> </includes> </configuration> </plugin> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.2.1</version> <executions> <execution> <id>generate-docs</id> <phase>prepare-package</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html</backend> <doctype>book</doctype> <attributes> <snippets>${project.build.directory}/generated-snippets</snippets> </attributes> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <phase>prepare-package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.outputDirectory}/static/docs</outputDirectory> <resources> <resource> <directory>${project.build.directory}/generated-docs</directory> </resource> </resources> </configuration> </execution> </executions> </plugin> ...

restdocs-mockmvc 디펜더시랑 plugin 을 추가하자.
필자의 파일 경로로 하지 않아도 된다.
필자는 generated-snippets 에 snippets 설정 했다.
여기에 실제 adoc 파일들이 추가된다.

그리고 테스트 케이스를 만들어야 된다.
테스트 케이스는 기존에 필자가 포스팅 한거랑 비슷한대 약간 추가 되는 부분이 있다. 살펴보자

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringTestApplication.class)
@WebAppConfiguration
@FixMethodOrder(MethodSorters.JVM)
public class AccountControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    MockMvc mockMvc;


    @Autowired
    private ObjectMapper objectMapper;


    @Rule
    public RestDocumentation restDocumentation =
            new RestDocumentation("target/generated-snippets");

    @Before
    public void before() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
                .apply(documentationConfiguration(this.restDocumentation))
//                .alwaysDo(document("{method-name}/{step}/"))
                .alwaysDo(document("{class-name}/{method-name}/"))
                .build();
    }

    @Test
    public void getAccounts() throws Exception {
        this.mockMvc.perform(get("/accounts").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.[0].name", is("wonwoo")));
    }

    @Test
    public void getAccount() throws Exception {
        this.mockMvc.perform(get("/account/{id}", 1).accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("wonwoo")));
    }

    @Test
    public void createAccount() throws Exception {
        Account account = new Account();
        account.setName("wonwoo123");
        this.mockMvc.perform(post("/account").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(account)))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name",is("wonwoo123")));
    }
}

나머진 일반 테스트 케이스처럼 작성하면 되고 before 부분만 변경 되었다.
before의 alwaysDo 메소드에 있는 class-name과 method-name은 위에서 말했던 generated-snippets/* 폴더 아래에 classname/methodname/
형식으로 추가된다.
작성 후에 테스트를 실행 하여 보자. 만약 성공 되었다면 target/generated-snippets/ 아래 폴더에 파일들이 생성 되어있는걸 볼 수 있다.

그리고 나서 src/main/asciidoc/index.adoc 를 추가하자. (필자 기준)

= RESTful Notes API Guide
Andy Wilkinson;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

[[overview]]
= Overview

[[overview-http-verbs]]
== HTTP verbs

RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP verbs.

|===
| Verb | Usage

| `GET`
| Used to retrieve a resource

| `POST`
| Used to create a new resource

| `PATCH`
| Used to update an existing resource, including partial updates

| `DELETE`
| Used to delete an existing resource
|===

[[overview-http-status-codes]]
== HTTP status codes

RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP status codes.

|===
| Status code | Usage

| `200 OK`
| The request completed successfully

| `201 Created`
| A new resource has been created successfully. The resource's URI is available from the response's
`Location` header

| `204 No Content`
| An update to an existing resource has been applied successfully

| `400 Bad Request`
| The request was malformed. The response body will include an error providing further information

| `404 Not Found`
| The requested resource did not exist
|===


== 사용자 리스트 조회 [get]

사용자를조회


include::{snippets}/account-controller-test/get-accounts/curl-request.adoc[]

=== 요청 구조

==== 요청 파라미터들

include::{snippets}/account-controller-test/get-accounts/http-request.adoc[]

=== 응답 구조

==== 응답 파라미터들

include::{snippets}/account-controller-test/get-accounts/http-response.adoc[]



== 사용자 조회 [get]

사용자를조회


include::{snippets}/account-controller-test/get-account/curl-request.adoc[]

=== 요청 구조

==== 요청 파라미터들

include::{snippets}/account-controller-test/get-account/http-request.adoc[]

=== 응답 구조

==== 응답 파라미터들

include::{snippets}/account-controller-test/get-account/http-response.adoc[]


== 사용자 입력 [post]

사용자를조회


include::{snippets}/account-controller-test/create-account/curl-request.adoc[]

=== 요청 구조

==== 요청 파라미터들

include::{snippets}/account-controller-test/create-account/http-request.adoc[]

=== 응답 구조

==== 응답 파라미터들

include::{snippets}/account-controller-test/create-account/http-response.adoc[]


윗 부분은 spring에서 퍼온거라.. asciidoc이라는 문법이다.
asciidoc-syntax-quick-reference 문법은 여기서 참고.

== 사용자 리스트 조회 [get] 이 부분 부터가 필자가 만들었다. 이쁘진 않지만..
나머지는 그냥 다 문법이고 추가 해야 되는 부분이 있는데 include하는 이 부분이다.
위에서 말했던 파일들을 include 하는 부분이다. 저기에 각각의 페이지가 include 된다.

실제 파일들이 어떻게 생겼는지 보자.
curl이 작성 되어있다. 파일명은 curl-request.adoc로 떨궈진다.

[source,bash]
----
$ curl 'http://localhost:8080/accounts' -i -H 'Accept: application/json'
----

다음으로 reqeust 정보이다. 실제 rest api 의 method, resource, message 정보들이 적혀있다. (물론 지금은 메시지가 없어서)

[source,http]
----
GET /accounts HTTP/1.1
Accept: application/json
Host: localhost

----

마지막으로 response 정보이다. 실제 서버에서 내려 받은 정보를 보여준다.

[source,http]
----
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 81

[ {
  "id" : 1,
  "name" : "wonwoo"
}, {
  "id" : 2,
  "name" : "kevin"
} ]
----

메이븐 기준으로 되어있으니 메이븐을 실행해보자.

mvn install

그러면 target/generated-docs 아래 index.html 파일이 생성 되어 있을 것이다.
한번 열어 보자! 그럼 깔끔한 api 문서가 만들어져있다.

spring-doc

잘 실행되었다면 위와 같은 화면을 볼 수 있을 것이다.

spring java config transaction

spring java config transaction

저번 포스팅에서 rollback이 안되는 문제 있었다. rollback도 안됐지만 service 계층의 영속성도 안됐다(물론 당연한 얘기지만)
해결은 했는데 잘 모르겠다.
RootContext를 버리니 잘 된다.
그래서 다시 책을 봤다. 하지만 안보인다.(대충 봐서)
다시 생각이 나서 김영한님의 github를 봤다.
RootContext가 없다.
솔직히 아직 잘 모르겠다. root와 servlet context의 대해.. 토비님 책을 다시 읽어야 겠다.
servlet이 하나라면 굳이 분리할 필요도 없다고 하는거 같기도 하고..
아무튼 지금은 잘모르겠다. 일단 나중에 다시 살펴보겠다.

이렇게 바꾸었다.

@Configuration
@EnableWebMvc
@ComponentScan(
  basePackages = "me.wonwoo.account"
)
@Import(RootConfiguration.class)
public class MvcConfiguration extends WebMvcConfigurerAdapter {
  //...
}
  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
//    AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
//    rootContext.register(RootConfiguration.class);

    AnnotationConfigWebApplicationContext dispatcherServlet = new AnnotationConfigWebApplicationContext();
    dispatcherServlet.register(MvcConfiguration.class);
    servletContext.addListener(new ContextLoaderListener(dispatcherServlet));

   //...
   //...
}

롤백도 되고 service에 영속성도 된다.
소스가 다시 개판 되었다..ㅡㅡ

save하면서 한건입력 후 강제로 에러를 냈다.
아무튼 소스는 다시 올려놔야 겠다.

@Transactional
public Account save(Account account) {
  accountRepository.save(account);
  throw new RuntimeException();
}

에러는 내는 부분

@Transactional(readOnly = true)
public Account findOne(Long id) {
  Account one = accountRepository.findOne(id);
  Product order = one.getOrder();
  order.getOrderedName();
  return one;
}
@OneToOne(fetch = FetchType.LAZY)

service 계층에 영속성 테스트 하는 부분

물론 일단 지금은 EAGER로 바꿔놨다.

spring java config

spring java config

맨날 스프링부트로 공부 하다 보니 자바 config가 기억이 잘 안난다.
그래서 대충 한번 해봤다.
일반 스프링과 jpa 구현체는 하이버네이트로 했다.

maven 프로젝트 webapp으로 만들었다.
pom.xml에 다음과 같이 추가를 했다.

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.5.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.2.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.1.5</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.16</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jul-to-slf4j</artifactId>
        <version>1.7.16</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>log4j-over-slf4j</artifactId>
        <version>1.7.16</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.6.5</version>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.191</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>1.9.4.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.11.Final</version>
        <exclusions>
            <exclusion>
                <groupId>org.jboss.spec.javax.transaction</groupId>
                <artifactId>jboss-transaction-api_1.2_spec</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>javax.transaction</groupId>
        <artifactId>javax.transaction-api</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.6</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.2.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.2.0</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

스프링 컨텍스트랑 webmvc를 추가 하고 로그는 logback을 사용했다.
데이터베이스는 h2 메모리 디비로 일단.
스프링 부트를 안쓰니 디펜더시 설정이 많다.

ROOT 컨텍스트 부터 살펴보자.

@Configuration
@EnableJpaRepositories(basePackages = "me.wonwoo.account")
@EnableTransactionManagement
public class RootConfiguration {

  @Bean
  public DataSource dataSource() {
    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    EmbeddedDatabase db = builder
      .setType(EmbeddedDatabaseType.H2)
      .build();
    return db;
  }

  @Bean
  public EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
    return entityManagerFactory.createEntityManager();
  }

  @Bean
  public FactoryBean<EntityManagerFactory> entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean containerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
    containerEntityManagerFactoryBean.setDataSource(dataSource());
    JpaVendorAdapter adaptor = new HibernateJpaVendorAdapter();
    containerEntityManagerFactoryBean.setJpaVendorAdapter(adaptor);
    containerEntityManagerFactoryBean.setPackagesToScan("me.wonwoo.account");
    Properties props = new Properties();
    props.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
    props.setProperty("hibernate.show_sql", "true");
    props.setProperty("hibernate.hbm2ddl.auto", "create");
    containerEntityManagerFactoryBean.setJpaProperties(props);
    return containerEntityManagerFactoryBean;
  }

  @Bean
  public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
    jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
    return jpaTransactionManager;
  }
}

dataSource는 h2 메모리 디비를 썼고 하이버네이트 셋팅을 했다. jpa는 처음 설정하는 거라 틀린 거 일지도 모른다.
맨날 boot가 알아서 해주니 (실제 회사에선 개발할때 맨날 mybatis만 써갖고..)
아무튼 이제 webconfig를 보자. 별거 없다.

@Configuration
@EnableWebMvc
@ComponentScan(
  basePackages = "me.wonwoo.account"
)
public class MvcConfiguration extends WebMvcConfigurerAdapter {

  @Bean
  @Primary
  public ObjectMapper objectMapper() {
    return new ObjectMapper();
  }

  @Autowired
  private AccountRepository accountRepository;

  @Bean
  public InitializingBean initializingBean() {
    return () -> {
      Arrays.asList(
        new Account(1L, "wonwoo"),
        new Account(2L, "kevin")
      ).forEach(accountRepository::save);
    };
  }
}

ComponentScan 스캔만 하고 api서버를 만들 예정이라 view는 필요 없고 test에 쓰기 위해 ObjectMapper를 빈으로 등록했다.
그리고 초기 데이터를 넣기 위해 InitializingBean을 사용하였다.

설정 마지막으로 디스파처 설정이랑 context를 등록하자. web.xml이라고 생각하면 된다

public class DispatcherServletInitializer implements WebApplicationInitializer {

  @Override
  public void onStartup(ServletContext servletContext)
    throws ServletException {
    AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
    rootContext.register(RootConfiguration.class);

    servletContext.addListener(new ContextLoaderListener(rootContext));

    AnnotationConfigWebApplicationContext dispatcherServlet = new AnnotationConfigWebApplicationContext();
    dispatcherServlet.register(MvcConfiguration.class);

    ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(dispatcherServlet));
    dispatcher.setLoadOnStartup(1);
    dispatcher.addMapping("/");

    FilterRegistration.Dynamic filter = servletContext.addFilter("CHARACTER_ENCODING_FILTER", CharacterEncodingFilter.class);
    filter.setInitParameter("encoding", "UTF-8");
    filter.setInitParameter("forceEncoding", "true");

  }
}

context를 등록하고 디스파처를 등록했다.
일단 설정은 이것으로 끝났다.
일반으로 쓰듯이 controller service repository를 만들면 되겠다.
소스는 github 에 올려놨다. 참고만…

테스트 해보니 롤백이 안된다.ㅜㅜㅜㅜㅜㅜㅜㅜ ㅜㅜㅜ
다시 알아봐야 겠다.