개념

일반 도커 단점

Docker는 container를 실행시키기 위해서는 Docker daemon을 통한다. 이 때, Docker daemon은 root권한 함께 실행되기 때문에 컨테이너를 실행하기 위해서는 root 권한이 필요하다. 이러한 방식은 컨테이너에서 보안 상의 문제가 발생했을 때, root권한이 탈취될 위험성이 있다.

도커 데몬이란?
도커 시스템에서 중추적인 역할을 하는 백그라운드 프로세스. 도커 데몬은 컨테이너, 이미지, 네트워크, 볼륨 등을 관리하고 사용자의 요청에 따라 도커 엔진의 핵심 작업을 수행한다. 데몬은 시스템이 부팅될 때 자동으로 시작되며, 지속적으로 백그라운드에서 동작하며 도커 관련 명령을 처리한다.

 

 

정의

각각의 컨테이너는 현재 접속 중인 user 또는 group의 권한 만으로도 생성, 실행 및 관리가 가능하며 이러한 컨테이너를 Rootless Container라고 한다. 사용자 네임스페이스 내에서 Docker 데몬과 컨테이너를 실행하여 일반 사용자 권한으로도 docker를 실행할 수 있게 한다. rootless docker를 사용 시 컨테이너에서 보안 상의 문제가 발생하더라도 컨테이너를 실행한 유저의 권한 만을 가질 수 있을 뿐, root권한은 보호할 수 있다. 이를 이용하여 컨테이너를 실행하는 유저에게 최소한의 권한만을 부여함으로써 보안성을 향상시킬 수 있다.

 

특징

  • 보안 안정성: 루트 권한이 없기 때문에 컨테이너가 호스트 시스템에 미치는 영향을 줄임
  • 네임스페이스 분리: 사용자 네임스페이스 활용
  • 특히 여러 사용자가 동일한 서버에서 컨테이너를 실행하거나, 루트 권한을 요구하지 않는 환경에서 적합함

 


rootless 설정

rootless docker는 일반 docker를 먼저 설치하고 rootless 설정을 활성화하면 된다.
Docker rootless 설정 공식 문서

기존 서비스 비활성화

기존에 docker daemon이 실행 중이라면 root 계정으로 다음 명령어를 실행하여 비활성화한다. shutdown/disable을 하지 않는 이상은 도커는 계속해서 rootful로 돌고 있을 것이다. 만약 안된다면 --force 옵션을 사용하라.

  • docker.sock: 도커 데몬과 클라이언트가 통신하기 위해 사용되는 소켓 파일. 기본적으로 도커 데몬 실행 시 생성된다.
systemctl disable --now docker.service docker.socket
rm /var/run/docker.sock

 

docker 서비스 재설치

만약 도커 20.0 버전 이상을 rpm 파일로 설치했다면, /usr/bin/dockerd-rootless-setuptools.sh 파일이 생성되어 있을 것이다. (없다면 docker-ce-rootless-extras 패키지 설치가 필요하다) root가 아닌 일반 계정으로 다음 파일을 실행하여 daemon을 설정한다. 만약 해당 서비스를 제거하고 싶다면 uninstall을 실행하면 된다.

/usr/bin/dockerd-rootless-setuptool.sh install
/usr/bin/dockerd-rootless-setuptool.sh uninstall

 

docker 실행

rootless docker 모드에서는 기본적으로 사용자 계정에 해당하는 ~/.config/systemd/user/docker.service 파일에 시스템 서비스가 설치된다. --user 옵션을 통해 해당 계정이 도커 데몬의 라이프 사이클을 관리하도록 설정하면 도커 데몬이 root가 아닌 사용자 세션에서 실행될 수 있다.

  • docker.service: 도커 데몬을 관리하는 systemd 서비스 유닛 파일. 이 파일에는 도커 데몬을 시작, 중지, 재시작, 상태 점검 등의 작업을 제어하는 명령어와 설정이 포함되어 있음
  • linger 모드 활성화: 해당 명령은 사용자가 로그아웃한 후에도 해당 사용자의 systemd 서비스를 계속 실행하도록 설정하는 옵션이다.
systemctl --user start docker  # 도커 데몬을 현재 세션에서 즉시 시작
systemctl --user enable docker  # 시스템 시작 시 도커 데몬을 자동으로 시작
loginctl enable-linger [일반계정ID]

 

Client 환경 변수 설정

rootless 모드로 변경됨에 따라 사용하는 소켓 파일의 위치도 변경되었다. 해당 환경변수 값을 수정한 뒤 echo를 통해 변경된 값을 확인할 수 있다. 이 경로는 위에서 rootless-setup 설치 시 디폴트로 설정된 위치이다.

  • 소켓 파일 경로
    • 디폴트 경로: $XDG_RUNTIME_DIR/docker.sock
    • $XDG_RUNTIME_DIR는 사용자별 런타임 디렉토리를 지정하는 환경 변수로, 일반적으로 /run/user/$UID로 설정된다. 여기서 unix://는 실제 파일 경로가 아니라 docker가 사용하는 소켓의 URI 형식을 나타낸다.
  • 데이터 디렉토리
    • 디폴트 경로: ~/.local/share/docker
    • docker 이미지, 컨테이너 데이터, 볼륨 등이 저장되는 경로. NFS(Network File System) 상에 위치하면 성능 문제가 발생할 수도 있고, docker와 같은 데몬이 파일 시스템에 빠르게 액세스해야 하는 경우에는 적합하지 않다.
  • 데몬 설정 디렉토리
    • 디폴트 경로: ~/.config/docker
    • 도커 데몬의 설정 파일을 관리하는 경로로, ~/.docker와는 다르다는 점에 주의해야 한다. 이 곳에는 client가 사용하는 설정과 인증 정보를 저장한다.
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
echo $DOCKER_HOST

# 환경변수 영구 저장 방법
vi ~/.bashrc
# 파일 하단에 export DOCKER_host=... 동일하게 입력
source ~/.bashrc

 

docker GPU 관련 설정

nvidia-ctk runtime configure --runtime=docker --cinfog=$HOME/.config/docker/daemon.json
systemctl --user restart docker
nvidia-ctk config --set nvidia-container-cli.no-groups --in-place

 


References

기본 금융권 아키텍쳐

은행 업무의 전산화, 계정계

옛날에 은행이 처음 전산화되던 시절에는 슈퍼컴퓨터가 사용되었다. IBM에서 만든 메인 프레임을 도입했고, 은행 거래에서 발생하는 수많은 입출금 데이터들을 이 곳에 저장했다. 이것이 은행의 최초의 전산화된 컴퓨터였고, 처음에 들어온 이 시스템을 은행 코어 시스템이라고 한다. 코어계, 기반계, 계정계라고도 부른다. 즉 은행에서 가장 중요한 부분을 담당하는 것이다. 여기에 주로 사용되는 언어는 COBOL, PL/1이었다. 그 옛날에 전공책에서나 보던 천공 카드에 구멍을 뚫던 시절이었다.(참고 블로그)

 

정보계의 등장

이렇게 데이터가 저장되면, CEO와 같은 대표들은 이 데이터를 활용한 통계 자료를 보고 싶어할 것이다. 어제 돈이 얼마가 들어왔고 고객이 얼마나 이용했는가? 단순히 생각하면 어려운 일은 아니다. 그냥 DB에 select group by하면 쉽게 결과가 나올 것이다. 하지만 해당 시스템은 계속해서 고객들의 돈이 거래되고 있다는 점이 문제다. 성능은 한정되어 있기 때문에 계속해서 거래가 이루어지고 있는 메인 프레임에 추가적인 처리를 요청하는 것은 시스템에 부하를 줄 수 있다.

 

그래서 계정계와 동일한 시스템과 DB를 복제해서 그러한 부수적인 업무만을 수행하도록 정보계를 만들었다. 당시에는 DB를 실시간으로 복제하는 기술이 없었기 때문에 하루 정도는 딜레이가 있었다. 이를 디퍼드(Deferred) 시스템이라고 한다. 디퍼드 시스템이란 동일 기종인 메인 프레임끼리 데이터를 (정보계로) 전송하는 것을 뜻하며, deferred의 의미 그대로 지연돼서 처리되는 것으로 이해하면 된다.

 

UNIX와 다운사이징

인터넷이 발달하며 이제는 사용자 친화적인 웹 UI가 일반적이지만, 아직도 메인 프레임을 쓰는 기업들이 있다. 코스트코에 가보면 아직도 직원들이 검은 화면에 SSH같은 터미널을 이용하는 모습을 볼 수 있다. 왜 아직도 메인 프레임을 쓰는 걸까? 효율이 매우 좋기 때문이다. KB은행도 아직 메인 프레임을 사용하고 있다. (SC제일은행은 추진?)

 

그런데 메인 프레임은 치명적인 단점이 있다. 비용이 너무 비싸다는 것이다. 은행이 비싼 IBM 금액을 부담으로 느끼던 와중, 유닉스(c/c++ 기반)와 이를 기반으로 한 다양한 프레임워크가 등장했다. 은행은 비용 절감을 위해 IBM의 메인 프레임에서 유닉스로 다운사이징을 하게 되었다. 성능이 조금 떨어지더라도 비용을 아끼는 것이다. 유닉스 환경으로 교체하기 위해서는 기존의 코볼과 PL/1에서 c언어 기반으로 바꿀 필요가 있었다. TMAX에서 개발한 proframe는 c언어 기반의 프레임워크를 제공하며, 실제 많은 은행에서 차세대 프로젝트에 해당 기술을 도입되었다. 코어계는 중요한 부분인 만큼 건들기 어려우니 정보계부터 유닉스로 바뀌게 되었고, 안정성이 검증되자 코어계도 서서히 바꾸기 시작했다. 이렇게 다운사이징하는 것을 금융권 차세대 프로젝트라고 한다.

 

KB은행의 차세대?

IBM의 메인 프레임도 일정 기간이 지나면 장비를 교체해야 한다. KB은행은 장비를 교체할 경우 IBM의 지원을 받기 위한 비용과, 다운사이징을 위해 시스템을 전부 c언어 기반으로 교체하는 데 드는 비용 2가지를 비교해 보았다. 그 결과 다운사이징 경우가 더 저렴한 것으로 판단히고 유닉스 시스템으로 전환하기로 결정했었다. (2012년부터 검토, 결정 후 2013년 10월에 금감원에 보고) 물론 비용뿐 아니라 IBM은 시스템 개방성이 떨어진다는 문제 등 다양하게 고려한 결과였다. 

 

그러나 14년 4월, 한국 IBM 대표가 경영진들에게 사적인 이메일을 보내면서 내분이 시작되었다. 유닉스 시스템 전환에 리스크가 클 것이고, 저렴한 가격으로 메인프레임을 제공할 수 있다고 제안하는 내용이었다. 그렇게 최종적으로 유닉스 전환 결정은 철회하고 계속해서 IBM의 메인프레임을 2020년까지 사용하기로 결정되었다. 이후로는 ‘코어뱅킹 현대화’를 통해 유닉스로 전환하는 계획을 밝혔으나, 현재 2025년 기준 국민은행의 ‘탈 IBM 메인프레임’ 전략은 사라졌다. 오히려 오는 2030년까지 IBM 메인프레임 기반의 ‘코어뱅킹1’을 구축하겠다는 것으로 전략이 바뀌었다.

 

LINUX와 다운사이징

다시 돌아와서, 유닉스의 등장으로 많은 시중 은행들은 메인 프레임에서 유닉스로 다운사이징 차세대를 진행했다. 그러다 이후에는 훨씬 더 저렴한 (자바 기반의) 리눅스가 등장하게 된다. 심지어 리눅스는 오픈소스라는 장점도 갖고 있다. IBM의 메인 프레임의 아키텍처가 공개되지 않은 closed system 이었다. 유닉스 때와 마찬가지로 여러 곳에서 다운사이징을 진행했다. 최근 정보계는 대부분 리눅스로 전환되었으며, 계정계도 리눅스로 전환되는 추세인 것 같다.

 

채널계와 MCI

기존 오프라인 창구 뿐 아니라 인터넷 뱅킹이 발달하고 스마트폰 앱도 활성화되는 등, 은행 시스템에 접근하는 채널이 점점 더 다양해졌다. 이렇듯 고객이 금융 기관의 서비스를 이용하기 위해 사용하는 다양한 수단(영업점 창구, 인터넷뱅킹, ATM, 스마트폰 등)을 통틀어 채널계라고 지칭한다.

 

이러한 요청들이 전부 계정계에 바로 직접적으로 전달이 된다면 다양한 형태의 요청을 처리하기가 복잡해질 것이다. 이를 해결하기 위해 MCI(Multi Channel Integration)라는 통합 표준 인터페이스를 만들었다. MCI는 채널계의 다양한 채널에서 들어오는 요청을 표준화된 형식으로 통합해주는 역할을 한다.

 

EDMS, EAI, FEP(대외계)

  • EDMS
    은행 시스템이 점점 복잡해짐에 따라 전자 문서를 다룰 필요성도 생기게 되었다. 문서파일의 작성부터 소멸될 때까지의 모든 과정을 관리하는 시스템을 EDMS(Electronic Document Management System), 전자문서 관리 시스템이라고 한다. 각종 전자 문서의 등록, 저장, 관리, 송수신, 조회 등을 통일된 인터페이스로 지원한다.
  • EAI
    지금까지 살펴본 것처럼, 하나의 기업 내에도 수많은 애플리케이션이 존재한다. 그리고 이 애플리케이션들은 서로 통신을 하게 된다. 만약 모든 애플리케이션이 각각 개별적으로 연결된다면, 시스템 간 개별적인 연결이 매우 많이 생기게 된다. 시스템이 6개일 때 15개의 연결이 필요한 것이다. 이럴 경우 유지보수는 물론 통신 환경에도 많은 어려움이 발생할 수 있다. 이런 문제점을 해결하기 위해 EAI(Enterprise Architecture Integration), 전사적 애플리케이션 통합 솔루션을 적용하게 된다. 이로 인해 중앙 집중화된 시스템 관리가 가능해진다. 은행은 계정계와 정보계에 각각 EAI가 존재하여 2중으로 되어있다.

  • FEP
    FEP(Front End Processor)란 원래 메인프레임에서 통신 과부하를 경감시키기 위해 전처리 작업을 하는 과정을 의미하나, 금융권에서는 의미가 조금 와전되어 B2B 연계(대외계)를 FEP라고 부른다. (타행 이체 시 요청 전달 등) 이러한 대외계는 아무래도 최신 기술의 변화에 느리게 대응할 수밖에 없다. 이 부분이 바뀌면 모든 은행이 바뀌어야 한다는 의미이기도 하기 때문이다. 그 대표적인 예로, 통신 시 HTTP 대신 TCP를 사용해야 하고, payload를 json이 아닌 fixed string을 사용해야 한다는 점이 있다. 아주 옛날에는 fixed length라고 해서 정해진 길이로 텍스트를 잘라 전송했고, ‘;’를 기준으로 구분하는 delimiter 방식, xml, json 순서로 발전해왔다. 요즘 대부분의 인터페이스들은 json format을 제공해주지만 대외계는 아직이란 거다.

 

OpenAPI

원래는 FEP 하나를 설치하는 것 자체로도 돈이 많이 들었다. 그런데 Open API가 등장하며 이 문제가 해결됨과 동시에 핀테크 서비스를 확장하는 데 도움이 되었다. 오픈 API란 이용자가 일방적으로 정보를 제공받는 데 그치지 않고 직접 응용 프로그램과 서비스를 개발할 수 있도록 공개된 API를 말한다. 금융권의 오픈 API를 활용하면 고객은 금융기관의 웹/앱에 직접 접속하지 않고도 오픈 API를 이용한 서비스를 사용하여 각기 다른 금융 기관의 계좌 조회나 송금 등을 한 번에 편리하게 실행할 수 있는 것이다. 보안이 필요하다면 VPN을 넣으면 된다. 하지만 그래도 openAPI에 개인정보는 가급적 포함하지 않도록 하며, 개인 정보가 필요한 경우에는 전부 FEP로 처리하고 있다.

💡VPN이란?
VPN의 목적은 크게 2가지가 있다. (1) 암호화 (2) 네트워크 가상화 이다. 여기서는 보안을 위해 사용하기 때문에 1번 역할을 하는 것이다. HTTPS는 프로토콜 자체가 암호화된 것인 데 반해, VPN은 장비가 암호화한다. 즉 원래는 VPN을 사용하기 위해서는 장비 설치가 필요하다. 하지만 최근에는 기술이 발전함에 따라 SW 서비스로도 제공된다. 다만 하드웨어에 비해 소프트웨어 VPN의 경우, 대용량 데이터가 빈번하게 들어오는 경우 속도가 많이 저하될 수 있다. 그렇기 때문에 안정성을 위해 대부분은 하드웨어 VPN을 사용한다. 하드웨어의 경우 장치가 암호화하고 것이라고 했는데, 이 말은 곧 보내는 쪽과 받는 쪽이 모두 동일한 장비를 사용해야 한다는 의미이다.

 


클라우드 시스템

서버 가상화 개념

IBM의 메인 프레임에는 하이퍼바이저라는 개념이 존재한다. 하이퍼바이저란 컴퓨터의 OS와 응용프로그램을 물리적 하드웨어에서 분리하는 프로세스를 말한다. 이를 통해 하나의 호스트 시스템이 여러 대의 가상 머신을 운영하여 컴퓨팅 자원을 더 효과적으로 사용할 수 있었다.

 

1990년대 말에 유닉스 서버의 전성기가 일어나고, 이때 서버 가상화라는 개념이 생기게 되었다고들 한다. 하지만 명확히 말하지면 서버 가상화라는 개념은 그 이전부터 존재했고, 유닉스와 함께 본격적으로 확장되었다고 볼 수 있다. IBM의 하이퍼바이저는 메인프레임이라는 특정 하드웨어에 종속된 개념이었기 때문이다.

 

그런데 유닉스에도 불편함은 있었다. 초기 OS가 개발된 이후로, 여러 기업이 각자의 버전으로 발전시킴에 따라 서로 호환이 잘 안된다는 점이다. 이렇듯 벤더마다 커널과 명령어 셋이 다르기 때문에 표준화된 가상화 솔루션을 만들기 어려웠다. 하지만 리눅스는 오픈 소스로 누구나 사용할 수 있었고, x86 아키텍처와 결합하면서 가상화 기술을 표준화 하기가 쉬웠다.

 

컴퓨터의 가상화

VMware의 VM은 Virtual Machine으로, 가상의 기계장치를 의미한다. 가상화 기술을 이용하여 한 개의 시스템으로 여러 개의 가상 데스크톱 환경을 구성하여 사용하는 것이다. 이러한 가상 머신에는 VirtualBox, VMware 등이 있다. VMware에는 주인(HOST)와 손님(GUEST)의 개념이 있다. 하나의 컴퓨터(host) 안에 여러 개의 가상 머신(guest)를 만드는 것으로 이해하면 된다. 이 host를 나눌 때 하이퍼바이저를 사용한다.

이러한 VM의 개념이 발전하여 SDC(Soft Defined Compute)가 등장했다. SDC는 소프트웨어 기반으로 컴퓨팅 리소스를 동적으로 관리하고 최적화하는 개념이다. VM은 하드웨어 중심적인 환경인 데 반해서, SDC는 소프트웨어를 통해 CPU, 메모리 등의 자원을 유연하게 관리할 수 있다. SDC + SDN(Soft Defined Network) + SDS(Soft Defined Storage) 3가지 개념을 합치면 결국 클라우드가 된다. 클라우드가 등장하면서 컴퓨팅 환경과 트렌드는 완전히 변화했다. 이렇게 좋은 클라우드 기술, 당연히 은행도 사용할까? 물론 사용하지만 VM도 여전히 사용 중이다.

 

은행은 왜 계속 VM을 쓸까?

이를 이해하기 위해서는 VM 과 컨테이너에 대한 비교가 필요하다. 우선 둘 모두 host OS 위에서 돌아간다. VM의 경우 하이퍼바이저를 사용하여 여러 Guest OS를 올려 여러 VM을 모두 별도의 OS를 가지고 있는 것처럼 사용 가능하다. 컨테이너의 경우 도커 등을 활용해서 여러 컨테이너들 간에 호스트 자원을 분리해서 사용할 수 있게 해준다. 이는 리눅스의 고유 기술인 namespace와 cgroup을 이용한 것이다.

은행이 컨테이너를 사용하지 않고 아직까지 VM을 사용하는 이유는 결국 보안성 때문이라고 생각한다. 컨테이너는 관리가 편하지만, 구조적으로 하나의 OS 커널을 공유하기 때문에 문제가 생겼을 때 다른 서비스에도 영향이 미칠 수 있다. 반면 VM은 서로 독립된 OS 환경을 제공하기 때문에 하나의 VM이 공격 당하더라도 각 VM은 독립적이기 때문에 다른 시스템에 피해가 가지 않는다.

 

OpenStack 플랫폼

다시 돌아와서, 클라우드 기술은 다양한 산업과 서비스에서 활발하게 사용되고 있다. AWS, Azure, GCP와 같은 서비스들이 대표적이다. 그런데 보안을 중요시하는 금융권에서 타사의 클라우드 서비스를 쓴다? 자신들만의 리소스를 활용해서 새로운 클라우드 서비스를 만들고 싶지 않을까? 이것이 프라이빗 클라우드이다.

그리고 프라이빗 클라우드 구축 시에 사용하는 것이 OpenStack이다. OpenStack은 퍼블릭/프라이빗 클라우드를 구축/관리하는 오픈소스 플랫폼이다. 하나금융의 하나클라우디아가 이를 기반으로 개발되었다.

 

Docker와 K8s

기존 VMware는 물리 서버(host) 하나를 하이퍼바이저를 이용해서 여러 개의 가상 머신으로 나누는 방식으로 동작했다. 이러한 방식은 무거운 구조를 가진다는 단점이 있었고, 이후에 나온 것이 컨테이너 기술이다. 대표적으로 Docker와 이를 관리하는 K8s(쿠버네티스)가 있다. 컨테이너는 VM과 다르게 host의 OS 커널을 공유하면서 프로세스 수준의 격리를 제공한다. 이는 VMware보다 훨씬 가볍고 빠르게 애플리케이션을 실행할 수 있도록 도와줬다. 이러한 컨테이너가 많아짐에 따라 이를 한 번에 관리/배포/확장/모니터링하기 위한 도구의 필요성이 등장했다. 처음에는 Docker swarm 등 다양한 툴이 있었지만, 현재로서는 사실상 쿠버네티스가 표준이 되었다고 볼 수 있다.

 

쿠버네티스는 오픈소스로 공개되었기 때문에 여러 기업에서 이를 기반으로 상용 제품을 만들었다. RedHat의 Open Shift는 여기에 기업용 기능(보안, 인증 등)이 추가된 것이다. OpenShift의 오픈소스 버전으로 OKD(Origin Community Distribution)도 있다.

 

IaC(Infrastructure as Code)

쿠버네티스는 애플리케이션이 실행되는 환경이 온프레미스든, 프라이빗 클라우드든, 퍼블릭 클라우드든 상관없이 동일한 실행 환경을 제공해준다. YAML 파일을 통해 필요한 리소스를 정의하며, 이 파일을 쿠버네티스 클러스터에 적용하면 완전히 동일한 상태를 언제 어디서든 재현할 수 있다. 이것이 바로 코드로 인프라를 관리하는 IaC의 예이다.

 

그보다 더 하위 레벨의 인프라(VM, 네트워크, LB 등)을 코드로 관리할 수 있도록 도와주는 도구가 바로 HashiCorp의 Terraform이다. 쿠버네티스와 테라폼 모두 코드로 인프라를 정의한다는 공통점이 있지만, 쿠버네티스는 컨테이너 오케스트레이션만 다루고 테라폼은 클라우드 자원의 생성/제어를 다룬다는 점에서 차이가 있다. Terraform은 원래 오픈소스로 시작했지만, 최근에는 라이센스 변경으로 인해 커뮤니티 주도로 OpenTofu가 오픈소스로 제공되고 있다.

 

여기에 Ansible을 같이 사용한다면 완전한 자동화를 만들 수 있다. Ansible이란 Python으로 개발된 오픈소스 IaC 솔루션으로, 시스템을 구성하고 애플리케이션을 배포하는 등의 IT 작업을 자동화할 수 있다. httpd를 설치하고, 방화벽을 설정하고, 패키지를 설치하고.. 등의 과정을 모두 하나의 Playbook 파일로 작성하는 것이다. 그렇다면 ansible과 쉘 스크립트의 차이점은 무엇일까? 가장 큰 차이는 멱등성인 것 같다. 멱등성이란 “어떤 연산이 여러 번 수행되더라도 결과가 달라지지 않는 성질”을 뜻하며, 쉘의 경우 같은 모듈을 반복 실행했을 때 중복 생성이나 충돌 등의 발생 가능성이 있다.

 


References

개요

Spring Cloud란?

Spring Cloud

  • 마이크로서비스 아키텍처(MSA)를 지원하기 위해 다양한 도구와 라이브러리를 제공하는 프레임워크
  • 분산 시스템과 클라우드 환경에서 애플리케이션을 효과적으로 개발하고 운영할 수 있는 기능을 제공
  • VMWare, 하시코프(HashiCorp), 넷플릭스 등 오픈 소스 회사의 제품을 전달 패턴으로 모아 놓은 도구들의 집합

 

Netflix OSS(Open Source Software)

  • Netflix에서 개발한 오픈소스 소프트웨어들의 집합
  • 클라우드 네이티브 애플리케이션을 만들기 위한 다양한 도구들을 자체적으로 사용 및 검증하여 라이브러리로 제공함
  • 대표적인 도구로는 Eureka, Hystrix, Ribbon, Zuul 등이 존재

 

Spring Cloud의 특징

API 게이트웨이(API Gateway)

서비스 간의 통신을 중앙에서 관리하는 서버. 주로 라우팅, 인증, 보안, 로드 밸런싱, 캐싱 등의 기능을 담당한다.

- Zuul: Netflix OSS 기반의 API 게이트웨이. 요청 라우팅, 필터링, 보안 등의 기능을 제공한다.
- Cloud Gateway: 스프링 클라우드에서 제공하는 API 게이트웨이. 비동기 및 논블로킹 방식으로 동작하여 Zuul 보다 더 높은 성능을 제공한다.

 

서비스 디스커버리(Service Discovery)

서비스들의 위치와 상태를 자동으로 찾고 관리하는 기능. 서비스는 자동으로 디스커버리 시스템에 등록되고 종료되면 자동으로 해제된다. client는 이를 통해 동적으로 사용 가능한 서비스를 검색하고 선택한다.

- Eureka: Netflix OSS 기반의 서비스 디스커버리 도구. 서비스 인스턴스가 실행될 때 자동으로 등록되며, 클라이언트는 Eureka 서버를 통해 서비스의 위치를 조회할 수 있다.

 

분산 설정(Distributed Configuration)

설정을 중앙에서 관리하고 업데이트하는 기능. 서비스는 해당 서버에서 설정을 동적으로 가져와 사용하며, 이는 서비스를 중단하지 않고 실행 중일 때 변경해도 즉시 적용할 수 있도록 한다.

- Spring Cloud Config: 중앙 집중식 구성 관리를 제공하는 프레임워크. Spring Cloud Bus를 사용하면 서비스 재시작 없이도 설정 반영이 가능하다.

 

분산 추적(Distributed Tracing)

트랜잭션 추적을 위한 기능. Spring Cloud Sleuth를 통해 발생한 작업을 기록하여 마이크로서비스 간의 요청과 응답을 추적하고 모니터링하는 데 사용한다.

- Zipkin: 각 요청에 고유한 추적 ID를 부여하여 요청의 흐름을 시각화할 수 있다. 요청의 시작부터 끝까지의 경로를 추적하여 지연 시간, 오류 등의 정보를 수집한다. 또한 특정 요청을 검색하고 필터링하여 문제를 진단할 수 있다.

 

로드 밸런싱(Load Balancing)

여러 서버에게 들어오는 트래픽을 균등하게 분배하여 부하를 분산. 세션 유지, scale out 과 같은 기능을 통해 네트워크 트래픽을 효과적으로 관리한다.

- Ribbon: Eureka와 연동하여 서비스 인스턴스 목록을 가져와 동적으로 로드 밸런싱을 수행할 수 있다. 클라이언트는 Ribbon을 통해 여러 개의 인스턴스 중 하나를 선택하여 요청을 보내는 방식으로, 분산 전략으로 라운드 로빈, 가중치 기반 등 다양한 알고리즘을 지원한다.

 

회로 차단기(Circuit Breaker)

서비스 간의 통신에서 발생할 수 있는 장애에 대응하는 메커니즘. 서비스 간 통신이 실패할 경우 일시적으로 연결을 차단하여 전체 시스템에 영향을 최소화하고, 일정 시간 이후에 재시도하거나 다른 대체 서비스를 호출한다.

- Hystrix: Netflix OSS 기반의 서킷 브레이커 라이브러리
- Resilience4j: Hystrix의 대체 도구로, 경량화되고 모듈화된 자바 기반의 서킷 브레이커 라이브러리. 특정 임계치를 초과한 요청을 차단하고, 일정 시간이 지나면 다시 정상 동작을 시도할 수 있다. 실패 시 Failback 매커니즘을 통해 대체 동작을 수행한다.

 

기술 비교

Spring Cloud vs Spring Boot

  • Spring Boot와 Spring Cloud는 모두 Spring Framework 기반으로 개발이 되었지만 각각의 목적과 기능의 차이가 있음
  • Spring Boot: ‘단일 애플리케이션 개발’을 위한 프레임워크
    ↔ Spring Cloud: ‘분산 시스템의 개발 및 운영’을 위한 프레임워크
  • Spring Cloud는 Spring Boot에서 제공하는 기능을 기반으로 분산 시스템에서 필요한 다양한 기능들을 추가로 제공

⇒ Spring Cloud라는 환경 하에 각각의 단일 애플리케이션은 Spring Boot를 이용하여 개발

 

Gateway vs Load Balancing

  • gateway
    • 정해진 규칙에 따라 들어온 요청을 올바른 서비스에 라우팅하는 역할
    • 로깅, 요청 URL 수정, 인증/권한 처리, 헤더 변경, 요청과 응답 필터링 등, 클라이언트와 서비스 사이에서 더 세밀한 처리가 가능
    • GW는 LB처럼 트래픽 부하를 조절하는 역할은 없음
  • load balancing
    • 인스턴스 간 트래픽 분배를 통해 서버나 컨테이너의 부하를 관리하는 역할
    • LB는 GW처럼 요청에 대해서 추가적인 처리를 하지는 않음

 


Config server

Spring Cloud Config

  • 개념
    • 분산된 환경에서 중앙 집중식 설정 관리를 제공
  • config 서버/클라이언트
    • server: 중앙에서 설정 파일을 관리하고 각 서비스에 제공하는 역할
    • client: config 서버에서 설정을 받아 사용하는 서비스
  • 설정 자동 갱신
    • 설정 변경 시 서버 재시작 없이 실시간으로 변경 사항을 반영 가능
    • 이를 통해 시스템의 중단 없이 즉시 변경 사항 적용 가능

 

Config 서버 로직

애플리케이션 구동 요청이 들어오면 Config 서버를 통해서 서버 실행에 필요한 설정 값들을 가져온다.그림에서는 설정 정보를 Git Repository에 저장하고 있지만, DB에 저장할 수도 있다.

 

config_server_application.yml 예시

spring:
  ### 애플리케이션 관련 설정 ###
  applcation:
    name: config-server
    profiles:  # 활성화할 Spring Profile을 설정
      active: json  # json 프로파일을 활성화하여 JSON 형식으로 구성된 파일을 사용하도록 설정
    
  ### DB 관련 설정 ###
  datasource:
    hikari:
      pool-name: CONFIG-DB-POOL  # DB 연결 풀의 이름. 연결 풀을 구분하는 데 사용됨
      driver-class-name: org.maradb.jdbc.Driver
      jdbc-url:
      username:
      password:
      maximum-pool-size:  # 동시에 사용할 수 있는 DB 연결의 최대 개수
      connection-timeout: 5000  # DB에 연결 시 최대 대기 시간
      validation-timeout: 3000  # 연결이 정상인지 확인하는 동안 대기하는 시간
      idle-timeout: 60000  # 연결이 유휴 상태일 때 연결 풀에서 제거되기 전까지 대기하는 시간
      max-lifetime: 180000  # DB 연결의 최대 생명 주기. 이 시간이 지난 연결은 풀에서 제거됨(idle 상태와 무관)
      data-source-properties:  # 추가적인 데이터베이스 연결 설정
        cachePrepStmts: true  # PreparedStatement 캐싱을 활성화하여 성능을 최적화
        prepStmtCacheSize: 500  # 캐시의 SQL 크기 제한
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: true  # 서버에서 PreparedStatement를 사용하는 옵션을 활성화

  ### spring cloud 관련 설정 ###
  cloud:
      config:
          server:
            jdbc: 
              sql: SELECT ~~  # config 조회 요청 시 실행되는 쿼리
              order: 1

 

 


Service Discovery

Eureka

  • 개념
    • 클라이언트가 서비스 인스턴스를 동적으로 찾을 수 있도록 지원
    • 서비스의 등록과 검색을 자동화하여 애플리케이션의 복잡성을 줄여줌
  • 서비스 레지스트리 (Service Registry)
    • Eureka는 서비스 레지스트리로 작동하여 모든 서비스 인스턴스의 위치를 중앙 저장소에 저장함
    • 각 서비스는 Eureka 서버에 자신의 위치(호스트 및 포트 정보)를 등록하고, 이러한 정보를 조회하여 통신할 수 있음
  • 헬스 체크(Health Check)
    • 서비스 인스턴스의 상태를 주기적으로 확인하는 헬스 체크 기능을 제공
    • 서비스 인스턴스는 주기적으로 자신을 등록한 Eureka 서버에 헬스 체크 정보를 보내며, Eureka는 이를 기반으로 서비스의 가용성을 판단함

 

Discovery 서버 로직

1. 서비스 등록

위에서 살펴본 config 서버가 같이 나와있다. gateway 서버도 결국 하나의 서비스이기 때문에, 최초 구동 시 config 서버를 통해 설정값을 읽어와야 정상적으로 실행된다. 자세한 내용은 아래에서 설명할 예정이므로, 여기선 간단히 client의 API 요청을 라우팅한다고만 알고 있으면 된다. discovery(eureka) 서버를 제외한 나머지 서비스들은 자신의 정보를 사전에 알려줘야 한다. 이 과정이 서비스 등록 단계이다. 

2. 서비스 조회

client가 gateway에 API 요청을 보내면 gateway는 URL에 해당하는 인스턴스 정보를 찾고 이를 전달해야 한다. 해당 정보를 찾기 위해 discovery 서버와 통신하여 요청을 라우팅할 서비스를 조회한다. 

3. 서버 간 통신

2번과 동일하게 요청이 Service1에 전달되었다고 해보자. 그런데 해당 서비스에서 모든 작업을 처리한 후 바로 리턴될 수도 있지만, 그 과정에서 서로 다른 서비스 간에도 요청을 주고받을 수 있다. 이 경우에도 로직은 위와 동일하다. service1에서 service2로 요청을 보내고자 할 때, discovery 서버에게 service2에 대한 정보를 요청하게 되고 endpoint를 리턴값으로 받게 된다. 이를 활용해서 서버 간에 다시금 요청을 보내며 통신할 수 있다. 

 

discovery_server_application.yml 예시

discovery 서버는 각 인스턴스의 정보를 저장하는 "서버"와, 기타 나머지 "client"들이 존재한다. 서버와 클라이언트의 yml 파일에서 설정해주는 값이 서로 다르다. 

####################### EUREKA SERVER 
eureka:
  ### Eureka 서버 관련 설정 ### 
  server:
    enable-self-preservation: true  # 자기 보호 모드 활성화
    response-cache-update-interval-ms: 5000  # 응답 캐시 갱신 주기
    activation-interval-timer-in-ms: 1000  # client로부터 활성화 여부 확인 주기
  ### Eureka 클라이언트 관련 설정 ### 
  client: 
      register-with-eureka: false # Eureka 서버에 서비스 등록 여부
      fetch-registry: false  # Eureka 서버에서 다른 서비스 목록을 가져오지 않음
      registry-fetch-interval-seconds: 2  # Eureka 서버에서 서비스 정보를 가져오는 주기
      service-url:
          defaultZone: <http://localhost:8761/eureka/>  # Eureka 서버의 기본 주소
      ### Eureka 인스턴스 설정 ### 
      instance:  # 인스턴스란 서버 또는 컨테이너 단위
        prefer-ip-address: true  # 호스트명 대신 IP 주소를 사용해 Eureka에 등록
        ip-address: {hostIP}  # 환경변수로 IP 지정

####################### EUREKA CLIENT 
eureka:
  instance:
    prefer-ip-address: true
    ip-address: 
    lease-renewal-interval-in-seconds: 1
    # 설정된 시간 안에 하트비트가 안오면 등록 해제. 정상종료의 경우 즉시 해제
    lease-expiration-duration-in-seconds: 5
  client:
    register-fetch-interval-seconds: 2
    register-with-eureka: true
    fetch-registry: true
    service-url: ~
      defaultZone: ~

 

 


Gateway Server

Zuul

  • 개념
    • 넷플릭스가 개발한 API 게이트웨이로, 모든 서비스 요청을 중앙에서 관리
  • 특징
    • 라우팅: client의 요청 url에 따라 적절한 백엔드 서비스로 요청 전달
    • 필터: 요청 전후 다양한 작업을 수행할 수 있는 필터 체인을 제공. 인증/권한 부여/로깅 등을 처리
    • 모니터링: 요청 로그 및 메트릭을 통해 서비스의 상태를 모니터링 가능

 

Gateway 서버 로직

위 discovery에서 보던 그림과 로직은 동일하며, client 그림이 추가되었을 뿐이다. 모든 API 요청은 gateway를 통하게 된다. 

 

gateway_server_application.yml 예시

gateway 기능을 제공하는 라이브러리는 Spring Cloud Gateway와 zuul이 있다. zuul을 보면 routes에서 service-id만으로 간단하게 URL을 정의하고 있는데, 이 값은 discovery 서버의 정보에서 찾을 수 있다. 각 서비스가 discovery 서버에 자신의 정보를 등록할 때 전달한다.  

####################### Spring Cloud Gateway
spring: 
  cloud:
    gateway:
    ### HTTP client 설정 ### 
      httpClient:
        pool:
          max-connections: 200
          max-idle-time: 30s  # 최대 유휴 시간
          max-life-time: 60s  # 커넥션 최대 생명 주기 
        connect-timeout: 3000
        response-timeout: 20s
      # x-forwarded 헤더 관련 설정
      x-forwarded: 
        enabled: true  # x-forwarded 헤더 사용 여부 (true로 설정하면, 요청에 x-forwarded 관련 헤더가 추가됨)
      ### 라우팅 설정 ###
      routes:
        - id: A-SERVICE
          uri: lb://A-SERVICE  # 서비스의 URI (부하 분산을 위한 lb:// 접두어 사용)
          predicates:
            Path=/cc/**  # 경로 조건 
          filters:
            - RewritePath=/aa$, /aa/  # 경로 재작성 필터 ("/aa"를 "/aa/"로 변경)
            - CustomFilter  # 사용자 정의 필터
        - id: B-SERVICE
          uri: lb://B-SERVICE
          predicates:
            Path=/bb/**
          filters:
            - RewritePath=/bb/(?<segment>.*), /$\\{segment}  # "/bb/segment"를 "/segment"로 변경)
            - CustomFilter
                        
####################### zuul
zuul:
  sslHostnameValidationEnabled: false  # SSL 호스트명 인증서 검증 비활성화
  ignored-headers: Access-Control-Allow-Credentials  # 무시할 헤더 목록. 라우팅 시 전달x
  sensitive-headers:  # 민감한 헤더 목록
  host: 
    connect-timeout-millis: 60000
    socket-timeout-millis: 60000
  routes:
    OCR-ADMIN:
      path: /aa/**
      service-id: A-SERVICE
    OCR-BATCH:
      path: /bb/**
      service-id: B-SERVICE

 


Load Balancing

Ribbon

  • 개념
    • Neflix가 개발한 클라이언트 사이드의 로드 밸런싱을 지원하는 도구로, 서비스 인스턴스 간의 부하를 효과적으로 분산시킬 수 있음
  • 특징
    • 서버 리스트 제공자(Server List Provider)
    • 디스커버리 서버로부터 서비스 인스턴스의 리스트를 제공받아, 이를 통해 서비스 인스턴스의 위치와 상태를 동적으로 업데이트하며 로드 밸런싱에 필요한 최신 정보를 유지함
    • 로드밸런싱 알고리즘 (Load Balancing Algorithms)
    • 라운드 로빈(Round Robin)과 가중치 기반(Weighted) 등 다양한 로드 밸런싱 알고리즘을 제공함. 라운드 로빈은 요청을 순서대로 분산시키고, 가중치 기반 로드 밸런싱은 각 인스턴스의 처리 능력에 따라 트래픽을 분산시킴
    • Failover
    • 요청 실패 시 자동으로 다른 서비스 인스턴스로 전환하는 기능. 만약 특정 인스턴스가 응답하지 않거나 오류가 발생하면 다른 가용 인스턴스를 선택하여 요청을 처리함

 

load_balancing_application.yml 예시

ribbon: 
  ServerListRefreshInterval: 1  # 서버 목록을 1ms마다 새로고침
  ReadTimeout:                  # 서버 응답 대기 최대 시간
  SocketTimeout:                # 서버와의 연결 유지 시간
  MaxTotalHttpConnections: 2000 # 전체 서버에 대해 허용할 최대 HTTP 연결 수
  MaxConnectionsPerHost: 2000   # 개별 서버(호스트)당 허용할 최대 HTTP 연결 수

 

'Backend > spring' 카테고리의 다른 글

Jasypt 라이브러리로 .env 변수값 암호화  (0) 2025.05.16

배경

타 관계사에 솔루션을 설치할 때, 전부 내부망이기 때문에 기본적으로 설치 파일을 보안 USB에 담아 가져가고 있다. 그 과정에서 USB 마운트라는 용어를 접하게 되었고, 파일 시스템의 종류에 따라 라이브러리가 추가로 필요한 경우도 있었다. 이를 이해하기 위해서 관련 개념들을 간단히 정리해보았다. 

 

USB 마운트

USB 마운트란?

USB를 운영체제의 파일 시스템에 연결하여 사용할 수 있도록 하는 과정을 의미한다.

마운트 시 일반적인 디렉터리처럼 USB 내부의 파일을 읽고 쓰고 수정할 수 있다.

 

마운트 과정

fdisk -l
cd /home
mkdir usb  # 없다면 생성 필요
mount [디바이스 경로] [디렉토리 경로]
# ex: mount /dev/sda1 /home/usb
umount [디바이스 경로]
  • fdisk는 현재 시스템에 연결된 디스크와 파티션 정보를 출력하는 명령어로, USB가 정상적으로 인식되었는지 확인하기 위해 실행한다.
  • 디바이스 경로는 usb 장치가 인식된 경로이고, 디렉토리 경로는 앞선 폴더의 데이터를 마운트할 대상 위치이다. 해당 디렉토리가 없다면 mkdir로 생성이 필요하다. 즉, 위 명령어 수행 시 USB에 저장된 파일을 ./usb 폴더 내에서 접근 가능하다.
  • umount는 unmount의 약자로, 장치를 제거하기 전에 꼭 실행해줘야 하는 작업이다. 이를 생략할 경우 데이터 손실이 발생할 수 있다. 

 

파일 시스템

USB는 다양한 형식의 파일을 지원한다. 파일 시스템이란 운영체제가 데이터를 저장하고 관리하는 방식을 의미하며, USB, SSD 등 모든 저장 장치는 특정한 파일 시스템을 사용해야 데이터를 읽고 쓸 수 있다. 그러면 하나의 저장 장치에는 한 종류의 파일밖에 담지 못할까? USB를 여러 파티션으로 나누면 각 파티션에 다른 파일 시스템을 적용할 수 있다. 파일 시스템의 종류는 다양하지만, 현재 내가 접한 종류는 다음과 같다. 

  • NTFS
    • Windows에서 기본적으로 사용하는 파일 시스템
    • 리눅스에서도 마운트 가능하지만, NTFS 전용 드라이버(ntfs-3g) 설치 필요
    • 파일 크기 제한이 거의 없다
  • exFAT(Extended File Allocation Table)
    • 마이크로소프트에서 개발한 파일 시스템으로, NTFS보다 가볍게 설계된 파일 시스템
    • 파일 크기 제한(4G)와 파티션 크기 제한(2TB)가 없는 FAT 계열 파일 시스템임
    • Windows, macOS, Linux 모두에서 호환 가능하여 대용량 USB나 SD카드에서 많이 사용

배경

회사에서 운영 업무를 맡게 되었는데, 우선 유지보수 시에 필요한 각 관계사별 서버의 스펙을 정리하게 되었다. 우리 솔루션은 AI 모델을 기반으로 서비스되기 때문에 GPU 환경이 필요하다. 이를 위해서는 CUDA, NVIDIA 드라이버의 설치가 추가로 필요하며 GPU 종류에 따라 그 버전이 상이한데, 설치 파일의 크기도 작지 않고 버전 호환성 문제에 민감하기 때문에 잘 체크하는 것이 좋다. 
기존에는 대략적으로 알고 있었던 개념을 이번 기회에 간단히 알아보고, GPU 종류에 맞는 버전을 확인하는 법도 정리해보려 한다. 
 


개념

NVIDIA 드라이버

드라이버는 운영체제(OS)와 하드웨어를 연결하는 통로라고 볼 수 있다. NVIDIA 드라이버는 그 중 GPU를 활용하기 위한 것으로, GPU의 하드웨어 리소스를 최적화하고 그래픽 작업을 처리하기 위해 명령을 전달하는 역할을 수행한다. 그렇기 때문에 GPU 버전에 맞는 드라이버를 설치하는 것이 중요하다.
 

CUDA(Computed Unified Device Architecture)

CUDA는 C/C++ 프로그래밍 언어를 기반으로 하며, GPU에서 병렬 코드를 작성하고 실행할 수 있는 풍부한 라이브러리와 도구를 제공한다. 따라서 딥러닝과 같은 연산량이 방대한 처리를 수행하고자 하는 경우 CUDA의 사용은 필수라고 볼 수 있다. 일반적으로 CUDA를 설치한다는 것은 CUDA Toolkit의 설치를 의미한다. CUDA도 결국 드라이버 위에서 동작하기 때문에 NVIDIA 드라이버의 버전에 따라 호환 가능한 버전이 달라진다. 


호환 버전 확인

버전을 확인하는 방법은 매우 간단하다. NVIDIA 공식 홈페이지에서 찾고자 하는 GPU의 카테고리/시리즈/제품/OS/CUDA 툴킷 버전을 고르면 된다. 내 경우 A40, A100, H100 과 같은 GPU를 사용하고 있었고 아래와 같이 검색했다. 

 
그럼 아래와 같이 해당 GPU에 추천되는 드라이버 버전과 CUDA 툴킷 버전이 나오는 것을 확인할 수 있다. 물론 View More Versions 버튼을 통해 더 다양한 버전을 확인할 수 있다.

 
 앞서 드라이버와 CUDA의 버전 호환성도 중요하다고 말했는데, CUDA의 버전이 올라간다면 드라이버 버전업이 필요할 수도 있다. 엔비디아 드라이버는 하위호환성을 지원한다. 

  • 상위 호환성(지원X): 구버전 드라이버가 새로운 cuda 버전을 지원함
  • 하위 호환성(지원O): 최신 드라이버가 기존 cuda 버전을 지원함

CUDA 툴킷 버전에 따른 드라이버 정보는 공식 홈페이지의 release note에서 찾아볼 수 있다. (CUDA Toolkit Archive)

 


References

'Infra' 카테고리의 다른 글

폐쇄망 docker설치(rpm 파일 다운로드)  (0) 2025.05.14
rootless docker란? 개념 및 설정 방법  (0) 2025.04.20

접근

접근 자체는 어렵지 않은 문제였던 것 같다. 크레인과 박스의 무게를 내림차순으로 정렬하여, 무거운 것부터 크레인에 하나씩 할당해주면 된다. 그럼 이제 그걸 어떻게 구현하느냐의 문제이다. 해당 짐을 실었는지 여부를 확인하는 방법은 다양하게 있겠지만, 결국 원본 리스트에서 remove를 하는 것이 제일 간편한 방식이다. 그래서 일반 일차원 배열보다는 ArrayList에 저장하여 remove 함수를 쓰기로 했다. 다만 remove 사용 시 주의할 점은, 전체 리스트의 길이가 동적으로 변하기 때문에 반복문을 돌 때 인덱스를 넘어가지 않도록 조심해야 한다.

 

코드

import java.util.*;
import java.lang.*;
import java.io.*;

class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = null;

        // 입력값 저장
        int n = Integer.parseInt(br.readLine());
        st = new StringTokenizer(br.readLine());
        ArrayList<Integer> crains = new ArrayList();
        
        for(int i = 0;i < n;i++)
            crains.add(Integer.parseInt(st.nextToken()));

        int m = Integer.parseInt(br.readLine());
        st = new StringTokenizer(br.readLine());
        ArrayList<Integer> boxes = new ArrayList();
        
        for(int i = 0;i < m;i++)
            boxes.add(Integer.parseInt(st.nextToken()));

        // 내림차순 정렬
        Collections.sort(crains, Collections.reverseOrder());
        Collections.sort(boxes, Collections.reverseOrder());

        int answer = solve(crains, boxes);
        System.out.println(answer);
    }

    static int solve(ArrayList<Integer> crains, ArrayList<Integer> boxes){
        int result = 0;

        // 불가능한 경우
        if(boxes.get(0) > crains.get(0)){
            result = -1;
            return result;
        }

        // 그리디 계산
        while(!boxes.isEmpty()){
            int boxIdx = 0;
            int crainIdx = 0;
            int crainSize = crains.size();

            while(crainIdx < crainSize){
                if(boxIdx >= boxes.size())
                    break;

                if(crains.get(crainIdx) >= boxes.get(boxIdx)){
                    boxes.remove(boxIdx);
                    crainIdx++;
                }
                else boxIdx++;
            }
            result++;            
        }
        
        return result;
    }
}

 

 

'Algorithm' 카테고리의 다른 글

[백준] 2589: 보물섬 JAVA  (0) 2025.05.20
[백준] 5430: AC JAVA  (0) 2025.05.18
[백준] 1976: 여행 가자 JAVA  (0) 2025.03.01
[백준] 11403: 경로 찾기 JAVA  (0) 2025.02.28
[백준] 9084: 동전 JAVA  (0) 2025.02.14

접근

바로 이전 게시글에서 풀었던 경로 찾기 문제와 매우 유사하다. 여행 동선을 체크할 때 중간에 여러 도시를 지나가도 되고 동일한 도시를 여러 번 방문해도 된다는 얘기는, 결국 A > B 도시로 이동 가능한 경로가 있는가만 확인하면 된다. 어차피 양방향 그래프이기 때문에 나는 스택에 저장해두고 뒤쪽 도시부터 반대 방향으로 확인했다. 

처음에 75%에서 틀렸다는 결과가 나왔는데, 질문 게시판에서 반례를 찾았다. 여행 계획이 A > A, 즉 같은 도시로 이동할 수도 있는거다. 문제에서 연결 정보를 입력받을 때에는 같은 도시에 대한 정보는 포함되어 있지 않으므로 이 부분이 누락됐던 것 같다. 그래서 if(i == j) graph[i][j] = 1 조건을 추가해주었다. 

 

코드

import java.util.*;
import java.lang.*;
import java.io.*;

class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = null;

        // 입력값 저장
        int n = Integer.parseInt(br.readLine());
        int m = Integer.parseInt(br.readLine());
        int[][] graph = new int[n+1][n+1];
        
        for(int i = 1;i <= n;i++){
            st = new StringTokenizer(br.readLine());
            
            for(int j = 1;j <= n;j++){
                graph[i][j] = Integer.parseInt(st.nextToken());
                if(i == j) graph[i][j] = 1;
            }
        }

        // 여행 계획
        st = new StringTokenizer(br.readLine());
        Queue<Integer> q = new LinkedList();
        while(st.hasMoreTokens()){
            int cur = Integer.parseInt(st.nextToken());
            q.add(cur);
        }

        // 플로이드 워셜 알고리즘
        solve(graph, n);

        // 결과 출력
        Boolean possible = true;
        int cur = q.poll();
        while(!q.isEmpty()){
            int next = q.poll();
            
            if(graph[cur][next] == 0){
                possible = false;
                break;
            }
            cur = next;
        }

        String answer = possible ? "YES" : "NO";
        System.out.println(answer);
    }

    static void solve(int[][] graph, int n){
        for(int k = 1;k <= n;k++){
            for(int i = 1;i <= n;i++){
                for(int j = 1;j <= n;j++){
                    if(graph[i][k] == 0 || graph[k][j] == 0) continue;  // 양수 길이 없음
                    graph[i][j] = 1;                    
                }
            }
        }
    }
}

'Algorithm' 카테고리의 다른 글

[백준] 5430: AC JAVA  (0) 2025.05.18
[백준] 1092: 배 JAVA  (0) 2025.03.02
[백준] 11403: 경로 찾기 JAVA  (0) 2025.02.28
[백준] 9084: 동전 JAVA  (0) 2025.02.14
[백준] 12865: 평범한 배낭 JAVA  (0) 2025.02.13

접근

1. 우선 그래프 문제이므로, 어떤 형태로 저장할지를 결정해야 한다. 이중 ArrayList로 할지, 아니면 단순 이차원 배열로 할지. 이는 사용하는 알고리즘과 문제에서 주어지는 N의 범위에 따라 다르다. 우선 N의 범위는 100 이하로 매우 작기 때문에 단순 배열로 저장해도 된다. 

2. 그래프 중에서 어떤 알고리즘으로 풀 것인가? 문제를 읽어보면 모든 노드 → 모든 노드로 가는 경로를 찾아야한다. 즉, 플로이드 워셜 알고리즘이 적절하다. 그렇기 때문에 간선 정보는 더더욱 배열로 저장해야 한다는 소리가 된다. 

 

위와 같이 생각하고 처음에 코드를 짰는데, 테스트 케이스와 결과가 다르게 나왔었다. 알고보니 3중 for문에서 k, i, j 순이 아니라 i, j, k순서로 했었다. 그런 자세한 부분까지 기억하고 있는 것은 아니라서 오히려 잘못된 부분을 찾기가 더 어려웠던 것 같다. 

 

코드

import java.util.*;
import java.lang.*;
import java.io.*;

class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = null;

        // 입력값 저장
        int n = Integer.parseInt(br.readLine());
        int[][] graph = new int[n][n];
        
        for(int i = 0;i < n;i++){
            st = new StringTokenizer(br.readLine());
            for(int j = 0;j < n;j++)
                graph[i][j] = Integer.parseInt(st.nextToken());
        }

        // 플로이드 워셜 알고리즘
        solve(graph, n);

        // 결과 출력
        StringBuilder sb = new StringBuilder();
        for(int i = 0;i < n;i++){
            for(int j = 0;j < n;j++){
                sb.append(graph[i][j]).append(" ");
            }
            sb.append("\n");
        }
        
        System.out.println(sb.toString());
    }

    static void solve(int[][] graph, int n){
        for(int k = 0;k < n;k++){
            for(int i = 0;i < n;i++){
                for(int j = 0;j < n;j++){
                    if(graph[i][k] == 0 || graph[k][j] == 0) continue;  // 양수 길이 없음
                    graph[i][j] = 1;                    
                }
            }
        }
    }
}

'Algorithm' 카테고리의 다른 글

[백준] 1092: 배 JAVA  (0) 2025.03.02
[백준] 1976: 여행 가자 JAVA  (0) 2025.03.01
[백준] 9084: 동전 JAVA  (0) 2025.02.14
[백준] 12865: 평범한 배낭 JAVA  (0) 2025.02.13
[백준] 13904: 과제 JAVA  (0) 2023.03.20

+ Recent posts