728x90
📌 들어가기 앞서

본 글은 향로 선생님의 1. Spring Batch 가이드 - 배치 어플리케이션이란? 의 내용을 바탕으로 작성되었습니다.
내용을 효과적으로 읽기 위해서는 위 글을 우선적으로 읽어보시는 것을 권장드립니다.


본 글은 아래의 내용을 포함하고 있습니다.
• 배치 애플리케이션의 정의 및 Spring Batch 등장 배경
• Batch vs Scheduler(Quartz) 개념 비교
• 배치 애플리케이션 사례


원본 글에 대한 인용 및 요약은 음영 처리된 형태로 표시됩니다.
글의 작성 방향은 이 글을 참고해주시면 감사하겠습니다.

관련 시리즈

1. Spring Batch 5 가이드 - 배치 어플리케이션이란?
2. Spring Batch 5 가이드 - Batch Job 실행해보기
3. Spring Batch 5 가이드 - 메타테이블엿보기
4. Spring Batch 5 가이드 - Spring Batch Job Flow
5. Spring Batch 5 가이드 - Spring Batch Scope & Job Parameter
6. Spring Batch 5 가이드 - Chunk 지향 처리
7. Spring Batch 5 가이드 - ItemReader
8. Spring Batch 5 가이드 - ItemWriter
9. Spring Batch 5 가이드 - ItemProcessor 

1. 배치 애플리케이션이란?

배치(Batch)는 일괄처리 란 뜻을 갖고 있습니다.

일반적인 웹 애플리케이션(Tomcat + Spring MVC)의 경우 큰 데이터를 읽고, 가공하고, 저장한다면 서버는 CPU 및 I/O 등의 자원을 소모하기 때문에 다른 요청을 처리하지 못 하게 됩니다.
또한 하루에 1번 또는 단발적으로 수행되는 이러한 요청을 API로 구성하는 것은 리소스적으로 낭비입니다.

이렇게 단발적으로 대용량 데이터를 처리하는 애플리케이션을 배치 애플리케이션이라고 합니다.

배치 애플리케이션의 경우 데이터가 너무 많아서 처리 중 실패가 나는 경우 실패한 지점부터 작업을 처리하거나, 같은 파라미티로 같은 함수를 실행할 경우 실행 여부에 따라 작업을 제어해야할 수도 있습니다.

즉, 비즈니스 로직 외에 부가적으로 신경써야 할 부분들이 많다는 것을 알 수 있습니다.

여기서 한가지 생각해볼것이, 웹 애플리케이션을 개발할 때 저희는 비지니스 로직에 최대한 집중할 수 있습니다.
그건 왜일까요? 바로 Spring MVC를 사용하기 때문입니다.
Spring MVC를 사용함으로 비지니스 로직에 최대한 집중할 수 있었습니다.

그럼 Spring에서 이런 배치 어플리케이션을 지원하는 모듈이 없을까요?
Spring 진영에선 Spring Batch가 있습니다.

 

배치 애플리케이션

  • 사용자의 개입 없이 일괄적으로 작업을 처리하는 애플리케이션 (대체로 대용량 데이터를 처리)
  • 서비스를 운영하는 관점에서 주기적으로 작업을 처리하기 위해 배치 애플리케이션 사용

 

배치 애플리케이션이 필요한 경우

  1. 필요한 데이터를 모아서 처리해야할 때 (e.g. 월별 거래 명세서 생성)
  2. 일부러 지연시켜 처리할 때 (e.g. 주문한 상품을 바로 배송 처리하지 않고, 일정 시간 뒤 처리)
  3. 자원을 효율적으로 활용해야할 때 (e.g. 트래픽이 적은 시간 대에 서버 리소스를 활용)

 

1.1. Spring Batch?

Spring Batch 프로젝트는 Accenture와 Spring Source의 공동 작업으로 2007년에 탄생했습니다.

Spring Batch는 Spring의 특성을 그대로 가져왔습니다.
그래서 DI, AOP, 서비스 추상화 등 Spring 프레임워크의 3대 요소를 모두 사용할 수 있으면서, Accenture의 Batch 노하우가 담긴 아키텍처를 사용할 수 있습니다.

 

Spring Batch 5 에서 지원하는 Reader & Writer는 아래와 같습니다.

DataSource 구분 설명
Database JDBC 페이징, 커서, 일괄 업데이트 등 사용 가능
Hibernate 페이징, 커서 사용 가능
JPA 페이징 사용 가능
File Flat file 지정한 구분자를 통해 파싱 지원
XML XML 파싱 지원
JSON JSON 파싱 지원
  • Flat file
    • 플랫 파일은 한 개 또는 그 이상의 레코드가 포함된 특정 파일을 의미합니다. 
    • 일반 텍스트 형태로 저장되는 데이터로, 데이터 포맷이나 의미를 정의하는 메타데이터가 존재하지 않습니다.

 

세부적인 설명은 이후 ItemReader, ItemWriter 편에서 진행합니다.

 

 

1.2. Batch vs Quartz?

Quartz는 스케줄러의 역할이지, Batch 와 같이 대용량 데이터 배치 처리에 대한 기능을 지원하지 않습니다.

반대로 Batch 역시 Quartz의 다양한 스케줄 기능을 지원하지 않아서 보통은 Quartz + Batch를 조합해서 사용합니다.

정해진 스케줄마다 Quartz가 Spring Batch를 실행하는 구조라고 보시면 됩니다.

 

Batch와 Scheduler(Quartz)는 다른 개념입니다.

 

 

1.3. Batch 사례

1. 일매출 집계
2. ERP 연동

데이터 처리 배치 애플리케이션

  1. 각 서비스의 데이터를 데이터 웨어하우스에 저장할 때 = ETL(Extract Transform Load)
  2. 아마존에서 연관 상품을 추천하는 데이터 모델을 만들 때
  3. 유저 리텐션, 엑티브 상태 등 마케팅에 참고할 데이터 지표를 집계할 때

 

서비스 배치 애플리케이션

  1. 메세지, 이메일, 푸시 등을 발송할 때
  2. 데이터를 마이그레이션할 때
  3. 실패한 트랜잭션을 재처리할 때
  4. 쿠폰, 포인트 등이 만료되었을 때 소진시키는 처리를 할 때
  5. 월말 또는 월초에 특징 데이터를 생성할 때 (e.g. 월별 거래 명세서)

 

 
728x90
728x90

본격적인 글을 작성하기에 앞서 본 시리즈의 게재 목적과 함께 작성 배경을 정리해보고자 합니다.

 

0.1. 배경

현재 업계를 보면 많은 회사들에서 목적 및 상황에 맞게 다양한 언어와 기술을 사용하고 있습니다.

다만 국내의 회사들 중 흔히 서버 혹은 백엔드 개발자로 분류되는 직군에서는 여러가지 사유로 인해 스프링 프레임워크를 많이 사용하고 있습니다.


그리고 데이터를 효과적으로 처리하기 위해 배치 애플리케이션을 구현할 때에도
이러한 배경 및 배경이 갖고 있는 다양한 장점 때문에 스프링 배치는 좋은 옵션으로 선택받습니다.

 

하지만 스프링 배치의 경우 스프링 MVC에 비해 비교적 사용 및 사용자가 적고,
이에 따른 인과 결과로 인해 국내에 양질의 자료가 많이 존재하지 않습니다.

 

개인적으로 생각할 때 2019~2020년 경에 향로 선생님이 블로그에 연재하신 Spring Batch 가이드가 국내에 존재하는 자료들 중 매우 친절하고 자세하게 설명이 되어있다고 생각합니다.

 

하지만 과거에 연재된 만큼 현재 작성 시점 기준과 비교해봤을 때 스프링 배치의 최신 버전이 변경됐고(4.x → 5.x),
현재 저는 Kotlin을 애용하고 있었기에 블로그에 게시된 내용을 바로바로 예제로 작성하는 과정에서 일부 지연이 있었습니다.

 

따라서 이번 기회에 위의 두 가지 사안을 반영해서 향로 선생님께서 작성하신 Spring Batch 가이드를 2024년 기준으로 오마주하는 형태로 내용을 정리하고자 합니다.

 

 

0.2. 작성 방향

내용은 향로 선생님의 글을 기반으로 아래의 내용이 포함될 예정입니다.

  • 원본 글의 요약
  • 버전 업그레이드에 따른 변경사항 적용
  • Kotlin 예제 코드
  • 추가적으로 학습한 내용

원본 글에 대한 인용 및 요약은 아래와 같이 음영 처리된 상자로 표시합니다.

Spring Batch 프로젝트는 Accenture와 Spring Source의 공동 작업으로 2007년에 탄생했습니다.
Accenture는 수년간의 노력으로 그들만의 배치 프레임워크를 만들었고, 그를 통해 얻은 경험을 가지고 있었습니다.
즉, Accenture의 배치 노하우 & 기술력과 Spring 프레임워크가 합쳐져 만들어진 것이 Spring Batch입니다.

 

본 게시글을 효과적으로 읽기 위해서는 각 게시글의 원본(향로 선생님의 글) 내용을 우선적으로 읽어보시는 것을 권장드립니다.

 

 

0.3. 작성 범위

현재 목표로 하는 커버 범위는 게재된 Spring Batch 가이드 내에서 아래 글들을 대상으로 계획하고 있습니다.

  1. Spring Batch 가이드 - 배치 어플리케이션이란?
  2. Spring Batch 가이드 - Batch Job 실행해보기
  3. Spring Batch 가이드 - 메타테이블엿보기
  4. Spring Batch 가이드 - Spring Batch Job Flow
  5. Spring Batch 가이드 - Spring Batch Scope & Job Parameter
  6. Spring Batch 가이드 - Chunk 지향 처리
  7. Spring Batch 가이드 - ItemReader
  8. Spring Batch 가이드 - ItemWriter
  9. Spring Batch 가이드 - ItemProcessor

상황에 따라 Spring Batch 5 마이그레이션 가이드, 추가적인 이용 방법 및 테스트 관련 글도 포함할 예정입니다.

 

0.4. 대상 독자

본 시리즈는 아래의 분들에게 유용할 것으로 예상됩니다.

  • 스프링 배치를 처음 시작하거나 버전 5를 새롭게 도입하시는 분
  • Kotlin 기반으로 스프링 배치 기반의 애플리케이션을 작성하시는 분

 

0.5. 함께 읽어보면 좋은 자료

스프링 배치에 대한 지식을 높이기 위해 본 시리즈와 함께 읽어보면 좋은 문서 및 블로그를 소개합니다.

728x90
728x90

들어가기 앞서

본 문서는 MacOS에서 특정 포트가 실행 상태를 확인하고, 해당 포트에서 실행되고 있는 프로세스를 종료하는 방법을 정리합니다.

 

관련 문서

TL;DR

# 8080 포트에서 실행되는 프로세스 및 PID를 확인합니다
$ lsof -i :8080

COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    96769 heedong   50u  IPv6 ********      0t0  TCP *:http-alt (LISTEN)

# 해당 프로세스의 아이디(PID)를 이용하여 프로세스를 종료합니다
$ kill -9 96769

 

배경

Description: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

8080포트가 사용 중이다

 

 

개발 도중 사용하고 있던 특정 포트가 종료되지 않아 다시 명령어를 입력했을 때 에러가 발생하는 경우를 한 번씩 겪는다.
(e.g. node:3000, django:8080, tomcat:8080)

 

예전 작성글에 Windows에서 명령 프롬프트(CMD)를 통해 특정 포트 검색 및 프로세스 종료 방법에 대해 정리를 한 적이 있어서 이번에 MacOS를 기준으로 정리하고자 한다.

 

특정 포트 검색 (lsof -i :{PORT})

lsof -i :{PORT 번호}

 

예를 들어 8080번 포트를 검색하고 싶은 경우 아래와 같이 명령어를 실행한다.

$ lsof -i :8080

COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    97510 heedong   50u  IPv6 ********      0t0  TCP *:http-alt (LISTEN)

 

명령어를 실행하는 경우 해당 포트를 점유하고 있는 프로세스의 아이디(PID)를 확인할 수 있다.

 

프로세스 종료 (kill -9 {PID})

kill -9 {PID}
왜 -9 를 붙이는가?
- kill 명령어는 Linux 시그널 번호를 활용하여 이벤트를 전달합니다.
- 이 때 사용하는 9는 SIGKILL을 의미하여, 실행 중인 프로세스를 강제로 종료하는 신호입니다.

 

 

방금 전에 확인한 8080번 포트를 점유하고 있는 프로세스의 아이디인 97510 프로세스를 종료하고 싶다면 아래와 같이 명령어를 실행한다.

kill -9 97510

 

해당 포트를 점유하고 있던 프로세스가 종료되었는지 확인하기 위해서는 위의 '특정 포트 검색'에서 실행했던 lsof 명령어를 한 번 더 실행해서 정상적으로 프로세스가 종료되었는지 확인할 수 있다.

728x90
728x90

Elastic Stack

  • 빅데이터 파이프라인을 구성하는 플랫폼

Flow

엘라스틱 스택의 구성

엘라스틱서치 : 분산 검색 엔진

검색 엔진 + 데이터베이스

  • 일반적인 검색 엔진(구글, 네이버)과는 다른 검색 엔진
    • 일반적으로 검색 엔진은 내부적으로 각 도큐먼트를 인덱싱하고 빠르게 검색하는 데 사용하는 기술
  • 엘라스틱서치는 모든 레코드를 JSON 도큐먼트 형태로 입력하고 관리
  • 일반적인 데이터베이스와 마찬가지로, 쿼리한 결과에 대해 일치하는 원본 도큐먼트를 반환
  • 일종의 NoSQL 데이터베이스라고 생각

빠른 검색 및 집계 성능

  • 엘라스틱서치는 텍스트나 도큐먼트의 경우 인덱싱 시점에 분석을 거쳐 용어 단위로 분해되고 역인덱스 사전을 구축
  • 숫자나 키워드 타입의 데이터들은 엘라스틱서치의 집계를 위해 집계에 최적화된 컬럼 기반 자료구조를 저장
    • 이렇게 최적화된 자료구조들을 바탕으로 병렬 처리나 분산 처리 가능
  • 엘라스틱서치가 타 관계형 데이터베이스, NoSQL 데이터베이스보다 빠른 검색과 집계 성능을 실현하는 이유는 검색 엔진인 동시에 데이터베이스이기 때문
    • 이론적으로 충분한 크기의 클러스터가 구성되어 있다면 데이터의 양과 무관하게 1초 이내의 응답 속도를 기대할 수 있음

스코어링 : 연관도에 따른 정렬

  • 단순히 필드값을 기준으로 한 정렬은 어떤 데이터베이스에서도 제공되지만, 엘라스틱서치는 검색어에 대한 유사도 스코어를 기반으로 한 정렬을 제공
    • 복잡한 문자열 콘텐츠에서 검색을 수행할 때 큰 효과를 기대할 수 있음
  • 이외에도 다양한 스코어링 방법을 포함해 사용자가 정렬 방식을 다양하게 정의할 수 있음

 

아파치 루씬(Apache Lucene)

  • 엘라스틱서치가 제공하는 데이터 타입 별로 최적화된 자료구조 지원이나 스코어링 등의 기능은 루씬에서 비롯된 특징
  • 엘라스틱서치는 이러한 기능을 분산 시스템으로 확장해서 성능, 유연성, 활용성을 개선

키바나 : 시각화와 엘라스틱서치 관리 도구

엘라스틱 스택의 UI

  • 엘라스틱서치는 사용자의 모든 입력을 REST API 형태로 받아들이기 때문에 별도의 도구없이 기능을 잘 활용할 수 있음
  • 하지만 복잡한 요청들을 하나하나 작성하기에는 불편함이 있음
  • 키바나를 통해서 엘라스틱서치에 대한 대부분의 관리 기능, API를 실행할 수 있는 콘솔, 솔루션 및 모니터링 페이지를 제공

로그스태시 : 이벤트 수집과 정제를 위한 도구

로그 수집 및 정제

  • 대량의 데이터를 검색하기 위해 가장 먼저 선행되어야 할 작업은 데이터를 적재하는 것
  • 데이터 수집과 가공 기능을 제공하는 로그스태시를 사용하면 로그, 매트릭, 웹 애플리케이션 등 다양한 소스로부터 로그를 수집할 수 있음
  • 필터 기능을 이용해 비정형이나 반정형 데이터를 분석하기 쉬운 형태로 정제할 수 있고, 엘라스틱서치 외에 다양한 플랫폼으로 정제된 데이터를 내보낼 수 있음

안정성

  • 로그스태시는 엘라스틱서치의 인덱싱 기능을 최적화하기 위한 배치 처리와 병철 처리가 가능하며 영속적인 큐를 사용
  • 이를 통해 현재 처리 중인 이벤트의 최초 1회 전송을 보장하고 데이터 양이 급증하는 부하 상황에서도 안정성을 보장
  • 로그스태시가 아닌 커스텀 애플리케이션을 작성하여 소스 데이터를 수집하는 방법도 존재
    • 하지만 커스텀 애플리케이션으로 구현할 경우 신경써야 할 부분이 많고, 문제 발생 시 디버깅이나 튜닝이 쉽지 않음

비츠 : 엣지단에서 동작하는 경량 수집 도구

경량 수집기

  • 로그스태시의 기능은 충분히 강력하지만 이벤트 정보를 수집하기 위해서는 실제 서비스가 동작하는 호스트에 수집기를 설치해야 하는 경우가 많음
    • 이 경우 로그스태시는 다양한 필터와 설정을 지원하기 만큼 무겁기 때문에 활용도가 떨어짐
  • 엘라스틱 스택에는 이를 위해 파일비트, 메트릭비트 등의 비츠라고 부르는 경량 수집기가 포함

엘라스틱 스택의 용도

  1. 전문 검색 엔진
  2. 로그 통합 분석
  3. 보안 이벤트 분석
  4. 애플리케이션 성능 분석

1. 전문 검색 엔진

전문 : 단순한 문장부터 뉴스 기사나 논문 등 다양한 글의 전체 내용

  • 대상 도큐먼트가 많지 않다면 일반적인 관계형 데이터베이스의 LIKE 질의만으로도 충분히 검색이 가능하지만 도큐먼트 수가 조금만 늘어나도 인덱스의 도움이 없이 빠른 검색은 불가능에 가까움
  • 전문을 빠르고 정확하게 검색하기 위해 전문을 용어(term) 단위로 분석해 인덱싱해두고 이를 기반으로 검색을 수행하는 역인덱싱 기법이 많이 활용
  • 전문 검색은 관계형 데이터베이스와 커스텀 애플리케이션을 이용하여 직접 개발할 수 있지만 완성도와 정확도가 떨어지고 추가적인 공수가 필요하기 때문에 비추

2. 로그 통합 분석

  • 애플리케이션에서 발생하는 로그의 양이 많지 않다면 일반적인 텍스트 에디터나 간단한 쉘 스크립트만으로도 발생되는 로그를 충분히 파악할 수 있음
  • 하지만 많은 로그가 발생되는 경우 로그의 발생 위치를 모두 파악해야 하고, 발생 순서를 추적하는 것도 어려움이 있음
  • 엘라스틱 스택에서는 비츠를 사용하여 적은 리소스로 각 장비의 로그를 빠르게 수집할 수 있고, 로그스태시를 활용하여 필터를 통해 일원화된 형태로 가공하고, 엘라스틱서치의 인덱싱 기능과 텍스트 검색 기능을 통해 흩어진 서비스 로그를 통합해서 분석할 수 있고, 키바나를 통해 직관적으로 모니터링할 수 있음

3. 보안 이벤트 분석

  • SIEM(Security Information and Event Management)나 ESM(Enterprise Security Management) 등으로 불리는 솔루션은 조직 내에 속한 다양한 장비들로부터 보안 이벤트를 수집하고 분석할 수 있게 하려는 목적으로 제작
  • 공격자의 공격 방식의 다변화에 따라 보안 이벤트들은 실시간으로 수집됨과 동시에 실시간 검색과 필터링을 통한 연관 분석과 머신러닝을 이용한 이상 징후 탐지까지 연결되어야 함
  • 키바나에서 제공하는 엘라스틱 SIEM(이벤트 분석)은 비츠 모듈을 통해 애플리케이션, 엔드포인트, 인프라스트럭쳐, 클라우드, 네트워크 등의 다양한 소스에서 수집한 이벤트를 기반으로 엔드포인트 활동, 인증 로그, DNS 트래픽, 네트워크 플로우에서 이상 징후, 불법적인 로그인 시도, 사용자 접근 패턴 등의 문제를 빠르게 찾아낼 수 있음

4. 애플리케이션 성능 분석

  • 엘라스틱 스택은 APM(Application Performance Monitoring) 기능을 제공하기 때문에 애플리케이션 상태를 지속적으로 모니터링할 수 있음

엘라스틱 스택의 연동

카프카와 연동

하둡 생태계와 연동

관계형 데이터베이스와 연동

  • 로그스태시를 활용하여 연동 가능

유사 제품

구성목적비교군

엘라스틱서치 데이터베이스 RDBMS(MySQL, PostgreSQL, ...), NoSQL(MongoDB, ...)
엘라스틱서치 검색 엔진 솔라(Slor)
로그스태시/비츠 데이터 수집 플루언트디(Fluentd)
키바나 시각화 그라파나(Grafana), 태블로(Tableau)
엘라스틱 스택 전체 스플렁크(Splunk)

참고

  • 엘라스틱 스택 개발부터 운영까지
728x90
728x90

상황

  • MySQL에서 IN 절 안에 조회하고 싶은 컬럼의 리스트를 넣은 후 조건으로 설정한 리스트의 순서대로 반환받고 싶을 때
  • ex.
조회해야 하는 Member들의 id와 반환되어야 할 순서로 정렬된 리스트를 입력받는다.
Member를 조회하고, 입력받은 순서에 맞게 정렬해서 Member List를 리턴한다.

 

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    // Example 1
    public List<Member> getMembers1(List<Long> memberIds) {

        List<Member> members = memberRepository.findAllById(memberIds); // ①

        List<Member> sortedMembers = new ArrayList<>(); 
        for (Long memeberId: memberIds) {
            Optional<Member> optionalMember = members.stream()  // ②
                    .filter(m -> m.getId().equals(id))
                    .findAny();

            optionalMember.ifPresent(sortedMembers::add);       // ③
        }

        return sortedMembers;
    }

    // Example 2
    public List<Member> getMembers2(List<Long> memberIds) {
        
        List<Member> members = memberRepository.findAllById(memberIds); // ①

        Map<Long, Member> memberMap = new HashMap<>();
        members.forEach(member -> memberMap.put(member.getId(), user)); // ②

        List<Member> sortedMembers = new ArrayList<>();
        for (Long memeberId: memberIds) {
            Member member = memberMap.get(memeberId);                   // ③

            if (member != null)
                sortedMembers.add(member);
        }

        return sortedMembers;
    }
}
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

    ...

    @Override
    List<T> findAllById(Iterable<ID> ids);

    ...
}
  • Example 1
    • ① : id list를 조회
    • ② : 조회한 멤버 리스트에서 알맞는 id의 회원을 조회
    • ③ : 해당 회원을 리스트에 추가
  • Example 2
    • ① : id list를 조회
    • ② : 조회한 멤버 리스트를 Map 형태로 보관
    • ③ : Map에서 아이디를 조회해서 리스트에 추가

해결

  • 지금의 나는 코드를 작성할 때 Example1이나 Example2를 생각했을 거고, Example2를 활용해서 값을 반환했을 것 같다.
  • 하지만 이 방법은 매우 비효율적이다. DB에서 값을 조회하고, 그다음 다시 반복문을 실행시켜야 한다는 점과 굳이 불필요한 Map 객체를 생성해서 처리해야 한다.
  • 따라서 이 부분을 애초에 쿼리를 실행할 때 정렬 순서를 조건으로 설정해서 값을 반환받을 수 있다면 불필요한 반복문 실행이나 객체 생성이 필요 없을 수 있다.

기존 쿼리

SELECT * FROM member WHERE id IN (2, 1, 3);
  • 조회할 아이디 값과 정렬된 리스트가 2,1,3 일 때

해결

SELECT * FROM member WHERE id IN (2, 1, 3) ORDER BY FIELD(id, 2, 1, 3);
  • ORDER BY FEILD를 설정한다.
    • ORDER BY FELID('컬럼명', '정렬 순서 1', '정렬 순서 2', '정렬 순서 3', ...)

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "SELECT * FROM member WHERE id IN (?1) ORDER BY FIELD(id, ?1);", nativeQuery = true)
    List<Member> findMembersByIdOrderByFeild(String ids); // 리스트를 문자열로 변환
}

 

728x90
728x90

Query 구현 - Code

Query : 데이터 조회
Mutation : 데이터 추가, 수정, 삭제

Query 루트 타입

type Query {
    teams: [Team]
}
  • 자료요청에 사용될 쿼리들을 정의
  • 쿼리 명령문마다 반환될 데이터 형태를 지정

Type

type Team {
    id: Int
    manager: String
    office: String
    extension_number: String
    mascot: String
    cleaning_duty: String
    project: String
}
  • 반환될 데이터의 형태를 지정
  • 자료형을 가진 필드들로 구성

Resolver

const resolvers = {
  Query: {
    teams: () => database.teams
  }
}
  • Query란 object의 항목들로 데이터를 반환하는 함수 선언
  • 실제 프로젝트에서는 MySQL 조회 코드 등..

Mutation 구현 - Code

Equipment 데이터 추가하기

Mutation - 추가 루트 타입

type Mutation {
    insertEquipment(
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    ...
}

Resolver

Mutation: {
    insertEquipment: (parent, args, context, info) => {
        database.equipments.push(args)
        return args
    },
    //...
}
  • SQL의 INSERT 문 등으로 구현

Test

mutation {
  insertEquipment (
    id: "laptop",
    used_by: "developer",
    count: 17,
    new_or_used: "new"
  ) {
    id
    used_by
    count
    new_or_used
  }
}

Equipment 데이터 수정하기

Mutation - 수정 루트 타입

type Mutation {
    editEquipment(
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    ...
}

Resolver

Mutation: {
    // ...
    editEquipment: (parent, args, context, info) => {
        return database.equipments.filter((equipment) => {
            return equipment.id === args.id
        }).map((equipment) => {
            Object.assign(equipment, args)
            return equipment
        })[0]
    },
    // ...
}
  • SQL의 UPDATE 문 등으로 구현

Test

mutation {
  editEquipment (
    id: "pen tablet",
    new_or_used: "new",
    count: 30,
    used_by: "designer"
  ) {
    id
    new_or_used
    count
    used_by
  }
}

Equipment 데이터 삭제하기

Mutation - 삭제 루트 타입

type Mutation {
    deleteEquipment(id: String): Equipment
}

Resolver

Mutation: {
      deleteEquipment: (parent, args, context, info) => {
          const deleted = database.equipments
              .filter((equipment) => {
                  return equipment.id === args.id
              })[0]
          database.equipments = database.equipments
              .filter((equipment) => {
                  return equipment.id !== args.id
              })
          return deleted
      }
}
  • 삭제 후 결과값으로 받아올 데이터를 deleted 변수에 저장
  • 데이터에서 해당 데이터 삭제 후 deleted 반환
  • 실제 프로젝트에서는 SQL의 DELETE 문 등으로 구현

Test

mutation {
  deleteEquipment(id: "notebook") {
    id
    used_by
    count
    new_or_used
  }
}

Reference

https://www.yalco.kr/@graphql-apollo/2-2/
https://www.yalco.kr/@graphql-apollo/2-3/

728x90

'GraphQL' 카테고리의 다른 글

[GraphQL] Apollo 서버 구축  (0) 2022.12.26
[GraphQL] GraphQL & Apollo  (0) 2022.12.26
[GraphQL] REST API 한계와 GraphQL 사용 이유  (0) 2022.12.26
728x90

Apollo 서버 구축

npm i graphql apollo-server

# ApolloServer
# - typeDef와 resolver를 인자로 받아 서버 생성

# typeDef
# - GraphQL 명세에서 사용될 데이터, 요청의 타입 지정
# - gql(template literal tag)로 생성

# resolver
# - 서비스의 액션들을 함수로 지정
# - 요청에 따라 데이터를 반환, 입력, 수정, 삭제

설치

npm i graphql apollo-server

Code

const database = require('./database')
const { ApolloServer, gql } = require('apollo-server')
const typeDefs = gql`
  type Query {
    teams: [Team]
  }
  type Team {
    id: Int
    manager: String
    office: String
    extension_number: String
    mascot: String
    cleaning_duty: String
    project: String
  }
`
const resolvers = {
  Query: {
    teams: () => database.teams
  }
}
const server = new ApolloServer({ typeDefs, resolvers })
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`)
})

ApolloServer

  • typeDef와 resolver를 인자로 받아 서버 생성

typeDef

  • GraphQL 명세에서 사용될 데이터, 요청의 타입 지정
  • gql(template literal tag)로 생성

resolver

  • 서비스의 액션들을 함수로 지정
  • 요청에 따라 데이터를 반환, 입력, 수정, 삭제
npm start

쿼리 테스트

query {
  teams {
    id
    manager
    office
    extension_number
    mascot
    cleaning_duty
    project
  }
}

Reference

https://www.yalco.kr/@graphql-apollo/2-1/

728x90

'GraphQL' 카테고리의 다른 글

[GraphQL] Query & Mutation  (0) 2022.12.26
[GraphQL] GraphQL & Apollo  (0) 2022.12.26
[GraphQL] REST API 한계와 GraphQL 사용 이유  (0) 2022.12.26
728x90

GraphQL

GraphQL의 장점

1. 필요한 정보들만 선택하여 받아올 수 있음
    - Overfetching 문제 해결
    - 데이터 전송량 감소

2. 여러 계층의 정보들을 한 번에 받아올 수 있음
    - Underfetching 문제 해결
    - 요청 횟수 감소

3. 하나의 endpoint에서 모든 요청을 처리
    - 하나의 URI에서 POST로 모든 요청 관리

실습 - Code

환경 설정

# 프로젝트 모듈 설치
npm i

# 프로젝트 실행 명령어 (해당 프로젝트 폴더에서)
nodemon index.js
# 브라우저에서 localhost:4000 으로 확인

팀 정보 받아오기

query {
  teams {
    id
    manager
    office
    extension_number
    mascot
    cleaning_duty
    project
  }
}

필요한 정보만 받아오기

query {
  teams {
    manager
    office
  }
}
query {
  team(id: 1) {
    manager
    office
  }
}

팀 정보와 해당 팀 멤버들의 정보들 받아오기

query {
  team(id: 1) {
    manager
    office
    members {
      first_name
      last_name
    }
  }
}

새 팀 추가

mutation {
  postTeam (input: {
    manager: "John Smith"
    office: "104B"
    extension_number: "#9982"
    mascot: "Dragon"
    cleaning_duty: "Monday"
    project: "Lordaeron"
  }) {
    manager
    office
    extension_number
    mascot
    cleaning_duty
    project
  }
}

특정 번호의 팀 정보 수정

mutation {
  editTeam(id: 2, input: {
    manager: "Maruchi Han"
    office: "105A"
    extension_number: "2315"
    mascot: "Direwolf"
    cleaning_duty: "Wednesday"
    project: "Haemosu"
  }) {
    id,
    manager,
    office,
    extension_number,
    mascot,
    cleaning_duty,
    project
  }
}

특정 번호의 팀 삭제

mutation {
  deleteTeam(id: 3) {
    id,
    manager,
    office,
    extension_number,
    mascot,
    cleaning_duty,
    project
  }
}

Apollo

- GraphQL은 단순한 형식이기 때문에 정보 제공 및 처리를 위한 솔루션 필요

- 다양한 솔루션이 존재하지만 Apollo의 경우 백엔드와 프론트엔드 모두 기능이 제공되고 간단하기 때문에 사용

Reference

https://www.yalco.kr/@graphql-apollo/1-3/
https://www.yalco.kr/@graphql-apollo/1-4/

728x90

'GraphQL' 카테고리의 다른 글

[GraphQL] Query & Mutation  (0) 2022.12.26
[GraphQL] Apollo 서버 구축  (0) 2022.12.26
[GraphQL] REST API 한계와 GraphQL 사용 이유  (0) 2022.12.26
728x90

REST API

  • 소프트웨어 간 정보를 주고받는 방식
    • GraphQL 이전부터 사용
    • GraphQL은 '다른' 방식 - 용도와 작업특성에 따라 적합한 것을 사용하기 위해

Data

[
  {
    name: '30분짜장',
    category: 'chinese',
    tel: '##-####-####',
    rating: 4.6
  },
  {
    name: '피자파자마',
    category: 'italian',
    tel: '##-####-####',
    rating: 3.9
  },
  {
    name: '공중떡볶이',
    category: 'snack',
    tel: '##-####-####',
    rating: 4.9
  },
  ///...
]
  • REST API는 데이터를 주고받을 주체들간 약속된 형식
    • URI 형식(어떤 정보를) + 요청 방식(어떻게 할 것인가)요청 형식용도
      GET 조회
      POST 생성
      PUT/PATCH 수정
      DELETE 삭제

실습 - Code

환경 설정

# nodemon 설치
npm install -g nodemon

# 프로젝트 모듈 설치
npm i

# 프로젝트 실행 명령어
nodemon index.js
# 브라우저에서 localhost:3000 으로 확인

API 리스트

  1. 팀(들), 팀원 목록 받아오기요청 형식URI
    GET localhost:3000/api/team
    GET localhost:3000/api/team/{id 번호}
    GET localhost:3000/api/people
    GET localhost:3000/api/people?{변수}={값}&{변수}={값} ...
    GET localhost:3000/api/team/{id 번호}/people
  2. 팀 추가하기요청 형식URI
    POST localhost:3000/api/team
  3. 팀 수정하기요청 형식URI
    PUT localhost:3000/api/team/{id 번호}
  4. 팀 삭제하기요청 형식URI
    DELETE localhost:3000/api/team/{id 번호}

REST API의 한계

Case 1. 각 팀의 매니저와 오피스 호수만 필요할 때 - Overfetching

[
  {
    "manager": "Mandy Warren",
    "office": "101A",
  },
  {
    "manager": "Stewart Grant",
    "office": "101B",
  },
  {
    "manager": "Smantha Wheatly",
    "office": "102A",
  },
  // ...
]
  • 필요한 정보만 받아올 수 없음

Case 2. 특정 팀의 매니저와 팀원들 명단이 필요할 때 - Underfetching

{
  "manager": "Mandy Warren",
  "members": [
    {
      "first_name": "Nathan",
      "last-name": "Jenkins"
    },
    {
      "first_name": "Isabella",
      "last-name": "Martin"
    },
    {
      "first_name": "Kate",
      "last_name": "Owen"
    },
    //...
  ]
}
  • 필요한 정보들을 요청 한 번에 받아올 수 없음

REST API의 한계와 GraphQL의 사용 이유

: Overfetching과 Underfetching을 해결하기 위해

Reference

https://www.yalco.kr/@graphql-apollo/1-2/

728x90

'GraphQL' 카테고리의 다른 글

[GraphQL] Query & Mutation  (0) 2022.12.26
[GraphQL] Apollo 서버 구축  (0) 2022.12.26
[GraphQL] GraphQL & Apollo  (0) 2022.12.26
728x90

1. YUV Format?

  • YUV format 은 RGB(Red, Green, Blue) 3 원색의 format 과 손실없이 1:1 변환(mapping)
  • 빛의 밝기를 나타내는 휘도(Y)와 Chroma Components 로 불리는 2개의 색상 신호(U, V)로 구성한다.
  • 인간의 눈이 색상신호보다 밝기 신호에 민감한 눈의 인지 원리를 이용한다.
  • 밝기를 담당하는 Y sample 은 모두 취하고, 상대적으로 둔감한 색상을 담당하는 U 나 V sample 은 4 개의 픽셀에서 1 개 또는 2 개의 픽셀만 취급하여 저장할 비디오의 용량을 줄일 수 있다.

1-1. YUV Format 종류

  • YUV format 은 Y, U(Cb), V(Cr)의 비율을 어떻게 하냐에 따라, 즉 샘플링 비율에 따라 YUV 444, YUV 422, YUV 420, YUV 411과 같이 이름을 명명
  • Y 가 4 바이트 올 때, U 와 V 가 각각 4 바이트가 오는 경우에는 YUV 444, 각각 2 바이트씩 가지는 경우에는 YUV 422, 각각 1 바이트씩 가지는 경우에는 YUV 411 로 명시한다.
  • 밑의 예제 코드에서는 YUV 420 format 동영상을 사용. YUV 420은 바이트 크기 상으로는 볼 때 YUV 411 format과 유사하지만 U, V sample의 배치할 때 차이가 발생한다.
  • YUV 420 format은 안드로이드 camera 클래스에서 제공되는 YUV format 으로 일반적으로 사용한다.
  • YUV 420 format 또한 앞서 설명한 포맷들과 유사하게 Y 값은 온전히 표현하되 U, V 정보를 줄여 데이터의 양을 줄이게 된다. Y 값을 가로와 세로의 곱한 크기만큼 읽고, U, V 는 Y 값의 1/4 만큼 읽어서 하나의 프레임 단위를 구성한다.

1-2. YUV format의 특징

  • YUV format 의 경우 파일의 정보를 표시하는 ‘헤더’가 별도로 존재하지 않는다. 앞서 설명한 방법으로 Y, U, V 값을 읽어 들여 아래 그림(YUV 420 format)과 같은 구조를 가지게 되고, 바이트 값을 통해 하나의 프레임(사진)을 구분할 수 있다.

2. YUV Format 실습

2-1. YUV Player

  • Sourceforge사이트를 통해 YUV Player를 다운로드받을 수 있다.

  • YUV Player를 이용하면 다음과 같이 영상을 재생할 수 있다.

2-2. 홀수 프레임 추출

  • 우리는 YUV format의 구조를 이해하기 위해 첫 번째로 YUV 420 format 영상을 이용하여 홀수 프레임만 추출해볼 것이다.
  • 주어진 30장의 YUV 파일에서 홀수 번째 Picture만 따로 추출하기 위해 한 프레임(픽쳐)의 크기를 먼저 구했고, 2개의 파일 포인터를 이용하여 하나의 파일 포인터는 Original 파일을 한 프레임의 크기만큼 읽어 들였고, 다른 하나의 파일 포인터는 Target 파일에 홀수 번째 프레임인 경우에만 저장했다.
  • 먼저 한 프레임의 크기를 구하기 위해 YUV 420 format의 데이터 구조를 알아볼 필요가 있다. YUV format에서 한 프레임의 경우 Y는 가로와 세로의 곱만큼의 크기를 갖고, U와 V는 가로와 세로의 곱의 크기의 1/4만큼 각각 갖게 된다. 따라서 한 프레임의 크기는 가로와 세로를 곱한 크기의 3/2배를 한 값을 갖게 된다. 따라서 아래 그림과 같이 버퍼를 할당했다.

  • 이후 반복문을 통해 Original 파일을 읽고, 홀수 번째 프레임의 경우에만 해당 버퍼를 Target 파일에 저장하여 홀수 번째 프레임만 추출한다.

  • 코드를 실행시켰을 때 아래 그림과 같이 정상적으로 홀수 번째 프레임만 추출되어 15개의 프레임만 실행되는 것을 확인할 수 있다. (전체 소스 코드는 글 말미에 첨부)

2-3. Y-Picture 추출

  • YUV 420 format에서 한 프레임(픽쳐) 내에서 Y sample이 차지하는 크기는 가로와 세로를 곱한 크기이고 이 값을 N이라고 칭한다면, 하나의 파일 포인터를 이용하여 매 프레임에서 N만큼 읽은 후 다른 하나의 파일 포인터를 통해 Target 파일에 저장한다. 이후 U, V에 해당하는 바이트 값인 N/2 값을 읽을 때에는 저장하지 않고 넘어간다.

  • 첨부된 코드와 같이 U, V에 해당하는 길이 만큼을 0으로 초기화하여 Target 파일에 저장한다. 해당 길이의 값을 0으로 초기화하여 값을 저장했을 때 다음 그림과 같이 Y picture에 해당하는 프레임만 추출된다.

  • 색상을 담당하는 U, V 값에 0으로 값이 초기화되었기에 초록색 화면으로 영상이 실행되었음을 알 수 있다. U, V 값을 제거하는 구현 과제와는 다르지만 흑백 화면으로 실행시키기 위해 U, V 값을 다음 코드와 같이 수정하여 실행했을 경우 흑백 화면이 나오는 것도 확인할 수 있다.

3. 소스 코드

3-1. 홀수 프레임 추출

#include <stdio.h>
#include <stdlib.h>
int main() {
    FILE *rf = NULL;
    FILE *wf = NULL;
    rf = fopen("./PeopleOnStreet_1280x720_30_Original.yuv", "rb");
    wf = fopen("./PeopleOnStreet_1280x720_15_OddFrames.yuv", "wb");

    int frame_size = 1280 * 720 * 3/2;
    unsigned char* frame = (unsigned char*)malloc(frame_size);
	
    int i = 0;
    for (i=0; i<30; i++) {
        if (i%2 == 0) {
            memset(frame, 0, frame_size);
            fread(frame, sizeof(unsigned char), frame_size, rf);
            fwrite(frame, sizeof(unsigned char), frame_size, wf);
        } else {
            fread(frame, sizeof(unsigned char), frame_size, rf);
            memset(frame, 0, frame_size);
        }
    }
    fclose(rf);
    fclose(wf);
}

3-2. Y-Picure 추출

#include <stdio.h>
#include <stdlib.h>
int main() {
    FILE *rf = NULL;
    FILE *wf = NULL;
    rf = fopen("./PeopleOnStreet_1280x720_30_Original.yuv", "rb");
    wf = fopen("./PeopleOnStreet_1280x720_30_YSamples.yuv", "wb");

    int frame_size = 1280 * 720;
    unsigned char* frame = (unsigned char*)malloc(frame_size);

    int i = 0;
    for (i=0; i<30; i++) {
        memset(frame, 0, frame_size);
        fread(frame, sizeof(unsigned char), frame_size, rf);
        fwrite(frame, sizeof(unsigned char), frame_size, wf);
        fread(frame, sizeof(unsigned char), frame_size/2, rf);
        memset(frame, 0, frame_size);
        fwrite(frame, sizeof(unsigned char), frame_size/2, wf);
    }
    fclose(rf);
    fclose(wf);
}
728x90

+ Recent posts