phantomjs 가 2018년 4월에 멈춰졌기로 확인해봤다니..


기존의 고생한 것도 있고. headless chrome 때문에 옮겼다고 한다.


https://groups.google.com/forum/#!topic/phantomjs/9aI5d-LDuNE


Hi,


I want to make an announcement.


I think people will switch to it, eventually. Chrome is faster and more stable than PhantomJS. And it doesn't eat memory like crazy.

I don't see any future in developing PhantomJS. Developing PhantomJS 2 and 2.5 as a single developer is a bloody hell.
Even with recently released 2.5 Beta version with new and shiny QtWebKit, I can't physically support all 3 platforms at once (I even bought the Mac for that!). We have no support.
From now, I am stepping down as maintainer. If someone wants to continue - feel free to reach me.

I want to give credits to Ariya, James and Ivan! It was the pleasure to work with you. Cheers!
I also want to say thanks to all people who supported and tried to help us. Thank you!

With regards,

Vitaly.


Posted by '김용환'
,

gradle 에러 내용은 다음과 같다. 



난 여태 gradle은 순서에 상관없는 빌드를 지원하는 것으로 알고 있었다.

그런데 아래와 같은 에러가 발생한다. 


아래와 같이 사용했더니


repositories {
mavenCentral()
}

plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id 'java'
}



아래와 같은 gradle 에러가 발생한다.

only buildscript {} and other plugins {} script blocks are allowed before plugins {} blocks, no other statements are allowed




https://docs.gradle.org/5.4.1/userguide/plugins.html 문서를 참고하면.

plugins (플러그인 바이너리) 스펙에 constraint를 설명하고 있다. 


에러 문구 대로 plugins {} 구문의 앞에는 buildscript {}와 기타 plugin{}만 사용될 수 있다. 





Posted by '김용환'
,


인터넷에 많이 공개된 Spring Rest Docs를 공부하고 적용해봤다.



* Spring Rest Docs가 Swagger2에 비해 갖는 장점

- 내가 원하는 형태로 문서화할 수 있다. (대신 단점도 됨)

- 테스트를 무조건 만들어야 하고 문서화를 진행해야 한다. (안전)

- Spring Controller의 지저분한 annotation이 깔끔해짐 (코드의 간결성)

- Swagger2는 2018년 중반 이후부터 더 이상 개발되지 않고 있다. 



* Spring Rest Docs가 Swagger2에 비해 갖는 단점

- 처음에 손이 많이 간다. 

  asciidoc로 문서화를 수행해야 한다. 처음에는 디폴트가 먼저도 모름. 헤맴. 그리고 만들어진 html 문서를 resource static 문서로 복사해야 한다. gradle, maven 자동화가 필요하다.

- UI를 못하거나 문서화를 자동으로 해주는 swagger2가 적당한 선이 아직까지는 내게 어울림

- swagger2의 try it out 기능이 없는 것 같다(또는 어디에 있는지 못 찾겠다.) 

   swagger2의 try it out은 api 연동의 핵심이라.. 의사소통을 많이 줄여준다. 


-> (개인적으로) 아직은 Swagger2가 훨씬 편한 듯 하다. 그러나 Spring Rest Docs의 철학이 아무 매력적이다. '테스트 없는 문서화가 의미가 있나?' 테스트를 강제하는 형태가 좋아보였다. 



Spring Rest Docs를 사용하면 아래와 비슷하게 나온다. 귀찮아서 정리를 못했지만 손이 조금 간다. 꼼꼼한 사람들에게는 좋을 것 같다. 







나머지는 코드이다. 


https://github.com/knight76/springboot2-restdocs


build.gradle


buildscript {
repositories {
mavenCentral()
jcenter()
}

dependencies {
classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.9'
}
}


plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id "org.asciidoctor.convert" version "1.5.9"
}

apply plugin: 'org.asciidoctor.convert'
apply plugin : 'java'
apply plugin : 'io.spring.dependency-management'
apply plugin : 'org.asciidoctor.convert'

ext {
lombokVersion = "1.18.8"
swagger2Version = '2.9.2'
oldSwagger2Version = '1.5.21'
}


group = 'com.google.knight76'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'


ext {
snippetsDir = file('build/generated-snippets')
}

test {
outputs.dir snippetsDir
}

asciidoctor {
inputs.dir snippetsDir
dependsOn test
}

bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static'
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude module: "spring-boot-starter-tomcat"
}
implementation "org.projectlombok:lombok:${lombokVersion}"
implementation 'org.springframework.boot:spring-boot-starter-undertow'

asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation('org.springframework.restdocs:spring-restdocs-mockmvc')
}


src/doc/asciidoc/api-docs.adoc

:sectnums:
:sectnumlevels: 5
:toc: left
:toclevels: 3
:page-layout: docs


= Hello World

== Hello World (Plain)

include::{snippets}/hello-world-test/hello-world/http-request.adoc[]
include::{snippets}/hello-world-test/hello-world/curl-request.adoc[]
include::{snippets}/hello-world-test/hello-world/http-response.adoc[]
include::{snippets}/hello-world-test/hello-world/request-body.adoc[]
include::{snippets}/hello-world-test/hello-world/response-body.adoc[]
include::{snippets}/hello-world-test/hello-world/httpie-request.adoc[]


== Hello World (Json)

include::{snippets}/hello-world-test/hello-world-json/http-request.adoc[]
include::{snippets}/hello-world-test/hello-world-json/curl-request.adoc[]
include::{snippets}/hello-world-test/hello-world-json/http-response.adoc[]
include::{snippets}/hello-world-test/hello-world-json/request-body.adoc[]
include::{snippets}/hello-world-test/hello-world-json/response-body.adoc[]
include::{snippets}/hello-world-test/hello-world-json/response-fields.adoc[]
include::{snippets}/hello-world-test/hello-world-json/httpie-request.adoc[]
include::{snippets}/hello-world-test/hello-world-json/request-parameters.adoc[]

== City (add-city)

include::{snippets}/cities-controller-test/add-city/http-request.adoc[]
include::{snippets}/cities-controller-test/add-city/curl-request.adoc[]
include::{snippets}/cities-controller-test/add-city/http-response.adoc[]
include::{snippets}/cities-controller-test/add-city/request-body.adoc[]
include::{snippets}/cities-controller-test/add-city/response-body.adoc[]
include::{snippets}/cities-controller-test/add-city/response-fields.adoc[]


== City (delete-city)

include::{snippets}/cities-controller-test/delete-city/http-request.adoc[]
include::{snippets}/cities-controller-test/delete-city/curl-request.adoc[]
include::{snippets}/cities-controller-test/delete-city/http-response.adoc[]
include::{snippets}/cities-controller-test/delete-city/request-body.adoc[]
include::{snippets}/cities-controller-test/delete-city/response-body.adoc[]
include::{snippets}/cities-controller-test/delete-city/response-fields.adoc[]

== City (get-cities)

include::{snippets}/cities-controller-test/get-cities/http-request.adoc[]
include::{snippets}/cities-controller-test/get-cities/curl-request.adoc[]
include::{snippets}/cities-controller-test/get-cities/http-response.adoc[]
include::{snippets}/cities-controller-test/get-cities/request-body.adoc[]
include::{snippets}/cities-controller-test/get-cities/response-body.adoc[]
include::{snippets}/cities-controller-test/get-cities/response-fields.adoc[]


== City (get-city)

include::{snippets}/cities-controller-test/get-city/http-request.adoc[]
include::{snippets}/cities-controller-test/get-city/curl-request.adoc[]
include::{snippets}/cities-controller-test/get-city/http-response.adoc[]
include::{snippets}/cities-controller-test/get-city/request-body.adoc[]
include::{snippets}/cities-controller-test/get-city/response-body.adoc[]
include::{snippets}/cities-controller-test/get-city/path-parameters.adoc[]
include::{snippets}/cities-controller-test/get-city/response-fields.adoc[]


예시

@Test
public void getCity() throws Exception {
// given
String response = "{'id': 1,'name':'Bratislava','population':432000}";

// when
when(cityService.get(1L)).thenReturn( Optional.of(new City(1L, "Bratislava", 432000)));

ResultActions result =
mockMvc.perform(get("/api/1/city/{id}", 1L)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
.andDo(print());

// then
result.andExpect(status().isOk())
.andDo(document.document(
pathParameters(
parameterWithName("id").description("도시의 id")),
responseFields(
fieldWithPath("id").description("도시의 id"),
fieldWithPath("name").description("도시 이름"),
fieldWithPath("population").description("도시 인구")
)))
.andExpect(jsonPath("id", is(notNullValue())))
.andExpect(content().json(response));
}






참고 

http://woowabros.github.io/experience/2018/12/28/spring-rest-docs.html

https://docs.spring.io/spring-restdocs/docs/current/reference/html5/


Posted by '김용환'
,


보통은 org.springframework.web.HttpMediaTypeNotSupportedException 예외는


Rest api에서 허용할 수 있는 MediaType이 아니면 에러가 난다.

@RequestMapping(path="/api/1", 
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, 
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)


ResultActions result =
mockMvc.perform(post("/api/1/city")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(requestJson))
.andDo(print());




그러나, 특별한 경우로 

SpringBoot의 UI 모델 또는 DTO에  lombok, Jackson의 JsonCreator가 잘못 결합될 때. 이상한 에러가 난다.

그래서 몇시간동안 삽질, 주화 입마에 빠질 수 있다.    

org.springframework.web.HttpMediaTypeNotSupportedException






문제가 되는 코드는 다음과 같다.


@RestController

@RequestMapping("/api/test")

public class CityController {


    @PostMapping

    public Product post(@RequestBody City city) {

.....

    }


}




@Data

public class City {

@NonNull

private Long id;


@NonNull

private String name;


@NonNull

private Integer population;


@JsonCreator

public City(Long id, String name, Integer population) {

this.id = id;

this.name = name;

this.population =  population;

}

}


또는 아래와 같이 AllArgsConstructor에 JsonCreator를 사용하면 안된다.


@Data

@Builder

@AllArgsConstructor(onConstructor = @__(@JsonCreator))

public class City {


@NonNull


private Long id;




@NonNull


private String name;




@NonNull


private Integer population;

}


HttpMediaTypeNotSupportedException가 발생한다.



따라서  아래와 같이 모델을 변경하니 잘 동작한다.

@Data

@AllArgsConstructor

@NoArgsConstructor

public class City {

@NonNull

private Long id;


@NonNull

private String name;


@NonNull

private Integer population;

}




이미 보고는 되어 있는데..  해결 방안은  딱히 없으니. 알아서 잘 피해야 할 것 같다. 



https://github.com/FasterXML/jackson-databind/issues/1239

https://github.com/spring-projects/spring-boot/issues/12568


Posted by '김용환'
,


에러 

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.example.model.City]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.model.City` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

 at [Source: (PushbackInputStream); line: 2, column: 3]

 


생성자에 Jackson의 @JsonCreator를 붙여 해결하고 싶겠지만. 결국 문제가 발생할 것이다. 




 @Data

public class City {

@NonNull

private Long id;


@NonNull

private String name;


@NonNull

private Integer population;


@JsonCreator

public City(Long id, String name, Integer population) {

this.id = id;

this.name = name;

this.population =  population;

}

}


 

아래 org.springframework.web.HttpMediaTypeNotSupportedException 이 발생하게 된다...

https://knight76.tistory.com/entry/orgspringframeworkwebHttpMediaTypeNotSupportedException-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0


최대한 Lombok을 잘 사용하는 것이 좋다. 

Posted by '김용환'
,



sprin test를 사용하다가 로그에 request의 body(특정 json)가 안나오는 문제가 있다. 


    

    MockHttpServletRequest:

      HTTP Method = POST

      Request URI = /api/1/city/

       Parameters = {}

          Headers = [Accept:"application/json"]

             Body = <no character encoding set>

    Session Attrs = {}

    


그래서 코드에 characterEncoding을 추가했더니 문제가 발생하지 않았다. 


    ResultActions result =

mockMvc.perform(post("/api/1/city/", 10L)

                .accept(MediaType.APPLICATION_JSON)

                .characterEncoding("utf-8")

                .content(requestJson))

       .andDo(print());

       

       

    

    

    다음과 같이 request의 body 정보가 출력되었다.

    MockHttpServletRequest:

      HTTP Method = POST

      Request URI = /api/1/city/

       Parameters = {}

          Headers = [Accept:"application/json"]

             Body = {

  "id" : 10,

  "name" : "Bratislava",

  "population" : 432000

}

    Session Attrs = {}

    



Posted by '김용환'
,


gradle, asciidoctor를 사용할 때 

maven plugin 예제를 참고하다 잘  안되는 경우가 있다.


(디폴트로) maven의 adoc 파일을 생성하는 위치는 src/main/asciidoc 인 반면,

gradle 의 경우는 src/docs/asciidoc 이다.


gradle 사용자는 주의해야 한다. 




Posted by '김용환'
,



Proxy를 사용해야 하는 환경에서

grails 2.x 컴파일을 하는데, 제대로 inhouse library를 다운받지 못한 현상이 발생했다.

마치 nonProxy 설정에 문제가 있다는 느낌..



grails add-proxy client --host=${PROXY_HOST} --port=${PROXY_PORT} --noproxy="${JAVA_NO_PROXY}"

grails set-proxy client

grails compile 



grails dependency-report를 실행해보면, inhouse lib을 못가져온다. 



원인은 setproxy 하면서 생성된 .grails/ProxySettings.groovy 파일을 삭제하고

grails compile 을  실행하니 제대로 동작한다.



Posted by '김용환'
,


예전에 2000년 쯤에 "구XX 커피"라는 회사에 몇 개월 알바를 한 적이 있다. 

이때 오리온 동천동 물류 창고(현재 동천역)에 납품한 적이 있었는데. 엄청난 규모의 물류 창고 크기에 압도된 적이 있다.실제 재고량에 압도되었던 그 때의 기억이 난다. 


최근에 물류 쪽 얘기를 들어보면서 모르는 단어에 대해 링크를 걸어본다.



SCM과 3PL의 관계

https://m.blog.naver.com/jackshin01/220969531915

https://m.blog.naver.com/jackshin01/220982419044


DAS, PAS, DPS

https://m.blog.naver.com/jackshin01/221039015878


냉동 창고

http://www.kharn.kr/news/article.html?no=7900


라스트 밀과 풀필먼트

https://froma.co.kr/446


Posted by '김용환'
,

파워풀

After reading book 2019. 7. 25. 01:37


인사관점에서 본 넷플릭스의 밝음과 어두움을 볼 수 있는 좋은 책이었다. 



기억나는 부분



성장 과정을 추억한 것이 아니라

높은 성과를 내는 조직문화를 만들려면 어떻게 해야 할까를 안내하는 내용이다.


낡은 평가 시스템으로 인해 조직 전체가 느려지고 쓸떼 없는 경쟁력을 소진시키는 현실들..불필요한 절차를 제거했다.


처음부터 고성과자를 뽑고 맞는 일을 준다. 

관리자는 직원에게 회사가 정확히 어디에 있는지 소개하고, 성취하려고 하는 것이 무엇인지 설명한다.

또한 당면과제와 비즈니스를 명확히 하고 소통하는데 시간을 많이 들여야 한다.


극도로 솔직하고 격렬히 토론한다.


직원의 가치만큼 보상한다.



(속이 다 후련한 문화이다..)




Posted by '김용환'
,