2020년 12월 28일 월요일

Cloudera Manager [2] - Kerberos 보안설정


CDH는 보안프로그램으로 커버로스를 제공한다. 유저가 많아지고 각 유저에 대한 권한이 세분화되어야 함에 따라 그 필요성이 중시된다. 서버환경은 EC2-CentOs7, CDH5.15으로 진행된다.


[ 커버로스 개념 ]

Protocol의 동작순서는 아래와 같다.











이미지 출처 : 

https://www.letmecompile.com/kerberos-protocol


간단히 이해하자면 커버로스 프로세스는 영화를 예매하는 것에 비유할 수 있다. 인터넷(AS)으로 영화를 예매한다. 예매코드를 가지고 영화관에서 티켓(TGS)를 뽑는다. 입장시 해당 티켓를 점원(SS)에게 제출한다. 3단계 보안프로세스를 통해 유효한 티켓이있는 유저만 접속제한하여 편리하게 관리하는 것이 가능하다.



[ 1.사전 설치 ]

JDK 1.8.0_161이상 버전을 설치하도록 한다.

커버로드 프로토콜에서 AES-256암호화를 이전버전에선 사용하지 못하기에 따로 셋팅을 해줘야하는 번거로움이 있기 때문이다.



[ 2.커버로스 설치 ]

Cloudera manager server가 설치된 EC2

$ yum install openldap-clients

$ yum install krb5-server 

 

모든서버 

$ yum install krb5-workstation

$ yum install krb5-libs


* SSH를 통해 패키지 설치

pem키를 cloudera manager가 설치된 EC2로 옮긴 후,

$ sudo chmod 400 my-key-pair.pem 

설정안할시 Load key "my.pem": bad permissions, Permission denied에러


이후 아래명령어로 접속하여 패키지 설치

$ ssh -i /path/my-key-pair.pem my-instance-user-name@my-instance-public-dns-name


서버가 수십대일시, 쉘스크립트를 짜서 실행



[ 3.커버로스 설정 ]

$ hostname -f    //해당호스트네임 보기


EXAMPLE.COM = 현재 호스트네임,

cent1.example.com = MIT KDC서버 호스트, Cloudera Manager host가 설치된 호스트네임으로 바꿔서 기입


$ vi /etc/krb5.conf

[logging]

default = FILE:/var/log/krb5libs.log

kdc = FILE:/var/log/krb5kdc.log

admin_server = FILE:/var/log/kadmind.log 


[libdefaults]

dns_lookup_realm = false

ticket_lifetime = 24h

renew_lifetime = 7d

forwardable = true

rdns = false

default_realm = EXAMPLE.COM

default_ccache_name = KEYRING:persistent:%{uid} 


[realms]

EXAMPLE.COM = {

kdc = cent1.example.com

admin_server = cent1.example.com

}

 

[domain_realm]

 .example.com = EXAMPLE.COM

 example.com = EXAMPLE.COM



$ vi /var/kerberos/krb5kdc/kdc.conf

해당 설정파일에 

max_life = 1d

max_renewable_life = 7d

설정값을 추가 한다.


[kdcdefaults]

kdc_ports = 88

kdc_tcp_ports = 88

[realms]

EXAMPLE.COM = {

#master_key_type = aes256-cts

acl_file = /var/kerberos/krb5kdc/kadm5.acl

dict_file = /usr/share/dict/words

admin_keytab = /var/kerberos/krb5kdc/kadm5.keytab

supported_enctypes = aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal camellia256-cts:normal camellia128-cts:normal des-hmac-sha1:normal des-cbc-md5:normal des-cbc-crc:normal


max_life = 1d

max_renewable_life = 7d

}


admin 권한을 위해 

/var/kerberos/krb5kdc/kadm5.acl 파일을 수정한다.

EXAMPLE.COM에 해당하는 부분 cloudera manager가 설치된 hostname으로 변경.


*/admin@EXAMPLE.COM     *




[ 4.커버로스 데이터베이스 생성 ]

DB와 비밀번호를 생성한다.


$ kdb5_util create -s

Enter KDC database master key: 비밀번호

Re-enter KDC database master key to verify: 비밀번호



[ 5.서비스 등록 ]

KDC서버를 재시작하고 EC2종료 후에도 실행되도록 한다.

$ sudo service krb5kdc restart

$ sudo service kadmin restart


$ sudo systemctl enable krb5kdc

$ sudo systemctl enable kadmin




[ 6.서비스 실행 ]

예시에선 해당계정을 cloudera-scm로 생성했다.

 

$ sudo kadmin.local

Authenticating as principal root/admin@ip-172-11-11-199.ap-northeast-1.compute.internal with password.

kadmin.local:  addprinc -pw 비밀번호 cloudera-scm/admin@ip-ip-172-11-11-199.ap-northeast-1.compute.internal

WARNING: no policy specified for cloudera-scm/admin@ip-ip-172-11-11-199.ap-northeast-1.compute.internal; defaulting to no policy

Principal "cloudera-scm/admin@ip-ip-172-11-11-199.ap-northeast-1.compute.internal" created.

kadmin.local:  modprinc -maxrenewlife 1week cloudera-scm/admin@ip-ip-172-11-11-199.ap-northeast-1.compute.internal


$ kinit cloudera-scm/admin@ip-172-11-11-199.ap-northeast-1.compute.internal

Password for cloudera-scm/admin@ip-172-11-11-199.ap-northeast-1.compute.internal: 비밀번호


$ klist -e

Ticket cache: KEYRING:persistent:1000:1000

Default principal: cloudera-scm/admin@ip-172-11-11-199.ap-northeast-1.compute.internal

Valid starting       Expires              Service principal

12/23/2020 16:07:04  12/24/2020 16:07:04  krbtgt/ip-172-11-11-199.ap-northeast-1.compute.internal@ip-172-11-11-199.ap-northeast-1.compute.internal

Etype (skey, tkt): aes256-cts-hmac-sha1-96, aes256-cts-hmac-sha1-96




[ 7.CDH에서 커버로스 연동 ]

최상단메뉴의 관리->보안 클릭 후 'Kerberos 설정' 버튼 클릭











시작에서 모두 체크 후 다음,

KDC정보에서 Kerberos 암호화 유형을 아래 aes256-cts-hmac-sha1-96로 설정,

나머지 정보는 cloudera manager설치 호스트네임 입력






나머지 설정은 아래와 같이 Default





















설정 완료 후 커버로스 설정이 됬음을 확인한다.




참조 : https://plenium.wordpress.com/2018/07/17/kerberos-setup-in-cloudera-hadoop


* CDH에서 커버로스 연동을 해제하고 싶다면 

https://www.programmersought.com/article/81434721806




2020년 12월 23일 수요일

[AWS] ELB 운영시 503에러



[ ELB 운영시 만날 수 있는 이슈 ]

















  • HTTP 502 Bad Gateway : 백엔드 인스턴스로부터 온 응답을 ELB가 못 받는 경우
  • HTTP 503 Service Unavailable : 인스턴스 등록이 되어있지 않은 경우 / 모든 인스턴스가 Unhealthy 상태인 경우
  • HTTP 504 Gateway Timeout : 인스턴스 CPU 점유율이 높거나 / 프로그램이 데이터베이스나 외부 API Dependency로 인해 응답을 못 받는 경우
  • Instance Out of Service : 인스턴스가 등록이 되지 않거나 / Unhealthy 상태인 경우
  • Health Check Failure : ELB 생성시 대상그룹을 통해 인스턴스에 200코드 값을 요청하는데, 이외의 코드를 반환할 경우 Health check fail 발생





[ 에러 화면 ] 

ELB 연결 작업시 503코드를 리턴 받아 그 원인을 짚어봄









1. EC2 문제인지 ELB문제인지 확인

프로그램은 php로 작성했기에 EC2 퍼블릭 아이피에 테스트 코드(info.php())를 찍어 

아래와 같은 화면이 나오는지 확인





















2. ELB생성시 대상등록 체크

위에서 언급했듯이 HTTP 503 Service Unavailable의 에러원인은 인스턴스 등록이 되어있지 않은 경우가 있다.

로드밸런싱 -> 대상그룹 -> 해당대상그룹클릭 -> Targets
에서 등록된 EC2를 확인 할 수 있다.

ELB 생성시 5단계 : 대상등록에서 해당 EC2를 체크하고 "등록된 항목에 추가" 버튼을 눌러 위의 등록된 대상 항목이 바뀌는 것을 보고 단계를 넘어가야한다.






















3. 대상그룹의 Status 확인 (Health Check)

대상그룹의 Status가 Unheathly가 된 경우에도 503에러를 띄울 수 있다. 즉 health check가 않았다. ELB를 연결한 해당 프로그램을 API용도로만 만들어서 http://example.com 도메인으로 접속시 어떤 페이지도 나타내지 않는다. 

따라서 도메인의 루트디렉토리(ex. /var/www/html )에 간단히 작성한 index.html를 넣어주면 200코드를 리턴하므로 수정 후 다시 테스트.

주의할 점이 ELB생성시 4단계 : 라우팅구성의 상태검사 경로를 다른 곳으로 지정했다면 200코드를 반환하는 파일을 지정한 곳에 위치시켜야한다. 










































[ 테스트 ]

대상그룹 Targets Health status가 healthy가 되어있는지 확인후
포스트맨으로 아래에 해당하는 URL에 접속해 기능테스트를 한다. 
  • EC2 Public IP
  • ELB DNS 이름(A레코드)





참조 :

https://www.slideshare.net/awskorea/3-operating-issue-solution-for-aws-customers

2020년 12월 15일 화요일

[Algorithm] 깊이/너비 우선탐색(DFS/BFS) [타겟넘버]

 

문제 ]

n개의 음이 아닌 정수가 있습니다.

이 수를 적절히 더하거나 빼서 타겟 넘버를 만들려고 합니다.

예를 들어 [1, 1, 1, 1, 1]로 숫자 3을 만들려면 다음 다섯 방법을 쓸 수 있습니다.


-1+1+1+1+1 = 3

+1-1+1+1+1 = 3

+1+1-1+1+1 = 3

+1+1+1-1+1 = 3

+1+1+1+1-1 = 3 


사용할 수 있는 숫자가 담긴 배열 numbers, 타겟 넘버 target이 매개변수로 주어질 때 숫자를 적절히 더하고 빼서 타겟 넘버를 만드는 방법의 수를 return 하도록 solution 함수를 작성해주세요.


[제한사항]

주어지는 숫자의 개수는 2개 이상 20개 이하입니다.

각 숫자는 1 이상 50 이하인 자연수입니다.

타겟 넘버는 1 이상 1000 이하인 자연수입니다.



DFS와 BFS ]


[ DFS ] 

깊이 우선 탐색(depth-first search)은 맹목적 탐색방법의 하나로, 노드가 트리일시 좌측노드를 기점으로 깊이 제한에 도달할 때 까지 목표노드를 찾는다. 제한 깊이까지 목표노드가 발견되지 않으면 최근노드의 부모노드로 되돌아가서 거치지 않은 다른 자식노드로 탐색을 시작한다. 여기서 부모노드로 되돌아오는 과정을 백트래킹(backtracking)이라고 한다.

















- 장점

  • 현 경로상의 노드들만을 기억하면 되므로 저장공간의 수요가 비교적 적다.
  • 목표노드가 깊은 단계에 있을 경우 해를 빨리 구할 수 있다.
- 단점
  • 해가 없는 경로에 깊이 빠질 가능성이 있다.
  • 얻어진 해가 최단 경로가 아닌 노드 전체를 탐색해야하는 경우가 있을 수 있다. 즉 최적의 방법으로 구한 해는 아닐 수 있다.




[ BFS ]

너비 우선 탐색(Breadth-first search)은 맹목적 탐색방법의 하나로, 루트 노드를 방문한 후 인접한 모든 노드들을 우선적으로 검색하는 방법이다. 더 이상 방문하지 않은 정점이 없을 때 까지 검색을 하는데 트리의 너비를 기준으로 검색한다.















- 장점

  • 출발 노드에서 목표노드까지의 최단 길이 경로를 보장한다.

- 단점

  • 너비경로가 길 경우 탐색가지가 증가함에 따라 보다 많은 기억 공간이 필요하다.



제출 DFS ] 

class Solution {
    public int solution(int[] numbers, int target) {
        int current = numbers[0];
        int answer = 0; 
        answer += dfs(current, 1, numbers, target);         
        answer += dfs(-current, 1, numbers, target); 
        
        return answer;
    }
    
    public int dfs(int prev, int index, int[] numbers, int target){
        
        if(index >= numbers.length){
            if(target==prev) return 1;
            return 0;
        }
        
        int cur1 = prev + numbers[index];
        int cur2 = prev - numbers[index];
        
        int ans = 0;
        ans += dfs(cur1, index+1, numbers, target);
        ans += dfs(cur2, index+1, numbers, target);
        return ans;
    }
}


풀이 DFS ]

재귀를 이용하여 DFS를 활용한다.





















제출 BFS ]

import java.util.Queue;
import java.util.LinkedList;

class Solution {
    
    class Pair{
        int cur;
        int index;
        
        Pair(int cur, int index){
            this.cur = cur;
            this.index = index;
        }
    }
    
    public int solution(int[] numbers, int target) {
        int answer = 0;
        Queue<Pair> queue = new LinkedList<Pair>();
        queue.offer(new Pair(numbers[0],0));
        queue.offer(new Pair(-numbers[0],0));
        
        while(!queue.isEmpty()){
            Pair p = queue.poll();
            if(p.index == numbers.length-1){
                if(p.cur == target){
                    answer += 1;
                }
                continue;
            }
            int c1 = p.cur + numbers[p.index+1];
            int c2 = p.cur - numbers[p.index+1];
            
            queue.add(new Pair(c1, p.index+1));
            queue.add(new Pair(c2, p.index+1));
        }
        return answer;
    }
    
}



풀이 ]


Queue는 인터페이스 형태로 LinkedList를 통해 생성한다. FIFO(Firtst in first out)이 큰 특징이다. 
Class Pair를 원소로 하는 Queue를 이용하여 너비우선탐색을 실행한다. Pair클래스는 깊이를 나타내는 index와 해당 값 (1 +1 +1 -1 ...)을 나타내는 cur를 변수로 가진다.
Index가 numbers길이와 같다면 cur를 target값과 비교해 일치하는지 확인하여 answer에 값을 +할지 안할지 결정한다. 

2020년 12월 14일 월요일

Apache Kafka - 토픽삭제가 안되는 경우


[ 토픽삭제 안되는 경우 ]


server.properties 파일의 delete.topic.enable=true임에도 

토픽이 삭제되지 않는경우


1. 카프카 dir.log파일과 관련 주키퍼로그 파일 삭제

2. 카프카 브로커를 재시작한다.


참조 

: https://stackoverflow.com/questions/23976670/when-how-does-a-topic-marked-for-deletion-get-finally-removed

: https://stackoverflow.com/questions/44564606/how-can-i-remove-kafka-topics-marked-for-deletion

2020년 12월 8일 화요일

Apache Phoenix [2] - CDH 피닉스 초기 설정 [Init Configuration]


CDH Phoenix 초기설정

1. HBase 서비스 탭

2. hbase-site.xml에 대한 HBase 서비스 고급 구성 스니펫(안전 밸브) 검색





[1] Secondary Index

피닉스의 Secondary Index를 사용하기 위해 설정값 추가

이름 : hbase.regionserver.wal.codec

값 : org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec


피닉스 인덱싱에 관한 참조 : 

https://phoenix.apache.org/secondary_indexing.html



[2] 사용자정의 함수 사용

사용자 정의함수를 사용하도록 다음 속성을 설정

이름 : phoenix.functions.allowUserDefinedFunctions

값 : true



[3] 양방향을 위한 컬럼 인코딩

피닉스쿼리서버로 데이터 저장시 HBase에선 인코딩된 값으로 보여지게 된다.

해당 설정을 통해 열 매핑을 사용하지 않으므로써 HBase에서 피닉스테이블 데이터를 인코딩되지 않은 값으로 볼 수 있다.

이름 : phoenix.default.column.encoded.bytes.attrib

값 : 0


피닉스 저장포맷 인코딩 참조 : 

https://phoenix.apache.org/columnencoding.html



[4] 스키마 생성을 위한 설정

쿼리서버 네임스페이스를 위한 스키마 생성과 삭제 등을 사용하기 위한 설정

이름 : phoenix.schema.isNamespaceMappingEnabled

값 : true 


이름 : phoenix.schema.mapSystemTablesToNamespace

값 : true


피닉스 Namespace Mapping 참조 : 

https://phoenix.apache.org/namspace_mapping.html



[5] 조인을 위한 설정

해쉬맵 조인, 서브쿼리 등 쿼리결과 사이즈를 정해주는 설정

설정 값보다 작으면 MaxServerCacheSizeExceededException오류, 기본값은 100MB

이름 : phoenix.query.maxServerCacheBytes

값 : 2097152000


피닉스 설정값 참조 : 

https://phoenix.apache.org/tuning.html




3. hbase-site.xml에 대한 HBase 클라이언트 고급 구성 스니펫(안전 밸브)

해당구성 값을 다른 xml에도 추가한다.

이름 : phoenix.schema.isNamespaceMappingEnabled

값 : true


이름 : phoenix.schema.mapSystemTablesToNamespace

값 : true




CDH Phoenix 설정방법 참고 : 

https://docs.cloudera.com/documentation/enterprise/6/latest/topics/phoenix_installation.html

https://docs.cloudera.com/documentation/enterprise/latest/topics/phoenix_mapping_schemas_namespaces.html

[Algorithm] 탐욕법 ( 체육복 )


문제 ]

문제 설명

점심시간에 도둑이 들어, 일부 학생이 체육복을 도난당했습니다. 다행히 여벌 체육복이 있는 학생이 이들에게 체육복을 빌려주려 합니다. 학생들의 번호는 체격 순으로 매겨져 있어, 바로 앞번호의 학생이나 바로 뒷번호의 학생에게만 체육복을 빌려줄 수 있습니다. 예를 들어, 4번 학생은 3번 학생이나 5번 학생에게만 체육복을 빌려줄 수 있습니다. 체육복이 없으면 수업을 들을 수 없기 때문에 체육복을 적절히 빌려 최대한 많은 학생이 체육수업을 들어야 합니다.

전체 학생의 수 n, 체육복을 도난당한 학생들의 번호가 담긴 배열 lost, 여벌의 체육복을 가져온 학생들의 번호가 담긴 배열 reserve가 매개변수로 주어질 때, 체육수업을 들을 수 있는 학생의 최댓값을 return 하도록 solution 함수를 작성해주세요.

제한 사항

  • 전체 학생의 수는 2명 이상 30명 이하입니다.
  • 체육복을 도난당한 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
  • 여벌의 체육복을 가져온 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
  • 여벌 체육복이 있는 학생만 다른 학생에게 체육복을 빌려줄 수 있습니다.
  • 여벌 체육복을 가져온 학생이 체육복을 도난당했을 수 있습니다. 이때 이 학생은 체육복을 하나만 도난당했다고 가정하며, 남은 체육복이 하나이기에 다른 학생에게는 체육복을 빌려줄 수 없습니다.


입출력 예

n    lost    reserve    return

5    [2,4]    [1,3,5]    5

5    [2,4]    [3]        4

3    [3]      [1]        2




제출 1]

import java.util.*;
class Solution {
    public int solution(int n, int[] lost, int[] reserve) {
        int answer = 0;
        
        List<Integer> reserve_list = new ArrayList<>(); 
        for (Integer t : reserve) { 
            reserve_list.add(t); 
        }
        
        for(int i=0; i<lost.length; i++){
            int idx = lost[i];
            int idx1 = 0;
            if(i+1 < lost.length) idx1 = lost[i+1];

            if(reserve_list.indexOf(idx)>-1){
                reserve_list.remove(reserve_list.indexOf(idx));
                continue;
            }

            else if(reserve_list.indexOf(idx-1)>-1){
                reserve_list.remove(reserve_list.indexOf(idx-1));
                continue;
            }

            else if(reserve_list.indexOf(idx+1)>-1){
                reserve_list.remove(reserve_list.indexOf(idx+1));
                continue;
            }
            else answer++;                
        }
        
        return n-answer;
    }
}


풀이 ]
문제 풀이를 위한 단계를 정리하자면,
1. 여벌학생들 중 잃어버린 학생이 있으면 reserve에서 삭제
2. 잃버학생들 앞번호에 여벌학생있으면 잃버학생 lost에서 삭제, 여벌학생 reserve에서 삭제
3. 잃버학생들 뒷번호에 여벌학생있으면 잃버학생 lost에서 삭제, 여벌학생 reserve에서 삭제
4. n - lost.len 으로 체육수업 듣는 학생수 계산
위 코드 제출시 테스트케이스 12번 오류가 발생한다. n=8, lost= [4,5] , reverse=[5,6] 인 경우에 lost [5]는 reserve[5]로 사전처리되어 lost[4]가 reserve[5]를 이용하지 못해야한다. 따라서 위의 코드는 n=8이 나온다.(정답 n=7)
즉 추가되는 예외처리는 "여분을 가져왔지만 체육복을 잃어버린사람은 체육복을 빌려주지 못한다"라는 조건이 추가되어야한다.



제출2 ]
import java.util.*;

class Solution {
    public int solution(int n, int[] lost, int[] reserve) {
        int[] people = new int[n];
        int answer = n;
       
        //여분을 가져옴과 동시에 잃어버린사람 선처리
        for (int l : lost)
            people[l-1]--;        
        for (int r : reserve) 
            people[r-1]++;
        //잃버사람, 앞뒤번호 여벌을 가져온사람에게 체육복 빌리기
        for (int i = 0; i < people.length; i++) {
            if(people[i] == -1) {
                if(i-1>=0 && people[i-1] == 1) {
                    people[i]++;
                    people[i-1]--;
                }else if(i+1< people.length && people[i+1] == 1) {
                    people[i]++;
                    people[i+1]--;
                }else 
                    answer--;
            }
        }
        return answer;
    }
}

2020년 12월 2일 수요일

[Algorithm] 완전탐색 (모의고사)


문제 ]

문제 설명

수포자는 수학을 포기한 사람의 준말입니다. 수포자 삼인방은 모의고사에 수학 문제를 전부 찍으려 합니다. 수포자는 1번 문제부터 마지막 문제까지 다음과 같이 찍습니다.

1번 수포자가 찍는 방식: 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ... 
2번 수포자가 찍는 방식: 2, 1, 2, 3, 2, 4, 2, 5, 2, 1, 2, 3, 2, 4, 2, 5, ... 
3번 수포자가 찍는 방식: 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, ...
1번 문제부터 마지막 문제까지의 정답이 순서대로 들은 배열 answers가 주어졌을 때, 가장 많은 문제를 맞힌 사람이 누구인지 배열에 담아 return 하도록 solution 함수를 작성해주세요.

제한 조건

  • 시험은 최대 10,000 문제로 구성되어있습니다.
  • 문제의 정답은 1, 2, 3, 4, 5중 하나입니다.
  • 가장 높은 점수를 받은 사람이 여럿일 경우, return하는 값을 오름차순 정렬해주세요.

입출력 예

answer : [1, 2, 3, 4, 5] / [1,3,2,4,2]

return : [1] / [1,2,3,]



제출 ]

import java.util.*;


class Solution {

    public int[] solution(int[] answers) {

        int[] n1_array = {1,2,3,4,5};

        int[] n2_array = {2,1,2,3,2,4,2,5};

        int[] n3_array = {3,3,1,1,2,2,4,4,5,5};

        int[] result = new int[3];

        int n1_len = n1_array.length;

        int n2_len = n2_array.length;

        int n3_len = n3_array.length;


        for(int i=0; i<answers.length; i++){

            if(answers[i] == n1_array[i%n1_len]) result[0]++;

            if(answers[i] == n2_array[i%n2_len]) result[1]++;

            if(answers[i] == n3_array[i%n3_len]) result[2]++;

        }


        // 가장 높은 점수 

        int max = Math.max(result[0], Math.max(result[1], result[2]));

        

        List<Integer> list = new ArrayList<>();

        if(max == result[0])

            list.add(1);

        if(max == result[1])

            list.add(2);

        if(max == result[2])

            list.add(3);

        

        int[] answer = new int[list.size()];

        int size=0;

        for(int i=0; i<list.size(); i++) answer[i] = list.get(i);

        

        return answer;

    }

}


풀이 ]

첫번째 for문만 잘 작성한다면 나머지는 문제될게 없다.

각 다른 길이의 배열을 동일한 인덱스 값으로 반복문을 돌 수 있게 만드는게  포인트이다.


- 수포자배열[ 인덱스 % 수포자배열길이 ]

수포자1 : 1  2  3  4  5  1  2  3  4  5  ....

수포자2 : 2  1  2  3  2  4  2  5  2  1  ....

수포자3 : 3  3  1  1  2  2  4  4  5  5  ....