토스에서 진행한 SLASH 22를 보고 Kafka에 대한 호기심이 강하게 들었다. (이름은 많이 들어봤는데 그동안 알아볼 생각을 안했다ㅠ)

현재 팀에서 내가 맡아서 관리해야 될 서버들이 점점 늘어나고 있는데 

여기서 이 서버들이 실제 오픈을 하고 운영 레벨로 넘어가게 되면 모니터링을 하기가 굉장히 어려워질 것임을 예상했다.

현재 팀에는 딱히 모니터링 시스템이라고 할 만한 게 없고 (AWS CloudWatch를 이용하는 정도?)

내가 원하는 것은 어플리케이션 단의 모니터링 시스템이 었기 때문에

이 참에 Kafka를 활용해서 한 번 모니터링 시스템을 구축해보면 어떨까 하는 생각이 들었다. 

일단 그 생각의 실현을 위한 첫 스텝으로 Kafka가 뭔지 알아보려 한다. 

 

 

아래는 최범균 님의 'kafka 조금 아는척하기 시리즈' 유튜브 영상을 보며 정리한 내용입니다. 

 

Kafka 

분산 이벤트 스트리밍 플랫폼

 

 

4개의 구성요소

  • 프로듀서 
    • 메시지(이벤트)를 카프카에 넣는다
  • 컨슈머
    • 메시지(이벤트)를 카프카에서 읽는다.
  • 카프카 클러스터
    • 메시지(이벤트)를 저장한다.
      • 하나의 카프카 클러스터는 여러 개의 브로커로 구성되어 있으며 각각 서버라고 보면 된다. 
      • 브로커는 메시지를 나눠서 저장하고, 이중화 처리도 하고, 장애가 나면 대체도 하는 등의 역할을 수행한다. 
  • 주키퍼 클러스터 (주키퍼 앙상블) 
    • 카프카 클러스터를 관리하는 용으로 클러스터 정보가 저장되어 관리가 됩니다.
    • 브로커가 한 개 밖에 없을 때에도 클러스터로 동작하는데 클러스터 내의 브로커에 대한 분산 처리를 주키퍼가 담당한다. 
      • 주키퍼
        • 분산 시스템에서 시스템 간이 정보 공유, 상태 체크, 서버들 간의 동기화를 위한 락 등을 처리해주는 '분산 코디네이션 시스템'. 카프카에서는 서버의 상태를 감지하기 위해 사용되며 새로운 토픽이 생성되었을 때 토픽의 생성과 소비에 대한 상태를 지정합니다. 

 

토픽과 파티션

  • 토픽
    • 카프카에서 메세지를 저장하는 단위가 토픽 
    • 여러 매세지가 있을 때 이 메세지가 어떤 종류의 메세지인지 구분할 필요가 있는데 이때 사용하는 것이 토픽이다
      • 예를 들어 주문용 토픽, 뉴스용 토픽 같이 각각의 메세지를 알맞게 구분하기 위해 토픽을 사용한다.
      • 파일 시스템의 폴더와 유사하다고 보면 된다. 
    • 한 개의 토픽은 한 개 이상의 파티션으로 구성된다. 
      • 파티션은 메세지를 저장하는 물리적인 파일 
        • 프로듀서와 컨슈머는 토픽을 기준으로 메세지를 주고받는다!
  • 파티션 (= 파일이라고 보면 된다)
    • 파티션은 추가만 가능한 파일이다. 
      • 각 메세지 저장 위치를 오프셋(offset)이라고 한다. 
        • 프로듀서가 카프카에 메세지를 저장하면 저장된 메세지는 offset1, offset2 이렇게 오프셋 값을 가지게 된다.
        • 여러 consumer가 한 topic(일종의 queue 개념)으로부터 여러 번에 걸쳐 메시지를 가져올 수 있습니다. 이런 방식이 가능한 이유는 클라이언트가 해당 queue에서 어느 부분까지 데이터를 받아갔는지 위치를 알려주는 'offset'을 관리하기 때문입니다.
      • 프로듀서가 넣은 메세지는 파티션의 맨 뒤에 추가한다.
      • 컨슈머는 오프셋 기준으로 메세지를 순서대로 읽는다.
      • 메세지는 삭제되지 않는다. (설정에 따라 일정 시간이 지난 뒤 삭제)
    • 한 파티션 내에서만 메세지 순서가 보장된다.

여러 파티션과 프로듀서

  • 프로듀서는 라운드로빈 또는 키로 파티션을 선택한다. 혹은 키를 이용해서 파티션을 선택한다.
    • 프로듀서가 카프카에 메세지를 전송할 때 토픽의 이름 뿐만 아니라 키를 지정할 수 있는데 키가 있는 경우에는 그 키의 해시값을 이용해서 저장할 토픽을 선택할 수 있게 된다. 그래서 같은 키를 갖고 있는 메세지는 같은 파티션에 저장이 된다 (같은 키에 대해서는 메세지 순서가 정해진다)

여러 파티션과 컨슈머 

그룹에 속해있는 컨슈머들이 특정한 파티션을 공유할 수 없다.

  • 컨슈머는 컨슈머 그룹이라는 거에 속하게 되어 있는데 컨슈머가 카프카 브로커에 연결할 때 나는 어떤 그룹에 속해있다고 지정하게 되어있음 
    • 한 개의 파티션은 그룹의 한 개 컨슈머에만 연결이 가능하다. (= 컨슈머 그룹 기준으로 파티션의 메세지가 순서대로 처리되는 것을 보장할 수 있게 된다.)

카프카 성능이 왜 좋을까요?

  • 페이지캐쉬 - 카프카는 파티션 파일에 대해서 OS에서 제공하는 페이지 캐쉬를 이용하기 때문에 파일 IO가 메모리에서 처리되기 때문에 IO속도가 빨라진다.
    • 페이지 캐시란? ( https://medium.com/@tas.com/ )
      • 처리한 데이터를 메인 메모리 영역(RAM)에 저장해서 가지고 있다가, 다시 이 데이터에 대한 접근이 발생하면 disk에서 IO 처리를 하지 않고 메인 메모리 영역의 데이터를 반환하여 처리할 수 있도록 하는 컴포넌트다. 즉 OS가 파일을 read하여 메모리에 올려두고 있다가, 빠르게 접근하여 사용하겠다는 것.
      • 다시 kafka와 내용을 같이 보면, producer가 서버인 broker에게 넣는 데이터는 consumer가 사용하기 전 일정 시간동안 page cache 올려두어, consumer가 데이터를 읽어 갈 때 그 읽어가는 속도를 빠르게 한다는 것으로 이해하면 되겠다.
  • 제로카피 - 디스크에서 데이터를 읽어다가 네트워크로 보내는 속도가 빠르다.
    • 파일에서 소켓으로 데이터를 전송하는 전통적인 과정  -> 비효율 (4개의 사본과 2개의 시스템 호출 )
      1. 운영 체제는 디스크에서 커널 공간의 페이지 캐시로 데이터를 읽습니다.
      2. 응용 프로그램은 커널 공간에서 사용자 공간 버퍼로 데이터를 읽습니다.
      3. 응용 프로그램은 데이터를 다시 커널 공간에 소켓 버퍼에 쓴다.
      4. 운영체제는 소켓 버퍼에서 네트워크를 통해 전송되는 NIC 버퍼로 데이터를 복사한다.
        • Disk > Kernel(PageCache) > User-Space(Buffer) > Kernel(Socket Buffer) > Kernel(NIC Buffer)
    • kafka는 OS가 페이지캐시에서 네트워크로 데이터를 직접 보낼 수 있으므로 위와 같은 재복사가 방지됩니다. 따라서 이 최적화된 경로에서는 NIC 버퍼에 대한 최종 복사본만 필요합니다. 데이터가 메모리에 저장되고 읽을 때마다 사용자 공간으로 복사되는 대신 페이지 캐시에 정확히 한 번 복사되고 소비할 때마다 재사용됩니다.
    • pagecache와 sendfile의 조합 덕분에 Kafka 클러스터에서 디스크가 완전히 캐시에서 데이터를 제공하기 때문에 디스크에서 읽기 활동을 볼 수 없음을 의미합니다.

 

  • 빠르다 - 브로커가 컨슈머에 대해서 할 수 있는 역할이 없어서 상대적으로 빠르다. (제재하지 않고 프로듀서와 컨슈머가 직접 함) 
  • 일괄작업 - 묶어서 보내고 묶어서 받는다. (Batch) 프로듀서와 컨슈머는 일정 크기만큼 메시지를 모아서 전송 그리고 조회가 가능하다. 따라서 낱개로 건건히 보내는 것보다 아무래도 더 빨라질 수밖에 없음 
  • 처리량 조절 쉬움 -  그냥 브로커 추가하고 파티션 추가하거나 컨슈머가 느리다고 생각되면 컨슈머 추가하면 됨.
  • 장애 복구 간단 - 장애가 났을 때 대처하기 위해 리플리카를 사용한다.  (리더/팔로워 구조)
    • 리플리카는 파티션의 복제본으로 복제수만큼 파티션의 복제본이 각 브로커에 생김
      • 하나가 리더 나머지가 팔로워가 되어서 팔로워는 리더로부터 데이터를 읽어와서 저장하므로 리더가 속한 브로커가 장애가 발생하면 이때 다른 팔로워 중에서 하나가 리더가 되고, 프로듀서와 컨슈머는 신규 리더를 통해 메세지를 처리할 수 있게 됩니다. 

프로듀서

 

Properties prop = new Properties();
prop.put("bootstrap.servers", "kafka01:9092, kafka01:9092, kafka01:9092");
prop.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
prop.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

KafkaProducer<Integer, String> producer = new KafkaProducer<>(prop);
producers.send(new ProducerRecord<>("topicname", "key", "value")); // 방법1
producers.send(new ProducerRecord<>("topicname", "value")); // 방법2

producer.close();

KafkaProducer 클래스는 send 메소드를 제공하며 이 send 메소드에 ProducerRecord를 전달하면 됩니다. 

ProducerRecord가 브로커에 전달할 카프카 메세지가 됩니다.

 

프로듀서 내부 동작 흐름

 

(강의에 나온 내용 대사 그대로를 옮겨 적었습니다.)

send() 메소드를 통해 레코드를 전달하면 Serializer를 통해 byte 배열로 변환하고

Partitioner를 이용해 메세지를 어느 토픽의 파티션으로 보낼지 결정합니다.

그리고 변환된 바이트 메시지를 버퍼에 저장하는데 버퍼에 바로 저장하지 않고 배치로 묶어서 저장하게 됩니다.

그리고 sender를 통해 배치를 차례대로 가져와 카프카 브로커로 전송합니다.

  여기서 Sender는 별도 쓰레드로 동작하며 배치가 찼는지 여부에 상관없이 보내며

Sender 쓰레드와는 별개의 쓰레드에서 send 메서드를 통해 메세지를 배치로 모으게 됩니다.

(즉, 메세지를 모으는 쓰레드와 배치를 전송하는 쓰레드는 다릅니다.)

배치하고 sender와 관련된 설정이 처리량에 영향을 주게 됩니다. (batch.size / linger.ms)

batch.size는 배치의 최대 크기를 지정하고 지정한 크기만큼 메세지가 차면 메세지가 바로 전송을 하게 됩니다.

그래서 배치 사이즈가 너무 작으면 한 번에 보낼 수 있는 메세지 크기가 작고, 전송 횟수가 많아 처리량이 떨어지게 되겠죠?!

linger.ms는 센더가 메시지를 보내는 대기 시간입니다.

기본값은 0이며 대기시간을 주게 되면 기다렸다 배치를 전송하기 때문에 한 번에 많은 메세지를 보내게 됩니다. 

 

send() 메소드를 통해 전송한 것은 결과를 확인하지 않습니다. (실패 여부 모름) 따라서 실패에 대한 별도 처리가 필요없는 메시지 전송에 사용합니다. 그런데 실패 여부를 알아야 될 때가 있는데 이 때 두 가지 방법이 사용 가능하다.

 

전송 후 실패 여부를 알고 싶다면

 

  1.   Future 사용 (처리량이 낮아도 정확해야 하는 경우)

get()을 사용하면 블로킹이 되기 때문에 루프를 돌면서 전송하는 경우에는 전송-블로킹-전송-블로킹이라

배치에 메시지가 1개씩만 들어가기 때문에 처리량도 떨어짐

Future<RecordMetadata> f = producer.send(new ProducerRecord<>("topic", "value"));
try {
	RecordMetadata meta = f.get(); // 블로킹
} catch (ExecutionException ex) {}

 

2.   Callback 사용 

콜백 객체는 전송 후 전송 결과를 onCompletion 메서드로 받게 되는데 Exception을 받게 되면 전송이 실패된 것.

처리량이 떨어지지 않는다. 

producer.sned(new ProducerRecord<>("simple", "value"),
	new Callback() {
    	@Override
        public void onCompletion(RecordMetadata metadata, Exception ex) {
        }
    });

 

전송보장과 ack

 

Producer는 전송을 보장하기 위해 ack값을 제공한다. ack가 0이면 처리량은 많아 지겠지만 메세지 전송 여부는 알 수 없습니다. ack가 1이면 파티션의 리더에 값이 저장되면 성공 응답을 알려줍니다. 따라서 리더에 장애가 발생하면 메세지가 유실될 가능성이 있습니다. (팔로워에 저장이 아직 안됐는데 성공 응답을 내려주었고, 이 상태로 리더에 장애가 발생하는 경우가 이에 해당된다) ack가 all 이면 모든 팔로워에 다 저장이 되었을 때 응답을 내려줍니다. 따라서 메시지 유실이 없어야 되는 경우에는 all로 주는 것이 맞다고 볼 수 있다. 

 

+ 전송하다 에러가 나는 경우 재시도가 가능한 경우에는 kafka에서 재시도를 수행한다. 

+ enable.idempotence 속성을 사용하면 메시지 중복 전송 가능성을 낮출 수 있다. 

 

재시도와 순서

재시도의 주의 사항은 중복 전송과 순서가 바뀐다는 것이다.

  • max.in.flight.requestes.per.connection 옵션
    • 블록킹 없이 한 커넥션에서 전송할 수 있는 최대 전송중인 요청 개수

이 옵션값이 1보다 크게 되면 재시도가 언제 이뤄지냐에 따라 메시지 순서가 바뀔 수 있다. 

따라서, 전송 순서가 중요하면 이 값을 1로 지정해야 한다. 

 

 


 

컨슈머

 

토픽 파트션에서 특정 레코드 조회

Properties prop = new Properties();
prop.put("bootstrap.servers", "localhost:9092");
prop.put("group.id", "group1");
prop.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
prop.put("value.deserializer", "org.apache.kafka.common.serialization.StringDesrializer");

KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String> (prop);
consumer.subscribe(Collections.singleton("simple")); // 토픽 구독
while (조건) {
	ConsumerRecords<String, String> recoreds = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record: records) {
    	System.out.println(record.value() + ":" + record.topic() + ":" + 
        	record.partition() + ":" + record.offset());
    }
}

consumer.close();

토픽 파티션은 그룹 단위로 할당된다. 

각 컨슈머가 파티션에 연결되는데 파티션보다 컨슈머가 더 많이 생기면 이후로 생기는 컨슈머는 놀게 된다. 특정 파티션에 연결될 수 없기 때문에.. 그래서 컨슈머 개수가 파티션 개수보다 커지면 안되고, 처리량이 떨어져서 컨슈머가 커져야 되면 파티션 개수도 함께 늘려야 합니다.

 

커밋과 오프셋

컨슈머의 poll 메소드는 이전에 커밋한 오프셋이 있으면 그 오프셋 이후의 레코드를 읽어 옵니다. 

만약에 poll 메소드로 레코드를 읽어오려는데 커밋된 레코드가 없는 경우에는 auto.offset.reset 옵션 설정값을 사용합니다.

 

auto.offset.reset

  • earliest : 맨 처음 오프셋 사용
  • latest : 가장 마지막 오프셋 사용
  • none :  익셉션 발생 

컨슈머 설정

조회에 영향을 주는 주요 설정

  • fetch.min.bytes
    • 조회시 브로커가 전송할 최소 데이터 크기
      • 기본값은 1이며 이게 크면 대기 시간이 늘지만 처리량은 올라감
  • fetch.max.wait.ms
    • 데이터가 최소 크기가 될 때까지 기다릴 시간 
      • 기본값은 500(0.5초)이며 브로커가 리턴할 때까지 대기하는 시간으로 poll() 메서드의 대기 시간과 다름
  • max.partition.fetch.bytes
    • 파티션 당 서버(브로커)가 리턴할 수 있는 최대 크기 
      • 기본값은 1MB

자동 커밋 / 수동 커밋

enable.auto.commit 설정을 통해 자동 커밋할지 수동 커밋을 할지 결정한다. 

true이면 일정 주기로 컨슈머가 오프셋을 커밋하고, false면 수동으로 진행되게 된다.

자동 커밋은 poll(), close() 메서드 호출시 자동으로 실행된다.

 

수동커밋 방법 

  1. 동기

ConsumerRecord<String, String> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
	처리
}
try {
	consumer.commitSync();
} catch (Exception ex) {
	// 커밋 실패시 에러 발생
    // 실패하면 알맞은 처리 하면 됨
}

  2. 비동기

ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
	..처리
}
consumer.commitAsync(); // commitAsync(OffsetCommitCallback callback)

비동기이기 때문에 코드 자체에서 실패 여부를 바로 알 수가 없고, 알고 싶으면 콜백을 받아서 처리해야 한다. 

 

컨슈머가 동일한 메시지를 읽어올 수 있다. 커밋이 실패했다거나 컨슈머가 추가 되는 케이스에서 리밸런싱이 일어나고, 이 과정에서 동일한 메시지를 읽어올 수 있다. 이때문에 멱등성(idempotence - 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질)을 고려해야 한다. 그리고 데이터 특성에 따라 타임스탬프, 일련 번호 등을 활용해야 한다. 

 

 

세션 타임아웃, 하트비트, 최대 poll 간격

컨슈머는 하트비트를 계속 브로커에 전송해서 연결을 유지한다. 브로커는 일정 시간 동안 컨슈머를 통해 하트비트를 전달받지 못하면 그룹에서 빼버린다. 그리고 리밸런싱을 진행한다. 

관련 설정은 

  • session.timeout.ms : 세션 타임 아웃 시간 (기본값 10초)
  • hearbeat.interval.ms : 하트비트 전송 주기 (기본값 3초)
    • session.timeout.ms의 1/3 이하 추천
  • max.poll.interval.ms : 정해진 시간 동안 poll()하지 않으면 컨슈머를 그룹에서 빼고 리밸런스를 진행한다. 

 

세션 종료

보통은 무한 루프를 돌면서 poll() 메서드로 레코드를 불러온느 코드를 작성하게 되는데 이 loop를 어떻게 벗어날 수 있을까! 바로 wakeup() 메서드를 호출한다. finally에서 consumer.close()를 해주자~

 

KafkaConsumer는 쓰레드에 안전하지 않기 때문에 여러 쓰레드에서 동시에 사용하지 않아야 한다. 

 


추가 내용

 

주키퍼 

• 주키퍼 사용용도

주키퍼는 클러스터에서 구성 서버들끼리 공유되는 데이터를 유지하거나 어떤 연산을 조율하기 위해 주로 사용

  • 설정 관리 : 클러스터의 설정 정보를 최신으로 유지하기 위한 조율 시스템으로 사용됩니다.
  • 클러스터 관리 : 클러스터의 서버가 추가되거나 제외될 때 그 정보를 클러스터 안 서버들이 공유하는 데 사용됩니다.
  • 리더 채택: 다중 어플리케이션 중에서 어떤 노드를 리더로 선출할 지를 정하는 로직을 만드는 데 사용됩니다. 주로 복제된 여러 노드 중 연산이 이루어지는 하나의 노드를 택하는 데 사용됩니다.
  • 락, 동기화 서비스 : 클러스터에 쓰기 연산이 빈번할 경우 경쟁상태에 들어갈 가능성이 커집니다. 이는 데이터 불일치를 발생시킵니다. 이 때, 클러스터 전체를 대상을 동기화해( 락을 검 ) 경쟁상태에 들어갈 경우를 사전에 방지합니다.

 

Kafka vs RabbitMQ

둘 다 pub/sub 기반의 메시지 큐 서비스인데 Kafka는 이벤트 브로커이고, RabbitMQ는 메세지 브로커이다. 

이벤트 브로커는 메세지 브로커의 기능을 포함하는 더 큰 범위의 개념이기에 이벤트 브로커가 메세지 브로커 역할을 수행할 수도 있다.

메세지 브로커는 중간 다리 역할을 수행하는 broker로 publisher가 생산한 메세지를 큐에 저장하고, consumer가 데이터를 가져가면 즉시 혹은 짧은 시간 내에 큐에서 데이터를 삭제한다. 보통 서로 다른 시스템 사이에서 데이터를 비동기 형태로 처리하고 싶을 때 사용하며 AWS에서는 비슷하게 SQS가 있다. 

 

이벤트 브로커는 publisher가 생산한 이벤트를 저장하고,  consumer가 해당 이벤트를 사용하더라도 이벤트가 저장된다는 특징으로 이후에 다시 재사용 할 수 있는 장점을 가지고 있다. 

 

(https://www.cloudamqp.com/blog/when-to-use-rabbitmq-or-apache-kafka.html)

RabbitMQ 사용 사례 
  • 일반적으로 단순/전통적인 pub-sub 메시지 브로커를 원하는 경우 확실한 선택은 RabbitMQ입니다. 요구 사항이 channels/queues을 통한 시스템 통신을 처리할 만큼 간단하고, 메세지를 보존하거나 스트리밍을 요구하는게 아닌 경우에 말이다. 
  • 두 개의 주요 사용 사례로 나눌 수 있다.
    • LONG-RUNNING TASKS
      • RabbitMQ를 사용하는데 오래 걸리는 작업이 백그라운드에서 안정적으로 실행되어야 할 때
    • MIDDLEMAN IN A MICROSERVICE ARCHITECTURES
      • 애플리케이션 내부 및 애플리케이션 간의 통신 및 통합을 위한 경우
      • 마이크로서비스 간의 중개자로서 시스템에게 단순히 작업을 실행하라는 것을 알릴 때 예를 들면 주문 처리나 주문 상태 업데이트 같은 경우다. 

Apache Kafka 사용 사례

  • 일반적으로 스트리밍 데이터를 저장, 읽기(다시 읽기), 분석하기 위한 프레임워크를 원한다면 Apache Kafka를 사용합니다. 감사를 받거나 메시지를 영구적으로 저장해야 하는 시스템에 이상적입니다.
  • 두 개의 주요 사용 사례로 나눌 수 있다.
    • DATA ANALYSIS (추적, 수집, 로깅, 보안 등)
      • 데이터를 분석해서 Insights를 얻고, 수많은 데이터에 대한 감사 또는 분석이 필요한 경우.
      • 주요 분석, 검색 및 저장 시스템
    • 실시간 처리
      • 처리량이 많은 분산 시스템 역할을 합니다. 소스 서비스는 데이터 스트림을 실시간으로 가져오는 대상 서비스로 푸시
      • Kafka는 적은 수의 소비자와 실시간으로 많은 생산자를 처리하는 시스템에서 사용할 수 있습니다. 즉, 주식 데이터를 모니터링하는 금융 IT 시스템.

 

  RabbitMQ Apache Kafka
무엇인가? 견고하고 성숙한 범용 메시지 브로커 높은 유입 데이터 스트림 및 재생에 최적화된 Message Bus
주요 용도 애플리케이션 내부 및 애플리케이션 간의 통신 및 통합으로 기 실행 작업 또는 안정적인 백그라운드 작업을 실행해야 하는 경우 스트리밍 데이터의 저장, 읽기(다시 읽기) 및 분석
메세지 지속성 수신 확인 시 삭제 보존 기간 옵션에 따라 메세지 유지
(수신 되어도 삭제하지 않음)
라우팅 소비자 노드에 정보를 반환할 수 있는 유연한 라우팅 지원 유연한 라우팅을 지원하지 않으며 별도의 주제를 통해 수행해야 합니다.
메시지 우선순위 지원 지원하지 않음
 
 

 

 

 

출처

https://www.youtube.com/watch?v=0Ssx7jJJADI (최범균 님의 kafka 조금 아는척하기 시리즈)

https://epicdevs.com/17

https://m.blog.naver.com/kgw1988/221212827363 (카프카에서의 데이터 저장 방식)

https://kafka.apache.org/documentation/#gettingStarted 공식문서

 https://programacion.tistory.com/156 [KA's Regalo:티스토리]

https://www.cloudamqp.com/blog/when-to-use-rabbitmq-or-apache-kafka.html

 

Spring Cloud Config Server가 있는 EC2에 사용 가능한 용량이 없다는 문제로 해당 서버가 정상 동작하지 않자 해당 서버를 통해 설정 파일을 주입받는 모든 서버에 영향이..^^

용량이 이렇게 부족해 진 것은 불필요한 Batch작업이 해당 EC2에서 진행되고 있어서였고, 이는 처리하였다.

하지만 용량이 너무 작은 것으로 설정된 것 같긴 해서 이참에 볼륨 크기를 확장하기로.

 

아래 명령어로 파일시스템 용량을 확인해보았다. 

$ df -h

결과는 아래와 같았다. 

Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        978M     0  978M   0% /dev
tmpfs           986M     0  986M   0% /dev/shm
tmpfs           986M  101M  886M  11% /run
tmpfs           986M     0  986M   0% /sys/fs/cgroup
/dev/xvda1      8.0G  8.0G   20K 100% /
tmpfs           198M     0  198M   0% /run/user/1000
tmpfs           198M     0  198M   0% /run/user/0

AWS Console에서 해당 EC2의 스토리지의 볼륨디바이스를 보니 볼륨크기가 8Gib에 /dev/xvda로 설정되어 있는 것을 확인할 수 있었다. 

 

AWS Console에서 EBS Volume을 2배 (16Gib)로 변경해주었고, 아래 명령어로 연결된 블록 디바이스를 확인해보았다.

$ lsblk

루트 볼륨은 2배로 잘 커져 있었지만 파티션인 xvda1은 여전히 8Gib인 것을 볼 수 있을 것이다.

해당 파티션도 늘려주기 위해서 아래 명령어를 입력하였다 . 

$ sudo growpart /dev/xvda 1

그랬더니 아래와 같은 에러가...

mkdir: cannot create directory ‘/tmp/growpart.4699’: No space left on device
FAILED: failed to make temp dir

 

AWS 문서에 이와 같은 상황에서 할 수 있는 해결책을 제시해주었다. 


블록 디바이스에 남은 공간 없음 오류를 방지하려면 임시 파일 시스템 tmpfs /tmp 탑재 지점에 탑재합니다. 그러면 /tmp에 탑재된 10M tmpfs가 생성됩니다.

$ sudo mount -o size=10M,rw,nodev,nosuid -t tmpfs tmpfs /tmp

위와 같이 진행하고 아래 명령어를 입력하니 성공!

$ sudo growpart /dev/xvda 1
CHANGED: partition=1 start=4096 old: size=16773087 end=16777183 new: size=33550303 end=33554399

 

여기까지는 파티션의 크기를 늘린 것이고,

이제는 파일시스템에 변경된 파티션 크기를 적용해야 한다. 

그래서 아래의 명령어를 통해 적용해보려 했지만 에러가 뙇

$ sudo resize2fs /dev/xvda1

resize2fs 1.42.9 (28-Dec-2013)
resize2fs: Bad magic number in super-block while trying to open /dev/xvda1
Couldn't find valid filesystem superblock.

 

원인을 찾아보니 리눅스의 파일 시스템이 xfs라서 그런거라고 한다.. 아래 명령어를 입력해서 확인해 볼 수 있다. 

$ df -Th
Filesystem     Type      Size  Used Avail Use% Mounted on
devtmpfs       devtmpfs  978M     0  978M   0% /dev
tmpfs          tmpfs     986M     0  986M   0% /dev/shm
tmpfs          tmpfs     986M  101M  886M  11% /run
tmpfs          tmpfs     986M     0  986M   0% /sys/fs/cgroup
/dev/xvda1     xfs       8.0G  8.0G   20K 100% /
tmpfs          tmpfs     198M     0  198M   0% /run/user/1000
tmpfs          tmpfs     198M     0  198M   0% /run/user/0
tmpfs          tmpfs      10M     0   10M   0% /tmp

/dev/xvda1의 Type이 xfs로 되어 있다.  리눅스의 파일 시스템이 xfs일 경우 발생한다고 한다. 

 

이럴때는 resize2fs가 아닌 xfs_growfs를 써야 한다.

$ sudo xfs_growfs /dev/xvda1

아래의 메시지와 함께 정보들이 나열되면 성공!!

data blocks changed from 2096635 to 4193787

 

df -Th로 확인해보면~

Filesystem     Type      Size  Used Avail Use% Mounted on
devtmpfs       devtmpfs  978M     0  978M   0% /dev
tmpfs          tmpfs     986M     0  986M   0% /dev/shm
tmpfs          tmpfs     986M  100M  887M  11% /run
tmpfs          tmpfs     986M     0  986M   0% /sys/fs/cgroup
/dev/xvda1     xfs        16G  8.1G  8.0G  51% /
tmpfs          tmpfs     198M     0  198M   0% /run/user/1000
tmpfs          tmpfs     198M     0  198M   0% /run/user/0
tmpfs          tmpfs      10M     0   10M   0% /tmp

/dev/xvda1의 Size가 16G이고 Use%가 100%에서 51%로 줄어있는 것을 확인할 수 있다.

 

 

 

용량이 어느새 다시 찼다...

알고 보니 log 디렉토리에 로그파일이 수두룩 빽빽하게 쌓여서 용량을 다 잡아 먹고 있었던 것이다.

그래서 근본적인 해결을 하기로 하였고.

$ sudo du --/ | sort -| tail -40 | sort --r

위 명령어를 통해 용량 차지 상위 순서대로 디렉토리 리스트를 뽑아봤고,

거기서 로그 디렉토리가 압도적인 것을 확인. 

스크립트를 짜서 15일에 한번씩 로그 파일들을 정리하기로 했다. 

 

 

끝!

 

 

 

출처

https://aws.amazon.com/ko/premiumsupport/knowledge-center/ebs-volume-size-increase/

https://velog.io/@hyeonseop/ec2-%EC%9A%A9%EB%9F%89-full%EC%9D%BC-%EB%95%8C-%EB%8C%80%EC%B2%98%EB%B2%95

https://nirsa.tistory.com/231

타 부서에서 현재 개발하고 있는 플랫폼에서 사용자 관리 부분을 따로 관리하고 싶다고 하였다.

그 관리는 SCIM 방식으로 하길 원했고, 나는 이에 따라 SCIM API Endpoint를 제공하기로 하였다....

(서비스 오픈을 앞두고 개인적으로 좀 진행하고 있던게 있었는데 그걸 멈추고 해야 했던지라 솔직히 하기 싫었다..ㅠㅠ....)

 

 

  • SCIM
    • 클라우드 기반 애플리케이션 및 서비스에서 사용자 ID를 보다 쉽게 ​​관리할 수 있도록 설계된 것
    • 사용자 관리에 들어가는 비용과 복잡성을 줄이고자 하는 목적으로 개발되어 SCIM은 공통 사용자 스키마 및 확장 모델을 제공하고 REST API를 통해 이 스키마를 교환하기 위한 패턴을 제공하는 바인딩 문서를 제공합니다.
  • SCIM Schema Structure
    • SCIM 스키마는 많은 기존 배포 및 스키마에서 발견되는 공통 속성을 포함하는 사용자 및 그룹(리소스)을 나타내기 위한 최소 핵심 스키마를 제공합니다.
    • Resource은 하나 이상의 스키마로 식별되는 속성 모음입니다. 속성은 이름과 하나 이상의 단순 또는 복합 값으로 구성됩니다. SCIM 스키마는 속성의 데이터 유형, 복수 및 기타 구별되는 기능을 정의합니다. 달리 지정되지 않는 한 모든 속성은 소비자가 수정할 수 있습니다. 불변(읽기 전용) 속성은 속성 정의 내에서 'READ-ONLY'로 지정되어야 합니다(SHALL). 
  • Model
    • SCIM 2.0은 리소스가 공통 분모이고 모든 SCIM 개체가 리소스에서 파생되는 개체 모델을 기반으로 합니다.
    • id, externalId, meta를 속성으로 갖고 RFC7643은 공통 속성을 확장한 User, Group, EnterpriseUser를 정의한다.

  • Schema Extenstion Model
    • SCIM 스키마는 LDAP에서 사용되는 ObjectClasses와 유사한 개체 확장 모델을 따릅니다. LDAP와 달리 상속 모델이 없습니다. 모든 확장은 추가됩니다. 스키마 확장은 공통 정의된 속성을 재정의해서는 안 되며 이 사양에 정의된 규칙을 따라야 합니다(SHOULD). 각 스키마 확장은 확장을 식별하는 데 사용되는 URI를 식별해야 합니다. XML은 XML 네임스페이스를 사용해야 하고 JSON 형식은 확장 리소스와 속성을 구별하기 위해 "스키마" 속성을 사용해야 합니다(MUST).
      • LDAP : 경량 디렉터리 액세스 프로토콜은 TCP/IP 위에서 디렉터리 서비스를 조회하고 수정하는 응용 프로토콜
  • SCIM Core Schema
    • 각 SCIM 리소스(사용자, 그룹 등)에는 다음과 같은 공통 속성이 포함됩니다. 이러한 속성은 확장된 리소스 유형을 포함하여 모든 리소스에 포함되어야 합니다. 핵심 스키마가 암시적으로 포함되어 있으므로 이 문서에서 리소스가 완전히 정의된 경우 schema 속성을 지정할 필요가 없습니다. 
    • Common Schema Attributes
      • id
        • 서비스 공급자가 정의한 SCIM 리소스의 고유 식별자입니다. 리소스의 각 표현은 비어 있지 않은 id 값을 포함해야 합니다(MUST). 이 식별자는 서비스 제공자의 전체 리소스 집합에서 고유해야 합니다(MUST). id 속성의 값은 항상 서비스 제공자에 의해 발행되며 서비스 소비자에 의해 지정되어서는 안 됩니다(MUST). 
      • externalId
        • 서비스 소비자가 정의한 리소스의 식별자입니다. externalId는 소비자가 자체 식별자로 리소스를 참조할 수 있도록 하여 서비스 소비자와 서비스 제공자 간의 리소스 식별을 단순화할 수 있습니다. 공급자. 각 리소스는 비어 있지 않은 externalId 값을 포함할 수 있습니다(MAY). externalId 속성의 값은 항상 서비스 소비자로 발행되며 서비스 공급자가 지정할 수 없습니다. 
      • meta
        • 리소스 메타데이터를 포함하는 복합 속성입니다. 모든 하위 속성은 선택 사항입니다.
        • created
          • 리소스가 서비스 공급자에 추가된 DateTime입니다. 속성은 DateTime이어야 합니다. 읽기 전용.
        • lastModified
          • 이 리소스가 최초 생성 이후 수정된 적이 없는 경우 값은 생성된 값과 동일해야 합니다(MUST). 속성은 DateTime이어야 합니다. 읽기 전용.
        • location
          • 반환되는 리소스의 URI입니다. 이 값은 Location HTTP 응답 헤더와 동일해야 합니다(MUST). 읽기 전용.
        • version
          • 반환되는 리소스의 버전입니다. 이 값은 ETag HTTP 응답 헤더와 동일해야 합니다. 읽기 전용.
        • attributes
          • PATCH 작업 동안 리소스에서 제거할 속성의 이름입니다.
  • "schema" Attribute
    • SCIM은 확장 가능한 스키마와 함께 다양한 유형의 리소스를 지원합니다. 각 리소스는 정규화된 URL을 사용하여 표시되어야 합니다(MUST). 특정 표현에 스키마 표현에 대한 기존 지원이 있는 경우 해당 표현의 전통적인 규칙을 적용해야 합니다(MUST). 예를 들어 XML을 사용하여 사용자를 나타낼 때는 XML Namespace를 사용해야 합니다.
    • schemas
      • schemas 속성은 SCIM 표현에 대해 지원되는 스키마 버전과 해당 표현이 지원하는 모든 스키마 확장을 검사할 수 있는 문자열 배열입니다. 각 문자열 값은 고유한 URI여야 합니다. 이 사양은 사용자, 그룹 및 표준 "엔터프라이즈" 확장에 대한 URI를 정의합니다. SCIM 스키마의 모든 표현은 해당 표현에서 지원하는 URI 값과 함께 0이 아닌 값 배열을 포함해야 합니다(MUST). 중복 값은 포함되어서는 안 됩니다(MUST NOT). 값 순서가 지정되지 않았으며 동작에 영향을 주어서는 안 됩니다(MUST). 필수의.
  • SCIM User Schema
    • SCIM은 'urn:scim:schemas:core:1.0' URI를 사용하여 식별되는 사용자를 나타내는 스키마를 제공합니다. SCIM 핵심 스키마에 정의된 속성 외에 다음 속성이 정의됩니다.
      • userName, name, displayName, nickName, profileUrl, title, userType, preferredLanguage, locale, timezone, active, password
  • SCIM Enterprise User Schema Extension
    • 다음 SCIM 확장은 비즈니스 또는 기업에 속하거나 이를 대신하는 사용자를 나타내는 데 일반적으로 사용되는 속성을 정의합니다. 엔터프라이즈 사용자 확장은 'urn:scim:schemas:extension:enterprise:1.0' URI를 사용하여 식별됩니다.
      • employeeNumber, costCenter, organization, division, department, manager (managerId, displayName)
  • SCIM Group Schema
    • SCIM은 'urn:scim:schemas:core:1.0' URI를 사용하여 식별되는 그룹을 나타내는 스키마를 제공합니다. 그룹 리소스는 명시적인 권한 부여 모델이 정의되어 있지 않더라도 공통 그룹 또는 역할 기반 액세스 제어 모델의 표현을 가능하게 하기 위한 것입니다. 
    • SCIM 핵심 스키마에 정의된 공통 속성에 추가로 다음의 단일 속성이 정의됩니다.
      • displayName : 사람이 읽을 수 있는 그룹 이름입니다. (REQUIRED)
    • SCIM 핵심 스키마에 정의된 공통 속성 외에 다음 다중값 속성이 정의됩니다.
      • members : 그룹의 구성원 목록입니다. 표준 유형 "사용자" 및 "그룹"은 읽기 전용입니다. 값은 SCIM 리소스의 "id"(사용자 또는 그룹)여야 합니다. 그룹 유형의 의도는 서비스 공급자가 중첩된 그룹을 지원할 수 있도록 하는 것입니다.
  • Service Provider Configuration Schema
    • SCIM은 'urn: scim: schemas: core: 1.0' URI를 사용하여 식별된 서비스 제공자의 구성을 나타내는 스키마를 제공합니다.
    • 서비스 공급자 구성 리소스를 사용하면 서비스 공급자가 SCIM 사양 준수를 표준화된 형식으로 노출할 수 있을 뿐만 아니라 추가 구현 세부 정보를 소비자에게 제공할 수 있습니다. 모든 속성은 읽기 전용입니다.
    • Core Schema에 정의된 공통 속성 외에 다음의 단일 속성이 정의됩니다.
  • Resource Schema
    • 리소스 스키마는 리소스를 구성하는 속성과 메타데이터를 지정합니다. Resources 스키마는 읽기 전용이며 URI 'urn:scim:schemas:core:1.0'을 사용하여 식별됩니다. 다른 핵심 자원과 달리 Resources 스키마는 하위 속성 내에 복합 객체를 포함할 수 있으며 다른 속성이 지정되지 않는 한 모든 속성은 필수입니다.
    • Attributes로는 일단  name, description, schema, endpoint 등등이 있음

 


JSON Representation

 

Minimal User Representation

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "id": "2819c223-7f76-453a-919d-413861904646",
  "userName": "bjensen@example.com",
  "meta": {
    "resourceType": "User",
    "created": "2010-01-23T04:56:22Z",
    "lastModified": "2011-05-13T04:42:34Z",
    "version": "W\/\"3694e05e9dff590\"",
    "location":
     "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646"
  }
}

 

 

Full User Representation(더보기를 클릭해서 확인하시면 됩니다)

더보기
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "id": "2819c223-7f76-453a-919d-413861904646",
  "externalId": "701984",
  "userName": "bjensen@example.com",
  "name": {
    "formatted": "Ms. Barbara J Jensen, III",
    "familyName": "Jensen",
    "givenName": "Barbara",
    "middleName": "Jane",
    "honorificPrefix": "Ms.",
    "honorificSuffix": "III"
  },
  "displayName": "Babs Jensen",
  "nickName": "Babs",
  "profileUrl": "https://login.example.com/bjensen",
  "emails": [
    {
      "value": "bjensen@example.com",
      "type": "work",
      "primary": true
    },
    {
      "value": "babs@jensen.org",
      "type": "home"
    }
  ],
  "addresses": [
    {
      "type": "work",
      "streetAddress": "100 Universal City Plaza",
      "locality": "Hollywood",
      "region": "CA",
      "postalCode": "91608",
      "country": "USA",
      "formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
      "primary": true
    },
    {
      "type": "home",
      "streetAddress": "456 Hollywood Blvd",
      "locality": "Hollywood",
      "region": "CA",
      "postalCode": "91608",
      "country": "USA",
      "formatted": "456 Hollywood Blvd\nHollywood, CA 91608 USA"
    }
  ],
  "phoneNumbers": [
    {
      "value": "555-555-5555",
      "type": "work"
    },
    {
      "value": "555-555-4444",
      "type": "mobile"
    }
  ],
  "ims": [
    {
      "value": "someaimhandle",
      "type": "aim"
    }
  ],
  "photos": [
    {
      "value":
        "https://photos.example.com/profilephoto/72930000000Ccne/F",
      "type": "photo"
    },
    {
      "value":
        "https://photos.example.com/profilephoto/72930000000Ccne/T",
      "type": "thumbnail"
    }
  ],
   "userType": "Employee",
  "title": "Tour Guide",
  "preferredLanguage": "en-US",
  "locale": "en-US",
  "timezone": "America/Los_Angeles",
  "active":true,
  "password": "t1meMa$heen",
  "groups": [
    {
      "value": "e9e30dba-f08f-4109-8486-d5c6a331660a",
      "$ref":
"https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a",
      "display": "Tour Guides"
    },
    {
      "value": "fc348aa8-3835-40eb-a20b-c726e15c55b5",
      "$ref":
"https://example.com/v2/Groups/fc348aa8-3835-40eb-a20b-c726e15c55b5",
      "display": "Employees"
    },
    {
      "value": "71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
      "$ref":
"https://example.com/v2/Groups/71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
      "display": "US Employees"
    }
  ],
    "x509Certificates": [
    {
      "value":
       "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMx
        EzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYD
        VQQDDAtleGFtcGxlLmNvbTAeFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFa
        MH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtl
        eGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBKZW5zZW4gSUlJMSIw
        IAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B
        AQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc
        1FLgzdLI8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5i
        PSi8xO8SL7I7SDhcBVJhqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZ
        zidPl8HZ7DhXkZIRtJwBweq4bvm3hM1Os7UQH05ZS6cVDgweKNwdLLrT51ikSQG3
        DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z9QiwlYEdRbLJ4zJQBmDr
        SGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sweTAJBgNV
        HRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZp
        Y2F0ZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAU
        dGeKitcaF7gnzsNwDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJt
        Ng5Tcq+/ByEDrBgnusx0jloUhByPMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1R
        C4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTVvxB/A67g42/vzgAtoRUeDov1
        +GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo="
    }
  ],
  "meta": {
    "resourceType": "User",
    "created": "2010-01-23T04:56:22Z",
    "lastModified": "2011-05-13T04:42:34Z",
    "version": "W\/\"a330bc54f0671c9\"",
    "location":
"https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646"
  }
}

 

 

Enterprise User Extension Representation (더보기를 클릭해서 확인하시면 됩니다)

-extension을 추가한 예시임, schemas를 보면 배열에 2개의 스키마가 정의되어 있다. 

더보기
{
  "schemas":
    ["urn:ietf:params:scim:schemas:core:2.0:User",
      "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
  "id": "2819c223-7f76-453a-919d-413861904646",
  "externalId": "701984",
  "userName": "bjensen@example.com",
  "name": {
    "formatted": "Ms. Barbara J Jensen, III",
    "familyName": "Jensen",
    "givenName": "Barbara",
    "middleName": "Jane",
    "honorificPrefix": "Ms.",
    "honorificSuffix": "III"
  },
  "displayName": "Babs Jensen",
  "nickName": "Babs",
  "profileUrl": "https://login.example.com/bjensen",
  "emails": [
    {
      "value": "bjensen@example.com",
      "type": "work",
      "primary": true
    },
    {
      "value": "babs@jensen.org",
      "type": "home"
    }
  ],
  "addresses": [
    {
      "streetAddress": "100 Universal City Plaza",
      "locality": "Hollywood",
      "region": "CA",
      "postalCode": "91608",
      "country": "USA",
      "formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
      "type": "work",
      "primary": true
    },
    
    {
      "streetAddress": "456 Hollywood Blvd",
      "locality": "Hollywood",
      "region": "CA",
      "postalCode": "91608",
      "country": "USA",
      "formatted": "456 Hollywood Blvd\nHollywood, CA 91608 USA",
      "type": "home"
     }
  ],
  "phoneNumbers": [
    {
      "value": "555-555-5555",
      "type": "work"
    },
    {
      "value": "555-555-4444",
      "type": "mobile"
    }
  ],
  "ims": [
    {
      "value": "someaimhandle",
      "type": "aim"
    }
  ],
  "photos": [
    {
      "value":
        "https://photos.example.com/profilephoto/72930000000Ccne/F",
      "type": "photo"
    },
    {
      "value":
        "https://photos.example.com/profilephoto/72930000000Ccne/T",
      "type": "thumbnail"
    }
  ],
   "userType": "Employee",
  "title": "Tour Guide",
  "preferredLanguage": "en-US",
  "locale": "en-US",
  "timezone": "America/Los_Angeles",
  "active":true,
  "password": "t1meMa$heen",
  "groups": [
    {
      "value": "e9e30dba-f08f-4109-8486-d5c6a331660a",
      "$ref": "../Groups/e9e30dba-f08f-4109-8486-d5c6a331660a",
      "display": "Tour Guides"
    },
    {
      "value": "fc348aa8-3835-40eb-a20b-c726e15c55b5",
      "$ref": "../Groups/fc348aa8-3835-40eb-a20b-c726e15c55b5",
      "display": "Employees"
    },
    {
      "value": "71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
      "$ref": "../Groups/71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
      "display": "US Employees"
    }
  ],
  "x509Certificates": [
    {
      "value":
       "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMx
        EzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYD
        VQQDDAtleGFtcGxlLmNvbTAeFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFa
        MH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtl
        eGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBKZW5zZW4gSUlJMSIw
        IAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B
        AQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc
        1FLgzdLI8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5i
        PSi8xO8SL7I7SDhcBVJhqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZ
        zidPl8HZ7DhXkZIRtJwBweq4bvm3hM1Os7UQH05ZS6cVDgweKNwdLLrT51ikSQG3
        DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z9QiwlYEdRbLJ4zJQBmDr
        SGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sweTAJBgNV
        HRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZp
        Y2F0ZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAU
        dGeKitcaF7gnzsNwDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJt
        Ng5Tcq+/ByEDrBgnusx0jloUhByPMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1R
        C4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTVvxB/A67g42/vzgAtoRUeDov1
        +GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo="
    }
  ],
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
    "employeeNumber": "701984",
    "costCenter": "4130",
    "organization": "Universal Studios",
    "division": "Theme Park",
    "department": "Tour Operations",
    "manager": {
      "value": "26118915-6090-4610-87e4-49d8ca9f808d",
      "$ref": "../Users/26118915-6090-4610-87e4-49d8ca9f808d",
      "displayName": "John Smith"
    }
  },
  "meta": {
    "resourceType": "User",
    "created": "2010-01-23T04:56:22Z",
    "lastModified": "2011-05-13T04:42:34Z",
    "version": "W\/\"3694e05e9dff591\"",
    "location":
"https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646"
  }
}

 

Example User 

이것은 자바 어플리케이션 내에서 사용자 데이터를 SCIM 객체로 변환하는 방법으로 제가 사용한 코드의 예시입니다. 
@Getter
@Setter
public class ScimUser extends BaseModel {
    private String schema = "urn:ietf:params:scim:schemas:core:2.0:User";
    private UUID id;
    private String userName;
    private List<ScimEmailAttributes> emails;
    private Boolean active = false;
    private Boolean externalTestField = false;
    private String created;
    private String lastModified;

    public ScimUser(){
    }

    public ScimUser(User user){
        this.update(user);
    }

    public void update(User user) {
        this.id = UUID.nameUUIDFromBytes(user.getEmail().getBytes(StandardCharsets.UTF_8));
        this.userName = user.getUserName();
        this.active = user.isEnabled();
        this.externalTestField = user.externalTestField();
        ScimEmailAttributes attributes = new ScimEmailAttributes();
        attributes.setValue(user.getEmail());
        this.emails = Arrays.asList(attributes);
        this.created = ""; // 유저 데이터 생성 날짜 입력
        this.lastModified = ""; // 유저 데이터 수정 날짜 입력
    }


    @Override
    public Map<String, Object> toScimResource(){
        Map<String, Object> returnValue = new LinkedHashMap<>();

        ScimExternalTestDetail externalTestDetail = new ScimExternalTestDetail(); // User에서 Custom하게 추가할 필드를 위해 스키마를 추가한다.

        returnValue.put("schemas", Arrays.asList(this.schema, externalTestDetail.getSchemas()));
        returnValue.put("id", this.id);
        returnValue.put("userName", this.userName);
        returnValue.put("emails", this.emails);
        returnValue.put("active", this.active);

        // Meta information
        Map<String, Object> meta = new HashMap<>();
        meta.put("resourceType", "User");
        meta.put("location", ("scim/v1/Users"));
        meta.put("created", this.created);
        meta.put("lastModified", this.lastModified);
        returnValue.put("meta", meta);

		// Custom 필드 추가
        externalTestDetail.setExternalTestField(this.externalTestField); 
        returnValue.put(externalTestDetail.getSchemas(), externalTestDetail.toScimResource());

        return returnValue;
    }
}

 

생성

 

@Getter
@Setter
public class CreateTest {
    @JsonProperty("schemas")
    private List<String> schemas;
    @JsonProperty("userName")
    private String userName;
    @JsonProperty("emails")
    private List<ScimEmailAttributes> emails;
    @JsonProperty("urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0")
    private ExternalTestDetail extenalTestDetail;
}
 
RequestBody
{
	"schemas": [
		"urn:ietf:params:scim:schemas:core:2.0:User",
		"urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0"
	],
	"userName": "test",
	"emails": [
		{
			"value": "test@naver.com"
		}
	],
	"urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0": {
		"extenalTestDetail": false
	}
}
 

ResponseBody

{
	"schemas": [
		"urn:ietf:params:scim:schemas:core:2.0:User",
		"urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0"
	],
	"id": "7d0d8b63-ce15-36b3-b9c7-e8ca82e89b5c",
	"userName": "테스트유저",
	"emails": [
		{
			"value": "test@naver.com"
		}
	],
	"active": true,
	"meta": {
		"created": "",
		"location": "scim/v1/Users",
		"lastModified": "",
		"resourceType": "User"
	},
	"urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0": {
		"externalTestField": false
	}
}
 
검색

User 검색의 경우에는 아래와 같이 작성하였습니다.

먼저 GET 방식으로 아래의 Request URL을 받습니다.

 

https://test.com/scim/v1/Users?filter=emails[value co "test@"]

SCIM 에서 제시하는 방법에 따라 'co (consist)'를 사용하였고, email로 검색하는 경우의 예입니다. 

검색결과는 JSON으로 아래와 같이 전달합니다.

schemas 내요을 보면 ListResponse인 것을 확인하실 수 있습니다. 

 

ResponseBody

{
	"schemas": [
		"urn:ietf:params:scim:api:messages:2.0:ListResponse"
	],
	"totalResults": 1,
	"Resources": [
		{
			"schemas": [
            "urn:ietf:params:scim:schemas:core:2.0:User",
            urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0"
            ],
            "id": "7d0d8b63-ce15-36b3-b9c7-e8ca82e89b5c",
            "userName": "테스트유저",
            "emails": [
                {
                    "value": "test@naver.com"
                }
            ],
            "active": true,
            "meta": {
                "created": "",
                "location": "scim/v1/Users",
                "lastModified": "",
                "resourceType": "User"
            },
            "urn:ietf:params:scim:schemas:extension:test:ExternalTestDetail:2.0": {
                "externalTestField": false
            }
        }
    ]
}

 

패치

 

@Getter
@Setter
public class PatchTest {
    @JsonProperty("schemas")
    private List<String> schemas;
    @JsonProperty("Operations")
    private List<ScimOperations> operations;
}

Request Body

{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{
        "op": "replace",
        "value": {
            "active": true
        }
    }]
}

Response Body

-유저 조회 결과와 동일

 

 

 

출처 

http://www.simplecloud.info/#Overview

http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#defs

https://datatracker.ietf.org/doc/html/rfc7643#section-6

 

SAML

 

- 마크업 랭귀지

인증 정보 제공자(identity provider)와 서비스 제공자(service provider) 간의 인증 및 인가 데이터를 교환하기 위한 XML 기반의 개방형 표준 데이터 포맷이 SAML이다. SAML이 기술하는 가장 중요한 요구사항은 웹 브라우저 통합 인증(SSO)이다.

 

Why SAML ?

 

SAML은 확장되는 수많은 플랫폼, 디지털 접점 및 장치에서 사용자를 인증하고 권한을 부여하는 안전한 방법을 제공합니다.

 

SAML은 IdP(사용자 자격 증명을 보유하는)와 SP 간에 IdP 방화벽 내부에서 발생하는 토큰 기반 인증 교환을 활성화하기 때문에 여러 웹 응용 프로그램 암호에 대한 필요성을 느끼지 못합니다. 

 

SAML은 SSO(Single Sign-On) 기능을 활성화하여 주요 문제를 해결합니다. 즉, 사용자가 일정 기간 동안 단일 자격 증명 집합으로 여러 속성에 대해 등록 또는 로그인하고 권한을 부여할 수 있습니다. SAML은 또한 단일 로그아웃 기능을 활성화하여 사용자가 한 사이트에서 로그아웃할 때 해당 회사가 소유한 다른 모든 서비스 제공업체에서 자동으로 로그아웃되도록 합니다.

 

SAML  용어

  • Identity Provider(IDP) : 인증의 중앙 집중식 지점 - SSO내에서의 ID저장소 
  • Service Provider(SP) : 유저가 로그인을 시도하는 응용 프로그램
  • SAML Request : SP가 IDP에게 커뮤니케이션을 보내는 것
  • SAML Response : IDP가 SAML Request에 응답하는 것 
  • Assertion : SAML Response에 담은 ID제공자가 사용자 인증을 포함하는 서비스 제공자에게 보내는 XML문서
    • Assertion은 인증/속성/권한 부여 세 가지 유형이 있다. 
      • 인증 Assertion은 사용자 식별을 증명하고, 로그인 한 시간과 인증 방법을 제공한다. 
      • 속성 Assertion은 SAML 속성을 서비스 공급자에게 전달합니다. SAML 속성은 사용자에 대한 정보를 제공하는 특정 데이터 조각입니다. 
      • 권한 부여 결정 Assertion은 사용자가 서비스를 사용할 수 있는 권한이 있는지 또는 IDP가 암호 오류 또는 서비스에 대한 권한 부족으로 인해 요청을 거부했는지 여부를 나타냅니다. 
  • XML Signatures (DSig) : Assertion이 사용하는 XML 서명
  • Assertion Consumer Service : SP에서 Assertion을 받는 특정 EndPoint이며 XML Document를 검사한다. 
  • Attribute : SAML Response에는 유저에 대한 여러 정보들 
  • Relay State: SAML Response에서 매우 중요하게 알아야 되는 정보들

SAML 사용 예에서 주체는 서비스를 서비스 제공자로부터 요청한다. 이 서비스 제공자는 식별 어서션(assertion)을 인증 정보 제공자로부터 요청하여 가져온다. 이 어서션(assertion)에 기초하여, 서비스 제공자는 접근 제어 결정을 할 수 있다. 즉, 연결된 주체에 대해 일부 서비스를 수행할지의 여부를 결정할 수 있다.

 

 

위 용어를 토대로 SAML을 설명하자면

IdP(ID 공급자)가 SP(서비스 공급자)에 인증 자격 증명을 전달할 수 있도록 하는 open standard이다.
SAML을 통해 하나의 자격 증명 세트를 사용하여 다양한 웹 사이트에 로그인할 수 있게 하며
ID 공급자와 서비스 공급자 간에 사용자 인증 및 권한 부여를 전달하는 안전한 방법이다.

 

How it works?

  1. 사용자가 Google에서 호스팅하는 서비스 또는 애플리케이션에 연결을 시도합니다.
  2. 서비스(응용 프로그램) 파트너는 파트너의 SSO 서비스에 대한 URL에 인코딩 및 포함된 SAML 인증 요청을 생성합니다. 식별자로 설정된 매개변수는 수정이나 검사 없이 다시 전달됩니다.
  3. 애플리케이션은 파트너의 SSO 서비스에 제출할 인코딩된 SAML 인증 요청을 포함하는 리디렉션을 사용자의 브라우저로 보냅니다.ID 파트너는 SAML 요청을 디코딩하고 사용자를 인증합니다.
  4. ID 파트너는 인증된 사용자의 ID 및 속성이 포함된 SAML 어설션을 생성합니다.
  5. 인코딩된 SAML 응답은 브라우저로 다시 전달되고 브라우저는 응답을 ACS(액세스 제어 서버) URL로 보냅니다.
  6. SAML 2.0 사양에 따라 이 응답은 파트너의 공개 및 비공개 DSA/RSA 키로 디지털 서명됩니다.
  7. 사용자가 Google 호스팅 애플리케이션에 로그인되어 있습니다.
사용자가 SAML 지원 애플리케이션에 로그인하면 서비스 제공자가 적절한 ID 제공자에게 권한 부여를 요청합니다. ID 제공자는 사용자의 자격 증명을 인증한 다음 사용자에 대한 권한을 서비스 제공자에게 반환하고 사용자는 이제 애플리케이션을 사용할 수 있습니다.
 

+ 정확하게 구현하지 않으면 보안에 취약한 점이 있기 때문에 주의가 필요하다.

 

 

 

출처 

 

통합 인증 - 위키백과, 우리 모두의 백과사전

통합 인증(영어: Single Sign-On; SSO)은 한 번의 인증 과정으로 여러 컴퓨터 상의 자원을 이용 가능하게 하는 인증 기능이다. 싱글 사인온, 단일 계정 로그인, 단일 인증이라고 한다. 예를 들어 어느

ko.wikipedia.org

https://web.archive.org/web/20160304032626/http://www.gigya.com/blog/the-basics-of-saml/

 

The Basics of SAML • Gigya

Gigya is the leader in customer identity management. Read our post, The Basics of SAML.

web.archive.org

 

 

출처 : 책 자바 성능 튜닝이야기 -'왜 자꾸 String을 쓰지 말라는거야' 편

 

이 책에 나온 초보 개발자 나초보는 서비스를 개발하고 나서 보니 GC가 많이 발생된단 사실을 알게 되었고

성능을 올리기 위해 GC에 영향을 끼치고 있던 String 부분을 개선해보기로 하였다. 

 

문제를 일으키던 부분은

String a = "test"; / for문을 통해 a += "XXX";

이런식으로 String에다 계속 값을 더하는 부분이었다.

String을 이렇게 사용하면 성능 적인 부분에서 문제를 일으킬 수 있다.

 

for문이 100,1000번 이상 돌아간다 가정해보면 메모리 사용률이 올라가고, 응답속도는 낮아질 것이다. 

 

그렇다면 어떻게 해야 할까?

  • StringBuffer 클래스와 StringBuilder 클래스를 사용하여야 한다. 
    • append() 메서드를 활용

 

StringBuffer 클래스와 StringBuilder 클래스의 차이는 

StringBuffer 클래스는 스레드에 안전하게 설계되어 있으므로 여러 개의 스레드에서 하나의 StringBuffer객체를 처리해도 전혀 문제가 되지 않는다. 반면 StringBuilder 클래스는 단일 스레드에서의 안정성만을 보장한다. 

 

String a = new String();
StringBuffer b = new StringBuffer();
StringBuilder c = new StringBuilder();

for(int loop = 0 ; loop < 10000; loop++) {
	a += "abcde";
}

for(int loop = 0 ; loop < 10000; loop++) {
	b.append("abcde");
}
String tempB = b.toString();

for(int loop = 0 ; loop < 10000; loop++) {
	c.append("abcde");
}
String tempC = c.toString();

이런 소스가 실행된다면 결과가 어떻게 될까? 어느 것이 가장 빠르고 메모리를 적게 사용할까? 

 

  응답시간 생성된 임시 객체수 메모리 사용량
a (String) 95초 4,000,000 약 95Gb
tempB (StringBuffer) 0.24초 1400 약 37.5Mb
tempC (StringBuilder) 0.17초 1400 약 37.5Mb

그렇다, 응답시간과 메모리 사용량에서 엄청난 차이가 보인다

응답시간과 메모리 사용량에서 가장 좋은 성능을 보인건 StringBuilder이다. 

 

왜 이럴까?

새로이 더해진 문자열은 새로운 주소를 갖는 객체가 생성이 되기 때문이다. 

a에 "abcde"를 더하면 새로운 String클래스의 객체가 만들어지고, 이전에 있던 a객체는 필요 없는 쓰레기 값이 되어 GC대상이 된다. 이러한 작업이 반복 수행되면서 메모리를 많이 사용하게 되고, 응답 속도에도 많은 영향을 미치게 된다. GC를 하면 할수록 시스템의 CPU를 사용하게 되고 시간도 많이 소요된다. 그래서 프로그래밍을 할 때 메모리 사용을 최소화하는 것은 당연한 일이다. 

 

Wrap-up

  • String은 짧은 문자열을 더할 경우 사용한다.
  • StringBuffer는 스레드에 안전한 프로그램이 필요할 때나, 개발 중인 시스템의 부분이 스레드에 안전한지 모를 경우 사용하면 좋다. (클래스에 static으로 선언한 문자열을 변경하거나, singleton으로 선언된 클래스에 선언된 문자)
  • StringBuilder는 스레드에 안전한지의 여부와 전혀 관계 없는 프로그램을 개발할 때 사용하면 좋다. 메서드 내에 변수를 선언하는 경우가 이에 해당되겠다.
  • WAS나 시스템이 JDK5.0 이상을 사용한다면, 컴파일러에서 자동으로 StringBuilder로 변환해 주긴 한다. 하지만 반복 루프를 사용해서 문자열을 더할 때는 객체를 계속 추가한다는 사실에 변함이 없으므로 StringBuilder, StringBuffer사용을 권장한다. 

 

https://shlegeris.com/2016/08/14/algorithms

 

My advice on studying algorithms

Software engineering interviews often ask whiteboard algorithms questions. Here’s my advice on how to study for them. (My credentials on this topic are: I have passed a lot of whiteboard interviews, including at Google and Apple; as part of my job I prep

shlegeris.com

  •  Hash tables
  •  Linked lists
  •  Set 
  •  Map

필수 메소드의 구현 방식, 런타임에 동작 방식

  •  Breadth-first search, depth-first search
  •  Quicksort, merge sort
  •  Binary search
  •  2D arrays
  •  Dynamic arrays
  •  Binary search trees
  •  Dynamic programming
  •  Big-O analysis

 

etc

  •  HTTP (at the protocol level)
  •  Databases (indexes, query planning)
  •  CDNs
  •  Caching (LRU cache, memcached, redis)
  •  Load balancers
  •  Distributed worker systems

 

  • 그래프 알고리즘: 너비 우선 탐색(breadth first search), 깊이 우선 탐색(depth first search), 다익스트라 알고리즘 (dikstra’s algorithm)
  • 빠른 정렬 알고리즘 하나. 병합 정렬(mergesort) 또는 퀵 정렬(quicksort)
  • 배열에서 수행하는 이진 검색. 이 알고리즘은 제대로 작성하기 매우 까다롭고 대략적으로 알고리즘을 이해하고 있더라도 코드로 작성해볼 가치가 있습니다.

 

 

-

https://edykim.com/ko/post/advice-on-learning-algorithms/

http://www.corej2eepatterns.com/SessionFacade.htm

 

자바 성능 튜닝 이야기 책에서 말한 핵심 디자인 패턴을 정리해보려고 한다. 

 

Business Delegate

Business Delegate를 사용하여 비즈니스 서비스에 대한 액세스를 캡슐화. Business Delegate는 조회 및 액세스 메커니즘과 같은 비즈니스 서비스의 구현 세부 정보를 숨깁니다.

 

Problem

비즈니스 서비스 구성 요소와의 원격 통신의 복잡성으로부터 클라이언트를 숨기고 싶습니다.

Forces

  1. 장치, 웹 서비스 및 리치 클라이언트와 같은 프레젠테이션 계층 구성 요소 및 클라이언트에서 비즈니스 계층 구성 요소에 액세스하려고 합니다.
  2. 클라이언트와 비즈니스 서비스 간의 결합을 최소화하여 조회 및 액세스와 같은 서비스의 기본 구현 세부 정보를 숨기려고 합니다.
  3. 원격 서비스의 불필요한 호출을 피하려고 합니다.
  4. 네트워크 예외를 애플리케이션 또는 사용자 예외로 변환하려고 합니다.
  5. 클라이언트에서 서비스 생성, 재구성 및 호출 재시도에 대한 세부 정보를 숨기려고 합니다.

Consequences

커플링 감소, 유지보수성 향상, 비즈니스 서비스 예외 번역, 가용성 향상, 비즈니스 계층에 더 간단하고 균일한 인터페이스를 제공, 성능 향상, 추가 레이어 도입

구현 방법

https://www.tutorialspoint.com/design_pattern/business_delegate_pattern.htm


Session Facade

비즈니스 계층 구성 요소를 캡슐화하고 거친 서비스를 원격 클라이언트에 노출합니다. 클라이언트는 비즈니스 구성 요소에 직접 액세스하는 대신 Session Facade에 액세스합니다.

 

Problem

비즈니스 구성 요소 및 서비스를 원격 클라이언트에 노출하려고 합니다.
Forces

 

  1. 클라이언트와의 긴밀한 결합을 방지하기 위해 클라이언트가 비즈니스 계층 구성 요소에 직접 액세스할 수 없도록 하고 싶습니다.
  2. Business Objects 및 기타 비즈니스 계층 구성 요소에 원격 액세스 계층을 제공하려고 합니다.
  3. 애플리케이션 서비스 및 기타 서비스를 집계하여 원격 클라이언트에 노출하려고 합니다.
  4. 원격 클라이언트에 노출되어야 하는 모든 비즈니스 논리를 중앙 집중화하고 집계하려고 합니다.
  5. 비즈니스 구성 요소와 서비스 간의 복잡한 상호 작용 및 상호 종속성을 숨겨서 관리 용이성을 높이고 논리를 중앙 집중화하고 유연성을 높이고 변경 사항에 대처하는 능력을 개선하려고 합니다.

Consequences

원격 클라이언트에 서비스를 제공하는 계층 도입, 계층 간의 결합 감소, 레이어링을 촉진하고 유연성과 유지보수성을 높입니다, 성능 향상, 세분화된 원격 방법 감소, 중앙 집중식 보안 관리, 트랜잭션 제어 중앙 집중화, 클라이언트에 더 적은 수의 원격 인터페이스 노출

구현 방법

https://www.tutorialspoint.com/design_pattern/facade_pattern.htm

 


Data Access Object

데이터 액세스 개체를 사용하여 영구 저장소에 대한 모든 액세스를 추상화하고 캡슐화합니다. 데이터 액세스 개체는 데이터를 가져오고 저장하기 위해 데이터 원본과의 연결을 관리합니다.

Problem

데이터 액세스 및 조작을 별도의 레이어에 캡슐화하려고 합니다.

Forces

  1. 영구 저장소의 데이터에 액세스하고 조작하기 위해 데이터 액세스 메커니즘을 구현하려고 합니다.
  2. 애플리케이션의 나머지 부분에서 영구 저장소 구현을 decouple하려고 합니다.
  3. RDBMS, LDAP, OODB, XML 리포지토리, 플랫 파일 등과 같은 다양한 유형의 데이터 소스에 대한 영구 메커니즘에 대한 균일한 데이터 액세스 API를 제공하려고 합니다.
  4. 데이터 액세스 논리를 구성하고 독점 기능을 캡슐화하여 유지 관리 및 이식성을 용이하게 하려고 합니다.

Consequences

중앙 집중식 제어, 투명한 사용성, 객체 지향 보기를 제공하고 데이터베이스 스키마를 캡슐화, 마이그레이션 용이, 모든 데이터 액세스 코드를 별도의 레이어로 구성

구현 방법

https://www.tutorialspoint.com/design_pattern/data_access_object_pattern.htm


Service Locator

서비스 로케이터를 사용하여 서비스 및 구성 요소 조회를 구현하고 캡슐화합니다. 서비스 로케이터는 조회 메커니즘의 구현 세부 정보를 숨기고 관련 종속성을 캡슐화합니다.

Problem

균일한 방식으로 비즈니스 구성 요소와 서비스를 투명하게 찾고자 합니다.

Forces

  1. JNDI API를 사용하여 엔터프라이즈 Bean 및 JMS 구성요소와 같은 비즈니스 구성요소와 데이터 소스와 같은 서비스를 조회하고 사용하려고 합니다.
  2. J2EE 애플리케이션 클라이언트에 대한 조회 메커니즘 구현을 중앙 집중화하고 재사용하려고 합니다.
  3. 레지스트리 구현에 대한 공급업체 종속성을 캡슐화하고 클라이언트로부터 종속성과 복잡성을 숨기려고 합니다.
  4. 초기 컨텍스트 생성 및 서비스 조회와 관련된 성능 오버헤드를 피하려고 합니다.
  5. 핸들 오브젝트를 사용하여 이전에 액세스한 엔터프라이즈 Bean 인스턴스에 대한 연결을 재설정하려고 합니다.

+ JNDI는 Java Naming and Directory Interface API의 머리글자입니다. 디렉터리 서비스에서 제공하는 데이터 및 객체를 발견(discover)하고 참고(lookup) 하기 위한 자바 API.

 

Consequences

추상화된 복잡성, 클라이언트에게 균일한 서비스 액세스 제공, EJB 비즈니스 구성 요소 추가 촉진, 네트워크 성능 향상, 캐싱을 통해 클라이언트 성능 향상

구현 방법

https://www.tutorialspoint.com/design_pattern/service_locator_pattern.htm


Transfer Object

전송 개체를 사용하여 계층 전체에 여러 데이터 요소를 전달합니다.

Problem

계층을 통해 여러 데이터 요소를 전송하려고 합니다.

Forces

  1. 클라이언트가 다른 계층의 구성 요소에 액세스하여 데이터를 검색하고 업데이트하도록 하려고 합니다.
  2. 네트워크를 통한 원격 요청을 줄이려고 합니다.
  3. 높은 네트워크 트래픽이 있는 응용 프로그램으로 인한 네트워크 성능 저하를 방지하려고 합니다.

Consequences

네트워크 트래픽 감소, 원격 개체 및 원격 인터페이스 단순화, 더 적은 수의 원격 호출로 더 많은 데이터 전송, 코드 중복 감소

 

구현 방법

 

제가 맡고있는 SpringBoot 프로젝트에선 JPA를 사용하고 있으며 분산 환경 데이터베이스를 구축하기 위해  AbstractRoutingDataSource 및 Spring Data JPA를 사용하여 Dynamic DataSource 라우팅을 하고 있습니다. 

@Data
@Configuration
@ConfigurationProperties(prefix = "test")
public class NamedDataSources {
   private List<NamedDataSource> namedDataSources;
}
@Getter
@Setter
public class NamedDataSource {
   private String name;
   private HikariConfig hikari;
}
  1. yml 파일에 입력된 db 설정 정보가 NamedDataSources 클래스에 로딩된다.
  2. DataSource를 생성한다. (DetermineRoutingDatasource)
    1. DetermineRoutingDataSource 인스턴스 생성
    2. DatermineRoutingDataSource에는 key,value 형태로 저장된 NamedDataSources 정보가 있다.
    3. Default 데이터소스가 등록된다.
    4. DataSoucre로 DetermineRoutingDataSource가 리턴된다.
  3. EntityManager Bean (LocalContainerEntityManagerFactory)을 등록한다.
    1. Datasource지정-> (2번에서 생성한 DetermineRoutingDataSource)로 설정
    2. Hibernate Property, Entity가 위치한 Package 지정
    3. Hibernate 기반으로 동작하는 것을 지정하는 JPA Vendor 설정
  4. TransactionManager Bean을 등록한다.
    1. LocalContainerEntityManagerFactory Bean을 주입받음
    2. Datasource와 EntityManagerFactoryBean에서 생성되는 EntityManagerFactory를 지정

 

@Configuration
@EnableJpaRepositories(basePackages = "com.xx.xxx.xx.xx",
        transactionManagerRef = "transcationManager",
        entityManagerFactoryRef = "entityManager")
@EnableTransactionManagement
public class NamedRoutingDataSources {

    private final TestProperties jpaProps;
    private final NamedDataSources namedDataSources;

    public NamedRoutingDataSources(TestProperties jpaProps, NamedDataSources namedDataSources) {
        this.jpaProps = jpaProps;
        this.namedDataSources = namedDataSources;
    }

    @Primary
    @Bean
    public DataSource createRoutingDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();

        for (NamedDataSource namedDataSource : namedDataSources.getNamedDataSources()) {
            targetDataSources.put(DatabaseCluster.valueOf(namedDataSource.getName()), new HikariDataSource(namedDataSource.getHikari()));
        }

        DetermineRoutingDataSource routingDataSource = new DetermineRoutingDataSource();
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(targetDataSources.get(DatabaseCluster.MZ));

        return routingDataSource;
    }

    @Bean(name = "entityManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(createRoutingDataSource());
        factoryBean.setPackagesToScan("com.xx.xxx.xx.xx");
        factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factoryBean.setJpaProperties(initJpaHibernateProperties());
        return factoryBean;
    }

    private Properties initJpaHibernateProperties() {
        Properties properties = new Properties();
        properties.put(AvailableSettings.FORMAT_SQL, jpaProps.getProperties().getHibernate().isFormatSql());
        properties.put(AvailableSettings.SHOW_SQL, jpaProps.getProperties().getHibernate().isShowSql());
        return properties;
    }

    @Bean(name = "transcationManager")
    public JpaTransactionManager transactionManager(
            @Autowired @Qualifier("entityManager") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        return new JpaTransactionManager(entityManagerFactoryBean.getObject());
    }
}

SpringBoot 프로젝트가 로딩 될 때 위와 같이 설정이 됩니다.

크게 보자면 먼저 Datasource를 생성하고, Spring 프로젝트에서 JPA를 사용하기 위해 EntityManager를 설정합니다. 그리고 Spring Container에서 동작하는 JPA의 기능을 활용하고, 스프링이 제공하는 일관성 있는 데이터 액세스 기술의 접근 방법을 적용할 수 있도록 LocalContainerEntityManager를 생성합니다.

 

 

AbstractRoutingDataSource는 조회 키를 기반으로 다양한 대상 데이터 소스 중 하나로 호출을 라우팅하는 DataSource의 추상 구현체입니다. AbstractRoutingDataSource는 현재 컨텍스트를 기반으로 실제 데이터 소스를 동적으로 결정하는 방법을 제공하기 위해 Spring 2.0.1 버전에 도입되었습니다. 컨텍스트 변경을 통해 전환되는 여러 데이터 소스의 맵을 유지 관리합니다.

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DetermineRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TestThreadLocal.getDatabaseCluster();
    }
}
import org.springframework.stereotype.Component;

@Component
public class TestThreadLocal {

    public static final ThreadLocal<DatabaseCluster> contextHolder = new ThreadLocal<>();

    public static DatabaseCluster getDatabaseCluster() {
        return contextHolder.get();
    }

    public static void setDatabaseCluster(DatabaseCluster databaseCluster) {
        contextHolder.set(databaseCluster);
    }

    public static void clear() {
        contextHolder.remove();
    }
}

https://www.websparrow.org/spring/spring-boot-dynamic-datasource-routing-using-abstractroutingdatasource

 

+ Recent posts