도커 환경에서 배포 + 폰트가 필요한 라이브러리 사용 시 "Error while loading available fonts" 오류가 발생할 때가 있다.

도커 jdk 컨테이너에는 폰트 파일이 없어서 그런 경우이다.


프로젝트에서 캡차 라이브러리, 제스퍼리포트 라이브러리를 사용할 때 폰트 문제 발생을 경험하였다.

docker compose 파일에 아래와 같이 entrypoint 를 추가하여 컨테이너 시작시 폰트 파일을 설치하도록 하여 해결하였다.

entrypoint: [ "apk add --no-cache fontconfig ttf-freefont && 다른 커맨드" ]

프로젝트에서 동료 개발자 분이 라이브러리 추가를 하는 상황에 발생했던 트러블에 대한 기록이다.

프로젝트에서 필요한 라이브러리가 생겨 maven 을 통하여 추가하려 하였으나 maven 리포터지터리의 문제인지 제대로 받아지지 않았다. 따라서 해당 라이브러리 jar 을 직접 받아 프로젝트 내부에 추가하였다.

로컬에서 실행시 문제 없이 잘 동작하였으나 서버에 올리기 위해 빌드 후 배포 했을때 문제가 발생하였다. 해당 파일의 클래스를 찾을 수 없어 스프링 빈이 주입되지 않는다는 오류를 만나게 되었다. 

원인을 파악하기 위해 빌드 파일을 확인한 결과 해당 라이브러리 파일이 누락되어 빌드가 진행된다는 사실을 확인하게 되었다. 이것저것 시도하다 결국 해결하게 되었는데, 해결 방법의 정확한 이유는 파악하지 못하였다... 라이브러리 파일이름이 "라이브러리-1.0.1.jar" 형태였는데 뒷 부분의 버젼 부분을 변경하여 "라이브러리.jar"로 변경 한 이후에는 빌드시 제대로 포함되는 것을 확인하였다.

대충 추측을 하자면 "-1.0.1" 과 같이 하이픈(-)을 사용할 경우 빌드 시 파일이름 처리에 문제가 있는게 아닌가 싶다. "라이브러리.jar" 형태로 파일을 포함하여 빌드할 경우 최종 프로젝트 빌드파일에 추가된 이름이 "라이브러리-1.0.1.jar" 형태가 되는 것으로 보아 빌드과정에서 메이븐이 파일 이름을 변경하는 무언가가 있고, 미리 하이픈이 들어가 있을 경우 해당 파일 처리에 문제가 있는 것이 아닌가 싶다.

요약: 라이브러리 파일이름이 "라이브러리-1.0.1.jar"의 형태일 경우 하이픈을 포함한 버젼을 지워보자


프로젝트에 알림 기능이 요구 사항이 생겨 기능 구현을 하고 있었다.
Spring WebSocket Config 코드 작성과 프론트단에서 서버에 웹소켓 연결을 요청하는 코드를 완성한 후 테스트를 하였다.

클라이언트에서 소켓 연결 로그, 서버에서의 CONNECT 로그 모두 잘 찍혔다. 그러나 클라이언트에서 연결이 후 subscribe 를 설정하는 callback 함수 실행이 되지 않았다. 서버쪽에서도 API 로그가 전혀 찍히지 않았다.

엔드 포인트에 오타는 없는지, 문법이 잘못되지는 않았는지 한참을 찾았으나 원인을 찾지 못하다 원인을 찾게 되었다. 예상치 못한 부분이었기에 한참을 걸려 찾게 되었다. 

원인은 새로 작성한 코드 부분이 아니라 기존에 존재하던 스프링 설정에 있었다. 프로젝트의 구조 및 성능을 위해 Spring Bean을 Lazy Init 하도록 설정이 되어있는 부분이 있었고 해당 부분이 원인이었다. 

WebSocket 연결을 위해 WebSocket Config 에 웹소켓을 통해 subscribe 및 publish 를 위한 엔드포인트의 prefix를 설정하는 부분 및 여러가지 설정을 하는 부분이 있다. 엔드포인트를 설정하는 부분이 스프링이 실행될 때 Bean이 생성되며 설정되어야 하지만 Lazy Init 을 할 경우 해당 Bean이 웹소켓 엔드포인트 설정이 되어야하는 시점이 지난 이후 생성되는 것이었다. 

결국 lazy init 하는 부분에서 Websocket 관련 Bean 은 lazy 하지 않게 변경한 이후 정상적으로 테스트가 성공적으로 되는 것을 확인하였다.

요약: WebSocket 관련 Bean은 LazyInit 하면 안된다.

운영 서버에서 로그를 확인해 보아야 하는 일이 생겼다.
그런데 왜인지 로그 파일을 저장하는 경로가 보이지 않는 것이다. 운영 서버 리눅스 계정이 root 계정이 아니었기에 권한의 문제로 보이지 않는 것으로 처음엔 생각하였다.

고객사 개발자분께 해당 경로의 로그 파일 확인을 요청하고 DevOps에 빠삭하신 차장님께 상의 하였다. 운영 서버를 한동안 체크하였고, 애초에 로그가 저장되지 않았다는 사실을 깨달았다. 고객사 개발자 분도 파일이 아예 존재하지 않는다고 연락을 주셨다. 전임자 분이 담당할 때 부터 지금까지 운영 서버 이슈는 DB쪽 체크를 하는 정도에서 문제를 해결할 수 있었기에 로그가 저장되고 있지 않다는 사실을 몰랐던 것이다.

로그가 보이지 않았던 이유는 읽기 권한이 없는 것의 문제가 아니라 쓰기 권한의 문제였다. Spring 서비스가 당시 제공 받은 계정으로 실행되고 있었고, 쓰기 권한이 없던 계정의 서비스이다 보니 로그 라이브러리에서 로그 파일을 쓰지 못하였던 것이다. 로그 파일 경로를 해당 계정의 하위 경로로 바꾸어 문제를 해결하였다.

요약: Spring 로그가 남지 않는다면 로그 경로에 대한 서비스의 실행 계정의 쓰기 권한을 확인해야 한다.

이번에 이야기할 내용은 이전 포스팅 했던 Apache FTPClient을 이용하여 개발을 할 때 있었던 또 다른 이슈 내용과 경험이다.

FTP 연결을 이용하여 이미지를 보여주던 화면에서 사진이 자꾸 짤려서 보이는 것이었다. 파일을 가져오기 위한 코드 플로우는 다음과 같았다.

 

-> FTPClient 라이브러리의 메서드를 이용
-> InputStream 타입을 리턴
-> FTP 받은 이후 FTP 연결을 disconnect
-> InputStream 에서 byte[] 읽기
-> response 

 

과거에 Stream 에 대하여 공부할 때 Stream은 데이터의 흐름을 처리해주는 타입이라는 내용을 공부했었다. 위의 플로우로 코드를 짜두고 사내 FTP 서버에서 테스트시 아무 문제 없이 이미지가 잘 불러와졌다.
해당 테스트를 하면서 나는 이렇게 이해하였다. InputStream 타입은 데이터의 흐름을 처리해주는 객체다 보니 disconenct 되었어도 해당 타입 내부에 데이터 정보를 가지고 있다라는 생각이었다.

하지만 실제 운영서버 FTP 서버에서는 테스트에서와 다르게 사진이 잘라져서 나왔던 것이었다. 결국 Stream 이란 타입은 단지 흐름을 위한 통로 역할만 해주는 것이 맞았던 것이다.
코드 흐름을 아래와 같은 플로우로 변경하자 사진이 짤리던 현상은 해결되었다.

 

-> FTPClient 라이브러리의 메서드를 이용
-> InputStream 타입을 리턴
-> InputStream 에서 byte[] 읽기
-> FTP 받은 이후 FTP 연결을 disconnect
-> response 

 

 

해당 문제를 해결하면서 운영서버에서 사용하던 FTP 서버는 HDD라서 느릴수 있다는 이야기를 들었고, byte[]를 읽은 후 disconnect 하도록 하였더니 문제가 없이 잘 불러와졌다.
이러한 사실과 관련한 내용들을 찾아보면서 당시의 문제를 예상해보았다.
수정전 코드에서 흐름에서 disconnect 하는 메서드이후 byte를 읽었지만, I/O 쓰레드는 별개의 쓰레드였고, disconnect 실행 후 I/O 쓰레드가 연결이 끊기기전에 byte를 읽는 코드가 실행되어 연결이 끊어지기 전까지 데이터를 받았을 것이다.
사내 FTP 서버의 경우 운영서버에서 사용하던 FTP 서버보다 속도가 빨랐기에 연결이 끊기기전에 이미지 데이터를 모두 보내줄 수 있었지만 운영서버에서 사용하던 FTP 서버는 그러지 않았을 것이다. 따라서 수정한 코드는 모든 데이터를 확실하게 받고 disconnect 하게 되었기에 문제가 없었을 것이다.

Stream을 주고 받는 데이터 그 자체라고 착각하던 나였기에 해당 경험은 잘못 알고있던 부분에 대해서 제대로 이해하게 해주는 좋은 경험이 된 것 같다. 또한 쓰레드에 대한 내용들을 찾아보면서 실제 데이터의 흐름에 대해 더 생각해 볼 수 있는 기회가 되었다.

이 내용은 Spring AOP를 이용하여 API 요청정보를 DB에 저장하는 기능을 구현할 때 알게된 정보이다.

당시 구현하고자 했던 기능은 어노테이션을 이용한 Advisor 클래스를 등록한 이후 API Controller 단에 해당 어노테이션을 적용하여 Request 의 Body 그리고 결과값으로 return 되는 값을 DB History 테이블에 저장하는 기능이었다.

해당 기능을 Request Body를 DB에 저장하기 위해 Advisor 클래스 @Before에서 읽어야 했고 구현하였고, Controller 에서 또한 요청 정보를 확인하기 위해 Request Body를 읽어야 했다. 하지만 코드를 작성한 후 테스트한 결과 Body를 읽을 수 없다는 Exception을 만나게 되었다.

구글링을 한 결과 애초에 Request Body 는 한 번만 읽어진다는 사실을 확인하였다. 해당 부분을 해결하기 위해선 HttpRequest 객체 자체를 Wrapper 객체로 감싸서 HttpRequest 객체 자체를 새로 등록하는 형태로 해결하는 방법이 있다는 내용들이었다. Wrapper 객체에 읽었던 Body 내용을 캐싱 할 수 있게 저장해서 사용하는 방법이었다.

해당 방법으로 해결하고자 하였으나 이사님과 상의한 결과 Request 객체 자체를 새로 등록하는 방법은 시스템 전체에 변경을 주는 방법이다 보니 위험하다는 의견을 주셨다. 고민 끝에 결국 내가 택한 방법은 Request 자체에 Body 읽은 값을 다시 setParameter 하여 저장하는 방법이었고, 문제 없이 해당 기능 구현을 완료할 수 있었다.

+ Recent posts