- 대부분의 Public Cloud는 Endpoint와 HTTP API를 제공하고 있으며, SDK는 이것을 감싸는 구조로 되어있다.
- 따라서 url 인코딩을 주의하여 사용해야 한다.
- 예를 들어서 얼마전에 테스트하다가 파일명에 특이한 문자들을 넣게 되었는데 아래와 같은 오류가 발생하였다.
2022-05-19 10:15:15.447 DEBUG 30235 --- [nio-8900-exec-6] com.amazonaws.request : Received error response: com.amazonaws.services.s3.model.AmazonS3Exception: The request signature we calculated does not match the signature you provided. Check your key and signing method. (Service: Amazon S3; Status Code: 403; Error Code: SignatureDoesNotMatch; Request ID: S80BNXP50DTW9SJM; S3 Extended Request ID: , S3 Extended Request ID:
2022-05-19 10:15:15.449 DEBUG 30235 --- [nio-8900-exec-6] com.amazonaws.retry.ClockSkewAdjuster : Reported server date (from 'Date' header): Thu, 19 May 2022 01:15:15 GMT
.
.
.
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:5054) [aws-java-sdk-s3-1.11.762.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:5000) [aws-java-sdk-s3-1.11.762.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.access$300(AmazonS3Client.java:394) [aws-java-sdk-s3-1.11.762.jar:na]
at com.amazonaws.services.s3.AmazonS3Client$PutObjectStrategy.invokeServiceCall(AmazonS3Client.java:5942) [aws-java-sdk-s3-1.11.762.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.uploadObject(AmazonS3Client.java:1808) [aws-java-sdk-s3-1.11.762.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.putObject(AmazonS3Client.java:1768) [aws-java-sdk-s3-1.11.762.jar:na]
- 인증토큰이나 이러한 부분에 문제인줄 알았는데 유사한 문제를 겪은 사람들이 상당히 많았고
- 가장 쉽게 접근하는 방법은 원본과 똑같은 공간을 하나 더 만들어서 순차적으로 탐색하는 방법이다. 이 경우 공간복잡도가 O(mn)이며 문제에서는 권장하지 않는 방법이다.
- 그 다음의 접근하는 방법은 0이 있는 위치를 기억하는 것이다. row 와 column 을 각각 체크할 수 있는 공간을 만든다면 공간복잡도는 O(m) + O(n)이 된다. 간단히 살펴보면 다음과 같다.
// o(m+n)
public void setZeroes(int[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
int[] rowCheck = new int[n];
int[] columnCheck = new int[m];
for(int i=0; i<n; i++){
for(int j=0; j< m ; j++){
if(matrix[i][j] == 0) {rowCheck[i]=1; columnCheck[j]=1;}
}
}
for(int i=0; i<n; i++){
if(rowCheck[i]==1){
for(int j=0;j<m;j++){
matrix[i][j]=0;
}
}
}
for(int j=0; j<m ; j++){
if(columnCheck[j]==1){
for( int i=0; i<n; i++){
matrix[i][j]=0;
}
}
}
}
- 최초 입력받은 matrix에서 row와 column에 '0' 인 위치를 기억해두었다가 해당 위치의 값을 변경하는 방식이다.
- 그렇다면 문제에서 제안하는 Best Solution은 무엇일까? 제공되는 힌트를 보면
# Hint 1 (추가 메모리 공간이 없도록 manipulate)
If any cell of the matrix has a zero we can record its row and column number using additional memory. But if you don't want to use extra memory then you can manipulate the array instead. i.e. simulating exactly what the question says.
# Hint 2 (불일치가 일어나지 않도록 market관리)
Setting cell values to zero on the fly while iterating might lead to discrepancies. What if you use some other integer value as your marker? There is still a better approach for this problem with 0(1) space.
# Hint 3 (row/columns를 분리해서 생각하기 보다는 하나로 가져갈수 있는방법)
We could have used 2 sets to keep a record of rows/columns which need to be set to zero. But for an O(1) space solution, you can use one of the rows and and one of the columns to keep track of this information.
# Hint 4 (! 모든 행과열의 첫번째 셀을 flag로 사용할 수 있다.)
We can use the first cell of every row and column as a flag. This flag would determine whether a row or column has been set to zero.
이와 같은 힌트를 동해 다음과 같이 접근해보자.
1. 첫번째 행과 첫번째 열을 우리가 위에서 마련했던 O(m+n) 의 공간으로 사용한다.
2. 단 이 때 기존에 세팅되어 있던 값을 보존해야 한다. 이후 matrix의 값을 "0"을 치환하는 과정에서 첫번째 행과 열의 값을 참조해야 하는데 값이 구분되지 않아서 불일치가 일어날 수 있다.
3. 따라서 최초 첫번째 행과 열에 있던 값을 기억하여 나중에 일괄처리 할 수 있도록 하며, 2번의 변환과정의 범위에서 첫번째 행과 열은 제외해야 한다.
var Registry = require('azure-iothub').Registry;
const chalk = require('chalk');
var connectionString = process.argv[2];
var fwVersion = '2.8.5';
var fwPackageURI = 'https://secureuri/package.bin';
var fwPackageCheckValue = '123456abcde';
var sampleConfigId = 'firmware285';
2. Update할 Configuration 을 정의한다.
// <configuration>
var firmwareConfig = {
id: sampleConfigId,
content: {
deviceContent: {
'properties.desired.firmware': {
fwVersion: fwVersion,
fwPackageURI: fwPackageURI,
fwPackageCheckValue: fwPackageCheckValue
}
}
},
// Maximum of 5 metrics per configuration
metrics: {
queries: {
current: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'current\'',
applying: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND ( properties.reported.firmware.fwUpdateStatus=\'downloading\' OR properties.reported.firmware.fwUpdateStatus=\'verifying\' OR properties.reported.firmware.fwUpdateStatus=\'applying\')',
rebooting: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'rebooting\'',
error: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'error\'',
rolledback: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'rolledback\''
}
},
// Specify the devices the firmware update applies to
targetCondition: 'tags.devicetype = \'chiller\'',
priority: 20
};
// </configuration>
deviceContent에는 Device가 참조할 값들이 들어가고, metrics에는 작업 수행 중 Device 가 기록하는 값(DeviceTwin)을 조회하여 모니터링 할 수 있는 쿼리가 들어간다.
3. RegistryManager를 통해서 해당 Configuration 정보를 등록한다.
해당 값을 String으로만 처리하는 경우에는 유효하지 않는 코드값이 들어오거나, 오타가 발생하는등의 오류를 잡아내기가 힘들며 또한 코드값이 변경/추가되었을때 유지보수가 용이하지 않기 때문에 되도록 enum type사용을 권장한다.
또한, enum type을 사용하더라도 코드값에 따른 로직분기의 경우 if - else if문의 반복을 통해서 수행하는 경우가 많은데 추후 요구사항의 변경이 발생하였을 때 코드의 가독성을 떨어뜨리고, 버그를 만드는 원인이 된다. 이부분 역시 enum type을 활용하여 소스를 깔끔하게 유지할 필요가 있다.
Firmware Update의 모니터링쿼리를 살펴보면 각 Status에 따라 쿼리의 조건문이 다르고, 인자의 수에 따라서도 미묘하게 다르게 생성되어야 하지만 공통으로 공유하는 조건도 있다.
예를 들어서 CURRENT인 경우 상태가 'current'인 device에 대해서 조회가 이루어져야 하며, APPLYING의 경우는 'downloading', 'verifying', 'applying' 의 3가지 경우에 대해서 조회가 이루져야 한다. 그러나 조회조건의 configurationId는 동일하다.
해당 로직을 외부에서 각각 구현한다면 중복로직이 존재하게 되고 추후 변경사항이 발생할 경우 영향을 받는 범위도 넓다. 이는 OCP원칙에 위배되기 때문에 확장에는 열려있고 변경에는 닫히도록 작성할 필요가 있다.
public enum FirmwareUpdate{
CURRENT(Arrays.asList(FirmwareStatus.CURRENT)),
REBOOTING(Arrays.asList(FirmwareStatus.REBOOTING)),
ERROR(Arrays.asList(FirmwareStatus.ERROR)),
ROLLEDBACK(Arrays.asList(FirmwareStatus.ROLLEDBACK)),
APPLYING(Arrays.asList(FirmwareStatus.APPLYING, FirmwareStatus.DOWNLOADING, FirmwareStatus.VERIFYING));
List<FirmwareStatus> statusList;
FirmwareUpdate(List<FirmwareStatus> statusList){
this.statusList=statusList;
}
String query(String configurationId){
String temp = "SELECT deviceId FROM devices WHERE configurations.[[" + configurationId + "]].status=\'Applied\' AND ";
if (statusList.size() == 0) {
throw new ArrayIndexOutOfBoundsException("the size of status list should be positive number");
} else if (statusList.size() == 1) {
temp += "properties.reported.firmware.fwUpdateStatus=\'" + statusList.get(0).label + "\'";
} else {
temp += "(";
int count=0;
for (FirmwareStatus status : statusList) {
temp += "properties.reported.firmware.fwUpdateStatus=\'" + status.label + "\'";
count++;
if(count < statusList.size()){
temp+=" OR ";
}
}
temp += ")";
}
return temp;
}
}
위와 같이 각 Status에 따라서 동작하는 고유의 로직은 enum형태로 가지고 있도록 하며, 외부에는 query 메소드만 노출하도록 한다.
만약 Status 에 따라서 더욱 구체적인 구현이 필요하다면 query 메소드를 abstract로 정의하고 type별로 개별구현하는 것도 가능하다.
아래 언급되는 내용들은 배치뿐 아니라 일반적인 Enterprise Application Architecture에서 공통적으로 해당되는 내용이다.
Language
최근 금융권 시스템에도 Java가 많이 도입되면서 배치에서도 Java를 사용하는 경우가 늘어나고 있다. 하지만 Java의 경우 가상머신(VM)을 통해서 수행되기 때문에 C나 COBOL에 비해서 속도에 약점을 보일 수 있어서 배치에는 적합하지 않다는 인식도 많이 있다.
실제로 프로젝트에서 가장 많이 나오는 불만이 "로직 변경없이 똑같이 작성하면 기존보다 느리다"는 것이다. 하지만 장점도 있다. 일반적으로 온라인과 동일한 Language를 사용하기 때문에 아키텍처 통일성이 확보되고 프로그램 재사용이 가능하다. 또한 다양한 Opensource의 활용이 용이하고 멀티쓰레드 처리에 쉽게 접근할 수 있다.
따라서 다양한 관점의 분석과 이해당사자들의 협의를 통하여 Language선택이 이루어져야 한다. 실제로 프로젝트를 수행할 때 성능 최적화에 가장 큰 요소는 Language가 아니라 업무에 관련된 부분이다.
불필요한 작업단계를 제거하거나 통합하고, CriticalPath작업의 위치를 조정하고 작업간 선/후행관계를 재조정하는 등 업무의 최적화에서 더 큰 효과를 볼 수 있다.
Resource Management
현재 시스템에서 수행되어야 할 작업의 상세한 내용을 5w1h원칙으로 정확하게 파악해야 한다.
who : 작업명은 무엇인가? 작업의 주체는 누구인가?
when : 언제 수행되는가? 일단위인가? 월단위인가?
what : 무엇을 수행하는 작업인가? File처리인가? 그렇다면 어느파일에 접근하는가? DB처리인가? 그렇다면 어느 테이블에 접근하는가?
why : 작업의 목적은 무엇인가? 어떤한 작업의 선행인가? 후행인가?
where : 어느서버에서 수행되는가? 어느계정으로 수행되는가?
how : 스케쥴링에 의해서 수행되는가? 원격호출로 수행되는가?
또한 배치 시스템의 특성상 많은 데이터가 생성되고 참조되는데 이에 대한 관리도 필요하다. 예를 들면 다음과 같다.
파일명명규칙 : 생성되는 파일에 대해서 명명규칙이 필요하다. 업무구분/보관주기/생성모듈등 정보관리가 필요하다. 특히 장기간 불필요파일들이 남아있을 경우 저장공간 부족을 야기시킬수 있기때문에 보관주기에 따라 주기적으로 정리하는 것이 좋다.
테이블레이아웃 : 배치에서 LOAD/UNLOAD작업을 하기 위한 테이블 정보도 관리 되어야 한다.
FTP목록 : 작업결과를 FTP로 전송할 경우가 있다. 이 때 사용되는 서버/계정정보/권한 등도 관리 되어야 한다.
이외에도 실제로 현장에서는 정보 관리주체가 각각 다르다 보니 한눈에 영향도를 파악하기 어려운 경우가 많이 있다.
Capacity
총 작업의 개수, 각 작업이 소모하는 자원, 작업의 처리대상/주기, Peak Time등을 알고 있어야 한다. 예를 들어서 평상시에는 정상적으로 동작하던 작업이 특정요일만 되면 지연현상이 발생하는 경우가 있다. 상황을 분석해보니 특정요일 같은시간대에 수행되는 다른작업이 같은 처리대상에 접근하고 있어서 Table Lock발생하는 경우였다. 또 월초/월말에 처리해야하는 데이터의 양이 큰 폭으로 증가하여 작업시간이 지연되거나 시스템에 부하를 발생하는 경우도 있을 수 있다. RuleEngine을 사용하는 배치작업의 경우 메모리를 많이 소모하여 동시에 다수의 작업이 수행될 경우 자원부족현상이 발생할 수도 있다.
이러한 상황을 예방하기 위해서 작업Dependency Diagram작성, 배치작업관리 메타시스템 구축등을 통해서 작업 패턴을 사전에 분석하는 등의 노력이 필요하다.
Deployment & FailOver
시스템 장애가 발생하여 부득이하게 다른 서버를 사용할 경우를 대비하여 사전에 준비가 되어 있어야 한다. 예를 들어서 배치프로그램은 물론이고 배치프로그램들이 생성하는 File들도 동일한 버전으로 배포가 되어 있어야 하며 스케쥴러와 같은 기타 솔루션들도 유지가 되어야 한다. 또한 설정정보들 역시 신속하게 변경/적용할 수 있어야 한다. 최근에는 파일공유 솔루션이나 NAS를 통해서 어플리케이션 및 파일을 공유하여 사용하고 있다.
개발표준
전체 개발표준 뿐만 아니라 대표적인 케이스를 선정하여 유형별 Sample코드를 제공하는 것도 하나의 방법이 될 수 있다. 배치의 경우 예를 들면 입력되는 데이터에 따라서 후행배치를 분기시키는 데몬배치도 있고 Resource를 읽어서 처리하는 비지니스 배치도 있다. 비지니스 배치도 처리하는 Resource유형에 따라서 File to File, DB to File등이 존재할텐데 이에 따른 처리패턴을 확보하여 Sample코드로 제공하면 개발뿐 아니라 성능테스트에도 많은 도움이 된다.
과거에 배치는 단순히 main()함수에 의해서 순차적으로 실행되도록 작성되는 프로그램이었고 그 수도 많지 않았으며 요건도 단순했다.
하지만 대용량의 데이터, 매일 수백,수천만 건의 데이터 처리작업이 매일 끊임없이 발생하는 엔터프라이즈 환경에서 필수적인 배치작업을 개발/실행/운영하기 위한 아키텍처의 필요성이 대두되고 있다.
현장에서 업무를 수행하면서 느꼈던 특히 대용량 배치시스템을 구성하기 위해서 어떠한 요소를 살펴야할지 정리해본다.
배치는 꼭 필요한 것인가
은행업무를 예로 들어본다면 다음과 같다.
고객이 창구에 방문하여 돈을 입금하면 직원은 돈을 받아서 금고에 넣고 원장에 금액을 기록한다. 이러한 업무는 최대한 빨리 처리해서 고객에게 결과를 주어야 한다. 빠른응답이 생명인 온라인 업무이다.
영업시간이 종료되면 그날 있었던 거래들을 집계하고, 타 은행과 정산이 필요할 경우 돈을 주고 받기도 한다. 즉시 처리할 필요는 없지만 비지니스를 위해서는 반드시 해야하는 업무이다. 이러한 것이 배치 업무이다.
가장 이상적인 시스템은 배치가 없이 모두 실시간으로 처리하는 것이다.
하지만 자원은 한정되어 있으며 해야할 일은 많다. 일의 우선순위를 나눌수 밖에 없다.
배치 어플리케이션 특징
수행방식
온라인 : 사용자의 요청을 항상 대기하고 있다가 요청이 들어오면 실시간으로 처리하여 결과를 돌려준다 일반적으로 다양한 채널(대외계, UI등)을 통해서 데이터를 입력받고, 해당내용이 Request로 Server에 전달되어 비지니스 처리 후 다시 Response를 통해서 응답하게 된다.
배치 : 사전에 약속된 Resource로부터 데이터를 입력받고, 데이터를 처리하여 결과를 다시 Resource에 반영한다. 대부분이 정해진 일정에 따라서 자동으로 수행한다.
최근에는 수천건 이내의 데이터의 경우 온라인 호출(sync or Async)을 통하여 배치를 구동하는 경우도 있다. 편의상 온라인 배치라고 부른다.
수행시간 및 자원소모
온라인 : 대부분 1건단위 요청으로 비지니스처리가 이루어지며 요청에서 응답까지 3초이내로 끝나는 것이 일반적이다.
배치 : 수백~수천만건단위 비지니스 처리이며 짧게는 수 분, 길게는 수 시간이 소요된다. 정해진 시간내에 수행이 완료되어야 한다.(후행작업을 위해서)
따라서 배치는 한정된 자원(CPU, Memory등)을 효율적으로 사용해야 하며, 과도한 자원사용으로 시스템이 down되는 것을 방지해야 한다. 그 예로 온라인의 경우 하나의 서비스 메소드 호출시 Connection,Statement등의 자원을 사용하고 즉시 반환하여 다른 호출에 자원을 양보하지만 배치의 경우 최초 점유한 자원을 끝까지 사용하는 것이 효율적이다.
처리 패턴의 다양성
배치업무의 처리패턴은 다양하다. 또한 많은 기술적요소와 업무적요소가 혼재되어 이를 구분하기 쉽지 않다. 파일전송, EAI연계, DB Load/Unload등 연계요소는 분리하여 공통화하도록 하고 업무적요소는 처리하는 Resource의 유형에 따라 분류하는 등 다양한 처리패턴에 대해서 정리하고 공통화할 필요가 있다.
논리적 계층
Batch Program : 일반적으로 개발자가 작업하는 하나의 단위이다. 모듈이라고 부르기도 한다. 일반적으로 데이터를 읽고 처리하여 다시 저장하는 형태이며 일부 처리결과를 타 시스템에 전달하는 경우도 있다.
Batch Job : 비지니스 관점에서 하나의 목표를 달성하기 위해서 구성되는 단위이다. 1개 이상의 Program을 순차적으로, 혹은 조건적으로 실행한다.
Batch Job Group : 각 Batch Job들의 실행조건, 선/후행관계, 실행시간 및 연관성 있는 Batch Job의 그룹이다.
Batch System : Batch Job Group이 모여서 기업을 서포트하기 위한 하나의 시스템이 구현된다.
아직까지는 단일 프로그램으로 배치작업을 구성하고, 스케쥴링 대신에 Jenkins나 crontab을 활용하는 곳도 많이 있다. 또한 Stored Procedure를 활용하여 대부분의 로직을 DB에 구성하고 DB Server내에서 모든 작업을 끝내는 경우도 있다. 배치작업의 개수가 수십개정도인 소규모시스템에서는 그러한 방향이 효율적일 수 있다.
하지만 일정규모이상의 데이터와 프로세스를 다루는 시스템을 구축하기 위해서는 배치 어플리케이션의 특징을 이해하고 이를 위한 아키텍처 구축에 많은 고민이 필요함을 알 수 있다.