Shell Script 전체 코드

#! /usr/bin/env bash

hosts = (a.example.com b.example.com)
LOG = "access.log"
pattern = "ERROR|WARN"

for host in $hosts
do
    if [ -z $pattern ]; then
        ssh deploy@$host "tail -F $LOG" | awk -v server=$host '{print "["server"]", $0}' &
    else
        ssh deploy@$host "tail -F $LOG" | awk -v server=$host -v pattern=$pattern '$0~pattern {print "["server"]", $0}' &
    fi
done

trap "ps | grep ssh | grep $LOG | awk '{print \$1}' | xargs kill -9" INT
wait

명령어

각 명령어에 대한 자세한 설명은 생략하고 간단하게 나열합니다.


tail

ssh deploy@$host "tail -F $LOG" &

원격 서버에 명령어를 전달하려면 ssh deploy@$host "{command}" 형태로 사용합니다.

명령어의 끝에 & 을 붙여서 모든 tail 명령어를 백그라운드에서 실행시키고 출력값만 현재 쉘에서 모아서 봅니다.


awk

awk -v server=$host '{print "["server"]", $0}'

여기서 awk 명령어는 tail 로 출력되는 로그들을 특정 포맷으로 감싸서 재출력하는 기능을 합니다.

-v 옵션은 awk action 에 특정 변수를 넘겨주고 싶을 때 사용하고 $0 은 레코드 전체를 의미합니다.

awk -v server=$host '{print "["server"]", $0}' 은 로그들을 [a.example.com] 로그내용~ 으로 변환해서 출력해줍니다.

로그들이 여러개 쌓이면 어디 서버에서 발생한 건지 알수 없기 때문이죠


awk (filter)

awk -v server=$host -v pattern=$pattern '$0~pattern {print "["server"]", $0}'

ERROR 또는 WARN 로그만 보고 싶을 수도 있습니다.

정규표현식 ~ 오퍼레이터를 사용하여 레코드 전체 ($0) 에서 정규표현식 $pattern 이 포함되어 있는 경우에만 출력하도록 필터를 걸었습니다.

처음에는 grep 명령어를 사용했으나 속도가 너무 느려서 실시간 전달이 안되고 로그가 뚝뚝 끊겨서 전달되어 보기 굉장히 불편했습니다.


trap & wait

trap "ps | grep ssh | grep $LOG | awk '{print \$1}' | xargs kill -9" INT
wait

& 기호를 붙여 백그라운드에서 실행했기 때문에 로그를 그만 볼때 모든 프로세스를 한번에 종료시켜야 합니다.

trap <handler> <signal> 은 특정 시그널을 받았을 때 handler 를 실행시키는 명령어입니다.

wait 은 자식 프로세스가 종료될 때까지 대기합니다.

즉, 위 코드는 실행한 쉘 스크립트를 대기 시켰다가 Ctrl + C 시그널을 받았을 때 백그라운드 프로세스를 모두 kill 합니다.

wait 명령어를 넣지 않으면 스크립트를 실행한 후에 전달할 시그널이 애매합니다.

그리고 로그 보고 있을 거니까 부모 프로세스를 굳이 조작할 필요도 없습니다.


Reference

에러 로그

TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing

원인

JPA 연관 관계 테스트 중에 발생했습니다.

FK 로 사용되는 컬럼값이 없는 상태에서 데이터를 넣으려다 발생한 에러입니다.

예를 들어 Person (id, name) 이라는 테이블과 House (id, address, person_id) 라는 테이블 관계가 있을 때, Person 데이터를 넣지 않고 House 데이터를 넣으려고 하면 person_id 값이 없어서 에러가 발생합니다.

Person person = new Person("Alice");
House house = new House("Seoul", person);

houseRepository.save(house);    // 에러 발생 (person 의 id 값을 모르는데 테이블에 넣으려고 해서)

해결 방법

연관 관계 매핑해줄 때 사용하는 @ManyToOne, @OneToOne, @OneToMany 어노테이션에 cascade 옵션을 설정해줍니다.

cascade 는 "영속성 전이" 라고 하는 개념인데 특정 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화 합니다.

저장할 때만 사용하려면 cascade = CascadeType.PERSIST 로 설정해주면 되며, 전체 적용인 CascadeType.ALL 로 설정해도 됩니다.

그럼 House 데이터를 저장하기 전에 Person 값부터 저장합니다.


Reference

Overview

쉘 스크립트 파일을 보면 파일 최상단에 #!/usr/bin/env bash 가 있는 걸 많이 봤을겁니다.

잘 모를때는 그냥 주석으로 실행 환경을 의미하는 건 줄 알았는데 사실은 굉장히 중요한 문장이었습니다.


1. SheBang (#!)

우선 #! 은 주석이 아니라 이 자체가 하나의 기호입니다.

어원은 위키피디아에 의하면 어원은 Hash(#) 또는 Sharp(#) 과 Bang(!) 의 합성어에서 유래한 것 같다고 합니다.

#! 은 2 Byte 의 매직 넘버 (Magic Number) 로 스크립트의 맨 앞에서 이 파일이 어떤 명령어 해석기의 명령어 집합인지를 시스템에 알려주는 역할을 합니다.

#! 바로 뒤에 나오는 것은 경로명으로, 명령어들을 해석할 프로그램의 위치를 나타냅니다.

가장 일반적으로 사용되는건 #!/bin/bash 이며 앞으로 나올 명령어들을 주석을 제외하고 순서대로 실행시킵니다.

만약 경로가 정확하지 않다면 bad interpreter 가 발생하고 다른 인터프리터를 지정하면 문법 오류로 실패합니다.


1.1. 문법

#!<interpreter> [optional-arg]

문법은 위와 같으며 #! 뒤에 공백 (Space) 이 하나 있어도 동작합니다.

<Interpreter> 에는 프로그램의 절대경로가 입력되어야 합니다.


1.2. 예시

#!/bin/sh
#!/bin/bash
#!/usr/bin/pwsh
#!/usr/bin/env python3

2. #!/usr/bin/env 란?

위 설명에서 <interpreter> 에는 프로그램의 절대 경로가 와야 한다고 했습니다.

하지만 절대경로는 시스템에 따라 달라질 수도 있습니다.

파이썬을 예로 들면 시스템에 따라 /bin/local/python 또는 /usr/bin/python 에 위치할 수도 있고 버전 또한 python2 와 python3 둘다 설치되어 있을 수 있습니다.

이럴 때 #!/usr/bin/env 로 설정하면 절대경로에 상관 없이 인터프리터의 위치를 찾아서 실행해줍니다.

그러니 여러 환경에서 실행되야할 스크립트라면 #!/usr/bin/env 를 사용하는 게 좋습니다.


3. Test

간단하게 테스트를 해봅시다.

터미널을 켜서 vi test 로 파일 하나를 작성합니다.

#! /usr/bin/env bash

echo "Hello This is Bash"

위 파일에 실행 권한을 추가하고 실행하면 정상적으로 동작합니다.

# 권한 추가
$ chmod +x test

# 실행
$ ./test
Hello This is Bash

이번엔 다시 파일을 열어 인터프리터를 파이썬으로 지정합니다.

#! /usr/bin/env python

echo "Hello This is Bash"

권한은 아까 주었으니 다시 파일만 실행시키면 에러가 발생합니다.

파이썬 문법에 맞지 않아 오류가 발생한 겁니다.

$ ./test
  File "./test", line 3
    echo "Hello This is Bash"
                            ^
SyntaxError: invalid syntax

다시 파일을 열어 파이썬 문법으로 고쳐줍시다.

#! /usr/bin/env python

print "Hello This is Python"

파일을 실행시키면 정상적으로 실행됩니다.

$ ./test
Hello This is Python

Conclusion

SheBang(#!) 문법은 스크립트 파일에서 어떤 프로그램으로 해당 파일을 실행시킬 지 결정합니다.

#!/usr/bin/env 를 사용하면 인터프리터의 절대 경로를 지정하지 않아도 알아서 경로를 찾아주기 때문에 여러 시스템 환경에서 사용할 때 유용합니다.

테스트를 보면서 알 수 있었던 한가지 특징은 확장자를 입력하지 않아도 #! 으로 인터프리터를 지정하면 정상적으로 실행된다는 사실입니다.


Reference

해결하고 나니 굉장히 사소한 실수였습니다.

Error creating bean with name 'tokenRedisRepository' defined in TokenRedisRepository defined in @EnableRedisRepositories declared on RedisRepositoriesRegistrar.EnableRedisRepositoriesConfiguration: 
Invocation of init method failed; 
nested exception is org.springframework.data.mapping.MappingException: Entity com.example.login.entity.
Token requires to have an explicit id field. Did you forget to provide one using @Id?
  • RedisRepository 를 적용하는 도중 위와 같은 에러가 발생
  • Redis 에 사용할 객체의 @Id 어노테이션 임포트를 잘못해서 발생 (JPA 에서 사용하는 @Id 를 임포트함)
  • javax.persistence.Id 대신 org.springframework.data.annotation.Id 을 임포트 해서 해결

1. Redis 란?

Redis 는 Key-Value 형태로 데이터를 관리하는 오픈 소스입니다.

Redis 는 빠른 속도와 간편한 사용법으로 인해 캐시, 인증 토큰, 세션 관리 등등 여러 용도로 사용됩니다.

  • In-Memory Data Strucutre Store
  • Key - Value 형태로 데이터 저장
  • 여러 가지 Value 타입 저장 가능 (String, Set, Hash, List 등등..)
  • Single Thread
  • 데이터 만료 시간 지정 가능

2. Redis 설치

도커로 설치 후 실행 가능합니다. (https://hub.docker.com/_/redis 참고)

# 이미지 다운 (docker images 로 확인 가능)
$ docker pull redis

# 컨테이너로 레디스 실행 (--name: 컨테이너 이름 설정, -p: 포트 포워딩, -d: 백그라운드에서 실행)
$ docker run --name some-redis -p 6379:6379 -d redis

# redis-cli 접속
$ docker exec -it some-redis redis-cli

3. Redis 명령어

Redis 는 여러 개의 데이터 타입을 저장할 수 있기 때문에 각각의 명령어가 여러개 존재합니다.

모든 명령어는 Redis Commands 를 참고하시고 여기에는 일부 자료구조의 간단한 명령어만 정리합니다.

다만 Redis 는 Single Thread 기반이기 때문에 keys, flushall, flushdb, getall 등 일반적으로 생각했을 때 O(N) 의 시간복잡도를 가질 것 같은 명령어는 운영 환경에서 사용하면 위험합니다.


3.1. String

가장 기본적인 Value 타입입니다.

  • 저장
    • set {key} {value} : key, value 를 저장
    • mset {key} {value} [{key} {value} ...] : 여러 개의 key, value 를 한번에 저장
    • setex {key} {seconds} {value} : key, seconds, value 저장 (설정한 시간 뒤에 소멸)
  • 조회
    • keys * : 현재 저장된 키값들을 모두 확인 (부하가 심한 명령어라 운영중인 서비스에선 절대 사용하면 안됨)
    • get {key} : 지정한 key 에 해당하는 value 를 가져옴
    • mget {key} [{key} ...] : 여러 개의 key 에 해당하는 value 를 한번에 가져옴
    • ttl {key} : key 의 만료 시간을 초 단위로 보여줌 (-1 은 만료시간 없음, -2 는 데이터 없음)
    • pttl {key} : key 의 만료 시간을 밀리초 단위로 보여줌
    • type {key} : 해당 key 의 value 타입 확인
  • 삭제
    • del {key} [{key} ...] : 해당 key 들을 삭제
  • 수정
    • rename {key} {newKey} : key 이름 변경
    • expire {key} {seconds} : 해당 키 값의 만료 시간 설정
  • 기타
    • randomkey : 랜덤한 key 반환
    • ping : 연결 여부 확인 ("ping" 만 입력하면 "PONG" 이라는 응답이 옴)
    • dbsize : 현재 사용중인 DB 의 key 의 갯수 리턴
    • flushall : 레디스 서버의 모든 데이터 삭제
    • flushdb : 현재 사용중인 DB 의 모든 데이터 삭제

3.2. Set

Redis 에서는 Set 에 포함된 값들을 멤버라고 표현합니다.

여러 멤버가 모여 집합 (Set) 을 구성합니다.

진짜 집합처럼 교집합, 차집합 등도 구할 수 있는데 여기선 간단하게 CRUD 만 알아봅니다.

  • sadd {key} {member} [{member} ...]
    • key 에 새로운 멤버들을 추가. key 가 없으면 새로 만듬
  • smembers {key}
    • key 에 설정된 모든 멤버 반환
  • srem {key} {member [{member} ...]}
    • key 에 포함된 멤버들 삭제. 없는 멤버 입력하면 무시됨
  • scard {key}
    • key 에 저장된 멤버 수를 반환
  • sismember {key} {member}
    • member 가 해당 key 에 포함되는지 검사

3.3. Hash

Redis 에서 저장가능한 자료구조 중에 Hash 도 있습니다.

Hash 자체를 나타내는 key 와 해당 key 에 포함된 field 까지 사용해서 값을 조회/저장할 수 있습니다.

  • hset {key} {field} {value} [{field} {value} ...]
    • key 를 이름으로 한 Hash 자료 구조에 field 와 value 값을 저장
  • hget {key} {field}
    • key Hash 값에 포함된 field 의 value 를 가져옴
  • hdel {key} {field} [{field} ...]
    • field 값으로 데이터 삭제
  • hlen {key}
    • Hash 가 갖고 있는 field 갯수 반환
  • hkeys {key}
    • Hash 가 갖고 있는 모든 field 출력
  • hvals {key}
    • Hash 가 갖고 있는 모든 value 출력
  • hgetall {key}
    • Hash 가 갖고 있는 모든 field 와 value 출력

Reference

'공부 > Database' 카테고리의 다른 글

MySQL Optimizer 와 USE INDEX vs FORCE INDEX  (0) 2022.06.21
MySQL Index 특징 및 유의사항 정리  (2) 2022.05.22
Cache 전략  (0) 2022.05.20

1. JWT 란 (Json Web Token)

JSON 객체를 사용해서 토큰 자체에 정보를 저장하는 Web Token 입니다.

Header, Payload, Signature 3 개의 부분으로 구성되어 있으며 쿠키나 세션을 이용한 인증보다 안전하고 효율적입니다.

일반적으로는 Authorization: <type> <credentials> 형태로 Request Header 에 담겨져 오기 때문에 Header 값을 확인해서 가져올 수 있습니다.


1.1. 장단점

  • 장점
    • 중앙 인증 서버, 저장소에 대한 의존성이 없어서 수평 확장에 유리
    • Base64 URL Safe Encoding 이라 URL, Cookie, Header 어떤 형태로도 사용 가능
    • Stateless 한 서버 구현 가능
    • 웹이 아닌 모바일에서도 사용 가능
    • 인증 정보를 다른 곳에서도 사용 가능 (OAuth)

  • 단점
    • Payload 의 정보가 많아지면 네트워크 사용량 증가
    • 다른 사람이 토큰을 decode 하여 데이터 확인 가능
    • 토큰을 탈취당한 경우 대처하기 어려움
      • 기본적으로는 서버에서 관리하는게 아니다보니 탈취당한 경우 강제 로그아웃 처리가 불가능
      • 토큰 유효시간이 만료되기 전까지 탈취자는 자유롭게 인증 가능
      • 그래서 유효시간을 짧게 가져가고 refresh token 을 발급하는 방식으로 많이 사용

1.2. Token 구성요소

  • Header

    • alg: Signature 를 해싱하기 위한 알고리즘 정보를 갖고 있음
    • typ: 토큰의 타입을 나타내는데 없어도 됨. 보통 JWT 를 사용
  • Payload

    • 서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보에 대한 내용을 담고 있음
    • JWT 가 기본적으로 갖고 있는 키워드가 존재
    • 원한다면 추가할 수도 있음
      • iss: 토큰 발급자
      • sub: 토큰 제목
      • aud: 토큰 대상
      • exp: 토큰의 만료시간
      • nbf: Not Before
      • iat: 토큰이 발급된 시간
      • jti: JWT의 고유 식별자
  • Signature

    • 서버에서 토큰이 유효한지 검증하기 위한 문자열
    • Header + Payload + Secret Key 로 값을 생성하므로 데이터 변조 여부를 판단 가능
    • Secret Key 는 노출되지 않도록 서버에서 잘 관리 필요

1.3. 토큰 인증 타입

Authorization: <type> <credentials> 형태에서 <type> 부분에 들어갈 값입니다.

엄격한 규칙이 있는건 아니고 일반적으로 많이 사용되는 형태라고 생각하면 됩니다.

  • Basic
    • 사용자 아이디와 암호를 Base64 로 인코딩한 값을 토큰으로 사용
  • Bearer
    • JWT 또는 OAuth 에 대한 토큰을 사용
  • Digest
    • 서버에서 난수 데이터 문자열을 클라이언트에 보냄
    • 클라이언트는 사용자 정보와 nonce 를 포함하는 해시값을 사용하여 응답
  • HOBA
    • 전자 서명 기반 인증
  • Mutual
    • 암호를 이용한 클라이언트-서버 상호 인증
  • AWS4-HMAC-SHA256
    • AWS 전자 서명 기반 인증

2. Refresh Token

JWT 역시 탈취되면 누구나 API 를 호출할 수 있다는 단점이 존재합니다.

세션은 탈취된 경우 세션 저장소에서 탈취된 세션 ID 를 삭제하면 되지만, JWT 는 서버에서 관리하지 않기 때문에 속수무책으로 당할 수밖에 없습니다.

그래서 탈취되어도 피해가 최소화 되도록 유효시간을 짧게 가져갑니다.

하지만 만료 시간을 30분으로 설정하면 일반 사용자는 30 분마다 새로 로그인 하여 토큰을 발급받아야 합니다.

사용자가 매번 로그인 하는 과정을 생략하기 위해 필요한 게 Refresh Token 입니다.


Refresh Token 은 로그인 토큰 (Access Token) 보다 긴 유효 시간을 가지며, Access Token 이 만료된 사용자가 재발급을 원할 경우 Refresh Token 을 함께 전달합니다.

서버는 Access Token 에 담긴 사용자의 정보를 확인하고 Refresh Token 이 아직 만료되지 않았다면 새로운 토큰을 발급해줍니다.

이렇게 하면 사용자가 매번 로그인해야 하는 번거로움 없이 로그인을 지속적으로 유지할 수 있습니다.


Refresh Token 은 사용자가 로그인 할 때 같이 발급되며, 클라이언트가 안전한 곳에 보관하고 있어야 합니다.

Access Toekn 과 달리 매 요청마다 주고 받지 않기 때문에 탈취 당할 위험이 적으며, 요청 주기가 길기 때문에 별도의 저장소에 보관 합니다. (정책마다 다르게 사용)


2.1. Refresh Token 저장소

Refresh Token 은 서버에서 별도의 저장소에 보관하는 것이 좋습니다.

  • Refresh Token 은 사용자 정보가 없기 때문에 저장소에 값이 있으면 검증 시 어떤 사용자의 토큰인지 판단하기 용이
  • 탈취당했을 때 저장소에서 Refresh Token 정보를 삭제하면 Access Token 만료 후에 재발급이 안되게 강제 로그아웃 처리 가능
  • 일반적으로 Redis 많이 사용

2.2. Refresh Token 으로 Access Token 재발급 시나리오

  1. 클라이언트는 access token 으로 API 요청하며 서비스 제공
  2. access token 이 만료되면 서버에서 access token 만료 응답을 내려줌
  3. 클라이언트는 access token 만료 응답을 받고 재발급을 위해 access token + refresh token 을 함께 보냄
  4. 서버는 refresh token 의 만료 여부를 확인
  5. access token 으로 유저 정보 (username 또는 userid) 를 획득하고 저장소에 해당 유저 정보를 key 값으로 한 value 가 refresh token 과 일치하는지 확인
  6. 4~5번의 검증이 끝나면 새로운 토큰 세트 (access + refresh) 발급
  7. 서버는 refresh token 저장소의 value 업데이트

Reference

'공부 > Web' 카테고리의 다른 글

웹 접근성과 WAI-ARIA  (1) 2021.11.10
URI 란?  (0) 2020.08.26

RabbitMQ 란

RabbitMQ 는 AMQP 프로토콜을 구현한 Message Broker 입니다.

  1. Producer 가 메세지를 전송 한다 (Publish)
  2. Exchange 가 Exchange Type 과 Routing Key 에 맞게 메세지를 Queue 에 전달한다.
  3. 메세지는 소비될 때까지 Queue 에 대기하고 있다가 Consumer 에 의해 소비된다.
Producer -> Exchange -> Queue -> Consumer

용어

Producer

  • 메세지를 보내는 Application
  • Message Queue 에 넣어주는(쏴주는) 사람

Publish

  • 메세지를 보내는 행위
  • Queue 에 넣는 행위

Queue

  • 메세지를 저장하는 버퍼

Consumer

  • 메세지를 받는 Application
  • 동일 업무를 처리하는 Consumer 는 보통 하나의 Queue를 바라본다.
  • 동일 업무를 처리하는 Consumer 가 여러개인 경우 같은 Queue를 바라보게 하면 자동으로 메세지를 분배하여 전달

Subscribe

  • Consumer 가 메세지를 수신하기 위해 Queue 를 바라보게 하는 행위

Exchange

  • Producer 가 전달한 메세지를 Queue 에 전달하는 역할
  • 메세지는 Queue 에 직접 전달되지 않고 Exchange Type 정의대로 동작

Exchange Type

  • fanout (Broadcast)
    • 알려진 모든 Queue 에 메세지를 전달 (Routing Key 를 무시하고 모든 Exchange 에 바인딩 된 모든 Queue 에 전달한다)
  • direct (unicast)
    • Exchange 에 바인딩 된 Queue 중에서 지정된 Routing Key 를 가진 Queue 에만 메세지를 전달
  • topic (multicast)
    • 지정된 패턴 바인딩 형태에 일치하는 Queue 에 모두 전달. #(여러단어), *(한단어)를 통한 문자열 패턴 매칭
      Routing Key 로 '#'를 지정한다면 fanout 과 동일하게 동작하고 #, * 없이 단어 하나만 입력하면 direct 와 동일하게 동작
  • header (multicast)
    • 헤더에 포함된 key=value 의 일치조건에 따라서 메세지 전달

Bindings

  • Exchange 와 Queue 를 연결해주는 것

Routing

  • Exchange 가 Queue 에 메세지를 전달하는 과정

Routing Key

  • 어떤 Queue 와 Binding 될 지 결정하는 기준

'공부 > Server' 카테고리의 다른 글

Nginx  (0) 2020.05.21
Forward Proxy, Reverse Proxy 정의와 차이점  (2) 2020.05.21

URI 란?

URI (Uniform Resource Identifier) 는 인터넷에 있는 자원을 나타내는 유일한 주소이다.

URI 에는 URL 과 URN 두 종류가 있는데 일반적으로 URL 을 많이 사용한다.


문법

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

RFC 3986 에 정의 되어 있는 URL 은 다음과 같은 형태를 나타낸다.

여러 구성 요소 중에 가장 중요한 세 가지는 scheme, host, path 이다.


요소

  1. scheme
    • 스킴이라고 하며 보통 프로토콜을 의미한다.
    • URL 나머지 부분들과 콜론(:) 으로 구분된다.
    • 대소문자를 구분하지 않는다.
    • 리소스에 어떻게 요청, 접근할 것인지 명시하는 how 를 담당한다.
    • ex) http, ftp, rtsp 등등..
  2. host
    • 접근하려고 하는 리소스를 가지고 있는 인터넷 상의 호스트 장비
    • 도메인명 (localhost) 또는 IP 주소 (127.0.0.1) 로 제공한다.
  3. port
    • 하나의 호스트에서 여러 개의 통신을 할 때 구분되는 값
    • 서버가 열어놓을 수 있다.
    • 포트 번호를 명시하지 않으면 default 로 80 이 사용된다.
  4. path
    • 리소스가 서버의 어디에 있는지 알려준다.


'공부 > Web' 카테고리의 다른 글

웹 접근성과 WAI-ARIA  (1) 2021.11.10
JWT (Json Web Token)  (3) 2021.05.22

+ Recent posts