※ LXC로부터 시작된 도커의 컨테이너 가상화 기술은 현재는 도커엔진이라는 자체 스펙으로 바뀌었지만, 근간은 여전히 LXC에 있다.

 

1. 컨테이너 내부 측정

처음 가상머신 생성 시 위와 같이 도커가 깔리는 드라이버와 OS가 깔리는 드라이버로 나누었다.

 

/var라는 폴더에 이미지라는 특정 OS와 특정 프로그램이 패키징된 스냅샷을 찍어두고 프로세스를 띄우는 방식으로 진행한다.

이 때 layer라는 개념을 통해 각각의 구성 요소를 조합하게 되고, 이미지 레이어는 Read Only 레이어와 Read, Write가 가능한 컨테이너 레이어가 있다.

 

이미지를 이용해 띄운 컨테이너는 내부적으로 OS가 먼저 실행되며 PID 1번 부여받음, 추가로 네트워크, CPU, 메모리, 디스크 등도 할당받음

이는 리눅스로 수행할 수도 있지만 편의성 증대를 위해 도커라는 기술을 활용한다.

 

내부 구조를 가져오기 위해 먼저 pull과 run을 이용하여 ubuntu 14.04 버전의 bash를 띄워준다.

ls로 조회해보면 시스템 영역에서 사용할법한 디렉터리들(opt, proc, root, run sbin 등)이 위치하는 것을 확인할 수 있다.

이를 chroot 혹은 pivot_root라고 부른다. → root file system을 구성하게 도와주고, 이를 통해 독립성이 보장된다.

 

컨테이너 안에서 조회해보면 위와 같이 디바이스의 사용량이나 가용 자원 등에 대해 나오는데, 특이점은 host docker가 설치된 영역인 /dev/sdb1이 그대로 공유되는 것을 볼 수 있다.

pivot_root와 mount namespace가 위 사례이다.

mount namespace : 장비를 mount시키는 것처럼 가동하는 기술

 

UTS namespace : 컨테이너 아이디와 호스트네임을 동기화시키는 작업

모든 OS는 호스트네임을 가지고, 컨테이너가 생성되면 container id가 부여된다. 해당 ID가 hostname으로 사용된다.

해당 프로세스가 작업을 수행하는데 필요한 것들을 격리시키는 기술인 PID 혹은 IPC namespace라는 기술이 적용된 것을 볼 수 있다.

ifconfig로 확인해보면 위와 같이 eth0에 내부 IP가 할당된 것을 볼 수 있고, 이를 network namespace라고 부른다.

위와 같은 여러 기술이 어우려져 host와는 격리된 환경에서 컨테이너가 하나의 프로세스로 동작한다.

 

 

2. 컨테이너 격리 기술

chroot 프로세스의 루트 디렉토리를 변경, 격리하여 가상의 루트 디렉토리 배정
pivot root 루트 파일시스템을 변경, 컨테이너가 전용 루트 파일시스템을 가지도록 함
mount namespace namespace 내에 파일 시스템 트리 구성
uts namespace host와 다른 별개의 hostname을 가지도록 함
pid namespace pid와 프로세스 분리
(systemd와 분리, 우분투의 1번 프로세스인데 잡히지 않음)
network namespace 네트워크 리소스 할당 (ip, port, route table, ethernet 등)
ipc namespace 전용 process table 보유

 

위와 같이 격리된 namespace 목록은 위와 같이 lsns 명령어로 조회할 수 있다.

 

 

3. 컨테이너의 라이프사이클

① create : image의 스냅샷으로 /var/lib/docker 영역 내에 컨테이너 생성

② start : process 영역에 컨테이너를 생성하여 실행 상태로 만들어줌

③ stop : process 영역에 컨테이너를 제거하여 종료 상태로 만들어줌

④ rm : create로 생성된 스냅샷 삭제

먼저, pull로 ubuntu:16.04 버전을 받아온다.

create 명령어로 새로운 컨테이너를 생성하고 조회해보면 위와 같이 Created 상태의 컨테이너가 만들어진 것을 확인할 수 있다.

Created 상태는 스냅샷을 만들어두기만 하고 프로세스로 가동은 하지 않은 상태이다.

프로세스로 올려서 실행시키기 위해 start 명령을 이용하고 확인해보면 위와 같이 Up 상태로 바뀐 것을 확인할 수 있다.

stop 명령어를 이용하고 확인해보면 위와 같이 Exited 상태로 바뀐 것을 확인할 수 있다.

Exited 상태는 프로세스로 구동만 안하고 있을 뿐 구동 준비는 된 상태이기 때문에 언제든 다시 start가 가능하다.

rm 명령어로 컨테이너를 삭제해주면 위와 같이 아무것도 조회되지 않는 것을 확인할 수 있다.

create와 start를 합친 run 명령어를 이용하면 위와 같이 바로 bash 창으로 들어가는 것이 가능하다.

# apt update
# apt -y install net-tools

각 컨테이너들은 격리환경이기 때문에 필요한 업데이트나 의존성 설정은 처음부터 해줘야 한다.

ifconfig로 확인하면 위와 같이 잘 출력되는 것을 볼 수 있다.

 

 

4. 컨테이너 내부 구조

먼저, ubuntu14.04 버전의 컨테이너를 띄운 후 위와 같은 작업들을 수행했다.

위와 같이 프로세스를 조회해보면 도커 컨테이너가 프로세스로 잡혀있는 것을 확인할 수 있다.

 

※ 스냅샷(이미지)은 Read only이지만, 컨테이너(프로세스)는 Read, Write가 가능하다.

 

root 권한 상태로 변경 후 find 명령어로 container.txt를 찾아보면 위와 같은 결과를 확인할 수 있다.

merged 경로로 이동한 후 ls를 해보면 컨테이너 내부의 파일 구성이 그대로 보이는 것을 확인할 수 있다.

즉, 컨테이너 내부는 사실 overlay2라는 스토리지 영역에 포함된 컨테이너 아이디 폴더에 종속되어 있는 파일들의 집합이라는 것을 알 수 있다.

다른 터미널에서 조회해보면 위와 같이 같은 내용이 나오는 것을 확인할 수 있다.

루트권한에서 a라는 파일을 생성한 후 다른 터미널에서 컨테이너 내부를 조회하면 잘 동기화되는 것을 확인할 수 있다.

 

☆ 즉, 컨테이너 내부에서 생성된 정보는 container layer + snapshot 영역에 저장된다.

 

 

5. 컨테이너 운영 명령어

먼저, 컨테이너 운영을 살펴보기 위한 노트 웹서버를 구축하기 위해 다음과 같은 파일을 작성해준다.

- app.js

const http = require('http');

const server = http.createServer().listen(5678);

server.on('request', (req, res) => {
    console.log('request');
    res.write("HostName: " + process.env.HOSTNAME + "\n");
    res.end();
});

server.on('connection', (socket) => {
    console.log("connected.");
});

 

- Dockerfile

FROM node:20-alpine3.17            // 어떤 os와 프레임워크 위에서 돌릴지
RUN apk add --no-cache tini curl   // 알파인 리눅스이므로 apk로 구동
WORKDIR /app                       // /app이라는 경로 생성 후 cd /app 실행
COPY app.js .                      // Dockerfile과 같은 경로의 app.js를 현재경로에 복사	
EXPOSE 5678                        // 포트바인딩 시 컨테이너 측의 노출포트가 5678
ENTRYPOINT ["/sbin/tini", "--"]	   // 내부적으로 app.js를 실행해주는 명령어
CMD ["node", "app.js"]             // (ENTRYPOINT와 CMD)

 

위 명령어로 이미지를 생성한다.

위와 같이 history 명령어로 nodeapp의 내용을 확인해볼 수 있다.

run 명령어로 컨테이너를 실행하고, 조회해보면 위와 같이 잘 띄워진 것을 확인할 수 있다.

-h 옵션은 hostname을 nodeapp으로 구성하겠다는 의미이다. 해당 태그를 주지 않으면 컨테이너 아이디가 hostname으로 부여된다.

 

※ 세부옵션

--env 컨테이너의 환경변수 지정
-d
--detach=true
백그라운드 실행모드 활성화, 컨테이너 아이디 등록
-t TTY 할당 (bash창 열어주기)
-i
--interactive
대화식 모드 열기 (컨테이너 내부에 명령어 주고받기)
--name 실행되는 컨테이너에 이름 부여 (미지정 시 랜덤한 2단어 조합명으로 부여)
--rm 컨테이너 종료 시 자동으로 컨테이너 제거 (stop 시 삭제)
--restart 컨테이너 종료 시 적ㅈ용할 재시작 정책 지정
(no, on-failure, on-failure:n(횟수), always)
-v
--volume=호스트경로:컨테이너경로
볼륨설정 (볼륨마운트)
-h 컨테이너의 호스트명 지정 (미지정 시 컨테이너 아이디를 호스트명으로 등록)
-p 호스트포트:컨테이너포트
--publish
호스트 포트와 컨테이너 포트를 바인딩
-P
--publish-all=true|false
컨테이너 내부의 EXPOSE 포트를 랜덤포트와 바인딩
--workdir
-w
컨테이너 내부의 작업 경로 (디렉터리)

 

docker top 명령어를 이용하여 컨테이너에서 현재 실행 중인 프로세스의 상태를 볼 수 있다.

docker port 명령어를 이용하여 포트정보를 알 수 있고, IPv4와 IPv6에 대한 정보가 모두 나온다.

위 명령어를 이용해 docker-proxy라는 대리포트 값을 조회할 수 있다.

위에서 얻은 프록시 값으로 위 명령어 입력 시 해당 포트바인딩 명령어의 정보가 저장된 위치가 나온다.

docker stats 명령어로 실시간으로 어떻게 자원을 소비하고 있는지 확인할 수 있다.

해당 컨테이너로 curl이나 브라우저 접속을 유도하면 갑자기 사용량이 증가하는 것을 관찰할 수 있다.

컨테이너명을 여러개 적으면 동시에 조회도 가능하며 흐름에 따른 갱신을 보고싶지 않다면 --no-stream 옵션을 추가하면 된다.

 

 

6. 모니터링용 이미지 및 컨테이너로 상태 감시

1) cadvisor 

→ docker stats로도 상태를 감시할 수 있지만 좀 더 전문적으로 감시할 수 있는 툴

→ 도커허브가 아닌 gcr에 올라와 있기 때문에 아래와 같은 명령어들로 볼륨마운트를 해야만 볼 수 있다.

$ docker run \
--restart=always \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:rw \
--volume=/sys/fs/cgroup:/sys/fs/cgroup:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8765:8080 \
--detach=true \
--name=cadvisor \
--privileged \
--device=/dev/kmsg \
gcr.io/cadvisor/cadvisor:latest

위와 같이 이미지를 받은 후 조회해보면 컨테이너가 잘 띄워진 것을 확인할 수 있다.

해당 IP와 포트로 접속해보면 위와 같이 잘 접속된다.

위와 같이 매트릭 형태로 실시간 트래픽들을 확인할 수 있다.

 

 

리눅스에서 반복문 수행 시 헬스체크를 수행하도록 할 수 있다.

※ 헬스체크 : 서버의 가동 여부를 지속적으로 확인하는 행위

$ while true; do curl 노드서버주소; sleep 초단위; done

위와 같은 반복문 수행 시 sleep으로 딜레이를 주기 때문에 해당 초마다 한 번씩 노드서버에 접속하게 된다.

 

console.log()를 이용하여 nodeapp에 지속적으로 요청을 넣어 찍히는 log를 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

7. cp 명령어로 컨테이너 내부에 호스트 파일 복사하기

간단한 파일 전송 시에는 볼륨마운트 대신 cp 명령어를 이용하여 넘기기도 한다.

$ docker cp 호스트파일명 컨테이너명:경로와파일명

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 일반적인 기업 내부 프로젝트용 이미지는 OS나 프레임워크 등에 대한 정보가 포함되어 있고 의존성 정보도 노출하기 때문에 프라이빗 저장소를 직접 구축하여 사용하기도 한다. → 도커허브에 공개된 registry라는 공식 이미지를 이용하여 구축하는 경우가 많다.

 

1. registry 구축

먼저, registry를 받아오고 확인해보면 25MB 정도로 용량이 상당히 작은 것을 볼 수 있다.

$ docker run -d \
-v /home/유저명/registry_data:/var/lin/registry \
-p 5000:5000 \
--restart=always \
--name=private-registry \
registry

-v 옵션 : 볼륨 → 호스트의 저장폴더와 컨테이너의 저장폴더를 동기화

위 명령어를 통해 호스트의 /home/jihyun/registry_data 폴더에 있는 파일을 컨테이너의 /var/lin/registry 폴더와 즉시 동기화한다.

포트 5000번으로 포트바인딩을 꺼지지 않고 재시작하도록 하는 registry 컨테이너를 띄운다.

history 명령어로 확인해보면 노출시키는 포트가 5000번이고 VOLUME 역시 이미 컨테이너측의 경로가 지정되어 있다.

위와 같이 컨테이너가 잘 띄워진 것을 확인할 수 있다.

위 명령어로 저장 요소를 검색해보면 현재 컨테이너에는 별다른 저장자료가 없기 때문에 위와 같이 출력된다.

위와 같이 보통 alias로 등록해두고 사용한다.

도커허브에 올릴 때 레포지토리명을 따로 이미지에 반영해줬듯이 위처럼 사설 레포지토리의 내부망 주소를 태그로 붙여준다.

해당 주소로 push 해보면 현재는 도커 엔진이 신뢰할 수 없는 저장소이기 때문에 위와 같은 오류가 발생한다.

위 오류를 해결하기 위해서 현재 컨테이너를 저장소로 등록해주고 반영해야 한다.

/etc/init.d/docker 파일의 DOCKER_OPTS 부분을 위와 같이 수정한 후 저장한다.

또한, 위와 같이 파일 생성 후 insecure-registries라는 key 값에 해당 IP 주소를 리스트 형식으로 등록해준다.

갱신 내역들을 반영하기 위해 도커를 재시작해준 후 docker info 명령어로 확인해보면 위와 같이 잘 등록된 것을 볼 수 있다.

이 상태로 push 해주면 위와 같이 잘 수행되는 것을 확인할 수 있다.

위와 같이 이미지 목록과 해당 이미지의 태그도 잘 조회되는 것을 확인할 수 있다.

 

 

2. 구축한 레지스트리로 이미지를 받거나 업로드하기

더보기

<시나리오>

1) host2에 host1의 주소를 기반으로 해서 DOCKER_OPS 에 환경설정을 하고 갱신

2) host2에서 이미지 생성하기

3) 해당 이미지를 host1 기반 private 레지스트리에 업로드

4) 업로드된 내역을 catalog로 조회하기

5) pull도 받아보기

먼저, 해당 주소가 안전한 저장소임을 등록하기 위해 위와 같이 환경설정을 해주어야 한다.

마찬가지로 insecure-registries라는 key 값에 해당 IP 주소(host1주소)를 리스트 형식으로 등록해준다.

도커를 재시작한 후 조회해보면 위와 같이 해당 IP가 잘 등록되어 있는 것을 확인할 수 있다.

docker pull을 해보면 위와 같이 host1 주소에 있는 이미지를 당겨서 받아오고 있는 것을 확인할 수 있다.

host2에서 hello-world 이미지를 생성했고, 해당 이미지를 host1의 레지스트리에 push 해주었다.

host1에서 catalog와 태그를 조회해보면 위와 같이 잘 추가된 것을 확인할 수 있다.

마찬가지로 pull을 해보면 사설 저장소에 있던 것을 당겨서 받아오는 것을 확인할 수 있다.

1. 도커 이미지

- 도커 이미지는 컨테이너 실행에 필요한 실행 파일, 라이브러리, 설정 값 등을 포함

- 이미지는 새로 만들지 않는 한 원본은 변경되지 않음

- 이미지 생성 및 실행 순서

① 애플리케이션 개발

② Dockerfile에 이미지 생성 프로세스 기입

③ 빌드하여 이미지로 생성

④ 이미지를 활용해 컨테이너 생성

⑤ 컨테이너 작동 여부 테스트

⑥ 도커허브에 업로드

⑦ ①로 돌아가기

 

1) 이미지 다운 방법

$ docker 이미지 pull 옵션 name:태그버전

docker.io : docker hub에서 기본적으로 이미지를 끌어오는 저장소 → 루트폴더처럼 굳이 적지 않아도 되는 주소로 간주

$ docker pull 192.168.31.101:9999/도커이미지

위와 같이 프라이빗 저장소를 지정하는 것도 가능하고, 주로 기업들이 이런 식으로 저장소 정보를 바꿔 사용한다.

이처럼 위와 같이 저장소 주소가 따로 들어가면 docker.io가 아닌 것을 볼 수 있다.

 

2) 이미지 내부 정보

$ docker image inspect 이미지명:버전

위 명령어를 통해 이미지 내부 정보를 JSON 형식으로 확인할 수 있다.

- 주요 정보

① Id : 이미지의 아이디

② RepoTags : 레포지토리:태그명

③ Created : 생성일자

④ GraphDriver : 이미지 레이어 정보

⑤ Architecture : CPU의 아키텍처 정보

 

- httpd:2.4 이미지 내부 정보 확인

.Os : 첫번째 계층에 있는 Os 정보만을 빠르게 찾아준다.

JSON 형식이기 때문에 위와 같이 두 개 이상 타고 들어가야 하는 정보도 있다. 

위 명령어를 통해 Config 아래 노출포트인 ExposedPorts 정보를 얻을 수 있다.

$ docker image history 이미지명:버전

마찬가지로 history를 이용해서도 이미지 정보를 얻을 수 있다.

위와 같이 history 명령어로 주요 정보에 대해 체크할 수 있다.

위와 같은 명령어를 통해 파일 형식으로 저장하는 것도 유용하다.

# cd /var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256/

이미지 구성을 위한 레이어의 계층별 파일은 distribution ID를 부여받고 위 경로에 저장된다.

# ls 번호일부* 명령을 통해 위 경로에 있는 특정 레이어 파일을 조회할 수 있다.

 

※ 하부에 깔리는 이미지는 불변(Read only)으로 만들어지고, 컨테이너는 이미지 위에 Container Layer가 입출력 가능한 계층으로 추가된다. 

→ 하부 레이어를 수정할 수는 없지만, 추가적인 레이어를 구성할 수는 있다.

 

 

2. 이미지 업로드하기

1) 도커 로그인

- 레지스트리 : Dockerfile을 빌드해서 나온 이미지 혹은 docker commit을 통해 생성한 이미지를 저장하는 사이트 자체

- 레포지토리 : 해당 사이트에 생성한 본인 명의의 저장소

- 기본적으로는 hub.docker.com 저장소를 이용하여 이미지 업로드 → docker push 명령어로 수행

- docker push 

  ① docker login : 깃허브와 깃의 관계처럼 내 레포지토리에 올리기 위한 인증

  ② docker tag : 버전별로 태그라는 것을 붙여서 식별

$ docker login
username : 계정명
password : 비밀번호
$ docker logout // 위 정보 파기

위와 같은 방식의 로그인의 경우 base64로 저장되므로 복호화가 상당히 쉽기 때문에 주의해야 한다.

안전한 인증 처리를 위해 도커허브 사이트 로그인 후 [Account Settings] - [Security] - [Access Tokens]에서 토큰 발급 

$ vi .access_token
$ cat .access_token | docker login --username 본인계정명 --password-stdin

발급받은 토큰으로 .access_token 파일을 생성하여 작성하고, 위 명령어를 사용하여 로그인한다.

위와 같이 로그인이 잘 된 것을 확인할 수 있다.

 

2) 이미지 생성 가상머신과 테스트 가상머신 분리

이미지를 생성하는 가상머신과 테스트 가상머신을 분리하기 위해 완전한 복사로 복제를 진행해준다.

$ sudo hostnamectl set-hostname hostos2

위 명령어를 통해 hostname을 변경해준 후 host가 배정받은 내부망 ip 주소 변경(여기서는 192.168.56.102로 설정) 후 재시작한다.

 

3) 본인 원격 레포지토리에 이미지 업로드

본인 소유의 레포지토리에 본인이 생성한 이미지를 업로드하기 위해서는 다음 조건을 만족해야 한다.

① 이미지가 로컬에 생성되어 있어야 한다.

② 원격 레포지토리가 존재해야 하고, 해당 레포지토리에 접근할 수 있는 아이디로 로그인이 되어있어야 한다.

먼저, 도커허브에서 위와 같이 원격 레포지토리를 생성해준다.

위와 같이 도커허브 레지스트리가 잘 생성된 것을 볼 수 있고, 해당 레지스트리에 어떻게 이미지를 업로드 하는 도커 명령어도 볼 수 있다.

실습용 이미지는 위와 같이 NGINX를 기반으로 index.html을 수정한 myweb 이미지를 1.0과 1.1 버전 2개로 준비한다.

이미지를 확인해보면 레포지토리가 붙은 이미지와 원본이미지의 IMAGE_ID 값은 동일하다는 것을 볼 수 있고, 레포지토리로 분류되는 항목에 저장소 주소(내 계정 이름)도 잘 추가된 것을 확인할 수 있다.

docker push 명령어를 통해 해당 레포지토리에 이미지를 업로드한다.

도커 허브에서 확인해보면 잘 업로드된 것을 볼 수 있다.

 

※ 요구하는 양식을 정확히 맞춘다면 push 명령어로 레포지토리가 자동 생성 되기는 하지만, 일반적으로는 도커허브에 먼저 생성해둔 후 거기에 맞춰 업로드하는 방식을 사용한다. 자동 생성되는 레지스트리는 무조건 public으로 설정된다.

 

1.1 버전도 마찬가지로 push 명령어를 사용하여 업로드해주었고, 이미 존재하는 Layer는 제외하고 변경된 부분만 push되는 것을 확인할 수 있다. 즉, 버전이 바뀌어도 기존에 사용하던 레이어는 불변이므로 특정 지점까지는 같은 레이어를 유지한다.

 

4) 올라간 이미지 다른 호스트에서 검증하기

올라간 이미지가 다른 환경에서도 돌아가는지 검증하기 위해 hostos2에서 확인해봐야 한다.

위와 같이 pull 명령어로 아까 올린 이미지를 받으면 이미지가 잘 조회되는 것을 확인할 수 있다.

또한, run 명령어로 해당 이미지를 이용하여 컨테이너를 띄워보면 잘 돌아가고 있는 것을 확인할 수 있다.

 

5) 도커허브 거치지 않고 이미지 옮겨보기

일반적으로는 도커허브를 매개로 주고받지만, 도커허브를 거치지 않고 전달해야 할 때는 도커의 save 명령어를 통해 로컬파일로 저장한 후 물리 파일을 옮겨놓고 load 명령어로 불러와서 다른 호스트에 장착시킬 수 있다.

 

 

 

6) 이미지 삭제에 유용한 명령어

이미지 전체 삭제
$ docker rmi $(docker images -q) 

특정 이미지명을 포함한 것만 삭제
$ docker rmi $(docker images | grep 이미지명)

특정 이미지명이 포함되지 않은 것만 삭제
$ docker rmi $(docker images | grep -v 이미지명)

상태가 exited인 container 전체 삭제
$ docker rm $(docker ps --filter 'status=existed' -a -q)

 

'네트워크캠퍼스 > DOCKER' 카테고리의 다른 글

CLI에서 컨테이너 관리  (0) 2024.01.31
도커 레지스트리 구축  (0) 2024.01.31
Portainer를 이용해 GUI로 컨테이너 관리  (0) 2024.01.26
로컬 환경에서 도커 다뤄보기  (0) 2024.01.25
Play with Docker  (0) 2024.01.23

<PC에 Nginx 설치 후 포트포워딩하여 내 휴대폰(셀룰러)으로 접속해보기>

먼저, Nginx 설치 후 다운로드한 위치에서 nginx.exe 파일을 실행한 후 localhost에 접속해보면 위와 같이 nginx 페이지에 잘 접속되는 것을 확인할 수 있다.

ipconfig 명령어를 이용하여 해당 공유기의 게이트웨이 주소를 알아낼 수 있다.

게이트웨이 주소로 접속해보면 위와 같이 공유기 관리자 페이지를 확인할 수 있고, 현재 해당 공유기는 브릿지로 연결되어 있고, 게이트웨이를 통해 외부와 통신이 가능하다.

nginx.conf 파일의 내용 중 http 부분에서 server.listen 항목은 nginx 서버가 열리는 포트번호를 의미한다.

해당 포트번호를 3386이라는 임의의 숫자로 설정해주었다. 

또한, index.html 파일을 통해 웹페이지에 출력되는 문구를 위와 같이 수정해주었다.

서버 재시작 후 위와 같이 방금 설정한 포트번호로 localhost에 접속해보면 변경한 대로 잘 출력되는 것을 확인할 수 있다.

앞서 확인했던 와이파이의 IP 주소와 설정한 포트번호로 접속해도 위와 같은 결과가 나오는 것을 볼 수 있다.

먼저, 네트워크 설정 정보를 확인하기 위해 해당 관리자 페이지에 접속을 했다.

위와 같이 IP주소와 게이트웨이주소 등을 확인할 수 있다. 

브릿지는 내부 사설망 간의 연결만 제어가 가능하므로 외부 통신 설정을 하기 위해서는 NAT 장비에서 해줘야 한다.

NAT 장비(Gateway)도 마찬가지로 관리자 비밀번호를 이용하여 접속할 수 있고, 위와 같이 네트워크 설정 탭의 NAT 설정을 통해 포트포워딩을 진행할 수 있다.

내부 IP 주소는 현재 접속된 PC의 IP 주소로 설정했다.

위와 같이 외부포트는 55555로 설정했고, 내부포트는 nginx.conf에서 임의로 설정했던 3386으로 해주었다.

즉, 공유기 IP 주소의 외부포트(55555)로 들어온 요청은 내부 IP 주소의 내부포트(3386)로 포트포워딩하라는 의미이다.

해당 공유기 범위 내에서 nginx 서버가 열린 내 PC의 IP 주소가 211.241.92.180인 것을 확인할 수 있다.

휴대폰 셀룰러를 이용하여 위에서 확인한 IP 주소와 외부포트(55555)로 접속해보면 위와 같이 nginx 서버에 잘 접속되는 것을 확인할 수 있다.

 

<내 핸드폰에서 PC로 어떤 과정을 거쳐서 접속 되는지 그려보기>

 

 

'네트워크캠퍼스 > NETWORK' 카테고리의 다른 글

네트워크 쉽게, 더 쉽게  (0) 2024.02.01
3주차  (0) 2024.02.01
2주차  (0) 2024.01.26
모두의 네트워크  (0) 2024.01.22
1주차  (0) 2024.01.22

1. TCP와 UDP

1) TCP(Transmission Control Protocol)

- 특징 : 접속형, 신뢰형, 흐름 제어, 혼잡 제어, 바이트 스트림 통신

- 플래그 비트 표

플래그 의미
URG 긴급 포인터가 있음을 표시
ACK 확인 응답번호가 기술돼 있음을 표시
PSH 데이터를 가능한 빨리 응용계층에 보내야 함을 표시
RST 연결을 재설정 하기를 원함을 표시
SYN 연결을 초기화하기 위해 순서번호 동기화
FIN 송신측이 데이터의 전송을 종료

 

※ tcp 헤더 포맷

 

2) TCP 연결 방식

① TCP 연결 설정

- 능동적 열림 : 클라이언트는 서버가 열어 놓은 포트로 TCP 연결 요청, SYN을 보내는 쪽

- 수동적 열림 : 서버는 네트워크 응용을 수행하기 위해 정해진 포트를 열고 클라이언트의 요청을 기다림, SYN 메시지를 수신하는 쪽

- Three-way Handshake를 통해서 TCP 연결

② TCP 연결 종료

- Four-way Handshake를 통해서 TCP 연결 종료

※ Three-way Handshake & Four-way Handshake

 

3) UDP(User Datagram Protocol)

- 특징 : 비연결형, 비상태정보, 경량의 오버헤드, 비정규적인 송신률, 최선의 서비스

※ UDP 헤더 포맷

 

4) TCP와 UDP의 차이점

서비스 TCP UDP
신뢰성   - 패킷이 목적지까지 도달했는지 확인
  - 패킷이 도달할 때마다 ACK를 수신
  - 신뢰성 있는 프로토콜
  - ACK를 사용하지 않음
  - 패킷이 그들의 목적지에 도달되는 것을 보장하지 않음
  - 신뢰성 없는 프로토콜
연결   - 연결 지향적
  - 핸드쉐이킹 과정 수행
  - 비연결지향적
패킷 순서   - 패킷 내에 순서 번호 사용 X
혼잡 제어 O X
용도   - 신뢰성 있는 전송   - 스트리밍 비디오와 브로드캐스트 등 실시간 전송
속도의 오버헤드   - 상당한 양의 자원을 사용하며 UDP보다 느림   - 더 적은 자원을 사용하고 TCP보다 빠름

 

 

2. Telnet, SSH

1) Telnet

- 원격지의 컴퓨터를 인터넷을 통해 접속하여 자신의 컴퓨터처럼 사용할 수 있는 원격 접속 서비스

- 원격 컴퓨터를 이용할 수 있는 사용자 계정이 필요

- TCP 포트 : 23번

- 원격 접속 후 통신 시 바이트스트림 형식으로 데이터를 주고받고, 데이터를 암호화되지 않아 정보가 노출될 가능성이 큼

 

2) SSH (Secure SHell)

- 네트워크 상의 다른 컴퓨터에 로그인하거나 원격 시스템에서 명령을 실행하고 다른 시스템으로 파일 복사도 가능한 응용 프로그램

- TCP 포트 : 22번

- 다른 컴퓨터와 통신 시 암호화 기법을 사용하기 때문에 안전한 통신 기능이 제공됨

 

 

3. DNS

1) DNS(Domain Name System)

- 웹사이트 접속 시 외우기 어려운 IP 주소 대신 사용하는 도메인 이름

- 입력한 도메인을 실제 네트워크 상에서 사용하는 IP 주소로 바꾸고 해당 IP 주소로 접속하는 일련의 과정

 

2) DNS 구성 요소

① 도메인 네임 스페이스(Domain Name Space)

- 도메인 네임 스페이스라는 규칙으로 도메인 이름 저장 분산

 

② 네임 서버(Name Server) : 권한 있는 DNS 서버

- 해당 도메인 이름의 IP 주소를 찾음

 

③ 리졸버(Resolver) : 권한 없는 DNS 서버

- DNS 클라이언트 요청을 네임 서버로 전달하고 찾은 정보를 클라이언트에게 제공하는 기능 수행

 

3) DNS 동작 방식

 

① 도메인 주소 naver.com 입력 시 도메인 주소들을 가지고 있는 DNS 서버에 접속 

② 서버에 접속한 도메인과 연결된 IP 정보 (223.130.195.95)를 확인 후 IP를 사용자 PC에 전달

③ 사용자 PC는 전달받은 서버의 IP 주소로 접속

④ 서버의 IP로 연결된 브라우저에 서버의 내용(홈페이지)을 출력

 

위와 같이 잘 접속되는 것을 확인할 수 있다.

 

 

4. 공인 IP, 사설 IP

 

 

1) 공인 IP

- 사용자의 로컬 네트워크를 식별하기 위해 ISP(Internet Service Provider)가 제공하는 IP 주소

- 공용 IP 주소, 즉 외부에 공개되어 있는 IP 주소

- 공인 IP는 전세계에서 유일한 IP 주소를 갖게 됨

- 인터넷에 연결된 다른 PC로부터의 접근 가능 → 방화벽 등의 보안 프로그램 설치 필요

 

2) 사설 IP

- 일반 가정이나 회사 내 등에 할당된 네트워크의 IP 주소

- 로컬 IP, 가상 IP라고도 함

- IPv4의 주소 부족으로 인해 서브넷팅된 IP로 라우터에 의해 로컬 네트워크 상의 PC나 장치에 할당

- 하나의 네트워크 안에서 유일

- 외부 접근 불가능

 

 

5. 포트포워딩, 외부포트, 내부포트

1) 포트포워딩

- 외부의 기기에는 고정된 IP만 전달하고, 외부 기기가 내부에 접근할 수 있도록 내부의 공유기 관리자에서 설정하는 과정

- 내부 컴퓨터에 접근하려면 특정 포트를 열어줘야 함

- 고유 IP에 특정 포트를 부여해줌으로써 내부망에 특정 PC를 찾아갈 수 있도록 설정해주는 것

위와 같이 PC들은 각각의 고유 주소를 가지고 있고, 고유 IP에 특정 포트를 부여해줌으로써 내부망에 특정 PC를 찾아갈 수 있도록 설정해준다. 즉, 외부 IP 172.19.20.12:3306으로 접속 시 라우터는 출발지 포트(3306)와 매칭되는 목적지 포트(3306)로 연결시켜준다.

 

2) 외부포트

- 외부에서 접속 시 사용할 포트

 

3) 내부포트

- 내 컴퓨터에서 애플리케이션에 접속 시 사용하는 포트

 

※ 포트포워딩 실습

- 내 PC에 Nginx 설치 후 포트포워딩하여 내 휴대폰으로 접속해보기 (와이파이 연결 X, 셀룰러로 연결)
- 내 핸드폰에서 PC로 어떤 과정을 거쳐서 접속 되는지 그려보기.

 

 

 

 

'네트워크캠퍼스 > NETWORK' 카테고리의 다른 글

네트워크 쉽게, 더 쉽게  (0) 2024.02.01
3주차  (0) 2024.02.01
포트포워딩 실습  (0) 2024.01.26
모두의 네트워크  (0) 2024.01.22
1주차  (0) 2024.01.22

※ 컨테이너를 매번 ps로 조회하지 않고 GUI로 좀 더 편리하게 관리할 수 있다.

 

먼저, pull 명령어를 통해 portainer 이미지를 받아온다.

로컬쪽과 정보를 공유시키기 위해 볼륨이라는 것을 생성한다.

$ docker run -d -p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
--restart=always \
portainer/portainer-ce

위 명령어를 통해 해당 이미지를 실행시킨다.

볼륨 연결을 통한 볼륨마운트라는 것을 통해 호스트의 파일을 컨테이너로 전송할 수 있는 통로를 마련한다.

위와 같이 ps 명령어로 조회해보면 잘 실행되고 있는 것을 확인할 수 있다.

브라우저를 열고 도커를 돌리고 있는 인스턴스 내부 ip와 포트포워딩한 포트로 접속해보면 위와 같이 계정 생성 창이 나온다.

위와 같이 계정을 생성한다. (pw:dockerubuntu)

생성 성공 시 위와 같이 환경설정과 현재 상태를 확인할 수 있다.

이와 같이 docker ps와 같은 결과를 GUI 형태로 보여주는 것을 확인할 수 있다. 

$ docker exec -it 컨테이너명 전달할내역

위 명령어 역시 대체하여 잘 사용되며 콘솔에 좀 더 편하게 접근할 수 있게 된다.

위와 같이 간단한 클릭만으로 쉘을 잘 띄워주는 것을 확인할 수 있다.

또한, 보편적으로 많이 사용하는 공식이미지의 경우 App Templates이라는 것을 이용하여 배포할 수도 있다.

'네트워크캠퍼스 > DOCKER' 카테고리의 다른 글

도커 레지스트리 구축  (0) 2024.01.31
도커 이미지 구조  (0) 2024.01.30
로컬 환경에서 도커 다뤄보기  (0) 2024.01.25
Play with Docker  (0) 2024.01.23
로컬 환경 설정 (virtualbox + ubuntuos)  (0) 2024.01.22

※ 객체지향으로 개발해야 하는 이유

- 개발 과정에서 자주 발생하는 문제를 객체지향 프로그래밍이라는 방법론으로 해결하기 위해 사용

- 변수와 함수를 밀접하게 하나의 클래스 소속으로 만들었기 때문에 관리가 쉽고 테스트가 용이함

 

1. 클래스와 상속

1) 상속과 다형성

- 상속은 extends 키워드 사용

- 상속을 전제로 부모타입 변수에는 부모타입이나 자식타입의 변수를 모두 대입 가능

- 부모타입으로는 다형성적으로 대입된 객체가 자식타입이어도 부모측 자원만 호출 가능

public static void main(String[] args) {
    Parent parent = new Parent(); // 부모타입 변수에 부모타입 객체 대입
    Parent parentTypeChild = new Child(); // 부모타입 변수에 자식타입 객체 대입, 다형성
    Child child = new Child();

    parent.process();
    System.out.println("--------------");
    parentTypeChild.process(); 
    //parentTypeChild.childProcess(); // Child 타입객체 내부에 선언된 요소는 Parent로 호출 불가
    System.out.println("--------------");
    child.process();
    child.childProcess();
}

 

 

2) 메서드 오버라이딩 (재정의)

- 부모클래스에서 상속받은 메서드를 자식에서 재정의하는 것

- 메서드의 시그니처(리턴타입, 메서드명, 요구하는 파라미터)는 같아야 함

- 오버라이딩이 정의되었다면 부모 타입으로도 자식 측의 재정의 메서드 호출 가능

public static void main(String[] args) {
    Parent parent = new Parent();
    Parent parentTypeChild = new Child(); // 다형성

    parent.process();
    parentTypeChild.process(); // 오버라이딩이 전제된 메서드는 부모타입으로도 자식측 메서드 자동 호출
}

 

 

3) 메서드 오버로딩 (중복정의)

- 메서드명이 같아도 요구 파라미터의 개수나 타입이 다르면 허용

- 리턴자료만 다른 것은 허용 불가

// 메서드 오버로딩 요건
// 1. 메서드 이름을 중복하여 여러 개 선언
// 2. 단, 선언된 같은 이름의 메서드 간 요구 파라미터의 개수나 타입은 달라야 함
// 3. 리턴타입의 동일여부는 오버로딩에 영향을 주지 않음
public int add(int num1, int num2) {
    return num1 + num2;
}

// return 타입만 다르게 오버로딩 하는 것은 불가능
// public long add(int num1, int num2) {
//   	return num1 + num2;
//}

public double add(double num1, double num2) {
    return num1 + num2;
}

public long add(long num1, long num2) {
    return num1 + num2;
}

 

 

4) 상속 관련 시 주의사항

- 상속 시 메서드와 필드를 모두 재사용할 수는 있지만 기본적으로는 필드 재사용을 전제로 해야 함

- 메서드 재사용을 위해 상속을 쓴다면 전략 패턴 구성 (Composite) 활용

☆ 리스코프 치환 원칙 : 부모타입으로 할 수 있는 일은 자식타입으로도 할 수 있어야 함

 

 

2. 추상 클래스와 인터페이스

1) 추상클래스

- abstract 키워드 활용

- 일반적으로 인스턴스를 생성할 수 없음 (생성자에서 추상메서드에 대한 오버라이딩을 직접 해주면 가능)

- 일반적으로 하나 이상의 추상 메서드 포함, 해당 추상 메서드를 상속하며 오버라이딩해야만 인스턴스 생성

public abstract class AbstractClass {
	public void implementedMethod() {
		System.out.println("AbstractClass 내부에서 직접 구현된 메서드");
		this.abstractMethod(); // 추후 구현될 템플릿 메서드
	}
	
	abstract public void abstractMethod();
};
public class ExtendedClass extends AbstractClass {
	@Override
	public void abstractMethod() {
		System.out.println("ExtendedClass에서 정의된 추상 메서드");
	}
}
public class AbstractClassExMain {
	public static void main(String[] args) {
		// 추상클래스의 인스턴스를 직접 생성해주고 싶다면?
		AbstractClass abstractClass = new AbstractClass() {
			@Override
			public void abstractMethod() {
				// 생성자에서 직접 추상메서드를 구현해주면 상속 없이 생성 가능
				System.out.println("Abstract Class 내부에서 정의한 abstractMethod()");
			}
		};
		
		abstractClass.implementedMethod();
		abstractClass.abstractMethod();
		
		System.out.println("--------------");
		
		AbstractClass extendedClass = new ExtendedClass();
		extendedClass.implementedMethod();
		extendedClass.abstractMethod();
	}
}

 

 

2) 인터페이스

- 다중상속이 가능

- 디폴트 메서드를 이용하여 정의된 메서드와 정의되지 않은 메서드 호출 가능

public interface SomeInterface {
	void someMethod();
	
	default void defaultMethod() {
		// default 키워드를 메서드에 붙이면 인터페이스 내부에서도 구현된 메서드를 가질 수 있음
		this.someMethod();
	}
}
public interface AnotherInterface {
	void anotherMethod();
}
public class ImplementsClass implements SomeInterface, AnotherInterface { // 다중상속 가능
	@Override
	public void anotherMethod() {
		// 리스코프 치환 원칙  : 자식의 실행 커버리지는 부모의 실행 커버리지보다 넓어져서는 안 됨.
		System.out.println("ImplementsClass의 anotherMethod()");	
	}

	@Override
	public void someMethod() {
		System.out.println("ImplementsClass의 someMethod()");	
	}
}
public class InterfaceExMain {
	public static void main(String[] args) {
		SomeInterface someInterface = new ImplementsClass();
		AnotherInterface anotherInterface = new ImplementsClass();
		
		someInterface.someMethod();
		anotherInterface.anotherMethod();
		
		ImplementsClass implementClass = new ImplementsClass();
		
		// SomeInterface, anotherInterface로는 양쪽 모두를 호출할 수 없음
		//someInterface.anotherMethod();
		//anotherInterface.someMethod();
		
		// ImplementsClass 구현체 타입으로는 양쪽 모두 호출 가능
		implementClass.anotherMethod();
		implementClass.someMethod();
	}
}

 

 

※ 일반적인 상황에서는 인터페이스를 쓰면 정답인 경우가 많다.

 

※ 추상클래스를 인터페이스 대신 사용하는 경우

① 인스턴스 변수(필드)를 정의해야 하는 경우 : 인터페이스는 상수만 정의 가능

② 생성자가 필요한 경우 : 인터페이스는 내부에 생성자 정의 불가

③ Object 클래스의 메서드를 오버라이딩 하고 싶은 경우

 

 

3. ENUM(이늄)

열거상수 이늄을 이용하면 강력한 객체지향 코딩 기법을 배울 수 있다.

import java.util.function.BiFunction;

public enum CalculateType {
	// basic과는 다르게 연산 종류와 연산 메커니즘을 함께 정의
	// 관련된 자료들이 잘 모여있음(응집도가 높아졌음)
	ADD((num1, num2) -> num1 + num2),
	MINUS((num1, num2) -> num1 - num2),
	MULTIPLY((num1, num2) -> num1 * num2),
	DIVIDE((num1, num2) -> num1 / num2);

	// 위에 붙인 익명함수들은 Bifunction이라는 타입으로 멤버변수를 정의해야 사용 가능
	// 따라서, 생성자에서 BiFunction을 주입받도록 처리
	CalculateType(BiFunction<Integer, Integer, Integer> expression){
		this.expression = expression;
	}
	
	private BiFunction<Integer, Integer, Integer> expression;
	
	public int calculate(int num1, int num2) {
		return this.expression.apply(num1, num2);
	}
}

이늄 자료를 위와 같이 정의하면 CalculateType은 총 4개의 자료만 가질 수 있다.

이때 타입에 따른 연산까지 함께 이늄에 정의해둘 수 있고, BiFunction은 입력자료형과 리턴자료형을 정의할 수 있도록 해준다.

함수를 마치 하나의 객체처럼 다룰 수 있게 해준다.

public class CalculateCommand {
	private CalculateType calculateType; // ADD, MINUS, MULTIPLY, DIVIDE 중 하나만 대입 가능
	private int num1;
	private int num2;
	
	public CalculateCommand(CalculateType calculateType, int num1, int num2) {
		this.calculateType = calculateType;
		this.num1 = num1;
		this.num2 = num2;
	}
	
	public CalculateType getCalculateType() {
		return calculateType;
	}
	
	public int getNum1() {
		return num1;
	}
	
	public int getNum2() {
		return num2;
	}
}
public class Client {
	public int process(CalculateCommand calculateCommand) {
		CalculateType calculateType = calculateCommand.getCalculateType();
		int num1 = calculateCommand.getNum1();
		int num2 = calculateCommand.getNum2();
				
		// basic쪽과는 달리 client에 상세한 계산 로직이 포함되지 않음
		// 클라이언트는 어떤 연산을 수행할지만 알고, 해당 로직의 상세한 내용을 모름
		// 클라이언트는 해당 로직에 대한 책임이 없음
		// 수정이 필요할 때 클라이언트측 코드를 볼 필요가 없음
		int result = calculateType.calculate(num1, num2);
		return result;
	}
}
public class AdvExMain {

	public static void main(String[] args) {
		// 클라이언트가 요청할 때 calculateCommand 객체가 제공한 데이터를 사용
		CalculateCommand calculateCommand = new CalculateCommand(CalculateType.ADD,100,3);
		Client client = new Client();
		int result = client.process(calculateCommand);
		
		System.out.println(result);
	}
}

 

 

 

4. 예외

1) checked exception vs unchecked exception

- checked exception : Exception 객체를 상속한 예외, 컴파일 시 예외처리를 문법적으로 강제

- unchecked exception : RuntimeException 객체를 상속한 예외, 예외처리를 따로 강제하지 않음

 

2) 왜 checked exception을 쓰지 않는가?

- 대부분의 경우 로직만으로는 예외를 처리할 수 없는 경우가 많음

ex) 사용자에게 파일 이름을 입력받아 서버에서 리턴해주는 로직

파일 잘못 입력 시 FileNotFoundException이 발생하는데, 사용자에게 파일을 입력해달라는 말 말고 직접적으로 처리할 수 없는데 굳이 예외처리 구문을 사용할 필요가 없음

- checked exception의 경우 내부에서 어떤 동작을 하다가 예외를 발생시켰는지 알 수 있는 경우가 많음 → 캡슐화 원칙이 깨지는 문제

 

 

5. Object 클래스

모든 클래스의 부모이자 Object 클래스가 가진 메서드들을 오버라이딩하여 사용하는 경우가 많음

 

1) .equals()

- 동일성 : 두 대상이 "똑같은 대상"이어야 성립 → ==를 이용해 비교

- 동등성 : 대상은 다르지만 어떤 다른 기준에 의해 같음을 확인했을 때 성립 → equals() 활용

 

2) .hashCode()

- 주로 equals와 함께 사용

- map 자료 등에 대해 비교 시 주로 hashcode를 이용해 1차적인 필터링을 하고, 그 결과에 대해 equals로 조회

 

 

 

 

 

 

 

 

 

 

 

'네트워크캠퍼스 > JAVA' 카테고리의 다른 글

객체지향적 코드 작성  (0) 2024.01.29
프로세스와 쓰레드  (0) 2024.01.24
자바 API  (0) 2024.01.18
자바의 예외처리전략  (0) 2024.01.18
예외처리  (0) 2024.01.17

+ Recent posts