-->

[Kafka] Kafka 구성요소

Distributed Partitioned Immutable Log = Kafka

 

Topic

Partition으로 구성된 일련의 로그 파일 

  • RDBMS의 Partitioned Table과 유사한 기능
  • Topic은 Key-Value 형식의 메세지 구조이며, Value로 어떤 타입의 메세지도 가능(문자열, 숫자값, 객체, Json, Avro, Protobuf 등)
  • 로그 파일과 같이 연속적으로 추가, 발생하는 데이터를 저장하는 구조
  • 시간의 흐름에 따라 메시지가 순차적으로 물리적인 파일에 write 됨 (RDB처럼 Update 같은 개념이 없이 전부 Append한다. TSDB처럼)
  • 하나의 Topic은 1개 이상의 Partition을 가질 수 있다.

 

Partition

Topic의 partition은 kafka의 병렬 성능과 가용성 기능의 핵심 요소

메시지는 병렬 성능과 가용성을 고려한 topic 내의 개별 partition에 분산 저장됨.

또한 topic의 partition 들은 단일 kafka broker 뿐만 아니라 여러 개의 kafka broker 들에 분산 저장 됨.

두번째 Broker가 죽으면 첫번째 Broker의 Partition #1이 Follower에서 Leader가 되어 정합성 보장.

Offsets

개별 파티션은 정렬되고, 변경 할 수 없는(immutable) 일련의 레코드로 구성된 로그 메시지

  • 개별 레코드는 offset으로 불리는 일련 번호를 할당 받음
  • 개별 Partition은 다른 파티션과 완전히 독립적임
  • 개별 Partition 내에서 정렬되고 offset이 할당됨

 

Producer

Producers are clients that write events to Kafka. The producer specifies the topics they will write to and the producer controls how events are assigned to partitions within a topic. This can be done in a round-robin fashion for load balancing or it can be done according to some semantic partition function such as by the event key.

  • Producer는 Topic에 메세지를 보냄(메세지 write)
  • Producer는 성능 / 로드밸런싱 / 가용성 / 업무 정합성 등을 고려하여 어떤 브로커의 파티션으로 메세지를 보내야 할지 전략적으로 결정됨

Topic과 Value는 필수값으로 Broker에 value를 전달하면 Partition에 나누어 저장됨.

Consumer

Consumers are clients that read events from Kafka.

The only metadata retained on a per-consumer basis is the offset or position of that consumer in a topic. This offset is controlled by the consumer. Normally a consumer will advance its offset linearly as it reads records, however, because the position is controlled by the consumer it can consume records in any order. For example, a consumer can reset to an older offset to reprocess data from the past or skip ahead to the most recent record and start consuming from “now”.

This combination of features means that Kafka consumers can come and go without much impact on the cluster or on other consumers.

  • Consumer는 Topic에서 메세지를 읽어 들임.
  • 여러 개의 Consumer들로 구성 될 경우 어떤 브로커의 파티션에서 메세지를 읽어들일지 전략적으로 결정함

 

auto.offset.reset

Consumer가 Topic에 처음 접속하여 Message를 가져올 때 가장 오래된 처음 (Broker의 message) offset 부터(earliest) 가져올 것인지, 가장 최근인 마지막 offset 부터 가져올 것 인지를 설정하는 파라미터

  • auto.offset.reset = ealiest :  처음 offset 부터 읽음
  • auto.offset.reset = latest : 마지막 offset부터 읽음 (default)
  • kafka-console-consumer 명령어를 사용 할 때 --from-beginning을 사용해야만 auto.offset.reset이 earlist로 지정됨

 

Serializer/Deserializer

Producer → Broker로 Key,Value가 전송될 때 Serialize 되어 ByteArray로 전달 된다. 그리고 Broker → Consumer는 ByteArray를 Deserialize하여 key, value를 획득한다.

ByteArray를 사용하여 전송하면 네트워크 대역폭도 잘 사용할 수 있고 압축도 된다. 자바 코드에서는 아래처럼 적용한다.

Kafka에서 기본적으로 제공하는 Serializer는 StringSerializer, ShortSerializer, IntegerSerializer, LongSerializer, DoubleSerializer, BytesSerializer가 있다. (그러나 업무에선 Custom 만들어야 할 듯)

 

Partitioner

메세지는 Producer를 통해 전송시 Partitioner를 통해 토픽의 어떤 파티션으로 전송되어야 할 지 미리 결정됨.

메세지 Key는 업무 로직이나 메세지 Produce/Consume시 분산 성능 영향을 고려하여 생성

  • Key 값을 가지지 않는 경우,
    라운드 로빈, 스티키 파티션 등의 전략이 선택되어 파티션 별로 메세지가 전송 될 수 있음.
    → Topic이 여러 개의 파티션을 가질 때 메세지의 전송 순서가 보장되지 않은 채로 Consumer에서 읽혀질 수 있음.
    (전송 순서를 보장하려면 Partition을 하나로 가져야 하는데 그럼 분산시스템의 이점이 없어짐)
  • Key 값을 가지는 경우,
    특정 key값을 가지는 메세지는 특정 파티션으로 고정되어 전송됨.
    → 단일 파티션 내에서 전송 순서가 보장되어 Consumer에서 읽혀짐.
반응형

'⚙Server > 🎆Kafka' 카테고리의 다른 글

[Kafka] 설치 및 환경구축  (0) 2024.01.30

[Kafka] 설치 및 환경구축

 

Oracle VM이나 AWS freetier로 리눅스 서버를 생성한다.

환경구축이 되어있다면 바로 환경을 구축한다.

 

JDK 설치

java 11을 사용하려고 한다.

$ sudo apt-get install openjdk-11-jdk
...
$ java -version
openjdk version "11.0.20" 2023-07-18

 

Confluent Kafka 설치

kafka는 왜 Apache가 아니고 Confluent를 사용하느냐면 Confluent에는 ksqldb나 schema-registry 등 기본적으로 필요한 라이브러리가 포함되어 있고 admin UI가 존재한다.

Apache Kafka는 kafka core만 있어 따로 설치를 해야한다.

다운로드해서 sftp 이용해도 되지만 수고로우니 wget으로 서버 내에 다운로드하자.

$ cd ~ # home 으로 이동
$ sudo wget https://packages.confluent.io/archive/7.1/confluent-community-7.1.2.tar.gz?_ga=2.134073358.1620010104.1705371707-1047466751.1705371655&_gl=1*182lsyw*_ga*MTA0NzQ2Njc1MS4xNzA1MzcxNjU1*_ga_D2D3EGKSGD*MTcwNTM3MTY1NS4xLjEuMTcwNTM3MTgwMi42MC4wLjA.
$ sudo tar -xvf "/confluent/confluent-community-7.1.2.tar.gz?_ga=2.168151329.1620010104.1705371707-1047466751.1705371655"

환경 변수 설정

$ sudo vi ~/.bashrc
 
# 아래 2줄 추가 (confluent kafka 압축 해제한 폴더로)
export KAFKA_HOME=/home/tkbell/confluent-7.1.2
export PATH=.:$PATH:$KAFKA_HOME/bin
 
$ . .bashrc # 수정사항 반영
 
# 터미널 재시작 후 정상적으로 환경변수 등록 확인
$ echo $KAFKA_HOME
/home/tkbell/confluent-7.1.2
$ echo $PATH
.:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/tkbell/confluent-7.1.2/bin

home/$user 경로는 현재 자신 서버에 맞게 설정하면 된다.

 

Kafka 실행

기동은 Zookeeper를 먼저 실행하고 Kafka를 실행한다.

$ cd $KAFKA_HOME/etc/kafka/ # 해당 경로에 properties 파일들이 존재. 여기서 zookeeper.properties 파일과 함께 올려서 실행시킨다.
 
$ zookeeper-server-start ./zookeeper.properties  mkdir: cannot create directory ‘/confluent/bin/../logs’: Permission denied # 아 sudo 안썼구나
[0.001s][error][logging] Error opening log file '/confluent/bin/../logs/zookeeper-gc.log': No such file or directory
[0.002s][error][logging] Initialization of output 'file=/confluent/bin/../logs/zookeeper-gc.log' using options 'filecount=10,filesize=100M' failed.
Invalid -Xlog option '-Xlog:gc*:file=/confluent/bin/../logs/zookeeper-gc.log:time,tags:filecount=10,filesize=100M', see error log for details.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
 
$ sudo zookeeper-server-start ./zookeeper.properties
sudo: zookeeper-server-start: command not found # 아.....
 
# 그냥 쉘 스크립트를 실행하자....
$ sudo sh zookeeper-server-start $KAFKA_HOME/etc/kafka/zookeeper.properties

어차피 나중에 쉘로 실행할 거니까.. 일단 주키퍼 띄우고 그 상태에서 새 터미널을 띄워 kafka를 띄워보자.

$ cd $CONFLUENT_HOME
$ sudo sh kafka-server-start $KAFKA_HOME/etc/kafka/server.properties
 
...
[2024-01-16 12:07:57,039] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)
[2024-01-16 12:07:57,189] INFO [BrokerToControllerChannelManager broker=0 name=alterIsr]: Recorded new controller, from now on will use broker ubuntu-20-minhalee.tkbell.gmarket.nh:9092 (id: 0 rack: null) (kafka.server.BrokerToControllerRequestThread)
[2024-01-16 12:07:57,213] INFO [BrokerToControllerChannelManager broker=0 name=forwarding]: Recorded new controller, from now on will use broker ubuntu-20-minhalee.tkbell.gmarket.nh:9092 (id: 0 rack: null) (kafka.server.BrokerToControllerRequestThread)

 

또 새 터미널을 띄워 topic을 만들어보자.

$ kafka-topics --bootstrap-server localhost:9092 --create --topic welcome-topic
Created topic welcome-topic.

 

종료할 때도 카프카를 먼저 죽이고 주키퍼를 죽이면 된다.

편하게 하게 위해서 일단 주키퍼 & 카프카 실행 쉘 스크립트를 만들어준다.

$ cd ~
$ sudo vi zoo_start.sh
sudo sh $KAFKA_HOME/bin/zookeeper-server-start $KAFKA_HOME/etc/kafka/zookeeper.properties
 
$ sudo vi kafka_start.sh
sudo sh $KAFKA_HOME/bin/kafka-server-start $KAFKA_HOME/etc/kafka/server.properties
 
$ sudo chmod +x *.sh # 누구나 실행할 수 있도록 권한 변경
 
$ ./zoo_start.sh
# 새 터미널 띄워서
$ ./kafka_start.sh

정상적으로 실행되면 완료!

 

로그 경로 변경

$ cd $KAFKA_HOME/etc/kafka
$ ls
connect-console-sink.properties    connect-mirror-maker.properties  server.properties
connect-console-source.properties  connect-standalone.properties    tools-log4j.properties
connect-distributed.properties     consumer.properties              trogdor.conf
connect-file-sink.properties       kraft                            zookeeper.properties
connect-file-source.properties     log4j.properties
connect-log4j.properties           producer.properties
$ sudo vi server.properties
 
...
# A comma separated list of directories under which to store log files
log.dirs=/home/tkbell/data/kafka-logs
...
 
$ sudo vi zookeeper.properties
 
...
dataDir=/home/tkbell/data/zookeeper
...

Virtual Box를 이용해서 kafka 테스트를 진행한다면 tmp 경로가 날아가니까 다른 데로 변경해야 하는데 Tinker 사용한다면 딱히 바꿀 필요는 없다.

그래도 운영서버에 구축한다면 주로 로그를 많이 봐야 하므로 zookeeper와 kafka의 로그 경로를 home으로 변경해 준다. 

카프카의 환경설정 파일은 server.properties 이고 Zookeeper의 환경 설정 파일은 zookeeper.properties 이다.

 

정상적으로 로그가 남는지 확인해 본다.

$ cd
$ kafka-topics --bootstrap-server localhost:9092 --create --topic welcome-topic # 신규 토픽 생성
Created topic welcome-topic.
$ cd ./data/kafka-logs/
$ ls
cleaner-offset-checkpoint    meta.properties                   replication-offset-checkpoint
log-start-offset-checkpoint  recovery-point-offset-checkpoint  welcome-topic-0

welcome-topic-0이라는 이름으로 토픽이 생성되었다. 파티션 단위로 로그가 생겨나는 것을 확인할 수 있다.

 

반응형

'⚙Server > 🎆Kafka' 카테고리의 다른 글

[Kafka] Kafka 구성요소  (2) 2024.02.14

[MongoDB] MongoDB Tool 이용한 접속

저번 포스팅에 mongoDB를 Docker로 설치하여 접속해 봤다.

mongod.conf 파일에서 bindIp 속성의 값을 허용 ip대역으로 설정하면 외부에서도 접속할 수 있다.

 

MongoDB Compass(GUI)

 

Try MongoDB Tools - Download Free Here

Free download for MongoDB tools to do more with your database. MongoDB Shell, Compass, CLI for Cloud, BI Connector and other database tools available.

www.mongodb.com

공식 홈페이지에서 제공하는 무료 GUI 툴이다.

접속 정보를 입력하고 저장 & 연결하면 접속이 된다.

역시 GUI가 편하다.. 손쉽게 데이터를 추가하거나 import 할 수 있는 구조이다. 별다른 설명서가 없어도 조작하는데 어려움이 없다.

 

MongoDB Shell(CLI)

압축 파일을 받아서 열기만 하면 mongosh.exe를 이용해 쉽게 쉘을 실행시킬 수 있다.

일일이 해당 파일을 실행해서 접속하기는 매우 귀찮으니까 Windows 기준으로 환경 변수 설정을 해준다.

시스템 환경 변수 편집으로 들어가 '환경 변수'를 클릭해 준다.

사용자 변수던 시스템 변수던 Path 변수를 선택해 편집을 눌러준다.

방금 설치한 mongoDB Shell의 위치 폴더 경로를 입력해 준다.

그리고 시스템 재부팅하거나 cmd 열어서 set 명령어 입력해 준다.

작업 표시줄에 있는 터미널 속성 들어가 준 다음에 고급에 관리자권한 실행 체크

 

mongosh 명령어를 이용해 접속정보와 DB, -u 유저, -p 비밀번호를 입력하면 접속이 된다.

반응형

'Database > 🌿MongoDB' 카테고리의 다른 글

[MongoDB] Docker로 MongoDB 설치  (0) 2023.07.28

[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>

 

외부에서 접속도 잘 된다

반응형

'Database > 🌿MongoDB' 카테고리의 다른 글

[MongoDB] MongoDB Tool 이용한 접속  (0) 2023.08.02

[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;
  };
}

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

반응형

[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()}

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

반응형

+ Recent posts