-->

[MongoDB] Docker로 MongoDB 설치

 

아 정말 오랜만에 블로그를 작성한다.

무려 8개월만인데 그동안 회사를 이직하며 문서정리를 다 회사 wiki에 하느라 개인 블로그에 투자할 시간이 없었다..

지난 1년간 이직 후 배운게 너무 많고 공부한 것도 많아서 날잡고 쓰고 싶지만 이런 저런 핑계로 미뤄왔다.. 그러나 이 것만큼은 포스팅 해야겠다고 생각이 들어서 적어본다.

 

서버는 Ubuntu 20.0 LTS 깡통 기준으로 작성하였다.

Docker 설치

$ sudo apt-get install ca-certificates curl gnupg lsb-release  
 
$ sudo mkdir -p /etc/apt/keyrings
 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
 
$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
 
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  docker-ce-rootless-extras docker-scan-plugin pigz slirp4netns
Suggested packages:
  aufs-tools cgroupfs-mount | cgroup-lite
The following NEW packages will be installed:
  containerd.io docker-ce docker-ce-cli docker-ce-rootless-extras docker-compose-plugin docker-scan-plugin pigz slirp4netns
0 upgraded, 8 newly installed, 0 to remove and 4 not upgraded.
Need to get 74.3 kB/108 MB of archives.
After this operation, 449 MB of additional disk space will be used.
Do you want to continue? [Y/n] y

...


# 설치 확인
$ systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
   Active: active (running) since Tue 2022-08-16 09:34:33 KST; 2min 34s ago
     Docs: https://docs.docker.com
 Main PID: 11179 (dockerd)
    Tasks: 8
   Memory: 34.5M
   CGroup: /system.slice/docker.service
           └─11179 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
 
# 부팅 시 자동 실행 
$ sudo systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.

 

DockerHub로부터 mongoDB 설치

먼저 Docker Hub에 접속하여 mongoDB 이미지를 확인해준다. 

현재 7 버전대가 나오고 있는데 LTS 버전은 6.0으로 확인된다.

https://hub.docker.com/layers/library/mongo/6.0/images/sha256-e4d7cef25cd0d70c3f6f72566f5b46bda6b1978716cee6a3b341d8bd3d9fe69a?context=explore 

 

Docker

 

hub.docker.com

따라서 6.0을 설치하도록 한다.

# 소켓 권한 부여
$ sudo chmod 666 /var/run/docker.sock

# mongodb 설치
$ docker pull docker pull mongo:6.0
6.0: Pulling from dockerhub/mongo
63b3880ad1ce: Pull complete
f69d2c554348: Pull complete
2b55622443ad: Pull complete
266a1ba864ad: Pull complete
9bc7e9e472a2: Pull complete
9347ae81eebc: Pull complete
d0dc2312eb8c: Pull complete
b6bd9b1064ba: Pull complete
08ac7def05d6: Pull complete
Digest: sha256:e24fd341fac9a246328ff1ef6df1d78b480c30d0ffdf19d6e55d15358be71f36

# 설치된 이미지 확인
$ docker images
REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
dockerhub/mongo          6.0       fb5fba25b25a   13 days ago   654MB

# 컨테이너 실행
$ docker run -d --name mongodb -p 27017:27017 fb5fba25b25a
fdf721e488e30391c5421259df837f84d4fbb62ea15a64f8f8c4d943bb8770c2

$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                   NAMES
fdf721e488e3   fb5fba25b25a   "docker-entrypoint.s…"   3 seconds ago   Up 2 seconds   0.0.0.0:27017->27017/tcp   mongodb

 

폴더 구조

data
├── app
│   └── docker-compose.yaml
└── pv
    └── mongodb
        ├── conf
        │   └── mongod.conf
        ├── data
        └── logs
            └── mongod.log

pv 폴더는 persistence volume 용도이다.

 

docker-compose.yaml 파일 작성

docker를 쓰는 사람이 docker-compose를 안쓴다는건 말이 안된다.

# docker-compose.yaml
version: '3.8'
services:
  mongodb:
    restart: always
    container_name: mongodb
    image: mongo:6.0
    ports:
      - 27017:27017
    volumes:
      - /data/pv/mongodb/data:/data/db
      - /data/pv/mongodb/logs:/var/log/mongodb
      - /data/pv/mongodb/conf/mongod.conf:/etc/mongod.conf
    command: ["mongod", "-f", "/etc/mongod.conf"]
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=1234
      - MONGO_INITDB_DATABASE=mydb

 

mongod.conf 파일 작성

# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db
  journal:
    enabled: true
#  engine:
#  wiredTiger:

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log

# network interfaces
net:
  port: 27017
#  bindIpAll: true
  bindIp: 0.0.0.0


# how the process runs
#processManagement:
#  fork: true
#  timeZoneInfo: /usr/share/zoneinfo

security:
  authorization: enabled

#operationProfiling:

#replication:

#sharding:

## Enterprise-Only Options:

#auditLog:

#snmp:

mongodb로 외부접속을 하기 위해서는 mongod.conf의 net 설정을 반드시 해주어야 한다. 

지금은 아무나 올 수 있게 bindIp를 0.0.0.0으로 설정함.

환경설정에 관한 추가 정보는 공식 문서에서 확인해보자.

 

컨테이너 접속

# 실행
$ docker-compose up -d
[+] Running 2/2
 ✔ Network app_default  Created                                                                                                       0.1s
 ✔ Container mongodb    Started                                                                                                       0.5s
 
 # 확인
$ docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED         STATUS         PORTS                           NAMES
bf72535cd54b   dockerhub/mongo:6.0   "docker-entrypoint.s…"   5 minutes ago   Up 5 minutes   0.0.0.0:20717->20717/tcp, 27017/tcp   mongodb
 
 
$ docker logs mongodb
{"t":{"$date":"2023-07-27T04:05:32.295+00:00"},"s":"I",  "c":"CONTROL",  "id":23285,   "ctx":"-","msg":"Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'"}
{"t":{"$date":"2023-07-27T04:05:32.298+00:00"},"s":"I",  "c":"NETWORK",  "id":4915701, "ctx":"main","msg":"Initialized wire specification","attr":{"spec":{"incomingExternalClient":{"minWireVersion":0,"maxWireVersion":17},"incomingInternalClient":{"minWireVersion":0,"maxWireVersion":17},"outgoing":{"minWireVersion":6,"maxWireVersion":17},"isInternalClient":true}}}
{"t":{"$date":"2023-07-27T04:05:32.300+00:00"},"s":"I",  "c":"NETWORK",  "id":4648601, "ctx":"main","msg":"Implicit TCP FastOpen unavailable. If TCP FastOpen is required, set tcpFastOpenServer, tcpFastOpenClient, and tcpFastOpenQueueSize."}
{"t":{"$date":"2023-07-27T04:05:32.302+00:00"},"s":"I",  "c":"REPL",     "id":5123008, "ctx":"main","msg":"Successfully registered PrimaryOnlyService","attr":{"service":"TenantMigrationDonorService","namespace":"config.tenantMigrationDonors"}}
{"t":{"$date":"2023-07-27T04:05:32.302+00:00"},"s":"I",  "c":"REPL",     "id":5123008, "ctx":"main","msg":"Successfully registered PrimaryOnlyService","attr":{"service":"TenantMigrationRecipientService","namespace":"config.tenantMigrationRecipients"}}
{"t":{"$date":"2023-07-27T04:05:32.302+00:00"},"s":"I",  "c":"REPL",     "id":5123008, "ctx":"main","msg":"Successfully registered PrimaryOnlyService","attr":{"service":"ShardSplitDonorService","namespace":"config.tenantSplitDonors"}}
{"t":{"$date":"2023-07-27T04:05:32.302+00:00"},"s":"I",  "c":"CONTROL",  "id":5945603, "ctx":"main","msg":"Multi threading initialized"}
{"t":{"$date":"2023-07-27T04:05:32.303+00:00"},"s":"I",  "c":"CONTROL",  "id":4615611, "ctx":"initandlisten","msg":"MongoDB starting","attr":{"pid":1,"port":27017,"dbPath":"/data/db","architecture":"64-bit","host":"bf72535cd54b"}}
{"t":{"$date":"2023-07-27T04:05:32.303+00:00"},"s":"I",  "c":"CONTROL",  "id":23403,   "ctx":"initandlisten","msg":"Build Info","attr":{"buildInfo":{"version":"6.0.8","gitVersion":"3d84c0dd4e5d99be0d69003652313e7eaf4cdd74","openSSLVersion":"OpenSSL 3.0.2 15 Mar 2022","modules":[],"allocator":"tcmalloc","environment":{"distmod":"ubuntu2204","distarch":"x86_64","target_arch":"x86_64"}}}}
{"t":{"$date":"2023-07-27T04:05:32.303+00:00"},"s":"I",  "c":"CONTROL",  "id":51765,   "ctx":"initandlisten","msg":"Operating System","attr":{"os":{"name":"Ubuntu","version":"22.04"}}}
{"t":{"$date":"2023-07-27T04:05:32.303+00:00"},"s":"I",  "c":"CONTROL",  "id":21951,   "ctx":"initandlisten","msg":"Options set by command line","attr":{"options":{"net":{"bindIp":"*"},"security":{"authorization":"enabled"}}}}
{"t":{"$date":"2023-07-27T04:05:32.305+00:00"},"s":"I",  "c":"STORAGE",  "id":22270,   "ctx":"initandlisten","msg":"Storage engine to use detected by data files","attr":{"dbpath":"/data/db","storageEngine":"wiredTiger"}}
{"t":{"$date":"2023-07-27T04:05:32.305+00:00"},"s":"I",  "c":"STORAGE",  "id":22297,   "ctx":"initandlisten","msg":"Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem","tags":["startupWarnings"]}
{"t":{"$date":"2023-07-27T04:05:32.306+00:00"},"s":"I",  "c":"STORAGE",  "id":22315,   "ctx":"initandlisten","msg":"Opening WiredTiger","attr":{"config":"create,cache_size=1453M,session_max=33000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,remove=true,path=journal,compressor=snappy),builtin_extension_config=(zstd=(compression_level=6)),file_manager=(close_idle_time=600,close_scan_interval=10,close_handle_minimum=2000),statistics_log=(wait=0),json_output=(error,message),verbose=[recovery_progress:1,checkpoint_progress:1,compact_progress:1,backup:0,checkpoint:0,compact:0,evict:0,history_store:0,recovery:0,rts:0,salvage:0,tiered:0,timestamp:0,transaction:0,verify:0,log:0],"}}
{"t":{"$date":"2023-07-27T04:05:33.303+00:00"},"s":"I",  "c":"STORAGE",  "id":4795906, "ctx":"initandlisten","msg":"WiredTiger opened","attr":{"durationMillis":997}}
{"t":{"$date":"2023-07-27T04:05:33.304+00:00"},"s":"I",  "c":"RECOVERY", "id":23987,   "ctx":"initandlisten","msg":"WiredTiger recoveryTimestamp","attr":{"recoveryTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2023-07-27T04:05:33.319+00:00"},"s":"W",  "c":"CONTROL",  "id":5123300, "ctx":"initandlisten","msg":"vm.max_map_count is too low","attr":{"currentValue":65530,"recommendedMinimum":1677720,"maxConns":838860},"tags":["startupWarnings"]}
{"t":{"$date":"2023-07-27T04:05:33.322+00:00"},"s":"I",  "c":"NETWORK",  "id":4915702, "ctx":"initandlisten","msg":"Updated wire specification","attr":{"oldSpec":{"incomingExternalClient":{"minWireVersion":0,"maxWireVersion":17},"incomingInternalClient":{"minWireVersion":0,"maxWireVersion":17},"outgoing":{"minWireVersion":6,"maxWireVersion":17},"isInternalClient":true},"newSpec":{"incomingExternalClient":{"minWireVersion":0,"maxWireVersion":17},"incomingInternalClient":{"minWireVersion":17,"maxWireVersion":17},"outgoing":{"minWireVersion":17,"maxWireVersion":17},"isInternalClient":true}}}
{"t":{"$date":"2023-07-27T04:05:33.322+00:00"},"s":"I",  "c":"REPL",     "id":5853300, "ctx":"initandlisten","msg":"current featureCompatibilityVersion value","attr":{"featureCompatibilityVersion":"6.0","context":"startup"}}
{"t":{"$date":"2023-07-27T04:05:33.323+00:00"},"s":"I",  "c":"STORAGE",  "id":5071100, "ctx":"initandlisten","msg":"Clearing temp directory"}
{"t":{"$date":"2023-07-27T04:05:33.325+00:00"},"s":"I",  "c":"CONTROL",  "id":20536,   "ctx":"initandlisten","msg":"Flow Control is enabled on this deployment"}
{"t":{"$date":"2023-07-27T04:05:33.325+00:00"},"s":"I",  "c":"FTDC",     "id":20625,   "ctx":"initandlisten","msg":"Initializing full-time diagnostic data capture","attr":{"dataDirectory":"/data/db/diagnostic.data"}}
{"t":{"$date":"2023-07-27T04:05:33.330+00:00"},"s":"I",  "c":"REPL",     "id":6015317, "ctx":"initandlisten","msg":"Setting new configuration state","attr":{"newState":"ConfigReplicationDisabled","oldState":"ConfigPreStart"}}
{"t":{"$date":"2023-07-27T04:05:33.330+00:00"},"s":"I",  "c":"STORAGE",  "id":22262,   "ctx":"initandlisten","msg":"Timestamp monitor starting"}
{"t":{"$date":"2023-07-27T04:05:33.333+00:00"},"s":"I",  "c":"NETWORK",  "id":23015,   "ctx":"listener","msg":"Listening on","attr":{"address":"/tmp/mongodb-27017.sock"}}
{"t":{"$date":"2023-07-27T04:05:33.334+00:00"},"s":"I",  "c":"NETWORK",  "id":23015,   "ctx":"listener","msg":"Listening on","attr":{"address":"0.0.0.0"}}
{"t":{"$date":"2023-07-27T04:05:33.334+00:00"},"s":"I",  "c":"NETWORK",  "id":23016,   "ctx":"listener","msg":"Waiting for connections","attr":{"port":27017,"ssl":"off"}}


# 접속
$ docker exec -it mongodb mongosh
Current Mongosh Log ID: 64c1ef8c9f2da91f777540e2
Connecting to:          mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.10.1
Using MongoDB:          6.0.8
Using Mongosh:          1.10.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/
 
To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
You can opt-out by running the disableTelemetry() command.
 
test>

# admin db로 바꾸고 새 계정 생성
test> use admin
switched to db admin
admin> db.createUser({user:"minggu",pwd:"1234",roles:[{"role":"root","db":"admin"}]})
{ ok: 1 }
admin>

 

MongoDB 조작

#Ctrl + D로 MongoDB 종료, 한번 더 눌러서 컨테이너 종료
 
# 재접속
$ docker exec -it mongodb bash
root@5f64c1a55e7a:/# mongosh -u minggu -p 1234
Current Mongosh Log ID: 64c33770b43db69197467fbc
Connecting to:          mongodb://<credentials>@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.10.1
Using MongoDB:          6.0.8
Using Mongosh:          1.10.1
 
For mongosh info see: https://docs.mongodb.com/mongodb-shell/
 
------
   The server generated these startup warnings when booting
   2023-07-28T03:32:57.729+00:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem
   2023-07-28T03:32:58.747+00:00: vm.max_map_count is too low
------
 
 # 데이터베이스 확인
test> show databases
admin   132.00 KiB
config   72.00 KiB
local    96.00 KiB

# 데이터베이스 생성
test> use exhibtionDB
switched to db exhibtionDB
exhibtionDB> 
 
# 데이터 삽입 
exhibtionDB> db.exhibitions.insert({"title":"Exhibition Test","createdBy":"minggu"})
DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
  acknowledged: true,
  insertedIds: { '0': ObjectId("64c33da9183745bf1c891027") }
}

# 데이터베이스 목록 확인
exhibtionDB> show dbs
admin        132.00 KiB
config        60.00 KiB
exhibtionDB    8.00 KiB
local         96.00 KiB
exhibtionDB>

 

외부에서 접속도 잘 된다

[Javacsript] 클립보드에 복사하기

 

Clipboard에 복사하는 방법은 전통적으로 document.execCommand('copy') 방식을 써왔다.

      const textArea = document.createElement('textarea');
      textArea.value = text;
      document.body.appendChild(textArea);
      textArea.select();
      try {
        document.execCommand('copy');
      } catch (err) {
        console.error('Unable to copy to clipboard', err);
      }
      document.body.removeChild(textArea);

해당 방법은 MDN 피셜 Deprecated

 

하지만 Clipboard API가 나와서 손쉽게 가능해졌다.

window.navigator.clipboard.writeText(window.location.href).then(() => console.log('success'));

 

다만 해당 API는 HTTPS에서만 작업이 가능해 http dev 환경에서는 불가능하다.

따라서 요렇게 분기 처리하면 되시겠다.

js

const unsecuredCopyToClipboard = (text) => {
  const textArea = document.createElement('textarea');
  textArea.value = text;
  document.body.appendChild(textArea);
  textArea.select();
  try {
    document.execCommand('copy');
  } catch (err) {
    console.error('Unable to copy to clipboard', err);
  }
  document.body.removeChild(textArea);
  afterCopy();      
};

let target = window.location.href; //현재 URL이나 복사하자

$('#copyClip').click(() => {
  $('#layer').hide();

  if (window.isSecureContext && navigator.clipboard) 
    navigator.clipboard.writeText(target).then(() => afterCopy());
  else 
    unsecuredCopyToClipboard(target);      
});

function afterCopy() {
  $('#layer').toggle(500);

  setTimeout(() => {
    $('#layer').toggle(500);
  }, 1500);
}

jQuery 못 잃어..

 

html

<!-- 알림 메세지   -->
<div id="layer" style="display: none">
  클립보드에 복사되었습니다.
</div>
<button id="copyClip"><img src="아무이미지" alt=""></button>

 

<참조>

https://developer.mozilla.org/en-US/docs/Web/API/Clipboard

 

Clipboard - Web APIs | MDN

The Clipboard interface implements the Clipboard API, providing—if the user grants permission—both read and write access to the contents of the system clipboard. The Clipboard API can be used to implement cut, copy, and paste features within a web appl

developer.mozilla.org

https://stackoverflow.com/questions/71873824/copy-text-to-clipboard-cannot-read-properties-of-undefined-reading-writetext

 

Copy text to clipboard: Cannot read properties of undefined reading 'writeText'

I have a button When I clicked on COPY copyImageLinkText({ mouseenter, mouseleave }, e) { this.showCopiedText = !this.showCopiedText navigator.clipboard.writeText(this.imageLink) clearTimeout(

stackoverflow.com

 

[Javascript] URLSearchParams를 활용한 QueryString 추출 및 세팅

 

코드 리팩토링을 진행하다가 기록하면 도움 될 것 같아 남긴다.

https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams

 

URLSearchParams - Web APIs | MDN

The URLSearchParams interface defines utility methods to work with the query string of a URL.

developer.mozilla.org

 

1. 쿼리스트링 추출하기

var QueryCollection = function () {
	this.collection = [];
};


QueryCollection.prototype.parseQuery = function (url) {
    if (!url) {
        return this;
    }

    var index = url.indexOf("?");
    if (index < 0) {
        return this;
    }

    var query = url.substr(index + 1);
    var queryPairs = query.split("&");
    for (var i = 0; i < queryPairs.length; i++) {
        var pair = queryPairs[i];
        var keyValue = pair.split("=");

        if (keyValue.length < 2) {
            continue;
        }

        var key = keyValue[0];
        var value = keyValue[1];

        if (!key || !value) {
            continue;
        }

        this.collection[decodeURIComponent(key)] = decodeURIComponent(value);
    }

    return this;
};

기존 소스는 위와 같은 방법으로 쿼리스트링을 parsing 했다.

 

parseQuery(url) {
    const queryPairs = url.slice(url.indexOf('?') + 1).split('&');
    queryPairs.map(param => {
      const [key, value] = param.split('=');
      if (value) this.collection[decodeURIComponent(key)] = decodeURIComponent(value);
})

다음과 같이 map을 이용하면 이렇게 간결화할 수 있다.

str.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
    obj[decodeURIComponent(key)] = decodeURIComponent(value);
});

정규식을 이용하면 이렇게 표현할 수도..

 

this.collection = {};

parseQuery(url) {
    const queryObj = Object.fromEntries(new URLSearchParams(new URL(url).search));
    return Object.assign(this.collection, queryObj);
};

es6에서는 애초에 배열이 아니라 Object로 선언해 다음과 같이 간결하게 나타낼 수 있다. (URL.searchParams이 ts문법)

Object.assign의 반환 값이 object 객체이기 때문에 리턴으로 해주면 간단하다.

중복 key값은 덮어 씌우고 신규는 이어 붙인다.

여기서 new URL(url).searchParams (Only Typescript) 대신에 new URLSearchParams(url)을 쓰면 다음과 같이 첫 파라미터가 딸려온다.

usp2
{https://www.naver.com/search?test: '', q: 'javascript query string parse', newwindow: '1', rlz: '1C1GCEU_koKR1006KR1006', ei: 'j3FDY6DUI4b9hwOL9KMo', …}

그래서 new URL.search를 사용하는 편이 낫다. (TypeScript에서는 new URL(url).searchParams 가능)

 

2. 쿼리 스트링 세팅하기

QueryCollection.prototype.setQueryTo = function (url, skipEncoding) {
    var pairs = [];

    for (var key in this.collection) {
        var value = this.collection[key];
        var newKey = skipEncoding ? key : encodeURIComponent(key);
        var newValue = skipEncoding ? value : encodeURIComponent(value);

        pairs.push(newKey + "=" + newValue);
    }

    var queryString = pairs.join("&");

    var newUrl = url;
    var queryIndex = newUrl.indexOf("?");
    if (queryIndex < 0) {
        newUrl += "?" + queryString;
    } else {
        newUrl = newUrl.substr(0, queryIndex + 1) + queryString;
    }

    return newUrl;
};

이것도 중복 코드를 없애고 map, entries를 사용하면 다음과 같이 표현이 가능하다.

const pairs = Object.entries(this.collection).map(e => e.join('=')).join('&');

URLSearchParams은 Object를 바로 묶어준다. +기호를 공백으로 치환하는 RFC3986을 따르고 있으니 다음과 같이 사용하여야 한다.

  setQueryTo(url, isSkipEncoding) {
    const pairs = isSkipEncoding ?
      new URLSearchParams(this.collection).toString()
      : Object.keys(this.collection).map((key) => {
        return encodeURIComponent(key) + '=' + encodeURIComponent(this.collection[key])
      }).join('&');

    return url.split('?')[0] + '?' + pairs;
  };

 

전체 기존 코드

var QueryCollection = function () {
    this.collection = [];
};

QueryCollection.prototype.parseQuery = function (url) {
    if (!url) {
        return this;
    }

    var index = url.indexOf("?");
    if (index < 0) {
        return this;
    }

    var query = url.substr(index + 1);
    var queryPairs = query.split("&");
    for (var i = 0; i < queryPairs.length; i++) {
        var pair = queryPairs[i];
        var keyValue = pair.split("=");

        if (keyValue.length < 2) {
            continue;
        }

        var key = keyValue[0];
        var value = keyValue[1];

        if (!key || !value) {
            continue;
        }

        this.collection[decodeURIComponent(key)] = decodeURIComponent(value);
    }

    return this;
};

QueryCollection.prototype.set = function (key, value) {
    this.collection[key] = value;
};

QueryCollection.prototype.get = function (key) {
    return this.collection[key];
};

QueryCollection.prototype.setQueryTo = function (url, skipEncoding) {
    var pairs = [];

    for (var key in this.collection) {
        var value = this.collection[key];
        var newKey = skipEncoding ? key : encodeURIComponent(key);
        var newValue = skipEncoding ? value : encodeURIComponent(value);

        pairs.push(newKey + "=" + newValue);
    }

    var queryString = pairs.join("&");

    var newUrl = url;
    var queryIndex = newUrl.indexOf("?");
    if (queryIndex < 0) {
        newUrl += "?" + queryString;
    } else {
        newUrl = newUrl.substr(0, queryIndex + 1) + queryString;
    }

    return newUrl;
};

 

전체 코드 리팩토링

export class QueryCollection {
  constructor() {
    this.collection = {};
  }

  set(key, value) {
    this.collection[key] = value;
  };

  get(key) {
    return this.collection[key];
  };

  parseQuery(url) {
    // const queryObj = Object.fromEntries(new URLSearchParams(new URL(url).search));
	Object.assign(this.collection, queryObj)
    return this;
  };

  setQueryTo(url, isSkipEncoding) {
    const pairs = isSkipEncoding ?
      new URLSearchParams(this.collection).toString()
      : Object.keys(this.collection).map((key) => {
        return encodeURIComponent(key) + '=' + encodeURIComponent(this.collection[key])
      }).join('&');

    return url.split('?')[0] + '?' + pairs;
  };
}

아직 테스트 안해봐서 오류 지적 환영합니다!

'Front-end' 카테고리의 다른 글

Pixar에서 만든 3D 포맷, USD  (0) 2024.08.01
[Javacsript] 클립보드에 복사하기  (0) 2022.11.24
[Webpack] Loader  (0) 2022.07.06
[Webpack] Webpack 기초  (0) 2022.07.03
[Intellij / React] 무작정 React Project 만들어보기 -1  (0) 2022.03.05

[Node.js] Error: listen EADDRINUSE: address already in use :::8080

 

node로 된 프로젝트를 Run 하려고 할 때 가끔 뜨는 에러이다.

프로젝트에서 사용할 포트가 이미 다른 프로세스에서 이용중일 때 생긴다.

node 자체가 여러개의 프로젝트를 띄우기에 용이하기도 하니 자주 사용하는 포트를 설정해두면 비일비재하게 발생한다. 

해결법은 다음과 같다.

https://minggu92.tistory.com/114

 

[Linux / Windows] 특정포트를 사용하는 프로세스 강제종료

[Linux / Windows] 특정포트를 사용하는 프로세스 강제종료 리눅스 작업을 해보면 참 사용할 일이 많은 일이다. 그런데 만약 windows에서 발생한다면? 이것도 linux에서 하는 것과 크게 다르진 않는

minggu92.tistory.com

 

 

[Linux / Windows] 특정포트를 사용하는 프로세스 강제종료

리눅스 작업을 해보면 참 사용할 일이 많은 일이다.

그런데 만약 windows에서 발생한다면? 이것도 linux에서 하는 것과 크게 다르진 않는데 생소할 수 있으니 한번 작업해보려고 한다.

 

1. netstat

터미널을 하나 열어서 작업할 건데 명령 프롬프트든, 파워쉘이든 본인이 원하는 터미널로 작업하면 된다.

linux나 windows나 netstat 명령어는 기본적으로 가지고 있다. (네트워크 세팅이 되어있다면)

$ netstat --help

프로토콜 통계와 현재 TCP/IP 네트워크 연결을 표시합니다.

NETSTAT [-a] [-b] [-e] [-f] [-n] [-o] [-p proto] [-r] [-s] [-t] [-x] [-y] [interval]

  -a            모든 연결 및 수신 대기 포트를 표시합니다.
  -b            각 연결 또는 수신 대기 포트 생성과 관련된 실행 파일을
                표시합니다. 잘 알려진 실행 파일이 여러 독립 구성 요소를
                호스팅할 경우 연결 또는 수신 대기 포트 생성과 관련된
                구성 요소의 시퀀스가 표시됩니다.
                이러한 경우에는 실행 파일 이름이 아래 [] 안에
                표시되고 위에는 TCP/IP에 도달할 때까지
                호출된 구성 요소가 표시됩니다. 이 옵션은 시간이
                오래 걸릴 수 있으며 사용 권한이 없으면
                실패합니다.
  -e            이더넷 통계를 표시합니다. 이 옵션은 -s 옵션과 함께 사용할 수
                있습니다.
  -f            외부 주소의 FQDN(정규화된 도메인 이름)을
 표시합니다.
  -n            주소 및 포트 번호를 숫자 형식으로 표시합니다.
  -o            각 연결의 소유자 프로세스 ID를 표시합니다.
  -p proto      proto로 지정한 프로토콜의 연결을 표시합니다. proto는
                TCP, UDP, TCPv6 또는 UDPv6 중 하나입니다. -s 옵션과 함께
                사용하여 프로토콜별 통계를 표시할 경우 proto는 IP, IPv6, ICMP,
                ICMPv6, TCP, TCPv6, UDP 또는 UDPv6 중 하나입니다.
  -q            모든 연결, 수신 대기 포트 및 바인딩된 비수신 대기 TCP
                포트를 표시합니다. 바인딩된 비수신 대기 포트는 활성 연결과 연결되거나
                연결되지 않을 수도 있습니다.
  -r            라우팅 테이블을 표시합니다.
  -s            프로토콜별 통계를 표시합니다. 기본적으로 IP, IPv6, ICMP,
                ICMPv6, TCP, TCPv6, UDP 및 UDPv6에 대한 통계를 표시합니다.
                -p 옵션을 사용하여 기본값의 일부 집합에 대한 통계만 지정할 수 있습니다.
  -t            현재 연결 오프로드 상태를 표시합니다.
  -x            NetworkDirect 연결, 수신기 및 공유 끝점을
                표시합니다.
  -y            모든 연결에 대한 TCP 연결 템플릿을 표시합니다.
                다른 옵션과 함께 사용할 수 없습니다.
  interval      다음 화면으로 이동하기 전에 지정한 시간(초) 동안 선택한 통계를 다시 표시합니다.
                통계 다시 표시를 중지하려면 <Ctrl+C>를 누르세요.
                이 값을 생략하면 현재 구성 정보가
                한 번만 출력됩니다.

우리는 pid를 알야 내야 하기 때문에 여기서 -a, -n, -o 옵션을 사용할 것이다.

 

2. netstat -ano

$ netstat -ano

활성 연결

  프로토콜  로컬 주소              외부 주소              상태            PID
  TCP    0.0.0.0:80             0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       1304
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:623            0.0.0.0:0              LISTENING       5492
  TCP    0.0.0.0:2701           0.0.0.0:0              LISTENING       5124
  .....

모든 연결들 중에서 외부 주소 및 포트를 도메인이 아닌 숫자 형태로 PID와 함께 출력하라는 명령어이다.

이때 status가 LISTENING인 연결만 주의 깊게 보면 된다.

 

3. grep, findstr

$ netstat -ano | grep LISTENING | grep 8080
  TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING       11100
  TCP    [::]:8080              [::]:0                 LISTENING       11100

git bash를 사용하거나 linux라면 grep 명령어를 통해 원하는 출력 형태의 문자열을 잡아낼 수 있다.

 

PS C:\Users\minggu92> netstat -ano | findstr "LISTENING" | findstr 8080
  TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING       11100
  TCP    [::]:8080              [::]:0                 LISTENING       11100

windows라면 findstr 명령어와 큰따옴표를 이용해서 잡아내야 한다.

 

4. kill, taskkill

Linux에서 프로세스를 강제 종료하려면 아주 간단한 명령어들이 있다.

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGEMT       8) SIGFPE       9) SIGKILL     10) SIGBUS
11) SIGSEGV     12) SIGSYS      13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGURG      17) SIGSTOP     18) SIGTSTP     19) SIGCONT     20) SIGCHLD
21) SIGTTIN     22) SIGTTOU     23) SIGIO       24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGPWR      30) SIGUSR1
31) SIGUSR2     32) SIGRTMIN    33) SIGRTMIN+1  34) SIGRTMIN+2  35) SIGRTMIN+3
36) SIGRTMIN+4  37) SIGRTMIN+5  38) SIGRTMIN+6  39) SIGRTMIN+7  40) SIGRTMIN+8
41) SIGRTMIN+9  42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMIN+16 49) SIGRTMAX-15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8  57) SIGRTMAX-7  58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4
61) SIGRTMAX-3  62) SIGRTMAX-2  63) SIGRTMAX-1  64) SIGRTMAX

기본적으로 -9 sigkill과 -15 terminate를 이용하는데 무슨 차이가 있는지는 리눅스 공부를 해보도록!

$ kill -9 11100

 

윈도에서는 taskkill 명령어를 사용한다.

PS C:\Users\minggu92> taskkill /?

TASKKILL [/S 시스템 [/U 사용자 이름 [/P [암호]]]]
         { [/FI 필터] [/PID 프로세스 id | /IM 이미지 이름] } [/T] [/F]

설명:
    이 도구는 프로세스 ID(PID) 또는 이미지 이름으로 작업을 종료하는 데
    사용합니다.

매개 변수 목록:
    /S    시스템           연결할 원격 시스템을 지정합니다.
    /U    [도메인\]사용자  명령을 실행해야 하는 사용자 컨텍스트를
                           지정합니다.
    /P    [암호]           해당 사용자 컨텍스트의 암호를 지정합니다.
                           생략한 경우에는 물어봅니다.
    /FI   필터             작업 집합을 선택하는 필터를 적용합니다.
                           "*"를 사용할 수 있습니다. 예: imagename eq acme*
    /PID  프로세스_ID      종료할 프로세스의 PID를 지정합니다.
                           TaskList를 사용하여 PID를 얻을 수 있습니다.
    /IM   이미지 이름      종료할 프로세스의 이미지 이름을
                           지정합니다. 와일드카드 문자 '*'를 사용하여
                           모든 작업 또는 이미지 이름을 지정할 수 있습니다.
    /T                     지정된 프로세스와 그 프로세스로부터 시작된
                           모든 자식 프로세스를 종료합니다.
    /F                     프로세스를 강제로 종료하도록 지정합니다.
    /?                     이 도움말 메시지를 표시합니다.
필터:
    필터 이름     유효한 연산자             유효한 값
    -----------   ---------------           -------------------------
    STATUS        eq, ne                    RUNNING |
                                            NOT RESPONDING | UNKNOWN
    IMAGENAME     eq, ne                    이미지 이름
    PID           eq, ne, gt, lt, ge, le    PID 값
    SESSION       eq, ne, gt, lt, ge, le    세션 번호.
    CPUTIME       eq, ne, gt, lt, ge, le    CPU 시간 형식
                                            hh:mm:ss
                                            hh - 시간,
                                            mm - 분, ss - 초
    MEMUSAGE      eq, ne, gt, lt, ge, le    메모리 사용(KB)
    USERNAME      eq, ne                    사용자 이름([domain\]user
                                            형식)
    MODULES       eq, ne                    DLL 이름
    SERVICES      eq, ne                    서비스 이름
    WINDOWTITLE   eq, ne                    창 제목

    참고
    ----
    1) /IM 스위치에 대한 와일드카드 문자 '*'는 필터가 적용될 때만
    사용할 수 있습니다.
    2) 원격 프로세스는 항상 강제적으로(/F) 종료될 수 있습니다.
    3) 원격 컴퓨터가 지정되면 "WINDOWTITLE"  및 "STATUS" 필터는
       지원되지 않습니다.

예:
    TASKKILL /IM notepad.exe
    TASKKILL /PID 1230 /PID 1241 /PID 1253 /T
    TASKKILL /F /IM cmd.exe /T
    TASKKILL /F /FI "PID ge 1000" /FI "WINDOWTITLE ne untitle*"
    TASKKILL /F /FI "USERNAME eq NT AUTHORITY\SYSTEM" /IM notepad.exe
    TASKKILL /S 시스템 /U domain\username /FI "USERNAME ne NT*" /IM *
    TASKKILL /S 시스템 /U 사용자 이름 /P 암호 /FI "IMAGENAME eq note*"

 

PS C:\Users\minggu92> taskkill /pid 11100
오류: 프로세스(PID 11100)를 종료할 수 없습니다.
원인: 이 프로세스는 /F 옵션을 사용하여 강제로 종료해야 합니다.
PS C:\Users\minggu92> taskkill /pid 11100 /f
오류: 프로세스(PID 11100)를 종료할 수 없습니다.
원인: 액세스가 거부되었습니다.

--관리자모드로 실행시
PS C:\Windows\system32> taskkill /pid 11100 /f
성공: 프로세스(PID 11100)가 종료되었습니다.

ㅂㄷ 터미널은 꼭 관리자 모드로 실행하자.

 

혹은 작업 관리자에서 죽일 수도 있다.

 

[Node.js] NVM exit status 1

 

nvm을 이용하면 다양한 버전의 node.js를 손쉽게 사용할 수 있다.

근데 갑자기 에러가 뜬다.

$ nvm ls

    18.6.0
    14.20.0
    10.24.1
  * 8.17.0 (Currently using 64-bit executable)

$ nvm use 10.24.1
exit status 1: �׼����� �źεǾ����ϴ�.

 

찾아보니 해당 에러는 권한이 없을때 생긴다는 것..

exit status 5 도 Access Denyed 같은 에러이다.

 

해결법은 간단한데 관리자 권한으로 터미널을 실행해서 작업하면 된다.

아무 터미널이나 관리자 권한으로 실행해서

하면 잘 됨.

 

근데 매번 이렇게 작업할거야...?

IDE를 관리자 권한으로 실행하면 되잖아..?

https://minggu92.tistory.com/112

 

[Intellij] 항상 관리자 권한으로 실행하기

[Intellij] 항상 관리자 권한으로 실행하기 intellij를 관리자 권한으로 실행시켜야 하는 경우가 반드시 있다. 특히 nvm ㅂㄷ 1. window 실행창 intellij를 검색하고 마우스 우측 버튼을 클릭해 관리자 권

minggu92.tistory.com

그래서 이 포스팅을 만든 것이다.

[Intellij] 항상 관리자 권한으로 실행하기

 

intellij를 관리자 권한으로 실행시켜야 하는 경우가 반드시 있다. 특히 nvm ㅂㄷ

 

1. window 실행창

intellij를 검색하고 마우스 우측 버튼을 클릭해 관리자 권한으로 실행하는 방법이 있다.

 

2. 작업표시줄

작업표시줄에 고정시켜놨을 때 마우스 우측버튼을 누른 후 intellij를 다시 마우스 우측 버튼 클릭하여 실행할 수 있다.

 

3. 속성값 수정

앱의 속성으로 들어가 준다. 

아래 두가지 방법 중 아무거나 사용해도 된다.

 

3-1. 바로가기 수정

고급 탭을 클릭해준다.

'관리자 권한으로 실행'을 체크한다.

 

3-2. 호환성 설정 수정

호환성 탭에서 설정 파트에 '관리자 권한으로 이 프로그램 실행'을 체크

 

여기서 무슨 호환성 문제 해결사로 들어가서 어쩌고 하라는 해결방법도 있는데 매우 귀찮은 방법이니 굳이 그렇게 할 필요 없다. 

 

4. 계정 컨트롤 수정

위의 방법대로 하면 실행할 때마다 관리자 권한을 묻고 관리자 권한으로 intellij가 실행될 것이다.

하지만 관리자 권한을 묻는 것도 귀찮다면 

사용자 계정 컨트롤 설정 변경으로 들어간 뒤에

알리지 않음을 설정하면 된다.

 

[Kotlin] Android 네트워크 연결 상태 확인

 

안드로이드로 작업을 하다 보면 네트워크 연결상태에 따라 분기 처리를 해줘야 할 때가 있다.

네트워크가 연결이 되었는지, 되었다면 어떤 경로인지(Wifi, Cellualar, VPN 등..)

이에 따라 네트워크 오프라인 일 때는 앱 이용을 못하게 한다거나, 특정 네트워크 상태에 따라 비즈니스도 변경될 수 있다.

구체적으로 사용하는 객체는 다음과 같다.

 

  • ConnectivityManager객체를 이용해 시스템 연결상태를 앱에 알린다.
  • Network 클래스는 기기가 현재 연결된 네트워크 중 하나를 나타낸다. Network 객체를 키로 사용하여 ConnectivityManager와 함께 네트워크 정보를 수집하거나 네트워크에서 소켓을 결합할 수 있다. 네트워크 연결이 끊어지면 Network 객체는 사용이 중지되고 나중에 기기가 동일한 어플라이언스에 다시 연결되더라도 새 Network 객체는 새 네트워크를 나타낸다.
  • LinkProperties 객체에는 네트워크에 설치된 DNS 서버, 로컬 IP 주소, 네트워크 경로 목록 등의 네트워크 연결 정보가 포함된다.
  • NetworkCapabilities 객체에는 전송(Wi-Fi, 셀룰러, 블루투스) 및 네트워크에서 사용할 수 있는 기능과 같은 네트워크 속성 정보가 포함된다. 예를 들어, 객체를 쿼리 하여 네트워크가 MMS를 전송할 수 있는지, 종속 포털을 지원하는지, 데이터 전송량 제한이 있는지 확인할 수 있다.

 

먼저 네트워크를 확인하기 위한 권한을 주어야한다.

AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

 

네트워크 연결 상태를 나타내는 함수

fun checkNetworkStatus(): Boolean {
    val isConnected: Boolean
    val connectivityManager = mContext.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
    var connectionType: String? = null

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val networkCapabilities = connectivityManager.activeNetwork ?: return false
        val networkCaps =
            connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
        connectionType = when {
            networkCaps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "Wifi"
            networkCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "Cellular"
            networkCaps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "Ethernet"
            networkCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> "VPN"
            else -> null
        }
    } else {
        connectivityManager.run {
            connectivityManager.activeNetworkInfo?.run {
                connectionType = when (type) {
                    ConnectivityManager.TYPE_WIFI -> "Wifi"
                    ConnectivityManager.TYPE_MOBILE -> "Mobile"
                    ConnectivityManager.TYPE_ETHERNET -> "Ethernet"
                    ConnectivityManager.TYPE_VPN -> "VPN"
                    else -> null
                }
            }
        }
    }

    Log.i(TAG, "Network Connection is $connectionType")
    isConnected = !ObjectUtils.isEmpty(connectionType)

    return isConnected
}

API 23 이후부터 NetworkCapabilities를 이용한 네트워크 구분이 가능하기에 다음과 같이 마시멜로 기준으로 분기 처리를 하였다.

mContext는 Context 객체이다.

 

 

 

 

<참조>

https://developer.android.com/training/basics/network-ops/reading-network-state

 

네트워크 상태 읽기  |  Android 개발자  |  Android Developers

네트워크 상태 읽기 알림 이 페이지를 개발자 프로필에 저장하여 중요 업데이트에 대한 알림을 받으세요. 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. And

developer.android.com

 

[C#] using 구문

 

C#에는 using keyword가 있다. C++에도 using 구문이 존재하고 사용법이 다른데 C#은 어떻게 사용하는지 보자.

두 가지 용도로 사용되는데 

 

1. using 지시자

using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Description;

특정 패키지를 삽입할 때 사용된다. java의 import, C언어의 include와 유사하다.

다음과 같이 별칭으로도 사용이 가능하다.

using System;
using tClass = ConsoleApp1.MyClass.testClass;

namespace ConsoleApp1
{
    class Sample
    {
        static void Main(string[] args)
        {
            //사용하지 않을 때
            ConsoleApp1.MyClass.testClass m1 = new ConsoleApp1.MyClass.testClass();

            //사용했을 때
            tClass m2 = new tClass();
        }
    }
}

namespace ConsoleApp1.MyClass
{
    class testClass
    {
        public testClass()
        {
            Console.WriteLine("NewClass instantiated...");
        }
    }

}

 

2. using 명령문

IDisposable 개체의 올바른 사용을 보장하는 편리한 구문을 제공한다.

이것이 무슨 말이냐면, File이나 Font 같은 관리되지 않는 리소스에 액세스 할 때는 사용 후 메모리를 Dispose 해주어야 하는데 이를 using 명령문 안에서 사용하면 자동으로 대행해준다. 

일반적으로 using 구문을 사용하지 않는 메모리 해제는 try-finally 구문을 사용하여 다음과 같이 작성하여야 한다.

string manyLines = @"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

{
    var reader = new StringReader(manyLines);
    try
    {
        string? item;
        do
        {
            item = reader.ReadLine();
            Console.WriteLine(item);
        } while (item != null);
    }
    finally
    {
        reader?.Dispose();
    }
}

 

using 구문을 사용하면 다음과 같이 코드를 줄일 수 있다.

string manyLines = @"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

using (var reader = new StringReader(manyLines))
{
    string? item;
    do
    {
        item = reader.ReadLine();
        Console.WriteLine(item);
    } while (item != null);
}

// C# 8.0부터는 중괄호가 필요하지 않는다.
using var reader = new StringReader(manyLines);
string? item;
do
{
    item = reader.ReadLine();
    Console.WriteLine(item);
} while (item != null);

 

다음 예제와 같이 단일 using 문에서 한 형식의 여러 인스턴스를 선언할 수 있다. 단일 문에서 여러 변수를 선언하는 경우에는 암시적으로 형식화된 변수(var)를 사용할 수 없다.

string numbers = @"One
Two
Three
Four.";
string letters = @"A
B
C
D.";

using (StringReader left = new StringReader(numbers),
    right = new StringReader(letters))
{
    string? item;
    do
    {
        item = left.ReadLine();
        Console.Write(item);
        Console.Write("    ");
        item = right.ReadLine();
        Console.WriteLine(item);
    } while (item != null);
}


// C# 8.0에서 다음과 같이 사용 가능.
using StringReader left = new StringReader(numbers),
    right = new StringReader(letters);
string? item;
do
{
    item = left.ReadLine();
    Console.Write(item);
    Console.Write("    ");
    item = right.ReadLine();
    Console.WriteLine(item);
} while (item != null);

 

 

<참조>

https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/using-statement

 

using 문 - C# 참조

using 문(C# 참조) 아티클 07/08/2022 읽는 데 6분 걸림 기여자 14명 이 문서의 내용 --> IDisposable 개체의 올바른 사용을 보장하는 편리한 구문을 제공합니다. C# 8.0부터 using 문은 IAsyncDisposable 개체의 올

docs.microsoft.com

 

[Kotlin] Android bitmap 최적화

 

 

저번에 겪었던 OutOfMemory가 단순 빌드 이슈가 아닌 성능 이슈로 다가왔다.

내 쪽에서 테스트는 문제가 없었는데 다른 기기들에서 간헐적으로 발생한다는 것.

아마 맨 처음에 만들었을 때 4개 정도 사진 찍을거로 생각해 이미지 최적화까지는 하지 않았었는데 

그게 20개로?왜?늘어서 캐시가 감당할 수 없을 정도의 메모리를 사용하고 있던 것.

그래서 저번에 만들었던 custom capture pad를 kotlin으로 마이그레이션 하면서 동시에 최적화 작업도 해보았다.

https://minggu92.tistory.com/11

 

[Android / Java] Camera Pad 만들기

[Android / Java] Camera Pad 만들기 * 목표 : 안드로이드의 카메라 기능 및 Custom Adapter를 이용해 Camera Pad 기능을 구현해보자 * 사이드 목표 :  - 1) 프래그먼트 화면에서 실행할 수 있도록 구현  - 2..

minggu92.tistory.com

(2년 전에 만든 건데...)

 

일단 OutOfMemory 이슈는 try catch로 잡힌다는 걸 알게 되었고... (심지어 app crash가 발생하지도 않음)

그에 따른 예외처리를 따로 해줘야 할 필요가 느껴진다.

아무리 자바엔 GC가 있다지만 사진을 이미지 축소하지 않고 사용하면 캐시 메모리는 금방 차 버린다. 명시적으로 메모리 해제할 일이 생길지도..

그래서 BitmapFactory의 isSampleSize Options을 줘서 크기를 1/4로 줄였다.

동적으로 ImageView의 크기를 계산해 isSampleSize option을 할당하는 방법도 있지만

그렇게까지 할 필요가 없어 코드는 사용하지 않았다.

Bitmap을 recycle 하는 방법도 있는데 동적으로 생성된 캡처 패드 객체라서 사용하기 어려워 보인다.

안드로이드를 손에서 놓은 지 좀 되었지만 최신 버전들을 사용해서 깔끔하게 작성해 보고 싶은 욕구가 생겼다.

 

package com.ming.gu.function

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.graphics.drawable.BitmapDrawable
import android.media.ExifInterface
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewManager
import android.view.ViewTreeObserver
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import com.gun0912.tedpermission.util.ObjectUtils
import com.ming.gu.R
import com.ming.gu.model.CaptureItem
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class CustomCapturePad {
    private var bnCaptureSave: Button? = null
    private var bnCaptureClear: Button? = null
    private var bnCaptureClose: ImageButton? = null
    private var mCurrentPhotoPath: String? = null
    private var ivCapture: ImageView? = null
    private var inputBitmap: Bitmap? = null
    private var outputBitmap: Bitmap? = null
    private var mFragment: Fragment? = null
    private var imgWidth: Int = 0
    private var imgHeight: Int = 0

    @SuppressLint("InflateParams", "QueryPermissionsNeeded", "SimpleDateFormat")
    fun makeCapturePad(inputFragment: Fragment, inputCaptureItem: CaptureItem?) {
        mFragment = inputFragment
        inputBitmap = inputCaptureItem!!.capture

        //새 inflater 생성
        val inflater = mFragment!!.activity
            ?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

        //새 레이아웃 객체생성
        val linearLayout = inflater.inflate(R.layout.popup_capture, null) as LinearLayout

        //레이아웃 배경 투명도 주기
        val myColor = ContextCompat.getColor(mFragment!!.requireContext(), R.color.o60)
        linearLayout.setBackgroundColor(myColor)

        //레이아웃 위에 겹치기
        val params = LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.MATCH_PARENT
        )

        //바깥 터치 안되게
        linearLayout.isClickable = true
        linearLayout.isFocusable = true
        mFragment!!.requireActivity().addContentView(linearLayout, params)

        //버튼
        ivCapture = linearLayout.findViewById(R.id.iv_capture_view)
        bnCaptureSave = linearLayout.findViewById(R.id.btn_capture_save)
        bnCaptureClear = linearLayout.findViewById(R.id.btn_capture_clear)
        bnCaptureClose = linearLayout.findViewById(R.id.btn_capture_close)

        ivCapture?.setImageBitmap(inputBitmap) //최초 로드시 기존꺼 세팅

        //disable both buttons at start
        bnCaptureSave?.isEnabled = false
        bnCaptureSave?.backgroundTintList = mFragment?.requireContext()
            ?.let { ContextCompat.getColor(it, R.color.disable) }
            ?.let { ColorStateList.valueOf(it) }


        //get ImageView size
        ivCapture?.viewTreeObserver?.addOnPreDrawListener(object :
            ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                imgWidth = ivCapture?.measuredHeight!!
                imgHeight = ivCapture?.measuredWidth!!
                ivCapture?.viewTreeObserver?.removeOnPreDrawListener(this)
                return true
            }
        })
        ivCapture?.setOnClickListener {
            val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)

            // Ensure that there's a camera activity to handle the intent
            if (takePictureIntent.resolveActivity(mFragment!!.requireActivity().packageManager) != null) {

                // Create the File where the photo should go
                var photoFile: File? = null
                try {
                    //set Temp FileDirectory
                    val tempDir = mFragment!!.requireActivity().cacheDir
                    //                    File tempDir = new File(mFragment.getContext().getFilesDir() + "/temp");

                    //set Temp File Name
                    val timeStamp = SimpleDateFormat("yyyyMMdd").format(Date())
                    val imageFileName = "JPEG_" + timeStamp + "_"
                    val tempImage = File.createTempFile(
                        imageFileName,  /* prefix */
                        ".jpg",  /* suffix */
                        tempDir /* directory */
                    )

                    // Save a file: path for use with ACTION_VIEW intents
                    mCurrentPhotoPath = tempImage.absolutePath
                    Log.i(TAG, "temp mCurrentPhotoPath : $mCurrentPhotoPath")
                    photoFile = tempImage
                } catch (ex: IOException) {
                    // Error occurred while creating the File
                }
                // Continue only if the File was successfully created
                if (photoFile != null) {
                    val photoURI = FileProvider.getUriForFile(
                        mFragment!!.requireActivity(),
                        mFragment!!.requireContext().packageName + ".fileprovider",
                        photoFile
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    mFragment!!.startActivityForResult(
                        takePictureIntent,
                        10
                    )
                }
            }
        }

        //CAPTURE SAVED
        bnCaptureSave?.setOnClickListener {
            try {
                val drawable = ivCapture?.drawable as BitmapDrawable
                val bitmap = drawable.bitmap
                inputCaptureItem.capture = outputBitmap
                outputBitmap = null
                if (bitmap != null) {
                    inputCaptureItem.status = true
                } else {
                    inputCaptureItem.status = false
                    inputCaptureItem.capture = (ResourcesCompat.getDrawable(
                        mFragment!!.requireActivity().resources,
                        R.drawable.icon_camera_999,
                        null
                    ) as BitmapDrawable?)!!.bitmap //default image
                }
                mCurrentPhotoPath = "" //initialize
                mFragment!!.onActivityResult(501, 10, null) //update item
                if (linearLayout.parent != null) (linearLayout.parent as ViewManager).removeView(
                    linearLayout
                )
            } catch (e: Exception) {
                Log.w(TAG, "SAVE ERROR!", e)
            }
        }

        bnCaptureClear?.setOnClickListener {
            Toast.makeText(mFragment!!.activity, "clear", Toast.LENGTH_LONG).show()
            ivCapture?.setImageBitmap(null)
            //disable both buttons at start
            bnCaptureSave?.isEnabled = true
            //        bnCaptureClear.setEnabled(false);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                bnCaptureSave?.backgroundTintList = mFragment?.requireContext()
                    ?.let { it1 -> ContextCompat.getColor(it1, R.color.button_default_color) }
                    ?.let { it2 -> ColorStateList.valueOf(it2) }
                //            bnCaptureClear.setBackgroundTintList(ColorStateList.valueOf(mFragment.getActivity().getResources().getColor(R.color.disable)));
            }
        }

        bnCaptureClose?.setOnClickListener {
            if (linearLayout.parent != null) (linearLayout.parent as ViewManager).removeView(
                linearLayout
            )
        }
    }

    fun onActivityResult(requestCode: Int, resultCode: Int) {
        try {
            //after capture
            when (requestCode) {
                10 -> {
                    if (resultCode == Activity.RESULT_OK) {
//                        val file = mCurrentPhotoPath?.let { File(it) }
//                        var bitmap = MediaStore.Images.Media
//                            .getBitmap(
//                                mFragment!!.requireActivity().contentResolver,
//                                Uri.fromFile(file)
//                            )
                        val options: BitmapFactory.Options = BitmapFactory.Options()
                        options.inSampleSize = 4
                        var bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, options)

                        if (!ObjectUtils.isEmpty(bitmap)) {
                            val ei = ExifInterface(mCurrentPhotoPath!!)
                            val orientation = ei.getAttributeInt(
                                ExifInterface.TAG_ORIENTATION,
                                ExifInterface.ORIENTATION_UNDEFINED
                            )

                            try {
                                bitmap = when (orientation) {
                                    ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
                                    ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
                                    ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
                                    ExifInterface.ORIENTATION_NORMAL -> bitmap
                                    else -> bitmap
                                }
                                outputBitmap = bitmap //원본은 회전시켜 저장
//                                options.inSampleSize = calculateInSampleSize(options, imgWidth, imgHeight)
//                                bitmap = Bitmap.createScaledBitmap(bitmap, imgWidth, imgHeight, true)

                                ivCapture?.setImageBitmap(bitmap)
                                bnCaptureSave!!.isEnabled = true
                                bnCaptureClear!!.isEnabled = true
                                bnCaptureSave!!.backgroundTintList = mFragment?.requireContext()
                                    ?.let {
                                        ContextCompat.getColor(it, R.color.button_default_color)
                                    }?.let { ColorStateList.valueOf(it) }
                            } catch (e: OutOfMemoryError) {
                                Log.e(TAG, "OutOfMemoryError!", e)
                            } finally {
//                                if (!bitmap.isRecycled) bitmap.recycle();
                            }
                        }
                    }
                }
            }
        } catch (e: Exception) {
            Log.w(TAG, "onActivityResult Error !", e)
        }
    }

    fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
    ): Bitmap {
        // First decode with inJustDecodeBounds=true to check dimensions
        return BitmapFactory.Options().run {
            inJustDecodeBounds = true
            BitmapFactory.decodeResource(res, resId, this)

            // Calculate inSampleSize
            inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

            // Decode bitmap with inSampleSize set
            inJustDecodeBounds = false

            BitmapFactory.decodeResource(res, resId, this)
        }
    }

    //축소 버전을 메모리로 로드
    private fun calculateInSampleSize(
        options: BitmapFactory.Options,
        reqWidth: Int,
        reqHeight: Int
    ): Int {
        // Raw height and width of image
        val (height: Int, width: Int) = options.run { outHeight to outWidth }
        var inSampleSize = 1

        if (height > reqHeight || width > reqWidth) {

            val halfHeight: Int = height / 2
            val halfWidth: Int = width / 2

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                inSampleSize *= 2
            }
        }

        return inSampleSize
    }

    companion object {
        val TAG = CustomCapturePad::class.java.simpleName.trim() //CustomCapturePad"

        //카메라에 맞게 이미지 로테이션
        fun rotateImage(source: Bitmap, angle: Float): Bitmap {
            val matrix = Matrix()
            matrix.postRotate(angle)
            return Bitmap.createBitmap(
                source, 0, 0, source.width, source.height,
                matrix, true
            )
        }
    }
}

 

import android.graphics.Bitmap

class CaptureItem {
    var title: String? = null
    var realPath: String? = null
    var category: String? = null
    var capture: Bitmap? = null
    var status: Boolean? = null
    override fun toString(): String {
        return "CaptureItem [" +
                "  title : $title"+
                ", category : $category"+
                ", realPath : $realPath"+
                ", capture : $capture"+
                ", status : $status"+
                "]"
    }
}

<참조>

https://developer.android.com/topic/performance/graphics/load-bitmap?hl=ko 

 

큰 비트맵을 효율적으로 로드  |  Android 개발자  |  Android Developers

이미지의 모양과 크기는 다양합니다. 많은 경우 이미지는 일반적인 애플리케이션 사용자 인터페이스(UI)에서 요구하는 크기보다 큽니다. 예를 들어, 시스템 Gallery 애플리케이션은 Android 기기의

developer.android.com

https://www.androidpub.com/android_dev_qna/1282821

 

Android: [FAQ] OutOfMemoryError bitmap size exceeds VM budget 메모리 에러 - 자주하는 질문 - 안드로이드 개발

메모리 부족 관련 질문들이 있어서 정리 합니다. (기억을 더듬어 작성하는거라 잘못된 부분이 있으면 댓글로 말씀주세요)안드로이드에서 OutOfMemoryError라 발생하는 가장 많은 경우는 바로 비트맵

www.androidpub.com

https://mainia.tistory.com/468

 

안드로이드 (Android) Bitmap 구현, 관리 하기

안드로이드 (Android) Bitmap 구현, 관리 하기 개발환경 : JDK 1.5, eclipse-galileo, android googleAPI 7, window XP 모든 프로그램에서 이미지 관리의 기본은 비트맵이다. 안드로이드에서도 마찬가지로 이미..

mainia.tistory.com

https://leveloper.tistory.com/167

 

[Android] ViewTreeObserver란? - View가 그려지는 시점 알아내기

ViewTreeObserver란? Android Developers에서는 ViewTreeObserver를 다음과 같이 설명하고 있다. A view tree observer is used to register listeners that can be notified of global changes in the view tree...

leveloper.tistory.com

https://stackoverflow.com/questions/25719620/how-to-solve-java-lang-outofmemoryerror-trouble-in-android

 

How to solve java.lang.OutOfMemoryError trouble in Android

Altough I have very small size image in drawable folder, I am getting this error from users. And I am not using any bitmap function in code. At least intentionally :) java.lang.OutOfMemoryError ...

stackoverflow.com

https://creaby.tistory.com/1

 

다수의 비트맵 처리 시 메모리 관리 (1)

그동안 하이브리드 앱을 만들다가 작년에 영상처리를 해주는 앱을 만들게 되었는 데, 이때 영상처리 파트 쪽에서 고화질의 이미지를 넘겨주길 원했습니다. 문제는 앱의 프로세스 상 한번에 수

creaby.tistory.com

https://marlboroyw.tistory.com/481

 

Android(Java) 에서 OutOfMemory 를 catch 할 수 있을까?

Stackoverflow 를 둘러보던 중, 아래와 같은 글을 발견했다. Java 에서 try-catch 로 OutOfMemoryError 를 잡을 수 있나?? 아니 어떻게 OS 단에서 컨트롤 하는 메모리에러를 잡지? 라고 생각하며 스크롤을 내렸

marlboroyw.tistory.com

 

[Kotlin] Android OutOfMemoryError 

 

Process: com.ming.gu, PID: 28384
java.lang.OutOfMemoryError: Failed to allocate a 31961100 byte allocation with 16774160 free bytes and 16MB until OOM
	at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
	at android.graphics.Bitmap.nativeCreate(Native Method)
	at android.graphics.Bitmap.createBitmap(Bitmap.java:977)
	at android.graphics.Bitmap.createBitmap(Bitmap.java:948)
	at android.graphics.Bitmap.createBitmap(Bitmap.java:879)
	at com.ming.gu.function.CustomCapturePad$Companion.rotateImage(CustomCapturePad.kt:203)
	at com.ming.gu.function.CustomCapturePad.onActivityResult(CustomCapturePad.kt:173)
	at com.ming.gu.document.EvidenceDocuFragment.onActivityResult(EvidenceDocuFragment.kt:240)
	at androidx.fragment.app.FragmentManager$9.onActivityResult(FragmentManager.java:2889)
	at androidx.fragment.app.FragmentManager$9.onActivityResult(FragmentManager.java:2869)
	at androidx.activity.result.ActivityResultRegistry.doDispatch(ActivityResultRegistry.java:362)
	at androidx.activity.result.ActivityResultRegistry.dispatchResult(ActivityResultRegistry.java:322)
	at androidx.activity.ComponentActivity.onActivityResult(ComponentActivity.java:634)
	at androidx.fragment.app.FragmentActivity.onActivityResult(FragmentActivity.java:164)
	at android.app.Activity.dispatchActivityResult(Activity.java:7273)
	at android.app.ActivityThread.deliverResults(ActivityThread.java:4520)
	at android.app.ActivityThread.handleSendResult(ActivityThread.java:4567)
	at android.app.ActivityThread.-wrap22(ActivityThread.java)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1695)
	at android.os.Handler.dispatchMessage(Handler.java:102)
	at android.os.Looper.loop(Looper.java:154)
	at android.app.ActivityThread.main(ActivityThread.java:6780)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1496)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1386)

흠.. 예전에 만들었던 앱에 무슨 에러가 그렇게 난다길래 보니까 OutOfMemory 에러가 발생했다.

아마 Custom pad를 만들어 사진 캡처하는 기능을 구현했었는데 이미지 처리하던 중 메모리 부족이 발생한 듯.

 

 

1.gradle.properties

#org.gradle.jvmargs=-Xmx2048m
org.gradle.jvmargs=-Xmx4096m

일단 jvm메모리를 2배로 늘려주었다.

 

2. AndroidManifest.xml

android:largeHeap="true"

안드로이드 설정도 바꿔줬다.

 

3.gradle clean

./gradlew clean && ./gradlew : assembleDebug --no-build-cache

 

 

[Kotlin] Local Storage에 logcat파일 만들기

 

Web Application은 서버에 로그가 남지만 Android 같은 경우에 테스트할 때가 아니면 각각의 디바이스 내에 로그가 생성되어 확인하기가 어렵다.

지금은 ADB를 이용한 Wifi logging이 가능하고 또 온라인 환경에서는 서버로 바로 로그를 보낼 수 있지만..

특히 오프라인 환경에는 더욱 에러트레이스 하기 어렵다.

따라서 앱이 기동되었을 때 로그 파일을 생성할 수 있도록 해보자.

 

1. AndroidManifest.xml

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.READ_LOGS"
        tools:ignore="ProtectedPermissions" />

세 개의 권한을 부여하자.

 

2. 함수 생성

fun setLogFile() {
        val date = Date(System.currentTimeMillis())
        val format = SimpleDateFormat("yyyyMMddhhmmss", Locale.KOREA)
        val time: String = format.format(date)

        when {
            isExternalStorageWritable -> {
                //read, write 둘다 가능
                val logDir =
                    File("data/data/com.minggu.myapp/logs")

                //logDirectory 폴더 없을 시 생성
                if (!logDir.exists())
                    logDir.mkdirs()

                val logFile = File(logDir, "log_$time.txt")
                Log.d(LoginActivity.TAG, "New Logging Start :: $logFile")

                //이전 logcat 을 지우고 파일에 새 로그을 씀
                try {
                    Runtime.getRuntime().exec("logcat -c")
                    Runtime.getRuntime().exec("logcat -f $logFile")
                } catch (e: IOException) {
                    e.printStackTrace()
                }

                //만약 한달 전 로그 있다면 삭제
                val deleteFileList = logDir.listFiles()
                val cal: Calendar = Calendar.getInstance()
                cal.time = Date()
                cal.add(Calendar.MONTH, -1)

                deleteFileList?.let {
                    for (file in deleteFileList) {
                        if (Date(file.lastModified()) < cal.time) {
                            Log.i(TAG, "Log File deleted : " + file.name)
                            file.delete()
                        }
                    }
                }

            }
            isExternalStorageReadable -> {
                //read 만 가능
                Log.e(LoginActivity.TAG, "Only Read!")
            }
            else -> {
                //접근 불가능
                Log.e(LoginActivity.TAG, "You can't access!")

            }
        }
    }

간단해보이지만 핵심 기능은 다 들어있다.

패키 지명을 가져오는 다양한 방법들이 있는데 원하는 방법을 선택하기로 하자. (난 샘플로 com.minggu.myapp으로 명명)

함수로 만들어두었으니 MainActivity 혹은 SplashActivity 혹은 LoginActivity에 넣어두면 된다.

 

 

안드로이드 스튜디오는 내장접근이 가능하니 저렇게 했지만 외부저장소로 빼려면

${Environment.getExternalStorageDirectory()}

을 사용하거나 서버로 바로 보내거나 바로 내보내기 기능을 구현하면 될 듯하다.

[Linux] WSL2를 이용한 윈도우에 리눅스 설치

 

개인 로컬 컴퓨터에서 만약 리눅스 환경을 구성해보고 싶다면.. 그리고 맥이 아니라 윈도우를 사용한다면.. WSL을 사용할 수 있다. 

 

1. WSL, WSL2?

리눅스용 윈도우 하위 시스템(Windows Subsystem for Linux, WSL)은 윈도우 10 윈도우 11에서 네이티브로 리눅스 실행 파일(ELF)을 실행하기 위한 호환성 계층이다. 

Linux용 Windows 하위 시스템을 사용하면 개발자가 기존 가상 머신의 오버헤드 또는 듀얼 부팅 설정 없이 대부분의 명령줄 도구, 유틸리티 및 애플리케이션을 비롯한 GNU/Linux 환경을 수정하지 않고 Windows에서 직접 실행할 수 있다.

그리고 2020년 5월, WSL2가 릴리즈 되었다.

WSL 2는 Linux용 Windows 하위 시스템 아키텍처의 새로운 버전으로, Linux용 Windows 하위 시스템이 Windows에서 ELF64 Linux 이진 파일을 실행할 수 있게 해 준다. WSL 2의 주 목표는 파일 시스템 성능을 높이고전체 시스템 호출 호환성을 추가하는 것이다.

개별 Linux 배포는 WSL 1 또는 WSL 2 아키텍처를 사용하여 실행할 수 있다. 언제든지 각 배포를 업그레이드하거나 다운그레이드 할 수 있으며 WSL 1 및 WSL 2 배포를 함께 실행할 수 있다. WSL 2는 실제 Linux 커널을 실행하는 이점을 제공하는 완전히 새로운 아키텍처를 사용한다.

 

2. Chocolatey?

초콜리티(Chocolatey)는 윈도우 소프트웨어를 위한 머신 수준의 명령 줄 패키지 관리자이자 인스톨러이다. NuGet 패키징 인프라스트럭처와 윈도우 파워쉘을 사용하여 소프트웨어의 다운로드와 설치 과정을 단순화시킨다.

리눅스에서 apt, yum 혹은 노드에서 npm 들을 사용해봤다면 쉽게 짐작 가능한 윈도우 기반의 패키지 매니저 툴이다.

 

3. Chochlatey Install

작업은 명령 프롬프트 CMD 관리자모드 기준이다.

powershell.exe -nologo -noprofile -command "& { Set-ExecutionPolicy AllSigned }"
powershell.exe -nologo -noprofile -command "& { Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) }"

 

 

4. WSL2 Install

https://apps.microsoft.com/store/detail/ubuntu/9PDXGNCFSCZV?hl=ko-kr&gl=KR

 

Get Ubuntu from the Microsoft Store

Install a complete Ubuntu terminal environment in minutes with Windows Subsystem for Linux (WSL). Develop cross-platform applications, improve your data science or web development workflows and manage IT infrastructure without leaving Windows. Key features

apps.microsoft.com

마이크로 소프트 스토어에서 설치하고 실행 할 수 있으며 나처럼 윈도우 터미널을 설치해뒀으면 그냥 명령어만으로 설치할 수 있다.

(CMD)

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

rem rebooting
Shutdown -r -t 0

( 재부팅후 )

choco install -y wsl2
choco install -y wsl-ubuntu-2004

powershell.exe -nologo -noprofile -command "& { $dir= (Get-Item \"C:\Program Files\WindowsApps\CanonicalGroupLimited.UbuntuonWindows_2004*\ubuntu.exe\");$ubuntu=$dir.DirectoryName+\"\ubuntu.exe\";Start-Process \"$ubuntu\" }"

 

만약 파워쉘로 작업한다면 아래와 같이 명령어만 입력해도 된다.

$dir= (Get-Item "C:\Program Files\WindowsApps\CanonicalGroupLimited.UbuntuonWindows_2004*\ubuntu.exe");
$ubuntu=$dir.DirectoryName+"\ubuntu.exe";
Start-Process "$ubuntu"

 

 

5. 확인

$ wsl -l -v
  NAME      STATE           VERSION
* Ubuntu    Running         2

version 2 임을 확인하면 된다.

 

 

 

윈도우 터미널에서 새 세션을 시작하는 아래 화살표를 클릭하면 Ubuntu가 추가되었다.

 

\\WSL$로 접속하면 디렉토리를 확인할 수 있다.

 

만약 WSL로 docker 환경을 구축하고 싶다면 Hyper-V 설정을 해야하는데..(Window 10 home 이라면) 다음 시간에 알아보도록 하자.

 

Dbeaver로 SQL Server 접속시 database와 scema를 앞에 붙여줘야 해서 쿼리하기 번거로울 때가 있다.

select top 10 * from ming.gu.good by created_at desc;

ming 데이터베이스의 gu 스키마의 good테이블을 조회하는데 상당히 번거롭다.




데이터베이스를 클릭하고 F4, 혹은 마우스 우측 클릭 Connection settings로 들어간 뒤에 Default Database, Default Schema를 지정해준다.

select top 10 * from good order by created_at desc;

 

다른 database 혹은 다른 schema를 참조하고 싶으면 기존과 똑같이 프리픽스를 넣고 닷으로 구분하면 된다.

Kotlin으로 만든 Android앱을 유지보수할 일이 있어 다시 apk 파일을 generate하려고 하는데 해당 오류가 발생했다.

 

Could not determine the dependencies of task ':app:lintVitalRelease'.
> Could not resolve all artifacts for configuration ':app:debugCompileClasspath'.
   > Could not find com.github.gcacace:signature-pad:1.2.1.
     Required by:
         project :app
   > Could not find gun0912.ted:tedpermission:2.1.0.
     Required by:
         project :app

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

일단 저 2개의 패키지는 github 오픈소스를 가져온 것이긴 한데...

그리고 오랜만에 만진 소스여서 build.app:project내에 repositories를 jcenter()에서 mavenCentral()로 바꾼 것뿐인데...

 

다시 jcenter()로 원복 하니까 됐다.

 

결론은 내가 참조하는 2개의 패키지가 jcenter()에는 존재하고 mavenCentral()에는 존재하지 않기 때문인 것이다.

그러니까 기존 레거시를 수정할 일이 있으면 바꾸지 말자 ^^

 

<참고>

https://docs.gradle.org/current/userguide/declaring_repositories.html

 

Declaring repositories

When searching for a module in a repository, Gradle, by default, checks for supported metadata file formats in that repository. In a Maven repository, Gradle looks for a .pom file, in an ivy repository it looks for an ivy.xml file and in a flat directory r

docs.gradle.org

 

+ Recent posts