꾸준한 스터디
article thumbnail

자바는 안전한 언어다.

네이티브 메서드를 사용하지 않으니 C, C++ 같이 안전하지 않은 언어에서 흔히 보이는 버퍼오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다. 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다. 메모리 전체를 하나의 거대한 배열로 다루는 언어에서는 누릴 수 없는 강점이다.

 

버퍼 오버플로(Buffer Overflow) 또는 버퍼 오버런(Buffer Overrun)
메모리를 다루는데 오류가 발생하여 잘못된 동작을 하는 프로그램 취약점
프로세스가 데이터를 버퍼에 저장할 때 프로그래머가 지정한 곳 바깥에 저장하는 것을 의미한다.
벗어난 데이터는 인접 메모리를 덮어 쓰게 되며 이 때 다른 데이터가 포함되어 있을 수도 있는데, 손상을 받을 수 있는 데이터는 프로그램 변수와 프로그램 흐름 제어 데이터도 포함된다.
이로 인해 잘못된 프로그램 거동이 나타날 수 있으며, 메모리 접근 오류, 잘못된 결과, 프로그램 종료 또는 시스템 보안누설이 발생할 수 있다.
버퍼 오버플로는 보통 데이터를 저장하는 과정에서 그 데이터를 저장할 메모리 위치가 유효한지를 검사하지 않아 발생한다. 이러한 경우 데이터가 담긴 위치 근처에 있는 값이 손상되고 그 손상이 프로그램 실행에 영향을 미칠 수도 있다.
흔히 버퍼 오버플로와 관련되는 프로그래밍 언어는 C와 C++로, 어떤 영역의 메모리에서도 내장된 데이터 접근 또는 덮어쓰기 보호 기능을 제공하지 않으며 어떤 배열에 기록되는 데이터가 그 배열의 범위 안에 포함되는지 자동으로 검사하지 않는다.
배열 오버런(Array Overrun)
배열의 인덱스를 벗어나는 위치에 접근하는 경우 발생한다.
일반적으로 배열은 메모리상에 연속적으로 할당되며, 각 요소는 인덱스를 사용하여 접근한다.
배열 오버런은 이러한 인덱스를 벗어나 다른 메모리 영역을 침범하거나 잘못된 값을 읽거나 쓸수 있는 상황을 의미한다.
일부 언어는 해당 인덱스에 접근할 때 예외를 발생시키거나 런타임 오류를 출력하는 등의 방식으로 오류를 처리하는데 다른 언어에서는 이러한 오류를 명시적으로 확인하기 어려울 수 있으며, 이 경우 메모리 침범이 발생할 수 있다.
자바에서는 배열의 인덱스를 철저히 검사하고 유효한 인덱스 범위를 벗어나는 접근을 방지한다.
자바의 배열은 크기가 고정되어 있으며, 인덱스는 0부터 시작해서 배열의 크기보다 작은 값을 가져야 한다.
만약 잘못된 인덱스에 접근하려고 하면 ArrayIndexOutOfBoundsException이라는 예외를 발생한다.
와일드 포인터
포인터 변수가 초기화되지 않거나 잘못된 주소를 가리키는 상태를 말한다.
이런 상태의 포인터를 사용하면 예기치 않은 동작이 발생할 수 있다.
일반적으로 포인터는 메모리 주소를 저장하고, 해당 주소에 위치한 데이터를 참조하는데 사용된다. 하지만 와일드 포인터는 유효하지 않은 메모리 주소를 가리키거나 아무런 주소를 가리키지 않은 상태인 경우를 의미한다. 이로인해 프로그램이 비정상적으로 동작하거나 충돌이 발생할 수 있다.
자바는 와일드 포인터와 같은 개념을 가지고 있지 않고 주로 C와 C++ 같은 저수준 언어에서 발생하는 문제이다.
또한 메모리 할당과 해제를 개발자가 직접 관리할 필요가 없고 메모리 관리는 가비지 컬렉터에 의해 자동으로 처리된다.

 

하지만 아무리 자바라고 해도 다른 클래스로부터 침범을 아무런 노력없이 다 막을 수 있는건 아니다.

그러니 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야 한다.

 

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다.

하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.

예컨대 기간(period)을 표현하는 다음 클래스는 한번 값이 정해지면 변하지 않도록 할 생각이었다.

 

얼핏 이 클래스는 불변처럼 보이고, 시작 시각이 종료 시각보다 늦을 수 없다는 불변식이 무리없이 지켜질 것 같다. 하지만 Date가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.

 

Period를 변경해보기위한 객체

 

Period의 상태가 변경되었다.

 

자바 8 이후라면 Date 대신 불변 Instant를 사용하면 된다. (LocalDateTime이나 ZonedDateTime을 사용해도 된다.)

Date는 낡은 API이니 새로운 코드를 작성할 때는 더이상 사용하면 안된다.

 

외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다. Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.

 

Period 생성자 수정
값이 변경되지 않았다

 

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.

순서가 부자연스러워 보여도 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.

컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다.

 

방어적 복사에 Date의 clone 메서드를 사용하지 않은 점에도 주목하자.

Date는 final이 아니므로 clone이 Date가 정의한게 아닐 수 있다. 즉 clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. 예컨대 이 하위 클래스는 start와 end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수 있다. 결국 공격자에게 Period 인스턴스 자체를 송두리째 맡기는 꼴이된다.

이런 공격을 막기 위해 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

 

 

이번엔 접근자 메서드가 내부의 가변정보를 직접 드러내기 때문에 다시 값이 변경되었다.

 

두번째 공격을 막아내려면 단순히 접근자가 가변필드의 방어적 복사본을 반환하면 된다.

새로운 접근자 까지 갖추면 Period는 완벽한 불변으로 거듭난다.

Period 자신 말고는 가변 필드에 접근할 방법이 없으니 확실하다. 모든 필드가 객체 안에 완벽하게 캡슐화되었다.

생성자와달리 접근자 메서드에서는 신뢰할 수 없는 하위 클래스가 아닌 Period가 가지고 있는 java.util.Date객체가 확실하니 방어적 복사에 clone을 사용해도 된다.

 

하지만 아이템 13에서 설명한 clone 재정의에 주의하라는 이유 때문에 인스턴스를 복사하는데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.

 

매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다.

메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지 생각해야 한다.

변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지 따져보라.

확신할 수 없다면 복사본을 만들어 저장해야한다.

 

내부객체를 클라이언트에 건네주기 전에 방어적 복사본을 만드는 이유도 마찬가지이다.

클래스가 불변이든 가변이든. 가변인 내부 객체를 클라이언트에 반환할 때는 반드시 심사숙고 해야한다.

안심할 수 없다면 방어적 복사본을 반환해야 한다.

길이가 1 이상인 배열은 무조건 가변임을 잊지 말자

 

방어적 복사는 성능 저하가 따르고 또 항상 쓸 수 있는 것도 아니다.

호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다. 이러한 상황이라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는게 좋다.

 

다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로  복사해 저장해야 하는 것은 아니다.

때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.

이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다. 클라이언트가 건네주는 가변 객체의 통제권을 넘겨받는다고 기대하는 메서드나 생성자에서도 그 사실을 확실히 문서에 기재해야한다.

 

 

핵심정리

클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.

 

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!