Lombok을 사용했을 때, @Data를 붙이면 기본생성자로 @NoArgsConstructor를 생성해주는 줄 알았다.

그런데 누가 물어봐서 대답하려고 보니, 명확하게 모르는 것을 깨닫고 찾아보았다. 1

@Data를 붙이게 되면, 일반적으로는 @NoArgsConstructor를 만들어주는 것처럼 보인다.

하지만 엄밀히 말하면 @NoArgsConstructor가 아니라 @RequiredArgsConstructor가 생성된다.

즉, final접근자가 붙어있거나 @Nonnull 애노테이션을 붙인 경우에는 해당 필드를 가진 생성자가 만들어진다.

보통의 경우는 final이나 @Nonnull이 없는 경우라서 @NoArgsConstructor를 만들어 주는 것 같은 착시효과를 보일 뿐이다.

Api를 호출하는 서비스에 대한 테스트는 잘 하지 않는 경우가 많다.

혹은 @Ignore를 붙여놓고 필요시에만 풀어서 쓰는 경우도 있다.

Mock Server를 제공해서 테스트 할 수 있는 걸 알고 있는데, 자주 쓰지 않으니 쓸 때마다 헷갈려서 기록해둔다.

 

새로 스프링 배치 프로그램을 만들었는데, 테스트 환경에서는 특별히 이슈가 없다가 운영 환경에서 오류가 발생했다.

결국은 8080 포트가 충돌나서 오류가 발생한 것이었다.

배치를 여러 번 실행시키는 경우 톰캣이 제대로 shutdown시키지 못해서 발생하는 문제였다.

그런데 스프링 배치인데, 굳이 톰캣을 8080 포트로 실행해야 할 이유가 있을까?

찾아보니 스프링 배치에서는 기본적으로 톰캣을 8080 포트로 로딩하고, 배치 모니터링을 제공한다.

(사실 엄밀히 따지면 배치가 실행되는 동안만 톰캣이 유효하도록 되어 있어서, 내 경우에는 별로 의미가 없는 모니터링이었다)

그래서 찾아봤더니, 아래처럼 설정을 하면 톰캣을 실행시키지 않는다고 한다.

앗, 그런데 deprecate되었다고 warning이 발생한다.

해당 설정을 하는 곳을 찾아봤더니,

위 옵션으로 서블릿을 실행할 것인지 아닌지 처리하는 설정이고,  2.0 버전부터 deprecate되었다고 친절하게 적어놨다.

SpringBoot 2.0 부터는 아래처럼 WebApplicationType을 통해서 설정이 가능하다고 한다.

결국 application.yml에 아래처럼 설정하면 된다.

배경

현재 개발하고 있는 것은 ‘서비스의 정산’에 대한 서비스이다. 그런데 정산이라는 것이 결제가 있다면 항상 들어가야 하는 미들웨어와 같은 성격을 지니고 있다.그러다보니 서비스의 설정 정보들이 각 프로젝트에 혼재해있어서 정리를 해보려고 한다.

 

현재 상황

전체적으로 정산에 대한 공통 부분을 사용하기 위해 settlement-common이라는 모듈을 사용하고 있다. 각 서비스에서는 settlement-common에 대한 의존성을 연결하여 사용한다. 그런데 정산에서 사용하는 property 정보들이 모두 하위 서비스에서 정의하여 사용하고 있다. 예를 들면 erp.url은 하위 서비스의 application.yml에 정의되어 있다.

 

하고 싶은 것

보통의 경우에는 common 공통의 설정을 로딩해서 사용하고, 일부의 케이스에 대해서만 하위 서비스에서 설정을 overriding하고 싶었다. application.yml에 다른 설정과 함께 섞여 있는 것도 제거하고 싶었다.

 

@ConfigurationProperties 사용

아래처럼 Property정보를 Bean으로 등록해서 사용하는 것이 @Value를 사용하는 것보다 나을 거란 생각을 했다.
@Component가 없어도 Bean으로 사용할 수 있는데 굳이 쓴 이유는, IDE에서 Bean Mapping이 안된다는 waring이 발생하는 게 싫어서이다. 그리고 @ConfigurationProperties는 setter를 이용해서 값을 주입하는 것도 약간은(?) 신기한 부분이다. 최근 스프링의 추세가 reflection을 통한 강제 주입이라서.

 

파일분리 (application.yml -> settlement.yml)

애사당초 얘기했던 것처럼, 하위 프로젝트의 application.yml에서 모든 것을 정의해서 사용하면, 어떤 property를 어떤 모듈에서 사용하는지 파악하기 힘들고, 이후에 리팩토링할 때에도 장애물이 된다. 그래서 @ConfigurationPropertieslocations attribute를 활용하여 파일을 분리하였다.

 

locations attribute deprecated

그런데 locations attribute가 deprecate되었다. 심지어 최신 버전의 springboot에서는 아예 제거되었다. 1 그래서 별로 권장하지 않는다고는 하지만, @PropertySource를 활용하기로 했다.

 

spring.active.profile 기능 활용

아래처럼 springEL을 활용해서 각 profile별로 필요한 설정을 import할 수 있다. 다만 같은 값의 overriding의 경우 선언한 순서에 영향을 받는다. 2 특히 아래와 같은 경우에는 @PropertySource에 선언된 Array 중에 나중에 선언된 것이 override된다.

 

문제 발생, yml 파싱 불가

위처럼 로직을 짜고 테스트를 돌려보면, NPE가 발생한다.
이유인즉슨, Bean이 제대로 setting이 되지 않았기 때문인데, 디버거를 통해 확인했더니 yml 파일을 인식하지 못한다. 3 그래서 yml 파일을 사용할 수 있도록 Factory 클래스를 추가 구현하였다.

 

yml 파싱은 잘되는데, profile은 어떻게…

application.yml처럼 Array 형식으로 정의하고 profile별로 property를 꺼내 쓰고 싶은데, 위의 YamlPropertySourceFactory를 보면, profile을 null로 넘겼다. 해당 profile을 넣어주면 제대로 파싱될 것 같은데, Factory 파일이 bean도 아니라서 Environment를 사용할 수 없고 어떻게 해야할지는 좀더 생각해봐야 할 것 같다.

 

하위 서비스에서 설정 완료

하위 서비스에서는 아래처럼 추가 설정을 overriding해서 사용할 수 있다.

 

또 다른 문제, 파일이 없을 경우

그런데 settlement-common은 하위 프로젝트들의 모든 spring.profiles를 알 수 없다. 그래서 정의되지 않은 profile이 들어올 경우, 리소스를 읽을 수 없다면서 settlement-${spring.profiles.active}.yml 구문에서 오류가 발생한다. 그래서 아래처럼 @PropertySource를 분리하고, ignoreResourceNotFound 속성을 추가해서 처리하였다.

그런데 계속 오류가 발생한다. 디버거를 통해서 확인하였더니, YamlPropertySourceFactory내에서 YamlPropertySourceLoader를 사용하고 있는데, 여기에서 리소스가 없는 경우에 대해 기존의 경우와 다르게 return을 하고 있다. 결국 NPE가 발생한다. (기본 factory를 사용하는 경우에는 문제없음)

 

결론

yml 포맷이 훨씬 구조화된 형식이라 properties 파일보다 권장되기도 하는데, 사용하려니 현실적인 벽에 많이 부딪혀서 properties 파일을 우선 사용하는 것으로 선회하였다. 끝까지 고쳐보지 못하고, 실패를 결론으로 기록하려니 좀 아쉽긴 한데, 시간도 시간인지라… 나중에는 다시 볼 일이 생기지 않을까 싶은 생각으로 일단 여기까지 기록해둔다.

 

추가1

resource 파일은 classpath로 복사될 때, 같은 이름이면 우선순위에 따라 overwrite된다. 즉, application.properties 또는 application.yml 파일이 settlement-common에 있는데, 하위 모듈에서 동일한 파일이 있는 경우에는 내용에 상관없이 하위 모듈의 것을 사용하게 된다. 심지어 해당 property가 없는 경우라 할지라도 common의 것을 사용할 수 없다. override가 아니라 파일이 overwrite되는 것에 유의!

추가2

파일이 다르고 키가 같은 property의 경우에는 해당 값을 우선순위에 따라 override한다.

 

최근 업무에서 /api/entities이란 URL이 존재하는데, 아무리 Source를 뒤져봐도 해당 URL을 찾을 수 없었다.

몇 번 삽질을 하다가 결국 spring-data-rest를 사용했음을 알게 되었고, 해당 Library를 초기 분석한 내용을 기록해둔다.
공식문서1에 따르면 dependency는 아래처럼 설정해주면 된다.

그리고 기본 BaseUrl은 아래처럼 설정할 수 있다. (구성에 따라 아래 3가지 방법중 어느 것을 선택해도 동작한다)

위 설정으로 인해 앞으로 자동으로 생성되는 모든 Rest URL은 /api하위에 생성된다.

EVO_INFLECTOR_ISPRESENT라는 것은 org.atteo.evo.inflector.English가 classpath에 존재하는지 체크하는 것이고,
일반적인 경우라면 spring dependency로 가져왔을테니까 일반적으로는 true값이 된다.

EvoInflectorTypeBasedCollectionResourceMapping.java는 TypeBasedCollectionResourceMapping.java를 래핑한다.
class type이라는 것은 rest api를 생성할 entity의 class name인데, uncapitalize()를 통과하면서 첫자가 소문자로 변경된다.
그리고 EvoInflectorTypeBasedCollectionResourceMapping.java를 실행해서 영어 기준의 복수형으로 변환하여 준다.

EvoInflectorTypeBasedCollectionResourceMapping.java의 English.java는 영어의 복수형을 제공하는 Library이다.
불규칙명사(Entity class name은 일반적으로 명사)인 경우도 모두 처리해주고 있다.
(단, person -> people은 처리되지 않고, persons라고 나온다)
자세한 건 evo-inflector2를 참고하면 좋을 것 같다.

생성되는 URL은 대략 아래와 같다.
아래 URL은 spring-data-rest-webmvc에서 제공하는 규칙을 따르는데, RepositoryEntityController.java를 참고하면 된다.

 

이번에 맡은 업무에서 Spring Batch를 중점적으로 사용하고 있는데, 기존 로직을 리팩토링하면서 겪게 된 이슈를 기록해본다.

먼저 기존 로직은 아래와 같다. (코드 스니핏이고, 일부 업무와 관계된 네이밍은 모두 dummy로 치환하였다)
결국 배치로직에서 bulk로 읽어온 것을 하나씩 read/process/write 구조로 처리하고,
해당 내용이 없어지면 다음 page를 bulk로 읽어온다.
이 포스팅에 다루는 문제의 핵심은 bulk로 받아온 List의 첫 element를 마치 Queue처럼 순차적으로 빼서 쓰는 부분이다.

그런데 조금 불필요한 부분(redundancy)이 보이는 것 같다.
entitiesAtDb.getContent()한 내용이 List<SomeEntity>인데 왜 굳이 new ArrayList()로 래핑을 한 것일까.

그래서 해당 부분을 제거하기로 결정했다.

이제 코드가 불편한 느낌이 없이 깔끔해졌다.
어? 그런데 테스트코드에서 런타임에 오류가 발생한다.

new ArrayList()로 래핑해놓은 이유가 있었다.
아마도 내부 List가 immutable하게 구성되어 있나보다. 말이 나온 김에 좀더 찾아보기로 했다.

일반적으로 JPA에서는 List 엄밀히 따지면, mutableList를 반환한다.
그래서 element를 추가, 삭제할 수 있다.
어찌보면 JPA 철학상 당연히 그래야 하는지도 모른다.

다만, Paging을 사용하는 경우에는 readonly처럼 생각한 모양이다.
그래서 element를 추가, 삭제할 수 없도록 immutableList를 반환하고 있다.

통계에서 사용하는 쿼리를 만들다보니, (약간은) 부득이 하게 JPA Query Method를 사용하지 않고 @Query annotation을 이용해서 아래처럼 구현하였다.

호출하는 쪽의 로직은 아래와 같다.

자세한 로직은 회사 업무상 대외비이기 때문에 약간 이상한 네이밍인 점은 신경쓰지 않기로 하자.

어쨌거나 위의 로직을 돌리면 오류가 발생한다.

for (int count : counts) 에서 ClassCastException이 발생한다.

심지어   List<Integer> counts 는 정상적으로 받아왔는데, for문에서 오류가 나는 것이 좀 당황스러웠다.

그래서 JPA 스펙을 찾아보니 아래처럼 기술되어 있다.

JSR338 (Java Persistence API v2.1)에 따르면 Count() 를 이용한 쿼리의 return value는 Long으로 정의되어 있다. 그리고 Count에 대한 결과가 존재하지 않을 경우에는 0을 반환하도록 되어 있다. ( sum() 의 경우는 null이 return된다)

결국 이 모든 것은 지정된 type(여기서는 Integer)으로 return 해주는 mybatis에 익숙했던 것이 오류였다.

다만, 위에 적은 것처럼  List<Integer> counts 로 return 값을 assign 할 때 오류가 나는 것이 아니라, 해당 값을 사용하려 할 때 오류가 나는 점은 여전히 설계에 문제가 있지 않나 생각해본다.

추가로 위 counts를 getClass() 해보면  class java.lang.Long 으로 표시된다. 분명 generic은 Integer인데도 말이다.

아무런 문제가 없어보이는 코드에서 오류가 발생했다.

분명 HttpMethod는 multi value를 지원하는 값이다.

나중에야 사용하고 있는 swagger에서 multi value를 지원하지 않는 것을 알게 되어 기록을 남겨둔다.

you can currently only have one HTTP verb per op.

https://github.com/swagger-api/swagger-ui/issues/183