Blog

이펙티브 코틀린(7)

이펙티브 코틀린(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 와 같은 도구를 이용해서 메모리 누수를 조사해보는것이 조다
객체를 수동으로 해제해야 하는 경우는 굉장히 드물다
일반적으로 스코프를 벗어나면서, 어떤 객체를 가르키는 레퍼런스가 제거될 때 객체가 자동으로 해제되기 때문이다
따라서 메모리와 관련된 문제를 피하는 가장 좋은 방법은, 변수를 지역 스코프에 정의하고, 톱레벨 프로퍼티 또는 객체 선언으로 큰 데이터를 저장하지 않는것이다