이펙티브 코틀린(7)
7-1. 불필요한 객체 생성을 피하라
•
객체 생성은 언제나 비용이 발생한다.
•
불필요한 객체 생성을 피하는 것이 최적화의 관점에서 좋다
•
JVM 에서는 하나의 가상 머신에서 동일한 문자열을 처리하는 코드가 여러개 있다면, 기존의 문자열을 재사용한다
예시 코드
•
Interger, Long 과 같은 기본 자료형도 작은 경우에는 재사용된다
◦
Int 는 -128 ~ 127 범위를 캐시한다
예시 코드
•
nullable 타입은 int 자료형 대신 Integer 자료형으로 사용하게 강제된다
•
기본 자료형은 null 일 수 없고, 타입 아규먼트로도 사용할 수 없다
객체 생성 비용은 항상 클까?
•
객체는 더 많은 용량을 차지한다
◦
64비트 JDK 에서 객체는 8바이트의 배수만큼 공간을 차지한다
▪
32비트 JVM 에서는 8바이트를 사용한다
◦
일반적으로 레퍼런스는 -Xmx32G 까지는 32비트, 64비트 플랫폼 모두 4바이트를 사용한다
▪
64비트 플랫폼에서 32G(-Xmx32G) 부터는 8바이트이다
◦
기본 자료형 int 는 4바이트 이지만, 64비트 JDK 에 Wrap 되어 있는 Integer 는 16 바이트 이다
•
요소가 캡슐화되어 있다면, 접근에 추자거인 함수 호출이 필요하다
•
객체는 생성되어야 한다
◦
객체는 생성되고, 메모리 영역에 할당되고, 이에 해대한 레퍼런스를 만드는 등의 작업이 필요하다
객체 선언
•
매 순간 객체를 생성하지 않고, 생성된 객체를 재사용하는 방법 (싱글톤)
예시 코드
캐시를 활용하는 팩토리 함수
•
입란적으로 객체는 생성자를 사용해서 만들지만, 팩토리 메서드를 사용해서 만드는 경우도 있다
•
팩토리 함수는 캐시를 가질 수 있다
◦
팩토리 함수는 항상 같은 객체를 반환하게 만들 수 있다
예시 코드
•
모든 순수 함수는 캐싱을 활용할 수 있다
◦
이를 메모이제이션(memoization) 이라고 부른다
예시 코드
•
메모이제이션 단점
◦
캐시를 위한 Map 을 저장해야 하므로, 더 많은 메모리를 사용한다
◦
메모리가 필요할 때 가비지 컬렉터 (Garbage Collector, GC) 가 자동으로 메모리를 해제해주는 SoftReference 를 사용하면 좋다
•
SoftReference
◦
GC 가 값을 정리할수도 있고, 정리하지 않을수도 있다
◦
일반적인 VNm 구현의 경우, 메모리가 부족해서 추가로 필요한 경우에만 정리한다
◦
캐시를 만들 때는 SoftReference 를 사용하는 것이 좋다
•
WeakReference
◦
GC 가 값을 정리하는것을 막지 않는다
◦
다른 레퍼런스가 이를 사용하지 않으면 곧바로 제거한다
•
캐시는 언제나 메모리와 성능의 트레이드 오프가 발생하므로, 캐시를 잘 설계해야 한다
무거운 객체를 외부 스코프로 보내기
•
컬렉션 처리에서 이루어지는 무거운 연산은 컬렉션 처리 함수 내부에서 외부로 빼는 것이 좋다
예시 코드
예시 코드
지연 초기화
•
무거운 클래스를 만들 때는 지연되게 만드는 것이 좋을 때가 있다
•
지연 초기화는 함수를 호출할 때 처음 호출할 때 초기화 되기 때문에 실행 시간이 느려질 수 있다
•
지연 초기화는 상황에 맞게 사용해야 한다
기본 자료형 사용하기
•
JVM 은 숫자와 문자 등의 기본 요소를 나타내기 위해 특별한 기본 내장 자료형을 갖고 있다. ( 기본 자료형, primitives )
•
코틀린/JVM 컴파일러는 내부적으로 최대한 이러한 기본 자료형을 사용하지만, 2가시 아황에서는 기본 자료형을 Wrap 한 자료형이 사용된다
1.
nullable 타입을 연산할 때
2.
타입을 제네릭으로 사용할 때
코틀린의 자료형 | 자바의 자료형 |
Int | int |
Int? | Integer |
List<Int> | List<Integer> |
•
숫자와 관련된 연산은 어떤 형태의 자료형을 사용하나 성능적으로는 큰 차이가 없고, 큰 컬렉션을 처리할 때 차이를 확인할 수 있다
◦
코드와 라이브러리의 성능이 굉장히 중요한 부분에서만 이를 적용하면 된다
정리
•
성능이 중요한 코드에서 성능을 조금이라도 향상시킬 수 있는 방법이 있지만, 이러한 최적화는 큰 변경이 필요하거나 다른 코드에 문제를 일으킬 수 있기 때문에 최적화를 미루는 것도 방법이다
7-2. 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라
•
inline 한정자의 역할은 컴파일 시점에 함수를 호출하는 부분 을 함수의 본문 으로 대체하는 것이다
예시 코드
•
inline 한정자의 장점
◦
타입 아규먼트에 reified 한정자를 붙여서 사용할 수 있다
◦
함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작한다
◦
비지역(non-local) 반환을 사용할 수 있다
•
inline 한정자의 단점
◦
비용이 발생한다
타입 아규먼트를 reified 로 사용할 수 있다
•
JVM 바이트 코드에는 제네릭이 존재하지 않는다.
◦
따라서 컴파일을 하면, 제네릭 타입과 관련된 내용이 제거된다
예시 코드
•
함수를 인라인으로 만들면, 이러한 제한을 무시할 수 있다
◦
함수 호출이 본문으로 대체되므로, reified 한정자를 지정하면, 타입 파라미터를 사용한 부분이 타입 아규먼트로 대체된다
예시 코드
함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작한다
•
모든 함수는 inline 한정자를 붙이면 조금 더 빠르게 동작한다
◦
함수 호출과 반환을 위해 점프하는 과정과 백스택을 추적하는 과정이 없기 때문이다
•
하지만, 함수 파라미터를 가지지 않는 함수에서 이러한 차이가 큰 성능 차이를 발생시키지 않는다
◦
코틀린/JS 에서는 자바스크립트가 함수를 일급 객체로 처리하므로 굉장히 간단하게 변환이 이루어진다
◦
코틀린/JVM 에서는 JVM 익명 클래스 또는 일반 클래스를 기반으로 함수를 객체로 만들어낸다
예시 코드
◦
JVM 에서 아규먼트가 없는 함수 타입은 Function0 타입으로 변환된다
▪
()->Unit
•
컴파일: Function0<Unit>
▪
()->Int
•
컴파일: Function0<Int>
▪
(Int)->Int
•
컴파일: Function1<Int, Int>
▪
(Int, Int)→Int
•
컴파일: Function2<Int, Int, Int>
•
함수 리터럴 내부에서 지역 변수를 캡처해야 할 때 더 큰 차이가 발생한다
◦
인라인이 아닌 람다 표현식에서는 지역 변수 l 을 직접 사용할 수 없다
◦
l 은 컴파일 과정 중에 레퍼런스 객체로 래핑되고, 람다 표현식 내부에서 사용한다
예시 코드
•
일반적으로 함수 타입의 파라미터가 어떤 식으로 동작하는지 이해하기 어려우므로, 함수 타입 파라미터를 활용해서 유틸리티 함수를 만들 때는 그냥 인라인을 붙여 준다고 생각하는것도 좋다
비지역 리턴(non-local return) 을 사용할 수 있다
•
일반 함수는 내부에서 리턴을 사용할 수 없지만, 인라인 함수는 사용할 수 있다
예시 코드
inline 한정자의 비용
•
인라인 함수는 재귀적으로 동작할 수 없다
◦
재귀적으로 사용하게 되면, 무한하게 대체되는 문제가 발생한다
◦
인텔리제이가 오류로 잡아 주지 못하므로 굉장히 위험한 문제이다
예시 코드
•
인라인 함수는 더 많은 가시성 제한을 가진 요소를 사용할 수 없다
◦
public 인라인 함수 내부에서는 private, internal 가시성을 가진 함수와 프로퍼티를 사용할 수 없다
◦
인라인 함수는 구현을 숨길 수 없으므로, 클래스에 거의 사용되지 않는다
•
inline 한정자를 남용하면, 코드의 크기가 쉽게 커지게 된다
crossinline, noinline
•
crossinline
◦
아규먼트로 인라인 함수를 받지만, 비지역적 리턴을 하는 함수는 받을 수 없게 만든다
◦
인라인으로 만들지 않은 닫른 람다 표현식과 조합해서 사용할 때 문제가 발생하는 경우 활용한다
•
noinline
◦
아규먼트로 인라인 함수를 받을 수 없게 만든다
◦
인라인 함수가 아닌 함수를 아규먼트로 사용하고 싶을 때 활용한다
정리
•
인라인 함수가 사용되는 주요 사례
◦
print 함수처럼 매우 많이 사용되는 경우
◦
filterIsInstance ㅎ마수처럼 타입 아규먼트로 reified 타입을 전달받는 경우
◦
함수 타입 파라미터를 갖는 톱레벨 함수를 정의해야 하는 경우
•
API 를 정의할 때 인라인 함수를 사용하는 경우는 거의 없다
•
인라인 함수가 다른 인라인 함수를 호출하는 경우, 코드가 기하급수적으로 많아질 수 있으므로 조심해야 한다
7-3. 인라인 클래스의 사용을 고려하라
•
코틀린 1.3 버전부터는 하나의 값을 보유하는 객체도 inline 으로 만들 수 있게 되었다
•
기본 생성자 프로퍼티가 하나인 클래스 앞에 inline 을 붙이면, 해당 객체를 사용하는 위치가 모두 해당 프로퍼티로 교체된다
예시 코드
•
inline 클래스의 메서드는 모두 정적 메서드로 만들어진다
•
inline 클래스는 다른 자료형을 래핑해서 새로운 자료형을 만들 때 많이 사용된다
◦
이 때 어떠한 오버헤드도 발생하지 않는다
•
inline 클래스를 주로 사용하는 경우
◦
측정 단위를 표현할 때
◦
타입 오용으로 발생하는 문제를 막을 때
측정 단위를 표현할 때
•
타입에 제한을 걸면 제네릭 유형을 잘못 사용하는 문제를 줄일 수 있다
◦
코드를 더 효율적으로 만들려면 다음과 같이 같이 인라인 클래스를 활용한다
예시 코드
타입 오용으로 발생하는 문제를 막을 때
•
ID 는 일반적으로 단순한 숫자인데, 여러개의 ID 를 가지고 있을 때 잘못된 값을 넣을 수 있다.
•
이를 inline class 를 사용해서 제한을 걸 수 있다
예시 코드
인라인 클래스와 인터페이스
•
인라인 클래스도 다른 클래스와 마찬가지로 인터페이스를 구현할 수 있다
•
하지만, 클래스를 inline 으로 만들었을 때 얻을 수 있는 장점이 하나도 없다
◦
인터페이스를 통해서 타입을 나타내려면, 객체를 래핑해서 사용해야 하기 때문이다
예시 코드
typealias
•
typealias 를 사용하면, 타입에 새로운 이름을 붙여줄 수 있다
◦
typealias 는 길고 반복적으로 사용해야 할 때 많이 유용하게 사용한다
◦
하지만, typealias 는 안전하지 않다
•
단위 등을 표현하려면 인라인 클래스를 사용하는게 좋다
예시 코드
정리
•
인라인 클래스를 사용하면 성능적인 오버헤드 없이 타입을 래핑할 수 있다
•
인라인 클래스는 타입 시스템을 통해 실수로 코드를 잘못 작성하는 것을 막아주므로, 코드의 안정성을 향상시켜 준다
•
의미가 명확하지 않은 타입, 측정 단위들을 함께 사용하는 경우에는 인라인 클래스를 적극 활용해야 한다
7-4. 더 이상 사용하지 않는 객체의 레퍼런스를 제거하라
•
GC 가 자동으로 메모리 관리를 해주지만, 이를 완전히 무시해버리면 메모리 누수가 발생해서 상황에 따라 OutOfMemory (OOM) 이 발생할 수 있다
◦
더 이상 사용하지 않는 객체의 레퍼런스를 유지하면 안된다 라는 규칙 정도는 지켜주는 것이 좋다
◦
특히, 메모리를 많이 차지하거나, 인스턴스가 많이 생성될 경우에는 규칙을 꼭 지켜줘야 한다
•
객체에 대한 참조를 companion 또는 static 으로 유지해 버리면, GC 가 해당 객체에 대한 메모리 해제를 할 수 없다
•
객체를 정적으로 유지하지 않는 것이 가장 좋다
◦
의존 관계를 정적으로 저장하지 말고, 다른 방법을 활용해서 적절하게 관리하는 방법을 찾아야 한다
◦
객체에 대한 레퍼런스를 다른 곳에 저장할 때는 메모리 누수가 발생할 가능성을 언제나 염두해야 한다
•
객체를 더 이상 사용하지 않을 때, 레퍼런스에 null 을 설정하기만 하면 된다
◦
거의 사용되지 않는 객체까지 이런것을 신경 쓰는 것은 오히려 좋지 않을 수도 있다
▪
쓸데없는 최적화가 모든 악의 근원이다
◦
많은 변수를 캡처할 수 있는 함수 타입, Any 또는 제네릭 타입과 같은 미지의 클래스 일 때는 null 로 처리해주는것이 좋다
•
일반적인 규칙은 상태를 유지할 때는 메모리 관리를 염두에 두어야 한다는 것이다
◦
일반적으로 가독성이 좋은 코드는 메모리와 성능적으로도 좋다
◦
가독성이 좋지 않은 코드는 메모리와 CPU 리소스의 낭비를 숨기고 있을 가능성이 높다
◦
물론, 둘 사이에 트레이드 오프가 발생하는 경우도 있을 수 있다
▪
이럴 때는 일반적으로 가독성과 확장성을 더 중시하는 것이 좋다
▪
예외적으로 라이브러리를 구현할 때는 메모리와 성능이 더 중요하다
정리
•
일반적으로 메모리 누수가 발생하는 부분
◦
절대 사용되지 않는 객체를 캐시해서 저장해두는 경우
▪
이런 경우, SoftReference 를 사용하는것이 좋다
•
SoftReference 를 사용하면, 메모리가 필요한 경우에는 GC 가 알아서 이를 해제하기 때문이다
▪
어떠한 경우에는 WeakReference 를 사용하는 것이 좋을 수 있다
•
메모리 누수는 예측하기 어렵기 때문에, heap profiler 와 같은 도구를 이용해서 메모리 누수를 조사해보는것이 조다
•
객체를 수동으로 해제해야 하는 경우는 굉장히 드물다
◦
일반적으로 스코프를 벗어나면서, 어떤 객체를 가르키는 레퍼런스가 제거될 때 객체가 자동으로 해제되기 때문이다
•
따라서 메모리와 관련된 문제를 피하는 가장 좋은 방법은, 변수를 지역 스코프에 정의하고, 톱레벨 프로퍼티 또는 객체 선언으로 큰 데이터를 저장하지 않는것이다