[9/20] 배열
글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Arrays를 한글로 요약 정리한 글입니다.
배열 (Arrays)
이번 장에서는 줄리아에서 가장 유용한 데이터 타입인 배열(Arrays)을 살펴볼 것이다. 또한 한 객체에 둘 이상의 이름이 있을 때 발생할 수 있는 작업에 대해서도 알아볼 것이다.
배열은 시퀀스이다
문자열처럼 배열은 일련의 값들이다. 문자열에서는 값들이 모두 문자이지만, 배열에서는 어떤 데이터 타입이든 값이 될 수 있다. 배열에서 값은 요소(element or item)라고 한다.
배열을 만드는 몇 가지의 방법이 있다. 가장 간단한 방법은 대괄호[]
로 요소들을 묶는 것이다.
1 |
|
위의 첫 번째 예시는 4개의 정수들을 요소로 한 배열이며, 두 번째는 3개의 문자열들을 요소로 한 배열이이다. 배열의 요소들이 모두 동일한 타입일 필요는 없다. 아래의 배열은 문자열과 소수, 정수, 그리고 또 다른 배열이 요소로 구성되어 있다.
1 |
|
위의 예시처럼 배열은 다른 배열을 중첩하여 포함할 수 있다.
요소들을 포함하지 않은 배열은 빈 배열(empty array)이라고 부른다. 빈 배열은 대괄호[]
만 입력해서 만들 수 있다.
예상했겠지만, 배열들은 변수의 값으로 사용할 수 있다.
1 |
|
typeof()
는 배열의 종류를 찾는 데 사용된다.
1 |
|
배열의 종류는 중괄호로 표시되어 출력되며, 앞에는 요소들의 데이터 타입을 보여준다. 뒤에 나오는 숫자의 의미는 배열의 차원이다. 또한 빈 배열은 Any
라는 데이터 타입을 가지고 있다. 즉, 모든 값들은 데이터 타입을 가질 수 있는 것이다.
배열은 변할 수 있다
배열의 요소에 접근하는 문법은 문자열에서 문자에 접근하는 문법과 동일하게 []
를 사용한다. 표현식은 []
에 특정 인덱스를 넣어서 호출한다. 인덱스는 1부터 시작한다는 것을 기억해라
1 |
|
문자열과 같이 배열들 또한 변할 수 있다. 대괄호 연산자가 할당문 왼쪽에 쓰인다면, 대괄호가 지정한 인덱스에 해당하는 배열 요소의 값이 할당된다.
1 |
|
위의 코드에서는 배열의 두 번째 값이 5로 재할당되었다.
배열 인덱스들은 문자열 인덱스들과 같은 방식으로 작동한다.
- 어떤 정수 표현식이나 인덱스로 사용할 수 있다.
- 만약 존재하지 않는 요소를 읽거나 사용하려고 한다면,
BoundsError
가 발생한다. - 키워드
end
는 배열의 마지막 인덱스를 가져온다.
∈
연산자 또한 배열에서 작동한다.
1 |
|
배열 순회 (Traversing an Array)
배열 요소들을 순회하는 가장 보편적인 방법은 for
루프를 사용하는 것이다. 문법은 문자열과 동일하다.
1 |
|
오로지 배열 요소들을 읽기만 하는 것은 위의 코드로 충분하다. 그러나 만약 요소를 다시 쓰거나 업데이트하기를 원한다면, 인덱스가 필요하다. 이를 해결하는 가장 일반적인 방법은 내장 함수인 eachindex()
를 사용하는 것이다.
1 |
|
위의 루프는 배열을 순회하여 요소들을 업데이트한다. length()
는 배열에서 요소들의 길이를 반환한다. 루프가 한 번씩 작동할 때마다 i
는 다음 요소의 인덱스로 갱신된다. 본문에서의 할당문은 i
를 사용하여 요소의 이전 값을 가져온 후 새로운 값으로 재할당한다.
아래의 for
루프는 절대 본문이 실행되지 않는다.
1 |
|
한 배열이 다른 배열을 포함하고 있을 때, 내부의 중첩 배열은 하나의 요소로 인식된다. 따라서 아래의 배열은 길이가 4이다.
1 |
|
배열 슬라이스 (Array Slices)
슬라이스 연산자 또한 배열에서 작동한다.
1 |
|
슬라이스 연산자인 [:]
는 전체 배열의 복사본을 가져온다.
1 |
|
배열은 가변적이기 때문에, 배열을 변경하기 전에 복사본을 만드는 방법은 종종 유용하게 사용된다.
변수 왼쪽에 사용된 슬라이스 연산자는 여러 개의 요소를 업데이트할 수 있다.
1 |
|
배열 라이브러리
줄리아는 배열에서 작동하는 함수들을 제공한다. 예를 들어, push!()
는 배열 끝에 새로운 요소를 추가한다.
1 |
|
append!()
는 첫 번째 인수 배열 끝에 두 번째 인수인 배열을 추가한다.
1 |
|
위의 예시에서 t2
는 수정되지 않은 채로 유지된다.
sort!()
는 낮은 단위에서 높은 단위 순으로 배열 요소들을 정리한다.
1 |
|
sort()
는 배열 요소들을 순서에 맞게 정리한 복사본을 반환한다.
1 |
|
sort()
와 sort!()
의 차이점은 배열이 영구적으로 변하는지에 있다. 쉽게 이야기하면, sort!()
를 사용하면 배열의 순서가 영구적으로 변하지만, sort()
를 사용하면 기존 배열은 변하지 않고 순서가 변경된 배열만 보여준다.
Note 줄리아에서 사용되는 !
는 인수를 변경하기 위한 함수 뒤에 추가한다.
맵, 필터 그리고 리듀스 (Map, Filter and Reduce)
배열에 모든 숫자들을 더하기 위해서는 아래의 루프를 사용하면 된다.
1 |
|
total
은 0으로 시작한다. 각 루프가 실행될 때마다 +=
는 배열로부터 한 개의 요소를 얻는다. +=
연산자는 변수를 업데이트하는 간단한 방법을 제공한다. 아래의 코드는 '증강 할당문(augmented assignment statement)'이며,
1 |
|
위의 코드는 아래의 코드와 같은 의미이다.
1 |
|
루프가 실행될 때, total
은 요소들의 합을 누적한다. 이런 방식으로 사용되는 변수를 '누적 계산기(accumulator)'라고 부른다.
배열 요소를 합산하는 것은 줄리아가 제공하는 내장 함수인 sum()
을 사용하면 된다.
1 |
|
시퀀스로 연결되어 있는 요소들을 하나의 요소로 결합하는 작업을 리듀스 작업(reduce operation)이라고 한다.
다른 배열을 만들기 위해서 한 배열을 순회하는 경우도 있다. 예를 들어, 아래의 함수는 기존 문자열들의 대문자를 반환하여 새로운 배열을 만든다.
1 |
|
res
는 빈 배열을 만든다. 루프가 매번 실행될 때마다, res
에 요소들을 추가한다. 따라서 res
도 누적 계산기로 봐도 상관없다.
capitalizeall()
와 같은 작동방식은 가끔 맵(map)이라고 불린다. 왜냐하면 uppercase()
처럼 순서대로 각 요소를 찾아 매핑(mapping)하는 함수가 있기 때문이다.
또다른 일반적인 작동방식은 배열에서 일부 요소를 선택하여 '하위 배열(subarray)'을 반환하는 것이다. 예를 들어 아래의 함수는 문자열 배열을 가져와서 대문자만 포함하는 하위 배열을 반환한다.
1 |
|
위의 예시처럼 조건에 적합한 요소만을 선택하여 가져오는 onlyupper()
과 같은 작동방식을 필터(filter)라고 한다.
가장 일반적인 배열 작동방식은 맵, 필터, 리듀스의 결합으로서 표현된다.
도트 문법(Dot Syntax)
^
와 같은 모든 이항 연산자(binary operator)에는 대응하는 도트 연산자가 있다. 아래의 코드는 요소마다 ^
연산자가 자동으로 실행되도록 정의하였다. [1, 2, 3] .^ 3
는 자체적으로 정의되지 않았지만, 컴퓨터는 뒤의 .^ 3
가 앞의[1, 2, 3]
에 각각 적용되어 [1^3, 2^3, 3^3]
의 결과를 도출한다.
1 |
|
모든 줄리아 함수는 어떤 배열에서든 도트 문법을 사용하여 요소별로 적용할 수 있다 예를 들어, 문자열 배열을 루프를 사용하지 않고도 대문자로 표시할 수 있다.
1 |
|
이것은 더 깔끔하게 맵을 만드는 방법이다. capitalizeall()
를 한 줄로 구현할 수 있다.
1 |
|
요소 삭제와 삽입
배열에서 요소를 삭제하는 방법은 여러 개가 있다. 만약 삭제하고 싶은 요소의 인덱스를 알고 있다면, splice!()
를 사용하면 된다.
1 |
|
splice!()
는 배열을 수정하고 해당 요소가 제거된 배열을 반환한다.
pop!()
은 마지막 요소를 제거한 후 반환한다.
1 |
|
popfirst!()
은 첫 번째 요소를 제거한 후 반환한다.
1 |
|
pushfirst!()
와 push!()
는 배열의 시작과 끝에 각각 요소를 삽입한다.
만약 제거된 요소가 필요없다면, deleteat!()
을 사용할 수 있다.
1 |
|
insert!()
는 주어진 인덱스에 요소를 삽입한다.
1 |
|
배열과 문자열
문자열을 문자의 나열이고 배열은 값들의 나열이지만, 문자 배열과 문자열은 다르다. collect()
를 사용하면 문자열에서 문자 배열로 변환할 수 있다.
1 |
|
collect()
는 문자열을 깨고 문자들을 각각의 요소로 변경해준다.
만약 문장을 단어 단위로 쪼개고 싶다면, split()
를 사용하면 된다.
1 |
|
split()
에서 구분 기호 등을 추가적 인수로 사용하면 구분 기호에 따라서 단어로 나눠준다. 아래의 예시는 하이픈(-)기호를 추가 인수로 사용하였다.
1 |
|
join()
은 split()
의 반대이다. 단어들 사이에 구분자를 넣고 싶다면 join()
을 사용하면 된다.
1 |
|
위의 예시에서는 구분 기호로 공백을 사용하였다. 만약 공백 없이 단어를 연결하고 싶다면 구분 기호를 인수로 추가하지 않으면 된다.
객체와 값
객체(object)는 변수가 참고할 수 있는 어떤 것이다. 지금까지는 객체와 값을 동일시하여 사용할 수 있었다.
아래의 할당문을 실행해보자.
1 |
|
위의 예시는 a
와 b
가 동일한 문자열을 가지고 있다는 것을 보여준다. 하지만 컴퓨터도 동일한 문자열로 인식하는지, 아니면 각각 다른 문자열로 인식하는지는 알 수 없다.

첫 번째의 경우는 동일한 값의 다른 객체를 a
와 b
가 가지고 있는 경우이며, 두 번째는 a
와 b
가 동일한 객체를 가지고 있는 경우이다.
두 변수가 동일한 객체를 가지고 있는지 확인하려면, ===
또는 ≡
(\equiv TAB
)연산자를 사용하면 된다.
1 |
|
해당 예시에서는 줄리아가 오로지 하나의 문자열 객체를 만들었다. 하지만 배열의 경우에는 각각의 배열 객체들이 생성된다.
1 |
|
==
연산자는 불 연산자로 두 변수의 값이 같은지의 여부를 ture
또는 false
로 반환한다. 위의 예시를 보면 a
와 b
의 값은 같기 때문에 ==
연산자의 결과는 ture
를 반환하지만, 객체가 동일한지 판단하는 ===
연산자의 결과는 false
를 반환한다.

위의 예시에서 두 배열은 동일한 요소들을 가지고 있기 때문에 두 객체가 같다(same)고 설명할 수 있지만, 두 객체가 동일(identical)하다고 말할 수는 없다. 즉, 두 객체가 동일하다면 같다고 말할 수 있지만, 두 객체가 같다고 해서 동일하다고 할 수는 없다.
정확하게 설명하면 객체는 값을 가지고 있다. 예로 [1,2,3]
는 값이 정수 시퀀스인 배열 객체를 얻는다. 만약 다른 배열에 동일한 요소가 있다면, 값은 같지만 동일한 객체라고 가정하지는 않는다.
에일리어싱 (Aliasing)
객체를 가진a
로 b=a
를 통해 b
를 할당한다면, 두 변수는 같은 객체를 가진다.
1 |
|

변수와 객체와의 연관성을 레퍼런스(reference)라고 한다. 이 예에서는 동일한 객체에 대한 두 개의 레퍼런스가 있다.
한 개 이상의 레퍼런스를 가진 객체는 둘 이상의 이름을 가지게 되므로 그 객체는 에일리어싱되었다고 설명한다.
에일리어싱된 객체의 값이 변하는 경우, 그 변화는 연결된 변수들에게도 적용된다.
1 |
|
WARNING 위의 방식은 유용하게 사용되지만, 동시에 오류가 발생하기 쉽다. 일반적으로 변경 가능한 객체로 작업할 때에는 에일리어싱을 피하는 것이 더 안전하다.
문자열과 같이 변경 불가능한 객체의 경우에는 에일리어싱이 큰 문제가 되지 않는다. 아래의 예시를 보자.
1 |
|
위의 예사에서는 a
와 b
가 동일한 객체를 가지는지 아닌지에 대한 여부는 큰 차이가 없다는 것을 볼 수 있다.
배열 인수
배열을 함수에 전달하면, 함수는 배열을 참조한다. 그래서 함수가 배열을 수정하면 사용자에게 변경된 내용이 표시된다. 예를 들어 deletehead!()
를 사용하여 첫 번째 요소를 삭제해보자.
1 |
|
실행 흐름을 확인해보면 인수로 입력된 배열을 가져와서 popfirst!()
로 첫 번째 요소를 삭제한다. !
가 입력된 함수는 기존의 배열을 수정한다.
1 |
|
매개변수 t
와 변수 letters
는 객체의 에일리어스이다. 아래의 그림에서 확인해보자.

배열을 수정하는 작업과 새 배열을 만드는 작업을 구분하는 것은 매우 중요하다. 예를들어 push!()
는 배열을 수정하지만 vcat()
은 새로운 배열을 만든다.
1 |
|
t2
는 t1
의 에일리어스이다.
아래의 코드는 vcat()
의 예시이다.
1 |
|
vcat()
은 t3
라는 새로운 배열을 만들었으며, t1
은 변하지 않았다.
이런 차이점은 배열을 수정해야 하는 함수를 작성할 때 매우 중요하다.
예를 들어, 아래의 함수는 배열의 앞부분(head)을 삭제하지 않는다.
1 |
|
슬라이스 연산자는 새 배열을 만들고 할당 연산자는 해당 배열을 참조하여 새 배열의 값을 정한다. 하지만 이것들은 호출된 배열에 영향을 주지는 않는다.
1 |
|
baddeletehead()
의 시작부분에서 t4
와 t3
는 동일한 배열을 나타낸다. 마지막에는 t4
는 새 배열을 나타내지만 t3
는 이전 배열을 그대로 유지하고 있다.
대안은 새로운 배열을 생성하고 반환하는 함수를 작성하는 것이다. 예를 들어 tail()
은 배열의 첫 번째 요소를 제외한 모든 요소를 반환한다.
1 |
|
위의 함수는 기존의 배열을 수정하지 않은 채로 유지한다.
1 |
|
디버깅
배열 및 기타 변경 가능한 객체 등을 주의해서 사용하지 않으면 오랜 시간 디버깅 할 수 있다. 일반적인 함정을 피하는 방법은 아래와 같다.
- 대부분의 배열 함수들은 인수를 수정한다. 이는 문자열 함수가 기존 문자열을 그대로 두고 새로운 문자열을 반환하는 것과 정확히 반대이다.
문자열 코드를 작성하는 데 익숙한 경우에는
1
new_word = strip(word)
다음과 같이 배열 코드도 작성하려고 한다.
1
t2 = sort!(t1)
sort!()
은 수정된 원래 t1
을 반환하고 t2
는 t1
의 에일리언스이다. 즉, sort!()
를 사용할 때는 원본이 수정되기 때문에 새로 할당할 필요가 없다는 것이다.
Tip 배열 함수와 연산자를 사용하기 전에 설명서를 주의해서 읽고 대화식 모드에서 테스트를 해보는 것이 좋다.
- 관용구를 선택해서 붙여라 배열과 관련된 문제 중 하나는 작업을 수행하는 방법이 너무 많다는 것이다. 예를 들어 배열에서 요소를 제거하려면
pop!
,popfirst!
,delete_at
또는 슬라이스 할당을 사용할 수도 있다. 요소를 추가하는 것은push!
,pushfirst!
,insert!
또는vcat
을 사용한다.t
가 배열이고x
가 배열 요소라고 가정하여 배열에 추가한다면 아래의 코드와 같다.
1
2
3insert!(t, 4, x)
push!(t, x)
append!(t, [x])
그리고 아래는 올바르지 못한 코드이다.
1
2
3insert!(t, 4, [x]) # WRONG!
push!(t, [x]) # WRONG!
vcat(t, [x]) # WRONG!
- 에일리언싱을 피하기 위해 복사본을 만들어라
sort!
와 같은 함수를 사용하려면 인수를 수정하지만 원래 배열도 가지고 있어야 한다. 복사본을 만들어서 가지고 있는 것이 좋다.
1
2
3
4
5
6
7
8
9
10julia> t = [3, 1, 2];
julia> t2 = t[:]; # t2 = copy(t)
julia> sort!(t2);
julia> print(t)
[3, 1, 2]
julia> print(t2)
[1, 2, 3]
위의 예시에서는 내장 함수인 기존의 배열은 유지하며 새로운 배열로 반환하는 sort
를 사용하는 것이 좋다.
1
2
3
4
5
6julia> t2 = sort(t);
julia> println(t)
[3, 1, 2]
julia> println(t2)
[1, 2, 3]