최근 운영 중이던 서버에서 데드락으로 인해 결제가 되지 않는 이슈가 발생하였다.
트랜잭션, 데드락 등의 개념은 항상 이해했다고 생각하면서도 문제 발생 시 바로 원인과 해결법이 떠오르지 않는 개념이라 생각한다. 이번 이슈에 있었던 내용과 함께 새로이 알게 된 내용을 정리해 보려고 한다.

 

데드락은 서로 다른 트랜잭션이 있을때 각 트랜잭션에서 점유된 내용을 대기할 때 교착상태에 이르는 경우라고 할 수 있다.
임의의 테이블 ORDER 테이블이 있다고 가정하자 이때 PK 는 ORDER_ID라고 하겠다
1번 트랜잭션과 2번 트랜잭션이 아래와 같은 상황일 때 데드락은 발생한다.

-- 1번 트랜잭션: ORDER_ID = 1 인 ROW에 대한 쓰기락
UPDATE ORDER
   SET ORDER_STATE = '신규값'
 WHERE ORDER_ID = 1;

                    -- 2번 트랜잭션: ORDER_ID = 2 인 row 에 대한 쓰기락
                    UPDATE ORDER
                       SET ORDER_STATE = '신규값'
                     WHERE ORDER_ID = 2;

-- 1번 트랜잭션: ORDER_ID = 2인 ROW에 대해 접근하나 2번 트랜잭션의 락으로 인한 대기
UPDATE ORDER
   SET ORDER_STATE = '신규값'
 WHERE ORDER_ID = 2;

                    -- 2번 트랜잭션: ORDER_ID = 1인 ROW에 대해 접근하나 
                    --              1번 트랜잭션의 락으로 인한 대기
                    UPDATE ORDER
                       SET ORDER_STATE = '신규값'
                     WHERE ORDER_ID = 1;

 

위의 예시처럼 트랜잭션에서 데이터에 수정이 이루어지면 row 에 lock 이 걸리기 때문에 다른 트랜잭션은 대기하게 되고 이러한 상황이 맞물리며 트랜잭션간 교착상태에 걸릴 경우 락이 걸리게 되는것이다.

이 ORDER 테이블에 ORDER_NO라는 유니크한 값이 있다고 할 때 해당 컬럼에 유니크 키 설정이 안되어있고, 아래와 같은 상황이 발생한 것이 운영중이던 서버에서 발생했던 락 상황이다.

-- 1번 트랜잭션: ORDER_ID = 1 인 ROW에 대한 쓰기락
UPDATE ORDER
   SET ORDER_STATE = '신규값'
 WHERE ORDER_ID = 1;

                    -- 2번 트랜잭션: ORDER_ID = 2 인 row 에 대한 쓰기락
                    UPDATE ORDER
                       SET ORDER_STATE = '신규값'
                     WHERE ORDER_ID = 2;

-- 1번 트랜잭션: ORDER_NO = 1 은 ORDER_ID = 1 인 row 와 같은 하나의 row
UPDATE ORDER
   SET ORDER_STATE = '신규값'
 WHERE ORDER_NO = 1;

                    -- 2번 트랜잭션: ORDER_NO = 2는 
                    --             ORDER_ID = 2인 row와 같은 하나의 row
                    UPDATE ORDER
                       SET ORDER_STATE = '신규값'
                     WHERE ORDER_NO = 2;

 

얼핏 보면 문제 없을 것 같은 상황이다. 1번 트랜잭션은 1번 row 에만, 2번 트랜잭션은 2번 row 에만 접근하고 있기에 서로의 트랜잭션에 영향을 주지 않을 것 같이 예상된다. 당시 운영 중이던 서버에서 발생한 상황도 위와 같았음에도 불구하고 트랜잭션 락에 걸리게 된다. 락에 걸리게 된 이유는 제목에 있는 내용처럼 유니크 키를 설정할 경우에 해결되는 상황이었다.

 

락에 걸린 상황에 대해 설명해보겠다.

각 업데이트 문을 1 ~ 4번 업데이트라고 순서대로 칭하겠다.

1 ~ 2번 업데이트는 평범한 락 획득 과정이다. 3번 업데이트가 발생할 때 무슨 일이 발생할까?

우선 ORDER 테이블의 ORDER_NO = 1 인 값을 테이블에서 찾아야 할 것이다. ORDER_NO는 유니크 키가 아니므로 해당 row 를 찾기 위해 인덱스 스캔이 아닌 풀스캔이 발생할 것이다. ORDER_NO = 1 인 값을 찾기 위해 테이블 전체를 스캔하게 될 것이고 2번 트랜잭션에서 ORDER_ID = 1 인 row에 락을 걸어두었기 때문에 1번 트랜잭션은 대기에 빠지게 된다. 같은 경우로 4번 업데이트 문도 대기에 빠지게 되며 두 트랜잭션은 데드락에 걸리게 되는 것이다.

 

하지만 유니크 키를 걸게 되면 어떻게 될까?
3번 업데이트문과 4번 업데이트문은 풀스캔이 아닌 인덱스 스캔을 하게 될 것이고 해당 row 에 락을 한 것은 동일 트랜잭션이므로 스캔하는 것에 문제가 없게 되고, 두 트랜잭션은 독립적으로 처리되어 데드락 문제가 해결되게 된다.

업데이트 문을 간단히 써두었지만 실제 운영하던 서버 코드에서는 해당 원인을 찾는게 쉽지 않았다.
락이 걸린 순간의 업데이트 문과 연관된 테이블을 체크한 이후 해당 테이블에 관한 트랜잭션의 로직들을 확인하면서 의구심이 가는 부분을 확인했고, 그것이 맞았다.

 

해당 내용에 대해 실제 체크하기 위하여 임시 테이블을 만들고 사용하는 DB 툴인 DBeaver 에서 두개의 세션 스크립트를 열어두고 오토 커밋이 아닌 메뉴얼 커밋을 이용하여 테스트를 진행하였다. 그 결과 위와 같은 원인을 찾을 수 있었다.

 

인덱스, 제약 조건 등은 데이터의 신뢰성 및 DB의 성능을 위해서는 잘 처리해두어야 하지만 가끔 초기 설계 시 누락되거나, 프로젝트 진행 중에 추가된 컬럼들로 인해 올바르게 정리가 되어있지 않을 때가 존재한다. 하지만 이처럼 올바르게 정의되어지지 않은 테이블을 이와 같은 예상치 못한 문제가 발생할 가능성이 있다는 것을 다시 한 번 생각하게 되었다.

유니크 키를 설정한 해당 서버는 현재까지 데드락 이슈가 발생하지 않고 있다.

프로젝트에서 Postgresql Fdw를 이용하면서 발생했떤 성능 이슈를 해결했던 경험을 기록에 남긴다.

 

Fdw(Foreign Data Wrapper) 테이블이란?

FDW는 즉, PostgreSQL 데이터베이스 외부의 다른 데이터베이스나 파일 시스템 등에 있는 데이터를 마치 로컬 테이블처럼 접근하고 사용할 수 있게 해주는 기술이다. 외부 데이터베이스에 바로 접근 할 수 있으므로 별도의 인터페이스 정의가 필요 없이 쉽게 데이터를 공유할 수 있는 방법이다.

프로젝트 내에서 성능 문제가 발생했던 내용은 엑셀 파일 다운을 위한 데이터 조회 부분에서 발생했다. 조회해야 하는 데이터가 ERP 시스템의 Fdw 테이블과 프로젝트 테이블을 Join이 필요했다. 조회 쿼리를 완성한 이후 테스트 결과 특정 데이터의 경우 약 2분 가까이 걸리는 것을 확인했다. 최종적으로 조회 되는 데이터는 200 row 정도로 그리 많치 않음에도 시간이 많이 소요되었다.

관련해서 fdw에 대해 찾아보던 중 아래와 같은 글들을 확인했다.

 

1. Fdw Join Performance Tip

https://www.crunchydata.com/blog/performance-tips-for-postgres-fdw

 

Performance Tips for Postgres FDW | Crunchy Data Blog

Chris has some tips on helping you get the most out of queries when working with a remote Postgres foreign data wrapper. He has an overview of how queries are executed with the fdw and then samples for using CTEs, sub-queries, materialized views, and more.

www.crunchydata.com

 

2. 배민 postgres fdw 마이그레이션

https://techblog.woowahan.com/20371/

 

postgres_fdw로 마이그레이션 생산성 높이기 | 우아한형제들 기술블로그

배달의민족 커머스 서비스들의 ‘상품’ 도메인 시스템 통합 프로젝트를 하며 postgres_fdw를 이용해 PostgreSQL DB 간의 별도 배치 애플리케이션 없이 마이그레이션을 경험했던 실제 사례 및 결과와

techblog.woowahan.com

 

 

1번 링크에 대해서 요약하자면 Fdw 와의 Join 을 수행할 경우 일정량 row의 cursor 단위로 원격데이터베이스에서 데이터를 받아오기 때문에 성능이 떨어진다는 내용과 함께 바로 Join 하는 형식이 아닌 다른 방법들을 추천 해주고 있다. 내용의 핵심은 Fdw 테이블에서 조회하는 내용을 현재 데이터베이스의 공간으로 옮긴 이후 Join 을 해야 한다는 내용이다. 그렇지 않으면 Fetch Count 단위로 반복된 cursor 조회가 발생할 것이고, 이로 인행 성능이 떨어지기 때문이다.

2번 링크는 배민의 기술 블로그에서 Fdw 를 이용해서 마이그레이션시 사용한 방법에 대한 내용이다. Fdw 테이블의 데이터를 로컬테이블로 옮긴 이후에 Join 문을 수행하여 1번 링크에서 설명하는 문제점을 해결하는 내용을 확인하였다.

위 내용들을 확인하고 현재 프로젝트의 이슈를 해결해보았다. 우선 1번 링크의 소개된 내용 중 하나인 With 절의 CTE 테이블을 이용하여 일종의 로컬 메모리에 데이터를 올리는 방식을 참고했다. 그러나 어째서 인지 성능은 변화되지 않았다. 내 방식에 문제가 있었는지는 모르겠지만 With 절을 이용한 방식은 효과가 없어 2번 링크에서 보았던 부분을 참고하여 임시 테이블에 저장하는 방식을 이용해보았다.

결과는 성공적이었다. 약 2분(120초) 가까이 소요되던 쿼리 시간은 1초 이내로 줄어들었다. 성능 향상은 상당히 만족스러웠으면 Postgresql의 Fdw 가 동작하는 방식에 대해서 고민하고 알 수 있는 기회였다.

요약: Local Table 과 Fdw Table Join시 성능 이슈 발생 시, 별도의 로컬 테이블에 임시 저장한 이후 Local Table 간의 Join 으로 변경 시 성능을 향상 시킬 수 있다.

최근 프로젝트에서 Oracle 프로시저 기반의 프로젝트를 Postgresql 로 변환하는 작업을 진행하고 있다.

Aws Schema Conversion Tool를 이용하여 1차 변환 후 문제가 없는지 확인하며 추가 변환작업을 진행하는 형태로 진행하였다. 변환 작업을 하면서 많이 느꼈던 부분은 PostgreSQL의 경우 "데이터 타입"에 많은 신경을 써야한다는 것이었다.

 

변환 작업을 하면서 변환 시 체크해야 하는 상황들을 일부 정리해본다.

 
1. CURSOR 이용시 RECORD 타입 선언 필요
FOR V_RECORD IN SOME_CURSOR
LOOP
    ~~~ V_RECROD 를 이용
END LOOP;

 

위와 같은 커서 문을 작성한다고 하면
Oracle은 V_RECORD 라는 변수를 바로 참조하여 사용할 수 있지만
PostgreSQL의 경우 V_RECORD라는 변수를 DECLARE 문에 RECORD 타입으로 미리 선언해주어야 했다.

 
2. FROM DUAL

테이블의 컬럼을 바라보지 않는 Select 문 사용 시
Oracle 은 FROM DUAL 문법을 뒤에 붙여주어야 하지만
PostgreSQL 의 경우 필요하지 않았다.

 
3. 파이프라인을 이용한 문자열 합치기
# Oracle Case
SELECT 'a' || NULL || 'b' FROM DUAL; -- 결과: 'ab'

# Postgresql Case
SELECT 'a' || NULL || 'b'; -- 결과: NULL

 

파이프(||)를 이용한 문자열 합치기를 사용할 경우 Oracle 의 경우 NULL 값을 공백으로 처리하여 합쳐진다. 이는 Oracle 의 경우 공백을 NULL 로 판정하기 때문에 발생하는 결과인 것으로 예상된다.
조회 관련 쿼리에서 '%' 를 문자열의 앞뒤로 붙여 LIKE 검색을 할 경우 파이프(||)를 이용할 시 Oracle과 PostgreSQL의 결과가 완전히 달라지므로 주의해야 한다. 변환 작업 시 CONCAT_WS를 이용하였다.

 
4. 현재 날짜 및 시간

Oracle 의 경우 SYSDATE,
PostgreSQL 의 경우 NOW()를 이용하여 현재 시간을 불러올 수 있다.

 
5. 날짜 연산

Oracle 의 경우 '+', '-' 연산을 이용하여 바로 날짜 계산이 가능하지만,
Postgresql 의 아래와 같은 문법으로 타입을 명시해야한다.

# Oracle Case
SELECT SYSDATE + 1 FROM DUAL;

# Postgresql Case
SELECT NOW() + '1 days'::INTERVAL; -- INTERVAL 타입으로 연산해야한다.
 
6. 데이터 수 제한 검색 문법 및 ORDER BY 주의점

Oracle의 경우 ROWNUM 을 WHERE 절에서 비교하여 SELECT 문의 조회 건수를 제한할 수 있으며
PostgreSQL의 경우 LIMIT 문법을 이용하여 제한하여 조회할 수 있다.

이 때 ORDER BY 절이 포함된 SELECT 문이라면 변환 시 주의해야 한다.
Oracle의 ROWNUM은 ORDER BY 로 정렬되기 전에 주어지고 WHERE 절에서 제한한다면,
PosgreSQL의 LIMIT의 경우 ORDER BY 된 상태에서 LIMIT 조회가 적용되기 때문이다.
Oracle에서 PostgreSQL로 변환 시에는 인라인뷰에서 ORDER BY 하여 SELECT 문을 작성한 이후 LIMIT 를 걸어야 했다.

# Oracle Case
SELECT *
  FROM A_TABLE A
 WHERE ROWNUM <= 10;
 ORDER BY A.A_COLUMN

# PostgreSQL
SELECT *
  FROM (SELECT *
          FROM A_TABLE 
         ORDER BY A_COLUMN) A
 LIMIT 10;
 
7. Alias

Oracle 의 경우 인라인뷰에는 Alias 가 필수가 아니지만
PostgreSQL의 경우 인라인뷰에 Alias 가 필수다.

Oracle의 경우 MERGE, UPDATE, INSERT 문 등에서도 테이블에 Alias를 붙이는 것에 제한사항이 없다.
PostgeSQL의 경우 변경되는 기준이 되는 컬럼에는 Alias를 붙이면 안된다.

# Oracle Case
UPDATE A_TABE A
   SET A.A_COLUMN = 'NEW_A';Z

# PostgreSQL
# A.ACOLUMN 이런식으로 쓰면 안된다. 우항에서 값을 참조할때는 Alias 를 사용 가능하다.
UPDATE A_TABE A
   SET A_COLUMN = 'NEW_A';
 
8. 재귀 쿼리

Oracle 의 경우 CONNECT BY LEVEL 문법이 존재하여 재귀 쿼리를 짧게 작성할 수 있다.
PostgreSQL 의 경우 WITH RECURSIVE 문을 이용하여 재귀 쿼리를 작성 해야한다. Oracle에 비하여 쿼리 길이가 길어지며 가독성이 떨어진다고 느꼈다. 쿼리 작성 난이도도 상승한다.

 
9. SELECT 'STRICT' INTO

Oracle 에서 변수에 값을 담기 위하여 SELECT INTO 문법 사용 시 조회 되는 값이 없을 경우 NO DATA EXCEPTION 이 발생한다.
PostgreSQL의 경우 SELECT INTO 문법 사용 시 조회 되는 값이 없어도 EXCEPTION이 발생하지 않는다.

Oracle -> PostgreSQL 변환 시 Oracle과 같은 형태로 Exception 발생을 위해선 SELECT INTO STRICT를 사용하여야 한다.

 
10. Mybatis 관련

Spring Mybatis 에서 Function 을 호출하여 CURSOR 결과를 리턴 받을 시 jdbcType 설정
Oracle의 경우 "CURSOR" 타입으로 설정한 후 CALL 값을 등호로 받는다.
PostgreSQL의 경우 "OTHER" 타입으로 설정한 후 Function의 추가 파라미터로 넣은 후 CALL 한다.

 
11. 프로시져 실행 시 CASE WHEN 문의 오류 검사

변환 작업 시 CASE WHEN 문을 이용하여 잘못된 결과일 경우 1/0 값을 결과로 하여 강제로 오류를 발생 시키는 로직이 있었다.

Oracle 의 경우 실제 CASE WHEN 문에서 조건에 해당할 경우 DIVISION BY ZERO 오류가 발생하여 원하는 대로 컨트롤 할 수 있지만.
PostgreSQL의 경우 CASE WHEN 조건에 걸리지 않더라도 바로 DIVISION BY ZERO가 발생하였다. 아마도 PostgreSQL 에선 CASE WHEN 문의 결과 값을 미리 체크하는 형태로 동작하는 것으로 생각되었다. 하지만 1/0의 값을 CASE WHEN 문에 바로 작성하지 않고 별도의 Function 에서 리턴하는 형태로 작성할 경우 오류가 발생하지 않고 조건에 걸리는 경우에만 발생하였다. 따라서 기존 Oracle 프로그램과 같은 형태로 변환하기 위하여 DIVISION BY ZERO 를 리턴할 수 있는 별도의 Function으로 처리하기로 하였다.

 

 

12. ROW 위치를 지정하는 논리적 주소 값

Oracle의 경우 Row 마다 위치를 지정하는 물리적 주소 값 ROWID 가 존재한다. UPDATE 시에도 변하지 않는다.
PostgreSQL의 경우 Row 마다 위치를 지정하는 물리적 주소 값 CTID 가 존재한다. UPDATE 시에는 값이 변한다.

Oracle 에서 UPDATE 로직이 있는 ROWID 와 관련된 값을 PosgreSQL로 변환하기 위해선 별도의 Unique 값을 채번해서 사용하여야 한다. 만약 UPDATE 가 없는 경우라면 CTID 값으로 변환하여 적용하여도 문제는 없을 것으로 판단된다.

자바스크립트를 브라우저에서 디버깅 할 때 "debugger"를 코드에 넣어 중단점을 설정할 수 있다.
그러나 평범한 html, js 기반이 아닌 프레임워크를 사용할 경우 해당 기능이 브라우저에서 안되는 경우가 있다. 이 경우 안되는 이유는 IDE 문제나 코드의 문제가 아니라 브라우저 설정의 문제이다.

크롬의 경우 
개발자도구(F12) => 설정(톱니바퀴) -> 무시목록(Ignore list) 설정
에서 해당 js 파일의 경로의 정규식을 등록하거나, 무시 옵션을 잠시 끄고 디버깅을 맞춘 후 원상복구 하면 된다. 

해당 옵션이 왜 존재하는지 검색한 결과 라이브러리 등의 코드 내부에 "debugger" 코드가 존재하여 원하지 않는 중단점에서 멈추는 경우들을 방지하는 것으로 확인하였다.

도커 환경에서 배포 + 폰트가 필요한 라이브러리 사용 시 "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"의 형태일 경우 하이픈을 포함한 버젼을 지워보자

개인 포트폴리오용 서버를 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 요청이 정상적으로 변경된듯 하다

+ Recent posts