Skip to main content

· 2 min read

NFS mount를 하는데 공유기를 사용하거나 하는 특정 환경에서 port forwarding이 필요한 경우가 있다.

(Mount 하고자 하는 경로가 있는 장비를 A, mount point를 지정할  (mount 명령을 실행할) 장비를 B라고 했을 때 A가 특정 환경에 놓인 경우에 해당함)

NFS mount에 대한 프로세스를 확인해보면 (rpcinfo -p), 3가지의 process가 관계되어 있음을 알 수 있는데 portmapper, nfs, mountd이다.

별도의 설정이 가해지지 않았다면 portmapper는 111, nfs는 2049 port가 사용되지만 문제는 mountd.

mountd는 기본적으로 임의의 port를 할당해서 사용하고 (service nfs-kernel-server restart 등으로 restart 해보면 port 번호가 바뀜) mountd의 port 번호를 고정하려면 /etc/services 파일을 수정해야 한다.

/etc/services 파일을 열어보면 mountd에 대한 내용이 없다.

원하는 위치에 mountd xxxxx/tcp와 mountd xxxxx/udp를 추가한 후 nfs-kernel-server를 restart 해보면 설정한 port로 고정됨을 확인할 수 있다.

 

Port forwarding은 111, 2049, xxxxx (mountd 설정 port) 세 가지로 해주면 문제없이 nfs로 mount 가능.

 

난 왜 mount를 했는가?

굳이 공유기를 사용하는 환경에서 mount를 하려고 했던 이유가 약 3기가 정도 되는 파일들을 복사하기 위함이었는데

속도가 느려서 파일 압축 후 scp로 전송해보니 scp가 더 빠르다.

파일 복사 등을 이유로 mount 하지는 말아야겠다.

· 3 min read

node.js로 개발할 때 귀찮은 것 중 하나가 수정사항을 reload 하는 것이다.

node process로 실행되기 때문에 항상 실행중인 process를 죽이고 다시 실행해줘야 하는데

개발중일 때에는 참 귀찮은 작업이다.

이런 경우에 많이 사용하는게 nodemon이나 supervisor 라는 패키지인데

nodemon의 경우 수정사항이 발생했을 때 자동으로 reload 해준다.

 

1. Eclipse에서의 nodemon 문제점

Eclipse에서 nodeclipse 라는 플러그인으로 개발중인데 nodeclipse 설정에는

node 경로와 express 경로에 대한 설정만 존재한다.

node 대신 nodemon을 입력해주면 nodemon으로 실행하지 않을까 싶었는데 오류가 발생한다.

nodemon의 내부를 보니 /usr/bin/env node로 path를 뒤져 node를 실행하도록 되어있는데

eclipse에서 env를 인식하지 못하는 것 같다. (아직 해결책을 찾지 못함)

 

2. forever & deploy

개발이 종료되고 서비스할 때 데몬으로 돌리기 위해 forever라는 패키지를 많이 사용하는 것 같다.

node.js로 개발한 것들을 백그라운드로 돌리기 위해 nohup을 사용하곤 했는데 그런 불편함을 덜어줄 수 있을 것 같다.

deploy를 위해서는 수정사항을 reload 할 수 있어야 하고 항상 백그라운드에서 안정적으로 동작하고 있어야 하는데

nodemon은 reload는 가능하지만 백그라운드로 동작시키려면 역시 nohup을 사용하는 등의 다른 방안을 함께 고려해야 한다.

게다가 시스템 로그를 남기는 문제도 생각해야 하는데 nohup만 사용해서는 뭔가 아쉽다.

forever는 백그라운드에서 돌릴 수 있고 restart 기능이 있다.

하지만 restart는 연결중인 session까지 끊어버릴거라 보완이 필요하다.

시스템 구조 측면에서의 deploy 방안을 고민해야 할 것 같다.

· 6 min read

GIT 설정을 따로 할 일이 별로 없었는데 repo까지 사용하는 환경에서 자동화 하다보니 필요해졌고, 공식 페이지가 진리라 따로 부연할 꺼리도 없지만 이번에 사용해 본 설정만이라도 정리해보기로 했다. 최소한 이런 설정을 어떤 경우에 사용할 수 있는지라도 설명이 될 수 있을 것 같다.

1. 상황

언급했듯이 repo를 사용하는 환경이다. repo에 대해서 간단히 설명하자면, 여러개의 GIT 저장소를 가지고 빌드를 하거나 개발을 할 때 편의를 제공하기 위해 Google에서 만든 툴로 AOSP (Android Open Source Project)에서 사용되고 있다. Python으로 개발된 script의 모음이고 manifest 내부를 보면 어느 저장소를 어느 경로에 갖다 두는지에 대한 내용들이 주를 이루고 있다.

repo 이외에 Gerrit도 사용하고 있다. Gerrit은 역시 AOSP에서 사용되는 Code Review 툴로 code review 이외에 다른 기능을 위해 사용하는 것도 가능하다. (예를 들면 코드 검증, review 만을 위해서가 아니고)

저장소를 Gerrit에 연결하기 위해서는 Gerrit project로 등록을 해야 하는데, 등록이 되면 저장소 별로 따로 사용자 인증에 필요한 절차를 수행할 필요 없이 Gerrit에서 관리가 가능해진다.

이 부분도 Gerrit의 장점이라고 할 수 있다. GIT을 쓰면서 제일 불편한 것중 하나가 사용자 인증과 관리니까. 또 그래서 여러가지 툴들도 꽤 등장했다. Gitblit, Gitlab, 등등. 서비스에서도 그런 기능을 제공하기도 한다. 많이 쓰고 있는 github이 그 예가 될 것이고.

Gerrit 설정이 갑자기 바뀌면서 repo init을 할 때 command 형태를 변경해야만 했는데 (계정과 gerrit의 port를 추가해야 했다. 일반적으로 별도 설정없이 Gerrit을 그대로 쓰면 계정과 port 정보를 꼭 넣어줘야 하는데 이곳 환경에서는 최초에 없어도 되었었지만, 추가해야 하는 형태로 다시 변경되었음), 문제는 init을 하더라도 manifest 파일에는 계정과 port 정보가 빠져있는 형태의 대표 url만 기록되는 곳에서 발생했다. repo sync 이전에 manifest 파일을 열어서 fetch url 부분을 수정해야만 했다.

2. 조치

원래 나와 같은 위치에 있는 사람들에겐, 대다수가 권한과 정보에 대해서 상당히 인색한 편이다. 아쉬울게 없다. 나만의 해결책을 만들어서 보고하고 마음에 들지 않으면 이슈로 제기하면 되니까. 그래서 처음엔 script를 만들었다. manifest 파일을 수정하는 script. 처음엔 만족스러웠지만 사실 manifest를 수정한다는건 (자동이든 수동이든) 부담도 있고 비효율적이다. 결국은 GIT config를 변경해서 script를 사용하지 않고도 문제를 해결할 수 있었다.

3. GIT config의 종류

Gerrit / repo 모두 GIT을 사용하기 때문에 GIT 설정만으로도 위의 문제를 해결하는게 가능한데, 우선 GIT의 설정은 크게 세 가지로 나뉜다.

local, global, system 설정이 그것인데 각각 저장소 대상 개별 설정, 사용자 대상 설정, 전체 사용자(시스템) 대상 설정을 의미한다.

git config 명령에 option으로 설정 범위를 선택하는게 가능한데 설정된 내용은 각각 $GIT_DIR/config, ~/.gitconfig, $(prefix)/etc/gitconfig 파일에 기록된다.

그러므로 config 명령을 사용하기 싫으면 각각의 파일을 직접 수정해도 무방하다.

4. GIT url insteadof 설정

아래와 같은 형태의 설정을 하면 GIT으로 요청하는 모든 url 중에 해당되는 url을 대체하는게 가능하다

url.<base>.insteadOf (당연히 대소문자 구분)

예를 들어, http://test.com에 대한 요청을 ssh://test@test.com:29418 로 변경하고자 한다면 이렇게 설정하기만 하면 간단히 정리된다.

git config --global url.ssh://test@test.com:29418.insteadOf http://test.com

 

이렇게 해결.

그런데 설정 형식이 조금 못생겼다는 생각을 지울수가 없다. 마침표를 왜 넣지? 다른 방법도 있었을텐데.

· 2 min read

일반적으로 IP를 가져오고자 할 때 InetAddress class의 static 함수를 사용해서 아래와 같이 호출하는 것 같다.

InetAddress.getLocalHost().getHostAddress();

하지만 여러가지 Network Interface가 붙어있는 machine인 경우 엉뚱한 IP가 리턴되는 경우가 있다.

나의 경우엔 loopback address만 나오는 상황이 있었는데 당연히 그 주소를 그대로 사용할 수는 없었다.

이런 경우 NetworkInterface class를 사용해서 Network Interface 별로 모두 주소를 가져와 확인하는게 가장 확실한 방법이 될 것 같다. 아래처럼 호출하면 Enumeration 으로 모든 Network Interface를 얻을 수 있고 그걸 기반으로 InetAddress를 얻어와 주소가 loopback address인지 등을 비교하면 정확한 주소를 얻을 수 있다.

NetworkInterface.getNetworkInterfaces();

나의 경우엔 loopback address가 아니고 site local address인 경우만 맞는 것으로 판단해서 사용했다.

· 8 min read

처음 자동화 업무를 시작하면서부터 ssh를 본격적으로 사용하기 시작했다.

그 당시에도 ssh가 제대로 지원되지 않는 환경인 경우 (거의 임베디드 리눅스 환경) 제대로 지원될 수 있도록 기능이나 기타 필요한 내용들을 관련 개발부서에 요청하곤 했었다. ssh를 사용하면 할수록 자동화에 필수적이라는 생각이 드는 이유는

1. 매우 간단하다

어차피 command의 형태로 되어있고 형식이 간단하고 linux와 os x의 경우 기본으로 포함되어 있기 때문에 특별히 뭘 구현하지 않아도 된다.

2. 인증여부를 묻지 않게 할 수 있다

간단하게 장비간 key만 교환하고 등록하면 인증시 계정의 패스워드가 아니라 key의 패스워드를 묻게 되는데 key 생성시 패스워드 없이 생성이 가능하기 때문에 인증여부를 묻지 않는 것처럼 보일 수 있다. 장비에 대한 제어를 자동으로 해야 하는데 인증여부 등을 묻거나 하면 해결하기가 어렵다. 물론 제어 가능하도록 무언가를 구현해서 사용해도 된다. 하지만 구석에 박혀있는 작은 기능이 필요할 때 이전에 구현된 거대한 덩어리들을 수정하지 않아도 되니 꽤 유용하다.

3. 권한만 있다면 해당 OS의 거의 모든 기능을 사용할 수 있다

ssh도 shell 이므로 shell에서 할 수 있는 기능들은 거의 사용 가능하다. 단 한번에 수행되어야 하는 작업이 많은 경우 문제가 생기기도 하고 command가 복잡해져서 나의 경우엔 복잡한 작업을 수행하는 script를 하나 따로 만들어서 ssh로는 그 작업을 하는 script만 호출하는 식으로 처리하곤 한다.

4. 파일 전송이 간단하다

ssh와 시리즈로 사용하는 scp로 파일 전송을 하곤 하는데, ftp를 별도로 서비스하거나 mount 같은 걸 하지 않아도 되니 아쉬울 때 쓰기 좋다.

 

하지만 ssh로 도배를 해보니 몇가지 아쉬운 점이 있었는데

1. 귀찮은 키교환

자동화에 사용하는 장비 수가 늘어날수록 키를 서로 교환하고 등록하는 과정이 무척 귀찮아진다. 이 과정 마저도 어느정도 자동화를 할 수도 있지만 신규 장비에 접속해서 키는 생성해야 하니 짜증스럽다. 예를 들어 한번에 장비가 10대만 늘었다고 하더라도 반복작업이 수십회는 된다. 장비간 키교환이 귀찮아 아예 키를 통째로 전송해서 같은 키를 사용하기도 하는데 이 경우 보안문제도 신경쓰인다.

2. 늘어나는 script

ssh가 간편하기도 하고 간단한 기능을 쉽게 땜질하는게 가능하지만 땜질이 늘어날수록 script가 늘어나게 된다. 한 번은 script가 수십개가 되었던 적도 있었는데 아무리 작명을 잘해도 어떤게 어떤건지 내용을 열어보거나 실행해봐야 아는 상태에 이르렀다. 게다가 특정 장비간 기능이 달랐기 때문에 script도 퍼져있어서 관리하기가 쉽지 않았다.

3. Known host 문제

장비의 IP가 변경되는 경우 서로 키를 교환하고 있는 상태이기 때문에 인증에는 문제가 없다고 하더라도 변경된 IP로 접속한 기록이 없기 때문에 최초 접속시 known host로 등록할거냐고 묻는다. 이게 자동화에 방해가 되므로 IP 변경시 한번씩은 접속을 해주거나 known host로 등록을 해줘야 하는 번거로움이 발생한다. 물론 sshd 설정에 아예 known host 여부를 따지지 않게 하는 부분이 있긴 하지만 자동화 이외의 다른 용도로 사용되거나 하면 그런 설정을 하기가 꺼려진다.

 

그래서 적합한 방안이라고 생각하는 것들은 이렇다.

1. Script의 공용화

Script는 가능한 적게 만드는게 좋을 것 같다. 그리고 여러 장비에서 사용해야 하는 script 라면 script는 장비 하나에만 두고 공통으로 사용할 수 있는 방안을 마련하는게 좋을 것 같다. 처음엔 한 곳에 script를 두고 필요할 때 마다 다른 장비에 복사해서 호출하게 하곤 했었는데 영 지저분하고 특정 상황에서는 script를 복사하면 안되는 경우도 있어서, 아예 script가 있는 경로를 각 장비에서 mount 해서 사용해보고 있는데 아직은 괜찮은 것 같지만 좀 더 지나봐야 알겠다.

2. 기능의 구조화 및 정리

단편적인 기능들을 간단하게 추가만 하다가 어느 시점이 되면 기능들을 모아서 별도의 모듈로 개발하는게 좋겠다는 생각을 한다. 물론 공수가 들어가는 문제이지만...

3. 별도의 인증과정

무작정 키를 장비 한대에서 등록하고 관리하고 교환하고 할 게 아니라 인증 기능을 담당하는 장비나 서비스를 별도로 분리하는게 좋을 것 같다. 난 이 작업을 진행하는 것 자체가 꽤 재미있을 것 같다. 이미 나와있는 것들도 많겠지만.

· 3 min read

Tomcat에 간단한 페이지들을 올려보기만 했지 다양한 관점에서 운용해보진 않아서 이제서야 이런걸 알아본다.

특정 context에 변경사항이 발생했는데, 나의 경우엔 DB 접근정보를 담고 있는 properties 파일 수정, 작지만 여러가지가 서비스되고 있었기 때문에 restart는 좋지 않은 대안이라는 생각이 들었다.

물론 작다는 이유로 그냥 restart 해버리고 메일 몇 개 버리는게 더 빠를지도 모른다.

 

알아보니 특정 context를 위와 비슷한 이유로 reload 하기 위해서는 몇가지 방법이 존재하는데 우선 내가 사용한 방법의 절차는 아래와 같다.

 

1. conf/tomcat-users.xml 파일 수정

Tomcat 관리자 계정 정보를 등록해줘야 한다.

Tomcat 관리자도 처음 등록해 봤다. 이런 것도 있었구나 하면서.

파일을 열어보면 <tomcat-users> 태그 안에 예문이 주석처리 되어있는데 나의 경우엔 manager-gui role을 하나와 사용자 계정을 하나 추가했다.

<role rolename="manager-gui"/>
<user username="admin" password="your_password" roles="manager-gui"/>

추가된 사용자 정보는 물론 바로 load 되지 않는다.

 

2. Tomcat root > Tomcat Manager > reload

설치된 Tomcat의 홈으로 가보면 좌측상단 메뉴에 Tomcat Manager 메뉴가 있다.

눌러서 Tomcat Web Application Manager 페이지로 가보면 각 application 들이 표시되어 있고 start / stop / reload / undeploy 기능이 제공되고 있으니 클릭.

 

3. URL 호출

URL 호출만으로 remote control이 가능한데, 아래와 같은 형태의 url을 wget이나 curl로 간단하게 호출하거나 또 다른 방법을 사용해도 되겠다.

http://{your_host}/manager/html/reload?path=/your_context

관리자 계정과 패스워드는 wget이나 curl이 제공하는 옵션을 사용하면 된다.

· 5 min read

예전에 구축해 놓은 시스템은 shell과 perl, jar로 구성되어 있다. 누구나 접근하기 쉽기도 하지만 사용해야 하는 다른 시스템도 대부분 shell과 perl로 되어 있었기 때문에 빠른 시간안에 구축하기 위해 선택한 방안이었다.

문제는 script 들로 구성되어 있다보니 재사용성이 낮았고 여기저기 파편화되어 구축 이후의 관리에 애를 먹게 되었는데 이번에 전체적으로 개선할 수 있는 기회가 만들어져서 이전의 문제들을 줄일 수 있는 방법들을 고민하게 되었다.

재사용성이 떨어지는 script 들을 가능한 줄이되 script를 이용하는게 더 효율적인 부분은 groovy를 사용해보기로 마음을 먹게 되었는데 깊은 부분을 몰라서 그런 것도 있지만 해결하기 어려운 부분이 생겼다. 임시 방안으로 땜질해 놓긴 했지만.

1. Groovy의 장점

1-1. 구조적인 script

Shell이나 perl과는 다르게 구조적으로 작성 가능하다는게 가장 큰 장점이라고 생각한다. 일단 표면에 내세운 것도 Object Oriented 인데, 실제 클래스 구현과 객체 생성이 가능하다. 재사용성을 높이면서 동시에 지저분해지기 쉬운 script를 간결하게 정리할 수 있다. 물론 본인이 마음을 먹어야 가능한건데, 작성방식에 따라 다른 script처럼 만들 수도 있겠다.

1-2. Java와의 유사성

기본 문법이 Java와 꽤 유사하다. 예약어나 접근 제한의 의미는 약간 다르고 문법적인 규칙의 강도는 Java에 비해서는 약한 편이다. 예를 들어 세미 콜론은 붙여도 되고 안붙여도 무방하며, println의 경우 괄호로 내용을 감싸도 되지만 풀어놔도 괜찮다. return도 마찬가지로 꼭 넣을 필요가 없고, data type을 굳이 명시하지 않고 def로 표현해도 되는데 이 부분은 Javascript와도 닮은 부분이다.

 

2. 문제가 되는 부분

2-1. Bash command

나의 경우엔 groovy에서 외부 프로세스를 생성해서 호출해야만 하는 상황이 여럿 존재하는데, 그 중에 source command로 메모리에 올려두고 다음 shell script에서 사용해야 하는 경우는 정말 방법이 없어보였다.

2-2. 연속된 command의 호출

연속된 shell script의 호출이나 command의 호출은 && 연산자로 어떻게 해볼 수는 있겠지만 근본적으로 깔끔하게 groovy에서 해결할 수는 없었다. groovy로 그 기능들이나 내용을 다시 구현하지 않는 이상.

 

3. 해결

결국은 기존의 shell script가 꼭 필요한 경우는 그대로 쓰기로 하고 groovy는 wrapping 하는 정도에서 마무리하게 되었다. Wrapping이야 shell script로 불가능한 것은 아닌데 훨씬 좋아진 점은 가독성이고, shell script가 위에서부터 아래 방향으로 순차적으로 해석되어 실행되는 것에 반해 groovy는 그렇지 않기 때문에 필요한 함수를 script 하단에 정의할 수도 있었기 때문에 훨씬 정리된 script를 만들 수 있었다. 특히 사용 가능한 라이브러리 (예를 들어, json 관련 클래스)가 많아서 짧게 구현하는데 유용했다.

· 4 min read

JSP로 간단한 페이지를 만드는데 로컬에 있는 로그파일을 읽어서 화면에 보여주기로 했다.

이미 WAS의 context path 안에 있는 파일이고 txt 라서 url 호출만으로도 빠르게 파일 내용을 보여주는데는 문제가 없었는데, 정말 raw 파일이라서 보기도 힘들고 예쁘지 않아 table 안에 넣어서 보여주기로 결정했는데 단순하게 BufferedReader class에 있는 readline을 사용해보니 너무 느렸다.

속도도 문제였지만 로그파일이 보통 1MB 이상이고 반복적인 로딩이 예정된 상태라 과부하를 신경쓰지 않을 수 없는 상태여서 다른 방안을 생각할 수 밖에 없었다.

readline 함수는 BufferedReader 라는 클래스에 정의된 함수로 유사한 함수들이 많다. 그 중 read 라는 함수가 있는데 정의를 살펴봤는데, 우선 BufferedReader는 Reader 클래스를 상속한 함수로 Reader 클래스에 정의된 간단한 형태의 read(char[] cbuf)는 read(char[] cbuf, int off, int len)을 호출하도록 정의되어 있다. 그리고 read(char[] cbuf, int off, int len)은 abstract 함수로 각각의 서브클래스에서 정의되는 형태이다.

그래서 BufferedReader 클래스의 read와 readline을 비교해 본 결과 아래와 같은 차이를 발견할 수 있었다.

- read는 loop 내부의 함수 호출이 작다.

- readline은 loop 내부의 함수 호출이 많다

- read는 system 함수나 원시 함수 위주의 호출

- readline은 read 보다 상위의 함수나 클래스 사용

- read는 buffer size 지정하지 않았을 때 기본 8K를 한꺼번에 읽어들임

- readline은 size와 관계없이 line feed까지만 읽어들임

 

요약해보면, readline은 무조건 linefeed를 기준으로 읽어들이는데 동작 자체가 무겁고 read 함수는 정의된 block 단위로 읽어들이는데 정의된 동작 자체가 가볍다. 당연히 line이 많아질수록 readline 함수는 반복횟수가 많아질 수 밖에 없는 구조로 보이는데 이런 단순한 차이가 실제로는 체감할 수 있는 엄청난 차이를 가져오는 것 같다.

 

구현에 있어서 편의성과 가독성, 성능을 모두 고려해야 하겠지만, 그리고 고려하겠지만, 무작정 습관적인 사용과 맹목적인 신뢰는 언제나처럼 금기시해야 한다는 걸 느낀다.

· 2 min read

Java sql package 안에 ResultSetMetaData 라는 클래스가 존재한다.

ResultSet 으로부터 metadata 정보를 저장해놓고 사용할 수 있는 용도인데 정의된 함수 사용 중에 이상한 현상이 발견되어서 다른 함수를 사용하는 것으로 변경하게 되었다.

사용중인 DB는 MySQL로 테스트용으로는 5.5.28, 실제 DB는 5.0.67 이었는데 MySQL version 차이에 따라 함수 호출 결과가 달랐는데

정확히는 getColumnName 함수에서 이상한 현상을 발견할 수 있었다.

View table의 metadata를 읽어서 column name을 보면 AS로 이름을 변경한 column name이 원래 이름으로 나타나는 것이 아닌가.

그것도 테스트했던 상위 버전의 MySQL에서는 문제가 없었고 5.0.67 에서만 말썽이었다.

결국 호출하는 함수를 getColumnName에서 getColumnLabel로 변경하는 것으로 해결했다.

getColumnLabel은 AS로 이름을 변경하지 않은 column의 경우 getColumnName과 결과가 동일하다고 문서에 되어있으니 사용해도 무방할 듯 싶었고

사실 이런 경우 뿐만이 아니라 일반적인 경우에도 getColumnLabel을 사용하는게 정확한 방법일 것 같다.

 

그런데 왠지 column 이름을 얻을 때 getColumnLabel 보다 getColumnName을 호출하고 싶어진다.

이런게 함수 명명의 중요성?

· 2 min read

간단하게 jsp로 페이지를 만들 일이 생겼다.

단순하게 DB에 쌓여있는 현황 데이터들을 보여주면 되는건데 태그 몇 개 사용해서 하자니 없어보이고 어줍잖게 디자인을 하자니 안하느니만 못할 것 같아서 고민을 하게 되었는데 그 와중에 예전부터 한 번 써보자 생각했었던 bootstrap 생각이 났다.

사용방법은 무척 간단했는데

1. 다운로드

2. 페이지에 CSS와 Javascript 포함

3. 클래스 적용

이렇게만 해놓으면 어느정도 통일성 있고 기본 태그만 사용할 때 보다는 훨씬 예쁜 모양의 페이지를 만드는게 가능하다.

특히 Bootstrap 공식페이지에 제공되고 있는 component 들에 대한 정리가 너무 깔끔하게 되어 있어서 아주 약간의 시간투자 만으로도 적용할 수 있었다. (나의 경우엔 아무것도 없는 상태에서 Bootstrap 적용된 JSP 페이지 만드는데 3시간 정도)

하지만 이미 정의된 CSS 클래스들을 사용하는 것이니만큼 커스터마이징은 어느정도 필요해 보인다.

그래도 이 정도의 편리함을 누릴 수 있다는건 정말 감사한 일이다.