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

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


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

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

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

코딩테스트 문제를 풀 다 시간 초과를 해결했던 경험에 대한 기록이다.

백준의 텀프로젝트 문제를 풀던 중(https://www.acmicpc.net/problem/9466)
내가 작성한 코드가 충분히 최적화 되었다고 생각했음에도 계속 시간 초과가 발생하였다.
관련하여 문제를 찾던 중 자바의 배열 생성이 시간 초과의 원인이 될 수 있다는 글을 발견하고, 해당 부분을 수정하여 통과하였다.


    private static int solution() throws IOException {
        int studentNum = Integer.parseInt(br.readLine());
        int[] team = new int[studentNum + 1];
        String[] input = br.readLine().split(" ");
        for(int i = 1; i <= studentNum; i++){
            team[i] = Integer.parseInt(input[i-1]);
        }

        checked = new boolean[studentNum + 1];
        result = studentNum;

        for(int i = 1; i <= studentNum; i++){
            if(checked[i]) continue;
            // 배열 초기화
            visited = new int[studentNum + 1];
            findTeam(team, i, 1, visited);
        }

        return result;
    }

    private static void findTeam(int[] team, int student, int seq, int[] visited){
        if(checked[student]) return;
        checked[student] = true;
        visited[student] = seq;

        int next = team[student];
        if(visited[next] != 0){
            result -= (seq - visited[next] + 1);
        }else{
            findTeam(team, next, seq + 1, visited);
        }
    }

 

시간 초과가 나던 시점의 내 코드는 위와 같았으며, 완전 탐색을 위하여 탐색 방문 배열을 new 명령어로 생성하고 있었다. 해당 배열의 크기는 최대 100001의 크기를 갖는 문제이다.

 

    private static int solution() throws IOException {
        int studentNum = Integer.parseInt(br.readLine());
        int[] team = new int[studentNum + 1];
        StringTokenizer st = new StringTokenizer(br.readLine());
        for(int i = 1; i <= studentNum; i++){
            team[i] = Integer.parseInt(st.nextToken());
        }

        checked = new boolean[studentNum + 1];
        int[] visited = new int[studentNum + 1];
        result = studentNum;
        for(int i = 1; i <= studentNum; i++){
            if(checked[i]) continue;
            findTeam(team, i, 0, visited);
        }

        return result;
    }

    private static void findTeam(int[] team, int student, int seq, int[] visited){
        if(checked[student]) return;
        seq++;
        checked[student] = true;
        visited[student] = seq;

        int next = team[student];
        if(visited[next] != 0){
            result -= (seq - visited[next] + 1);
        }else{
            findTeam(team, next, seq, visited);
        }

        // dfs 내부에서 사용후 값 원상복구
        visited[student] = 0;
    }

}

 

시간 초과를 해결한 코드는 위와 같다. dfs를 반복하기 이전에 생성한 배열의 값을 new 가 아닌 직접 초기화 하여 배열을 사용하였다. 참고한 글(https://okky.kr/questions/1450047)에 따르면 배열을 생성한다는 것은 새로운 객체의 메모리에 할당 받는 부분, java의 경우 해당 배열의 초기 값을 초기화하는 부분 등으로 인하여 런타임 실행 시간이 늘어날 수 있다고 한다. 단순한 코드의 차이였지만 객체 생성의 효율에 대해 고민할 수 있었다.

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

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

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

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

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

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

코테에서 자주 쓰이는 알고리즘 기본형 With GPT

DFS

import java.util.*;

public class DFS {
    public static void dfs(int v, boolean[] visited, List<List<Integer>> graph) {
        visited[v] = true;
        System.out.print(v + " ");
        for (int i : graph.get(v)) {
            if (!visited[i]) {
                dfs(i, visited, graph);
            }
        }
    }

    public static void main(String[] args) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            graph.add(new ArrayList<>());
        }
        graph.get(0).add(1);
        graph.get(0).add(2);
        graph.get(1).add(0);
        graph.get(1).add(3);
        graph.get(2).add(0);
        graph.get(2).add(4);
        graph.get(3).add(1);
        graph.get(4).add(2);

        boolean[] visited = new boolean[5];
        dfs(0, visited, graph);
    }
}

BFS

import java.util.*;

public class BFS {
    public static void bfs(int start, List<List<Integer>> graph) {
        boolean[] visited = new boolean[graph.size()];
        Queue<Integer> queue = new LinkedList<>();
        queue.add(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int v = queue.poll();
            System.out.print(v + " ");
            for (int i : graph.get(v)) {
                if (!visited[i]) {
                    visited[i] = true;
                    queue.add(i);
                }
            }
        }
    }

    public static void main(String[] args) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            graph.add(new ArrayList<>());
        }
        graph.get(0).add(1);
        graph.get(0).add(2);
        graph.get(1).add(0);
        graph.get(1).add(3);
        graph.get(2).add(0);
        graph.get(2).add(4);
        graph.get(3).add(1);
        graph.get(4).add(2);

        bfs(0, graph);
    }
}

다익스트라

import java.util.*;

public class Dijkstra {
    static class Node implements Comparable<Node> {
        int vertex, weight;
        Node(int v, int w) {
            this.vertex = v;
            this.weight = w;
        }

        public int compareTo(Node other) {
            return this.weight - other.weight;
        }
    }

    public static void dijkstra(int start, List<List<Node>> graph) {
        int n = graph.size();
        int[] dist = new int[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[start] = 0;

        PriorityQueue<Node> pq = new PriorityQueue<>();
        pq.add(new Node(start, 0));

        while (!pq.isEmpty()) {
            Node node = pq.poll();
            int v = node.vertex;
            int w = node.weight;

            for (Node neighbor : graph.get(v)) {
                int newDist = w + neighbor.weight;
                if (newDist < dist[neighbor.vertex]) {
                    dist[neighbor.vertex] = newDist;
                    pq.add(new Node(neighbor.vertex, newDist));
                }
            }
        }

        System.out.println(Arrays.toString(dist));
    }

    public static void main(String[] args) {
        List<List<Node>> graph = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            graph.add(new ArrayList<>());
        }
        graph.get(0).add(new Node(1, 10));
        graph.get(0).add(new Node(2, 3));
        graph.get(1).add(new Node(2, 1));
        graph.get(1).add(new Node(3, 2));
        graph.get(2).add(new Node(3, 8));
        graph.get(3).add(new Node(4, 7));

        dijkstra(0, graph);
    }
}

플로이드워셜

import java.util.*;

public class FloydWarshall {
    public static void floydWarshall(int[][] graph) {
        int n = graph.length;
        int[][] dist = new int[n][n];

        for (int i = 0; i < n; i++) {
            System.arraycopy(graph[i], 0, dist[i], 0, n);
        }

        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (dist[i][k] + dist[k][j] < dist[i][j]) {
                        dist[i][j] = dist[i][k] + dist[k][j];
                    }
                }
            }
        }

        for (int[] row : dist) {
            System.out.println(Arrays.toString(row));
        }
    }

    public static void main(String[] args) {
        int INF = 99999;
        int[][] graph = {
            {0, 3, INF, 5},
            {2, 0, INF, 4},
            {INF, 1, 0, INF},
            {INF, INF, 2, 0}
        };

        floydWarshall(graph);
    }
}

벨만포드

import java.util.*;

public class BellmanFord {
    public static void bellmanFord(int[][] edges, int V, int start) {
        int[] dist = new int[V];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[start] = 0;

        for (int i = 0; i < V - 1; i++) {
            for (int[] edge : edges) {
                int u = edge[0], v = edge[1], w = edge[2];
                if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                }
            }
        }

        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], w = edge[2];
            if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) {
                System.out.println("Negative weight cycle detected");
                return;
            }
        }

        System.out.println(Arrays.toString(dist));
    }

    public static void main(String[] args) {
        int V = 5;
        int[][] edges = {
            {0, 1, -1}, {0, 2, 4},
            {1, 2, 3}, {1, 3, 2}, {1, 4, 2},
            {3, 2, 5}, {3, 1, 1}, {4, 3, -3}
        };

        bellmanFord(edges, V, 0);
    }
}

개인 포트폴리오용 서버를 Docker + Nginx + Vue + SpringBoot 환경으로 배포를 시도하고 있는 중이다.
수많은 문제점들과 SSL 인증서까지 다 적용에 간신히 성공했다. 수많은 트러블 중 만났던 하나의 트러블에 대한 해결방법을 남긴다.

 

SSL 인증서를 발급 받은 이후 docker nginx 에서 도메인으로 들어오는 http(80포트) 에 대해 https(443포트)로 리다이렉트 하도록 설정해 두었다. Frontend -> Backend API 요청은 문제없이 리다이렉트 되어서 해결되는 듯 하였지만 스웨거 페이지가 문제였다.

 

어째서인지 Swagger UI 에서의 요청은 Spring 에 닿지조차 못하였다. 그러던 중 요청시 https 페이지임에도 불구하고 curl 명령어가 http로 뜨는것을 확인했다. Swagger UI 에서는 curl 명령어로 실행해서 요청을 호출하는 것 같았다. 해당 명령어를 실행한 결과 Spring이 아니라 nginx 에서 Redirect 했다는 내용이 나오는 것을 확인했다.

 

아마 Swagger에서의 요청은 리다이렉트된 Spring 까지 도달하지 못하였던듯 하다. 관련해서 찾아보다 설정으로 처리 하는 방법을 발견하여 적용하였고 해결하였다.

 

server.forward-headers-strategy=framework

 

위와같은 설정을 application.properties로 추가하니 요청이 정상적으로 도달하였다.
해당 설정을 넣기전에는 스웨거의 curl 명령어가 https 라는 헤더정보가 누락된었던 듯 하며, 해당 설정 이후 정보들이 제대로 전달되어, swagger 의 curl 요청이 정상적으로 변경된듯 하다

Minikube

# minikube 상태확인  
minikube status  

# minikube 클러스터 삭제
minikube delete

# minikube 클러스터 생성
minikube start --driver=${가상머신}  

# minikube 서비스 접근
minikube service ${deployment-name}

Kubectl

# deployment 생성, 가상머신이므로 로컬 image가 아닌 hub repository 이용
kubectl create deployment ${deployment-name} --image=${remote-image} 

# deployment 제거
kubectl delete deployment ${deployment-name}

# deployment check
kubectl get deployments

# pod check
kubectl get pods

# LoadBalancer 이용하여 port 노출 및 service 생성  
kubectl expose deployment ${deployment-name} --type=LoadBalancer  --port=8080

# service check
kubectl get services

# service 접근  
minikube service ${deployment-name}  

# 다중 컨테이너 pods 실행  
kubectl scale deployment/${deplyment-name} --replicas=3

# 기존 deployment 컨테이너 새로운 이미지 설정  
kubectl set images deployment/${deployment-name} ${before-image-name}=${new-image-docker-hub-repository}

# deployment update 확인  
kubectl rollout status deployment/${deployment-name}  

# deployment history  
kubectl rollout history deployment/${deployment-name}  
kubectl rollout history deployment/${deployment-name} --revision=${revision}  

# deployment rollback  
# 이전  
kubectl rollout undo deployment/${deployment-name}  
# 특정 revision
kubectl rollout undo deployment/${deployment-name} --to-revision=${revision}  

# service 제거  
# kubectl delete service ${deployment-name}

# deployment 제거  
kubectl delete deployment ${deployment-name}

 

선언형 파일 이용
```shell
kubectl apply -f ${file-name}.yaml
```

# 선언형 파일 적용
kubectl apply -f ${file-name}.yaml

 

'Notes > Kubernetes' 카테고리의 다른 글

Window, WSL kubectl config 공유하기  (0) 2024.07.27

Window 에서 kubectl 을 설치 후 WSL 에서 클러스터 사용을 공유하기 위해 config 파일을 공유하는 방법


# wsl Linux 에 kubectl 설치
# https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"

# kubectl 실행 권한 설정
chmod +x /kubectl

# 설정파일 저장 디렉토리 생성
mkdir -p ~/.kube

# 윈도우 설정파일 링크
ln -sf "/mnt/c/users/$windowsUser/.kube/config" ~/.kube/config

'Notes > Kubernetes' 카테고리의 다른 글

쿠버네티스 명령어(kubernetes command)  (0) 2024.07.27


프로젝트에 알림 기능이 요구 사항이 생겨 기능 구현을 하고 있었다.
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 하면 안된다.

+ Recent posts