반응형

개발서버로 쓰던 CentOS 가 지원중단이 되면서 RockyLinux로 OS를 변경하게 되었다.

SVN 도 이관대상이기 때문에 이전글에서 개인 놋북에 Docker로 RockyLinux를 설치 후 SVN 설정까지 모두 완료했었다.

[RockyLinux] svn 설치 및 실행, 연동까지. - https://kimfish.co.kr/359

 

[RockyLinux] svn 설치 및 실행, 연동까지.

RockyLinux에 SVN을 이관설치할 일이 생겼다. 아직 플젝 투입 전이라 외부에서 겸사겸사 테스트해보느라 쓰는 글.대충 환경은 다음과 같다. Windows PC에 docker로 RockyLinux8.10을 올리고, 여기에 svn을 설치

kimfish.co.kr

 

이번에는 이론상으로 생각해보던 SVN Repository를 tar 압축 후, 신규 서버에서 압축해제 했을 때 정상동작 하는지를 테스트 해보고자 한다. 스포일러를 조금 하자면, 같은 버전이라면 그냥 뭐 된다. 예상했던 결과이긴 하지만.

다른 버전의 경우에는 어디까지 호환되는지 궁금하긴 한데, 지금 이관 규칙 자체가 최대한 같은 버전이기 때문에 큰 문제의 여지는 없다고 판단된다.

 

1. SVN 저장소 경로 확인 및 tar 압축

리눅스 SVN 저장소는 저장소 디렉토리 하위에 SVN 설정들이 포함되어있는 형태로 구성된다. 이걸 통째로 압축하고, 신규 서버에서 동일하게 압축해제하면 될거라는 판단이 든다. 저장소 디렉토리를 확인 한 후, tar 압축을 수행한다.

[root@93e37f9f73ea /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv           svn_repository  testRepo  usr
dev  home  lib64  media       opt  root  sbin  svn_repo.tar  sys             tmp       var
[root@93e37f9f73ea /]# cd svn_repository
[root@93e37f9f73ea svn_repository]#
[root@93e37f9f73ea svn_repository]# ls
README.txt  conf  db  format  hooks  locks  svnserve.log
[root@93e37f9f73ea svn_repository]#  tar -cvf svn_repo.tar svn_repository

 

2. 신규 서버에 tar 압축해제 및 구동

svn_repo.tar 파일을 신규 서버에 업로드 후, tar 압축해제를 수행한다.

[root@82559cbe0973 /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv           sys  usr
dev  home  lib64  media       opt  root  sbin  svn_repo.tar  tmp  var
[root@82559cbe0973 /]# tar -xvf svn_repo.tar
svn_repository/
svn_repository/locks/
svn_repository/locks/db-logs.lock
(중략)
[root@82559cbe0973 /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv           svn_repository  tmp  var
dev  home  lib64  media       opt  root  sbin  svn_repo.tar  sys             usr

 

이제 svn 구동을 수행한다.

[root@82559cbe0973 /]# svnserve -d -r /svn_repository --listen-port 18081 --log-file /svn_repository/svnserve.log

 

3. Tortoise SVN Repo-Browser 확인

Tortoise SVN 으로 접속해서 기존 서버에서 생성한 사용자계정 및 Commit 된 이력을 확인한다.

잘 들어와 있넹.

 

4. 결론

그냥 뭐 잘 된다... 문제는 현재 기동중인 SVN 서버의 저장소 경로가 어디냐는 것이긴 한데, 이걸 파악하는게 먼저일것 같기는 하다.

사실 생각해보면 당연한게, SVN 설정의 모든부분(계정/Commit파일/메세지/권한/설정 등)은 SVN 저장소 경로의 하위에 모두 담겨있기 때문이다. 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

RockyLinux에 SVN을 이관설치할 일이 생겼다. 아직 플젝 투입 전이라 외부에서 겸사겸사 테스트해보느라 쓰는 글.

대충 환경은 다음과 같다. Windows PC에 docker로 RockyLinux8.10을 올리고, 여기에 svn을 설치해서 Windows에서 테스트 해보는 식으로 진행해보았다.

기본적인 명령어는 생략하고, SVN 위주로 작성해본다.

 

Windows 11 Pro
Docker - RockyLinux 8.10
JDK - OpenJDK 1.8

 

1. SVN 설치

yum install -y subversion

[root@82559cbe0973 /]# yum install -y subversion
Rocky Linux 8 - AppStream                                             5.8 kB/s | 4.3 kB     00:00
Rocky Linux 8 - AppStream                                             7.5 MB/s |  11 MB     00:01
Rocky Linux 8 - BaseOS                                                7.8 kB/s | 4.3 kB     00:00
Rocky Linux 8 - BaseOS                                                5.7 MB/s | 6.0 MB     00:01
Rocky Linux 8 - Extras                                                4.7 kB/s | 3.1 kB     00:00
Rocky Linux 8 - Extras                                                 22 kB/s |  14 kB     00:00
Dependencies resolved.
======================================================================================================
 Package               Arch        Version                                       Repository      Size
======================================================================================================
Installing:
 subversion            x86_64      1.10.2-5.module+el8.7.0+1065+42200b2e         appstream      1.1 M
Installing dependencies:
 apr                   x86_64      1.6.3-12.el8                                  appstream      128 k
 apr-util              x86_64      1.6.1-9.el8                                   appstream      105 k
 libserf               x86_64      1.3.9-9.module+el8.7.0+1065+42200b2e          appstream       59 k
 subversion-libs       x86_64      1.10.2-5.module+el8.7.0+1065+42200b2e         appstream      1.5 M
 utf8proc              x86_64      2.6.1-3.module+el8.7.0+1065+42200b2e          appstream       72 k
Installing weak dependencies:
 apr-util-bdb          x86_64      1.6.1-9.el8                                   appstream       24 k
 apr-util-openssl      x86_64      1.6.1-9.el8                                   appstream       26 k
Enabling module streams:
 subversion                        1.10

Transaction Summary
======================================================================================================
Install  8 Packages

Total download size: 3.0 M
Installed size: 10 M
Downloading Packages:
(1/8): apr-util-1.6.1-9.el8.x86_64.rpm                                1.4 MB/s | 105 kB     00:00
(2/8): apr-util-openssl-1.6.1-9.el8.x86_64.rpm                        985 kB/s |  26 kB     00:00
(3/8): apr-util-bdb-1.6.1-9.el8.x86_64.rpm                            186 kB/s |  24 kB     00:00
(4/8): libserf-1.3.9-9.module+el8.7.0+1065+42200b2e.x86_64.rpm        1.7 MB/s |  59 kB     00:00
(5/8): apr-1.6.3-12.el8.x86_64.rpm                                    479 kB/s | 128 kB     00:00
(6/8): subversion-libs-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64.r 8.5 MB/s | 1.5 MB     00:00
(7/8): subversion-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64.rpm    5.9 MB/s | 1.1 MB     00:00
(8/8): utf8proc-2.6.1-3.module+el8.7.0+1065+42200b2e.x86_64.rpm       1.0 MB/s |  72 kB     00:00
------------------------------------------------------------------------------------------------------
Total                                                                 3.5 MB/s | 3.0 MB     00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                              1/1
  Installing       : apr-1.6.3-12.el8.x86_64                                                      1/8
  Running scriptlet: apr-1.6.3-12.el8.x86_64                                                      1/8
  Installing       : apr-util-bdb-1.6.1-9.el8.x86_64                                              2/8
  Installing       : apr-util-openssl-1.6.1-9.el8.x86_64                                          3/8
  Installing       : apr-util-1.6.1-9.el8.x86_64                                                  4/8
  Running scriptlet: apr-util-1.6.1-9.el8.x86_64                                                  4/8
  Installing       : libserf-1.3.9-9.module+el8.7.0+1065+42200b2e.x86_64                          5/8
  Running scriptlet: libserf-1.3.9-9.module+el8.7.0+1065+42200b2e.x86_64                          5/8
  Installing       : utf8proc-2.6.1-3.module+el8.7.0+1065+42200b2e.x86_64                         6/8
  Installing       : subversion-libs-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64                 7/8
  Running scriptlet: subversion-libs-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64                 7/8
  Installing       : subversion-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64                      8/8
  Running scriptlet: subversion-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64                      8/8
  Verifying        : apr-1.6.3-12.el8.x86_64                                                      1/8
  Verifying        : apr-util-1.6.1-9.el8.x86_64                                                  2/8
  Verifying        : apr-util-bdb-1.6.1-9.el8.x86_64                                              3/8
  Verifying        : apr-util-openssl-1.6.1-9.el8.x86_64                                          4/8
  Verifying        : libserf-1.3.9-9.module+el8.7.0+1065+42200b2e.x86_64                          5/8
  Verifying        : subversion-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64                      6/8
  Verifying        : subversion-libs-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64                 7/8
  Verifying        : utf8proc-2.6.1-3.module+el8.7.0+1065+42200b2e.x86_64                         8/8

Installed:
  apr-1.6.3-12.el8.x86_64
  apr-util-1.6.1-9.el8.x86_64
  apr-util-bdb-1.6.1-9.el8.x86_64
  apr-util-openssl-1.6.1-9.el8.x86_64
  libserf-1.3.9-9.module+el8.7.0+1065+42200b2e.x86_64
  subversion-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64
  subversion-libs-1.10.2-5.module+el8.7.0+1065+42200b2e.x86_64
  utf8proc-2.6.1-3.module+el8.7.0+1065+42200b2e.x86_64

Complete!

 

2. SVN 저장소 디렉토리 생성 및 저장소 생성

2-1. mkdir [저장소 디렉토리]

정해진 규칙은 크게 없어보이나, 나는 루트 디렉토리에 만들어주었다.

[root@82559cbe0973 /]# mkdir svn
[root@82559cbe0973 /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv  sys  usr
dev  home  lib64  media       opt  root  sbin  svn  tmp  var

 

2-2. svnadmin create [저장소 디렉토리]

해당 명령어를 입력하면 별다른 메시지가 출력되지는 않지만, 디렉토리에 들어가보면 SVN 관련 파일이 생성되어있음을 확인할 수 있다.

[root@82559cbe0973 /]# svnadmin create /svn
[root@82559cbe0973 /]# cd svn
[root@82559cbe0973 svn]# ls
README.txt  conf  db  format  hooks  locks

 

3. SVN 설정 변경 

3-1. SVN 구동설정 변경

conf 디렉토리 하위의 svnserve.conf 파일을 변경해준다.

[root@82559cbe0973 svn]# cd conf
[root@82559cbe0973 conf]# ls
authz  hooks-env.tmpl  passwd  svnserve.conf
[root@82559cbe0973 conf]# vi svnserve.conf

 

설정 파일이 매우 길어서 찾아서 변경하는것이 빠르다.

각 파라미터별로 주석이 되어있는 것들을 찾아 주석을 해제한다.

주의할 점은 단순히 주석만 삭제할 것이 아니라, 각 파라미터 앞의 공백문자열(스페이스바)도 제거해주어야 한다. 이걸로 삽질을 몇시간이나 했던지;

(중략)
[general]
anon-access = read
auth-access = write
password-db = passwd
realm = My First Repository
(생략)

 

3-2. SVN 계정 생성

SVN에 접속가능한 계정을 생성한다. 계정별 권한도 부여가 가능한데 이건 좀 더 나중에.

알아보니까 리눅스에서는 VisualSVN이 지원되지 않아서, 직접 계정별 권한을 생성해줘야 한다고 한다 -_-;

conf 디렉토리 하위의 passwd 파일을 변경해준다.

[root@82559cbe0973 conf]# vi passwd

(중략)
[users]
# harry = harryssecret
# sally = sallyssecret
# 아이디 = 비밀번호
test = test12!@ 

나는 test 계정에 test12!@ 비밀번호로 설정해주었다.

 

4. SVN 구동 및 확인하기

svn 구동은 다음과 같은 명령어로 수행한다. 

svnserve -d -r [저장소 디렉토리] --listen-port [사용포트] --log-file [로그디렉토리/파일명]

로그 및 포트를 변경하지 않는다면 해당 부분의 옵션은 사용하지 않아도 무방하다.

[root@82559cbe0973 conf]# svnserve -d -r /svn --listen-port 18081 --log-file /svn/svnserve.log

SVN에 왜 기본 로그를 사용하지 않는지는 조금 의문이긴 한데, 아무튼 해당 로그 디렉토리에 가보면 구동에 성공했다 뭐다 별로 표시도 안난다.

그러니까 직접 Windows PC 에서 SVN에 접속해보자.

 

SVN 툴로는 TortoiseSVN을 많이 사용한다. 난 이미 설치가 되어있다.

Windows 탐색기에서 요렇게 Repo-Browser를 실행한다.
svn 접속주소를 설정한다.

docker에서 띄웠으니 localhost 로 작성하고, docker는 18081로 포트포워딩 된 상태이다. 위에서 SVN은 18081로 커스텀포트를 지정했으니 이렇게 설정하고 접속하면 된다.

만약, svn 포트를 지정하지 않았다면 (버전에 따라 다르지만) 3690 포트를 사용하게 된다. 그렇다면 docker도 포트포워딩을 3690으로 해줬어야 같은 환경에서 접속이 가능할 것이다.

 

접속에 성공한 모습

정상적으로 접속에 성공하면 별다른 메세지 없이 바로 실행된다. 겸사겸사 위에서 띄워둔 로그를 보면 뭔가 길게 잔뜩 있다.

163 2024-08-14T00:50:20.194435Z 172.17.0.1 - svn open 2 cap=(edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops) / SVN/1.14.3%20(x64-microsoft-windows) TortoiseSVN-1.14.7.29687
163 2024-08-14T00:50:20.195027Z 172.17.0.1 - svn get-latest-rev
163 2024-08-14T00:50:20.195658Z 172.17.0.1 - svn stat /@0
163 2024-08-14T00:50:20.196204Z 172.17.0.1 - svn get-latest-rev
163 2024-08-14T00:50:20.196747Z 172.17.0.1 - svn get-lock /
164 2024-08-14T00:50:20.201736Z 172.17.0.1 - svn open 2 cap=(edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops) / SVN/1.14.3%20(x64-microsoft-windows) TortoiseSVN-1.14.7.29687

(생략)

 

이제 커밋을 해보자. 잘 올라가는지.

 

5. SVN 커밋

제대로 하려면 IDE 세팅하고 직접 소스코드 커밋도 해보고 그랬을텐데, 내가 하려는 것은 SVN 디렉토리 백업이 정상적으로 동작하는지를 보려는 것이기 때문에 이 과정은 생략했다. 그냥 파일 커밋을 하고 이게 디렉토리로 복붙해도 잘 되는지만 보면 되니까.

SVN Repo-Browser에서 마우스 오른클릭 후 Add File를 선택한다. 그 후에 아무거나 파일을 선택해서 커밋하면 된다.

메뉴 띄우고
대충 파일 아무거나 넣고, 커밋메시지 공들여 작성해준다. 이것도 백업되는지 확인해봐야 하니까.

깜빡하고 계정정보 입력하는 부분을 캡쳐하지 못했는데, 위의 3-2. SVN 계정 생성 에서 생성한 계정 정보를 입력하면 된다. 그럼 커밋 완료. 서버로그도 같이 확인한다.

 

SVN Repo-Browser 에 Commit 된 모습

163 2024-08-14T00:50:20.194435Z 172.17.0.1 - svn open 2 cap=(edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops) / SVN/1.14.3%20(x64-microsoft-windows) TortoiseSVN-1.14.7.29687
163 2024-08-14T00:50:20.195027Z 172.17.0.1 - svn get-latest-rev
163 2024-08-14T00:50:20.195658Z 172.17.0.1 - svn stat /@0
163 2024-08-14T00:50:20.196204Z 172.17.0.1 - svn get-latest-rev
163 2024-08-14T00:50:20.196747Z 172.17.0.1 - svn get-lock /
164 2024-08-14T00:50:20.201736Z 172.17.0.1 - svn open 2 cap=(edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops) / SVN/1.14.3%20(x64-microsoft-windows) TortoiseSVN-1.14.7.29687
164 2024-08-14T00:50:20.202276Z 172.17.0.1 - svn get-latest-rev
164 2024-08-14T00:50:20.202793Z 172.17.0.1 - svn stat /@0
165 2024-08-14T00:50:20.207001Z 172.17.0.1 - svn open 2 cap=(edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops) / SVN/1.14.3%20(x64-microsoft-windows) TortoiseSVN-1.14.7.29687
(생략)

 

대충 업로드가 다 되었다. 이로써 기본적인 SVN 설치, 설정, 연동까지 수행했다.

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

개발을 하다가 누군가가 세팅해준 서버에 접속하여 몇가지 명령어를 쓰다보면 그냥 뭐 잘 된다. 

그러다가 우리가 리눅스 서버를 설정 후 쓰려면 여간 불편한게 한두개가 아니다. 가령 vi 명령어나 뭐 그런 여러가지 잡다구리 한 것들. 그래서 내가 쓰는 몇가지들을 정리하고, 이후에 까먹으면 이걸 보기로 했다.

 

로컬 개발서버이기 때문에 yum 설치로 수행하였다.

 

1. yum 패키지 업데이트 
yum update -y


2. 기본 유틸 패키지
yum install -y shadow-utils : 사용자 계정 설정 패키지
yum install -y util-linux-user : 시스템 관리 설정 패키지
yum install -y passwd : 비밀번호 설정 패키지

 

3. ps 명령어 패키지
yum install -y procps-ng

 

4. vim 에디터
yum install -y vim

 

5. wget
yum install -y wget

 

6. JDK 1.8
yum install -y java-1.8.0-openjdk-devel

 

7. ls 컬러링
vi ~/.bashrc
alias ls='ls --color=auto' 추가
source ~/.bashrc

 

8. history 명령어 갯수 설정

vi ~/.bashrc

export HISTSIZE=[숫자]

source ~/.bashrc

 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

간단하게 테스트할 서버 구축이 필요해졌다. 신규 OS를 설치 후, 기존의 OS에 설치된 프로그램들의 호환성을 테스트검증 해볼 일이 있었는데, 플젝 끝나고 시간이 한가해서 도와드리겠다고 한 상황. 

이러다가 Docker를 구축해서 사용하게 되었고, 대략적으로 필요한 소프트웨어들까지 설치하면서 까먹을까봐 정리하는 겸 작성해본다.

 

대부분의 명령어들은 ChatGPT-4o 를 통해 생성해냈고, 사용 PC 환경은 윈도우11 Pro, 라이젠7 Pro 5850U, 16기가램을 사용중에 있다.

윈도우 Docker 설치는 어렵지 않으니 이 부분은 그냥 패스하자.

기본적인 Docker 명령어

현재 Powershell 에서 사용중인 기본적인 Docker 명령어는 다음과 같다.

1. RockyLinux 설치 Docker 명령어
docker pull rockylinux/rockylinux:8.10-ubi

2. Docker Images 확인(다운로드된 Docker image 확인)
docker images
결과)
PS C:\WINDOWS\system32> docker images
REPOSITORY              TAG        IMAGE ID       CREATED        SIZE
rockylinux/rockylinux   8.10-ubi   017fe84c871d   2 months ago   186MB

3. Docker run (이미지 첫 실행)
docker run -ti -p [도커포트]:[로컬포트] --name [Name] [imageID] /bin/bash 
예시) docker run -ti -p 22:22 -p 8080:8080 --name RockyServerTest 017fe84c871d /bin/bash

4. Docker ps (도커 컨테이너 확인)
docker ps -a 
추가) -a 옵션은 중지된 컨테이너도 포함하여 보여준다
결과)
PS C:\WINDOWS\system32> docker ps
CONTAINER ID   IMAGE          COMMAND       CREATED          STATUS         PORTS                                                  NAMES
93e37f9f73ea   017fe84c871d   "/bin/bash"   15 minutes ago   Up 2 seconds   0.0.0.0:22->22/tcp, 0.0.0.0:8080-8084->8080-8084/tcp   RockyServerTest
PS C:\WINDOWS\system32> docker ps -a
CONTAINER ID   IMAGE          COMMAND       CREATED          STATUS                      PORTS                                                  NAMES
93e37f9f73ea   017fe84c871d   "/bin/bash"   15 minutes ago   Up 18 seconds               0.0.0.0:22->22/tcp, 0.0.0.0:8080-8084->8080-8084/tcp   RockyServerTest
82559cbe0973   017fe84c871d   "/bin/bash"   47 hours ago     Exited (0) 17 minutes ago                                                          nexusTest
04003e982085   017fe84c871d   "/bin/bash"   2 days ago       Exited (0) 17 minutes ago                                                          my-rockylinux-ftp

4. Docker start (컨테이너 시작)
docker start [Container ID]

5. Docker Shell 접속 (컨테이너 시작 이후)
docker exec -it [Container ID] /bin/bash

6. 중지된 Docker 컨테이너 삭제
docker rm [Container ID]

7. Docker 이미지 삭제
docker rmi [Image ID]

 

이외에도 몇몇이 더 있긴 한데, 기본적으로 이정도만 알고 있어도 당장 사용하는데에는 큰 무리가 없다. 

특히 docker exec -it 명령어는 실행 한 후에 직접 쉘 접속을 하는 경우가 존재하므로 가장 중요하다 생각된다.

 

 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

이번 요구사항 중 특이한 요구사항이 있었다. 사실 특이... 까진 아니긴 한데, 설계 하다보니 이건 특이할 수 밖에 없는 상황이 생겨버림. 대략적인 설명은 다음과 같다.

기본전제 - FTP 업로드/다운로드

1. 한 명의 사용자가 여러개의 데이터를 FTP 업로드/다운로드 신청을 할 수 있다
2. 여러명의 사용자가 1번의 요구사항과 같이 신청할 수 있다
3. 한 명의 사용자는 한 건의 FTP 사용량만 가진다. 즉, 한 명이 10건을 신청해도 1건씩 업로드/다운로드가 되는 형식
4. 5명의 사용자가 각각 5건의 FTP 데이터 전송 요청을 하는 경우, 5건의 FTP 전송만 생성된다
5. 일반적으로 전송되는 데이터의 사이즈는 최소 수십 GB를 기본으로 한다 (FTP 속도가 오래걸림)

 

원래대로라면 그냥 신청을 하는 WEB단에서 API 서버로 FTP 전송 요청을 하면 수행하는 구조로 생각했는데, 기본적인 전송 단위가 몇기가 단위이다보니 자칫하다가는 Web Session Timeout이 발생할 수도 있는 상황. 그리고 3번과 같은 요구사항에 의해 결국 FTP 전송과 관련된 DB Table을 설계하고, API 서버에서는 해당 Table을 주기적(Scheduled)으로 Select 하면서 전송건이 있는지를 체크하는 방식으로 설계를 했다.

그리고 4번의 전송건과 관련해서 결국 Async 방식으로 FTP 호출을 하는 것으로 구현했다. 즉, 5명의 사용자가 각기 5개의 데이터를 전송요청 하는 경우, Async Thread에 의해 5개의 FTP 세션을 생성하고 전송할 것이다.

 

그런데 여기서 문제가 발생한다.

가령 DB 호출 스케쥴러가 10분에 한 번씩 작동한다고 가정하자. 첫 구동시 A 데이터(사이즈 100기가)가 Select 되어 FTP 전송을 하는데, 대략적으로 100분이 걸린다고 할 때, 전송이 완료되는 시점까지 스케쥴러는 이 데이터를 계속 호출할 것이다. 단순하게 DB 쿼리로 걸러내면 되지 않을까? 라고 생각했으나, DB 쿼리로 걸러낸다 하더라도 내부적으로 방지하는 로직을 구현해야 할 것으로 판단했다. 

 

중복실행 방지 Async를 위해  제미니에게 물어보니, Async Queue를 관리하는 로직을 구현해주었고, 사용법이 쉬워 적용해보았더니 아주 잘 동작하게 되었다. 이하는 해당 로직의 간단한 소스코드와 예제이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
 
@Component
public class QueueCache {
    private static final QueueCache instance = new QueueCache();
    private final ConcurrentHashMap<String, Boolean> cache = new ConcurrentHashMap<>();
 
    private QueueCache() {
    }
 
    public static QueueCache getInstance() {
        return instance;
    }
    
    // key가 Cache에 있는지 확인
    public boolean isReq(String key) {
        return cache.containsKey(key);
    }
    
    // 신규 작업시 Key를 Cache에 등록
    public void putReq(String key) {
        cache.put(key, true);
    }
 
    // 작업 완료시 Key를 Cache에서 삭제
    public void removeReq(String key) {
        cache.remove(key);
    }
}
cs

 

싱글톤 패턴으로 모든 클래스에서 단 하나의 인스턴스만 생성하는 구조로, 여러개의 쓰레드가 접근하여도 단 하나만의 키를 관리하는 방식으로 구현되어있다.

주석에 달린 설명과 같이 사용법이 크게 어렵지는 않다. 나의 경우에는 아래와 같은 방식으로 로그가 발생하였다.

1
2
3
4
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-1
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-2
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-3
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-4
cs

 

USERKEY-1~4 까지 Unique 한 값으로 QueueCache에 등록하고, 모든 로직을 수행하기 직전에 isReq(USERKEY-1)하는 식으로 Queue에 등록되어있는지를 확인한다. 없는 경우, Queue에 등록(putReq)을 한 후, 모든 작업이 종료되거나 또는 exception이 발생하는 경우 removeReq를 통해 Queue 에서 삭제를 한다.

 

이로인해 수시간 동안 동작하는 경우에도 안정적으로 Async FTP 전송을 수행할 수 있었다. 메데타시 메데타시.

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

아니 왜 응답 날짜값을 UTC로 주는지 모르겠다 증말. 이런건 그냥 심플하게 심플데이터 포맷으로 yyyyMMddHHmmssSSS로 찍어주면 어디 덧나나? 와 진짜. 아무튼 주는대로 받아먹어야하니 대충 정리해보았다.

 

날짜 형식은 다음과 같다.

2024-06-30T06:42:02.148+00:00

 

대충 찾아보니 UTC 형식의 DateTime으로, Simpledateformat을 기본으로 쓰는 내 시스템과는 근본적으로 달라서 날짜비교나 뭐 그런 기본적인 펑션이 하나도 먹지 않는다. 그래서 변환해서 DB에 넣고 꺼내기로 함.

 

java 1.8, SpringBoot 2.7.8 에서 실행했는데 이정도는 딱히 뭐 버전을 탈 일은 없겠지. 아무튼 잘 동작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Date;
import java.util.TimeZone;
 
 
/**
 * UTC To SimpleDateFormat
 */
public static String utcToSimpleDateFormat(String utcTime) {
    String simpleDateResult = "";
    
    // UTC 형식을 설정함
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    
    Date utcDate;
    try {
        utcDate = sdf.parse(utcTime);
        Instant utcInstant = utcDate.toInstant();
        
        // Asia/Seoul 로 타임존 설정, 대소문자에 유의
        ZoneId seoulZone = ZoneId.of("Asia/Seoul");
        Instant seoulInstant = utcInstant.atZone(seoulZone).toInstant();
        
        // 변경할 시간 양식을 설정해주자
        SimpleDateFormat resultStr = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        resultStr.setTimeZone(TimeZone.getTimeZone(seoulZone));
        
        simpleDateResult = resultStr.format(Date.from(seoulInstant));
    } catch (Exception e) {
        e.getMessage();
    }
    
    return simpleDateResult;
}
cs

 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

이번엔 외부시스템과 인터페이스 통신을 하는데, SSL 통신을 한다. 근데 뭐 고객사에서는 SSL 통신을 하는데 인증서도 안주고 도메인으로 통신하는 것도 아닌 IP 통신인지라 당연히 PKIX 오류가 발생하기 마련.

이 오류는 SSL 통신시 인증서 검증에서 실패했다는 뜻으로, 우리가 흔히 웹 브라우저로 인터넷 하다가도 '신뢰 할 수 없는 인증서입니다~ 무시하고 계속할래~?' 하는 식으로 묻는것도 이와 같은 맥락이라고 볼 수 있다. 

curl 에서는 -k 옵션 하나로 간단히 무시할 수 있는데, JAVA 에서는 쉽지 않았다. 이하는 try catch 부분.

 

java 1.8 및 SpringBoot 2.7.8 에서 수행했다.

 

오류는 다음과 같다.

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

 

대충 curl 호출부는 다음과 같다. 이렇게 호출하면 성공한다.

curl -H 'Content-Type: application/json' -H 'Authrization: Basic toke' -k -x POST 'https://111.111.111.111:443/test/v1/auth/token' ;

 

curl 호출시 -k 옵션은 인증서 검증 우회하겠다는 의미이다.

 

이하는 java 소스코드.

전반적으로 httpURLConnection과 같은 양상을 띄지만 SSL 통신 설정부라거나, 기타 잡다한 설정들이 더 추가가 되어있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package com.test.common.utils;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
 
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
 
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class HttpsURLConnector {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    public JSONObject postURL ( String urlData , Map<String, Object> headers , Map<String, Object> param) {
        JSONObject jsonResult = new JSONObject();
        
        //Http 요청시 필요한 URL 주소의 변수 선언
        String totalUrl = "";
        totalUrl = urlData.trim().toString();
        
        // http 통신 객체 선언
        URL url = null;
        HttpsURLConnection httpConn = null;
        
        // http 통신 응답에 대한 변수
        String resData = "";
        BufferedReader buff = null;
        StringBuffer strBuff = null;
        
        // 호출 결과값에 대한 변수
        String returnData = "";
        try {
            ObjectMapper mapper = new ObjectMapper();
            String paramData = mapper.writeValueAsString(param);
            
            // url.openConnection을 시도하기 전에 아래 설정을 완료한다
            TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
 
                public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                }
 
                public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                }
            } };
            
            SSLContext sslContext;
            sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, null);
            
            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
            
            // 파라미터로 들어온 url을 사용하여 connection 시도
            url = new URL(totalUrl);
            httpConn = (HttpsURLConnection) url.openConnection();
            httpConn.setHostnameVerifier(new HostnameVerifier( ) {
                
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    // return true로 해주어야 모든 SSL 검증을 무시함
                    return true;
                }
            });
            
            // http 요청 설정
            httpConn.setRequestMethod("POST");
            httpConn.setRequestProperty("Content-Type""application/json; utf-8");
            httpConn.setRequestProperty("Accept""application/json");
            
            // header 파라미터 설정
            if ( headers != null ) {
                for ( Map.Entry<String, Object> header : headers.entrySet()) {
                    httpConn.setRequestProperty(header.getKey(), header.getValue().toString());
                }
            }
            
            // Body부 생성 - doOutPut을 true로 주어야함
            httpConn.setDoOutput(true);
            try (OutputStream os = httpConn.getOutputStream() ){
                byte requestData[] = paramData.getBytes("utf-8");
                os.write(requestData);
                os.close();
            } catch ( Exception e) {
                logger.error("== OutputStream ERROR ::::: " + e.getMessage());
            }
            
            //커넥션~
            httpConn.connect();
            
            // 응답에 대한 데이터를 Buffer로 받아 처리
            buff = new BufferedReader(new InputStreamReader(httpConn.getInputStream(), "UTF-8"));
            strBuff = new StringBuffer();
            while ( ( resData = buff.readLine() ) != null ) {
                strBuff.append(resData); // StringBuffer에 응답받은 데이터를 순차적으로 저장함
            }
            
            returnData = strBuff.toString();
                
            //https 응답코드
            String resCode = String.valueOf(httpConn.getResponseCode());
            JSONParser parser = new JSONParser();
            Object obj = new Object();
            try {
                obj = parser.parse(strBuff.toString());
            } catch (Exception e) {
                logger.error("StringBuff Parser ERROR ::::: " + e.getMessage());
            }
            jsonResult = (JSONObject) obj;
        } catch (Exception e) {
            logger.error("TOTAL Error ::::: " + e.getMessage());
        }
        
        return jsonResult;
    }
}
cs

 

TrustAllCerts 부분과 setHostnameVerifier 부분을 잘 염두에 두면 큰 문제 없이 성공할 수 있다.

가장 중요한 부분은 url.openconnection  하기 전에 trustAllCerts를 선언해주어야 한다는 점이었다....

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

sftp를 활용할 프로젝트가 생겼다. 대충 개발망에서는 라이브러리가 없으니까, 외부 개인 놋북에서 샘플소스코드를 작성 하고 라이브러리를 반입하기로 했다. 이하는 대충 짜보고 찾아보고 제미니에게 물어본 소스코드.

 

Springboot 2.7.8, jcraft jsch 라이브러리는 0.1.55 버전 (0.1.54 버전도 동작됨을 확인함) 이다.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>
cs

 

1. SftpUtil.java

유틸과 같이 target Path , target FileName 정도만 입력받으려고 구성했다. 어차피 sftp 접속정보는 application.properties에 작성되어있을거니까.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.test.common.utils;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
 
public class SftpUtil {
    private static final String SESSION_CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
    private static final Logger logger = LoggerFactory.getLogger(SftpUtil.class);
 
    private String host;
    private String username;
    private String password;
    
    private int port;
    private int timeout = 15000;
 
    public SftpUtil(String host, String username, String password, String port) {
        this.host = host;
        this.username = username;
        this.password = password;
        this.port = Integer.parseInt(port);
    }
 
    private ChannelSftp createSftp() throws Exception {
        JSch jsch = new JSch();
 
        Session session = createSession(jsch, host, username, port);
        session.setPassword(password);
        session.connect(timeout);
 
        Channel channel = session.openChannel("sftp");
        channel.connect(timeout);
 
        return (ChannelSftp) channel;
    }
 
    private Session createSession(JSch jsch, String host, String username, int port) throws Exception {
        Session session = null;
 
        if (port <= 0) {
            session = jsch.getSession(username, host);
        } else {
            session = jsch.getSession(username, host, port);
        }
 
        session.setConfig(SESSION_CONFIG_STRICT_HOST_KEY_CHECKING, "no");
        logger.info("===== session ::::: " + session);
 
        return session;
    }
 
    private void disconnect(ChannelSftp sftp) {
        try {
            if (sftp != null) {
                if (sftp.isConnected()) {
                    sftp.disconnect();
                } else if (sftp.isClosed()) {
                }
                if (null != sftp.getSession()) {
                    sftp.getSession().disconnect();
                }
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }
 
    // Rest에서 호출함
    public boolean uploadFile(String targetPath, String fileName, File file) throws Exception {
        FileInputStream fis = new FileInputStream(file);
        ChannelSftp sftp = this.createSftp();
        try {
            sftp.cd(targetPath);
            sftp.put(fis, fileName);
            return true;
        } catch (Exception e) {
            logger.debug("==== exception ::::: " + e.getMessage());
            throw new Exception("Upload File failure");
        } finally {
            this.disconnect(sftp);
        }
    }
 
    // Rest에서 호출함
    public boolean downloadFile(String remoteFilePath, String localSavePath) throws Exception {
        ChannelSftp sftp = this.createSftp();
        try {
            sftp.get(remoteFilePath, localSavePath);
            return true;
        } catch (Exception e) {
            logger.debug("==== exception ::::: " + e.getMessage());
            throw new Exception("Download File failure");
        } finally {
            this.disconnect(sftp);
        }
    }
 
}
cs

 

소스코드가 좀 길기는 한데, 어차피 Upload / Download는 공통적인 connection / disconnection 과정을 거치기에 상관은 없다.

 

호출부는 다음과 같다.

 

2. Rest Controller 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@PostMapping("/testSftpUpload")
public String testSftpUpload(@RequestBody String a) {
    String result = "";
    
    SftpUtil ftp = new SftpUtil( sftpHost, sftpUsername, sftpPassword, sftpPort );
    File file = new File("C:\\Users\\KimFish\\Desktop\\TEST.csv");
    try {
        ftp.uploadFile("/C:/sftp""1234.csv", file);
    } catch (Exception e) {            
        e.printStackTrace();
    }
    
    return result;        
}
 
@PostMapping("/testSftpDownload")
public String testSftpDownload(@RequestBody String a) {
    String result = "";
    
    SftpUtil ftp = new SftpUtil( sftpHost, sftpUsername, sftpPassword, sftpPort );
    File file = new File("C:\\Users\\KimFish\\Desktop\\TEST.csv");
    try {
        ftp.downloadFile("/C:/sftp/1234.csv""/C:/Users/KimFish/Desktop/test1234.csv");
    } catch (Exception e) {            
        e.printStackTrace();
    }
    
    return result;        
}
cs

 

이정도면 충분한듯.

각각의 호출부는 host , port , userid , userpw 모두 입력받고, 메서드에 따라 파일 및 경로 등을 같이 인자값으로 던지게 된다. 일단은 이정도로 되었다... 

 

 

*** 알아둘 점 ***

리눅스에서 디렉토리 구분자는 / 를 사용하는데 반해, 윈도우에서는 디렉토리 구분자를 \를 써야 한다.

그런데 ftp(sftp포함)에서는 무조건 /를 사용해야하니 저렇게 작성된 것. 그에 비해 File 메서드 같은 경우는 Windows OS에서 구동되는 자바 프로그램이기 때문에 \ 를 사용함을 볼 수 있다.

이렇게 또 하나 배우는구만... 어차피 리눅스 드가면 슬래쉬 쓸거...

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

windows 11 에서 sftp 개발을 할 일이 생겼다.

별도 개발서버도 없고 하니 그냥 개인 노트북에 sftp 서비스를 열고, 관련된 계정을 생성하면 될 것이라 대충 예상하고 구글링을 시작. 다음과 같은 순서대로 sftp 서비스를 windows11 에서 구동했다.

 

1. sftp 관련 windows 추가프로그램 설치

노란색 색칠한거 다 설치함

 

제어판 > 프로그램 및 기능 > Windows 기능 켜기/끄기 메뉴에서 위와 같은 노란색의 항목들을 모두 설치했다. 그런데 이것만으로는 안열리길래, 좀 더 찾아보니 다음과 같은것도 설치해야 하더라.

 

 

설정 > 시스템 > 선택적기능 메뉴에서 OpenSSH 서버 및 OpenSSH 클라이언트를 모두 설치했다. 비로소 서비스를 구동할 준비가 된 셈.

 

2. sftp 전용 계정 생성

컴퓨터 관리 > 로컬사용자 및 그룹 > 사용자 에서 오른클릭을 통해 새 사용자를 만들어준다.

이 때 사용자 이름 및 암호/암호확인은 자신의 취향껏 작성해주고, 귀찮지 않으려면 '암호사용기간 제한없음' 옵션을 추가하자. 어차피 로컬 개발용이니까 개발하는데는 문제는 없다.

 

새로 만들어준 계정에 IIS_IUSRS 그룹권한을 넣어준다. 이 부분은 깊게 파보진 않았는데 IIS_IUSRS쪽이 IIS 서비스를 담당하는 그룹 권한인 것으로 추정된다. 아무튼, 이 계정으로 이제 sftp를 접속할 수 있는 셈.

 

3. sftp 서비스 시작

OpenSSH Server 시작

서비스 메뉴로 들어가서 OpenSSH Server 를 실행하자. 이제 기본포트(sftp의 경우 22) 로 서버가 구동된 셈이다.

 

Windows Powershell을 통해 sftp sftpUser@localhost 명령어를 입력하면 sftp에 접속된 것을 확인할 수 있다. 

이로써 sftp 서비스 구동은 끝.

 

IIS 서비스 쪽에서 sftp 바인딩을 18800으로 해줬는데도 접속을 못하길래  삽질하다가 알고보니 기본포트로 서비스가 열린것을 확인했다. 18800 포트로 어떻게 바꾸는거지? 이걸 좀 더 찾아봤다.

 

4. sftp 서비스 포트 변경

C:\ProgramData\ssh 디렉토리에 sshd_config 라는 파일이 존재하고, 이를 열어보면 다음과 같이 설정됨을 확인할 수 있다.

 

#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

 

주석으로 막혀있다는 것은 기본으로 설정된 것이 22 라는거고, 이걸 18800 으로 바꾸고 주석을 해제하면 잘 되겠지? 한 번 해보자.

 

관리자 권한을 요구하길래, 메모장 프로그램을 오른클릭 > 관리자권한 으로 실행한 다음 sshd_config 파일을 열어 수정했다.

 

그리고 서비스 > OpenSSH SSH Server를 재시작!

 

테스트용 소스코드가 정상적으로 동작됨을 확인했다.

Powershell 에서 sftp 접속하는 명령어는 sftp -P port userId@host 가 된다. ㅋㅋㅋ 잘 되네 .

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

이거 뭐라 오류가 난 것도 좀 거시기 하고 해서 쓰기가 애매하긴 한데, 아무튼.

Springboot 2.7.8 버전에서 Controller > Service > ServiceImpl > mapper > sql 로 이루어지는 과정에 있어 Return VO가 null 인 것을 확인했다.

 

쿼리도 그렇고 VO도 그렇고 아무런 문제가 없는것임에도 불구하고 도저히 뭐가 문제인지를 모르겠어서, sql에서 resultType을 VO가 아닌 java.util.hashMap 으로 받아보았더니 정상적으로 DB 쿼리가 조회되는데, Key 값이 스네이크 케이스로 들어온 것을 확인했다. 이러니까 카멜케이스 기반으로 작성된 vo에 매핑이 안되어서 null이 뜨는거지.

 

그런데 이게 대체 왜 지금와서 문제지? 지금까지 수행해온 프로젝트에서는 이거 다 매핑이 되었는데? 싶었다.

대충 찾아보니 mybatis 에서 이걸 옵션값으로 넣어주어야 한다는데, 내가 처음부터 밑바닥부터 세팅한 프로젝트는 지금이 처음이라서 그런가 싶었다.

 

아무튼, application-local.properties에 아래와 같이 넣어주니 스네이크 케이스의 컬럼값이 카멜케이스의 vo 값에 잘 넣은것을 확인할 수 있었다.

1
2
3
 
mybatis.configuration.map-underscore-to-camel-case=true
 
cs

 

와 진짜 별거 아닌걸로 30분은 날린 것 같네. 지금까지는 어디선가 이 옵션을 누가 넣어줬던걸 썼나... 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,