주안점
자바에서 출발한 컬렉션은 코틀린으로 오며 왜 지금의 형태를 띄게 되었는가?
자바 컬렉션
- 자바 컬렉션의 가변성은 처음에는 혁신적인 무기였음.
- 자바 컬렉션은 극단적 가변 컬렉션
- 컬렉션을 자유롭게 변경시켜 이용하며,
sort
,reverse
와 같은 메서드들은 이를 편하게 만들어 도왔음.
- 컬렉션을 자유롭게 변경시켜 이용하며,
가변 때문에 만나게 된 에러
public static int sufferScoreFor(List<Journey> route) {
Location start = getDepartsFrom(route);
List<Journey> longestJourneys = longestJourneysIn(route, 3);
return sufferScore(longestJourneys, start);
}
start
변수를 따로 빼둔 것이 별다른 정보를 알려주지 않기 때문에 inline 형태로 변경하고 싶었다.
public static int sufferScoreFor(List<Journey> route) {
List<Journey> longestJourneys = longestJourneysIn(route, 3);
return sufferScore(longestJourneys, getDepartsFrom(route));
}
- 분명 테스트는 통과했는데, 리팩토링 후부터 알 수 없는 버그가 발생한다.
public static List<Journey> longestJourneysIn(
List<Journey> journeys,
int limit
) {
journeys.sort(comparing(Journey::getDuration).reversed()); // <1>
var actualLimit = Math.min(journeys.size(), limit);
return journeys.subList(0, actualLimit);
}
journeys.sort()
가 문제를 일으키는 것을 알아냈다.List.sort()
는 내부적으로Arrays.sort()
메서드를 사용하며, 이는 컬렉션 객체를 변경한다.
- 처음 리팩토링할 당시엔 중간에 컬렉션이 변경될 것이란 예측을 하지 못했으므로, 이러한 버그를 만들어냈다.
가변 컬렉션은 때때로 우리에게 예상치 못한 버그를 안겨준다.
현대 개발자들의 해결책
Collections.unmodifiableList()
와 같은 메서드를 통해 컬렉션을 불변으로 만든다.- 가변 메서드를 사용하면
UnsupportedOperationException
을 던진다.
- 가변 메서드를 사용하면
- 불변 컬렉션을 사용하면, 버그가 있을 때 더 빨리 눈치챌 수 있다.
- 단, 매번 방어적으로 불변 컬렉션을 다시 생성하는 것은 매우 귀찮다.
공유된 컬렉션을 변경하지 말라
- 격리된 코드와 컬렉션을 공유하는 경우, 불변 컬렉션을 사용하라.
- 컬렉션을 파라미터로 넘기거나, 컬렉션을 반환하거나 하는 등의 작업에 가변 컬렉션 사용을 지양하라.
- 생성하되 변경하지 마라
어쩔 수 없이 가변 컬렉션을 공유해야 한다면, 공유 범위를 최대한 제한하라.
불변 컬렉션을 사용하는 데 드는 비용이 물론 있지만, 알아내기 쉽지 않은 미묘한 버그를 만들어 디버깅하는 시간에 비하면 훨씬 저렴하다.
언어별 컬렉션 차이
- 자바: 가변 컬렉션이 새로운 기술인 시대에 가변 컬렉션을 설계함.
- 스칼라: 영속적인 불변 컬렉션이지만, 데이터 공유를 통해 성능을 향상시키는 컬렉션을 설계함.
- 코틀린: 자바의 컬렉션에서 상태를 변경하는 메서드를 제거함.
- 상태 변경을 하고 싶다면,
List
가 아닌,MutableList
,MutableCollection
과 같은 클래스를 이용해야 함
- 상태 변경을 하고 싶다면,
코틀린이 가변 컬렉션 문제를 해결하는 방법
- 코틀린의 불변
List
인터페이스를 통해java.util.List
를 받아낼 수 있음.- 원한다면,
kotlin.collections.MutableList
로 취급하는 것도 가능은 함.
- 원한다면,
코틀린식 문제 해결 방법의 불완전성
- 다만,
MutableList
를List
로 변경해도list[0] = "xxx"
와 같은 형식으로 리스트의 내용 변경이 가능하므로 이 해결방법은 불완전하다. List
는 불변이 아닌read-only
즉, 읽기 전용의 개념 인터페이스이다. 우회적인 방법으로 값 수정이 일어날 수 있다.- 결국, 해법은 불변 컬렉션과 가변 컬렉션 사이의 하위 타입 관계를 아예 없애는 것이다.
- ex)
String
과StringBuilder
처럼 빌더 형식을 이용해 분리할 수 있다.
코틀린이 불완전한 해결 방법을 사용하는 이유
- 극단적 실용주의 언어이기 때문이다.
- 약간의 불완전성을 포용함으로써, 얻는 이익이 훨신 크기 때문이다.
- 자바와의 상호 운용성을 가져가며, 적당한 범위 내에서 안전하다.
코틀린 표준 라이브러리의 예시
inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
val result = ArrayList<R>()
for (item in this)
result.add(transform(item))
return result
}
- 위
map()
메서드의 결과는 사실 가변 컬렉션이다. 그러나, 반환 타입 인터페이스를 통해 겉으론 불변처럼 보이게 한다. - 이런 방식을 통하면, 컬렉션의 내용물이 변경되지 않을지 염려할 필요는 없어진다.
자바에서 코틀린 컬렉션으로 리팩토링하는 방법
- 공유된 컬렉션을 변경하지 말라.
컬렉션 불변성 지키기: sorted
메서드 작성하기
public static List<Journey> longestJourneysIn(
List<Journey> journeys,
int limit
) {
journeys.sort(comparing(Journey::getDuration).reversed()); // <1>
var actualLimit = Math.min(journeys.size(), limit);
return journeys.subList(0, actualLimit);
}
- 파라미터로 받은
journeys
컬렉션을 변경하고 있다. longestJourneysIn()
을 한번 수행하고 나면, 외부의 컬렉션이 변화한다.
@SuppressWarning("unchecked")
public static <E> List<E> sorted(
Collection<E> collection,
Comparator<? super E> by
) {
var result = (E[]) collection.toArray();
Arrays.sort(result, by);
return Arrays.asList(result);
}
List
를 복사하고, 정렬하고, 반환하는 메서드를 하나 작성한다.- 이 메서드가 원본을 건드리지 않는 다는 것은 어떻게 확신할까?
toArray()
메서드의 설명엔 이 메서드를 호출한 사람은 자유롭게 반환된 배열을 변경해도 된다는 설명문이 있으니 안심하자. 원본에 영향을 미치지 않는다는 뜻이다.- The returned array will be "safe" in that no references to it are maintained by this list. (In other words, this method must allocate a new array). The caller is thus free to modify the returned array.
asList(array)
에 의해 생성된 리스트는 고정 길이를 갖기 때문에 원소를 추가하거나 삭제할 수 없다.
생성 시에 변경을 받아들이되, 외부에 대해서는 변경을 허용하지 마라.
sorted()
이용하기: longestJourneysIn
리팩토링
public static List<Journey> longestJourneysIn(
List<Journey> journeys,
int limit
) {
var actualLimit = Math.min(journeys.size(), limit);
return sorted(
journeys,
comparing(Journey::getDuration).reversed()
).subList(0, actualLimit);
}
- 직접 만든
sorted()
메서드를 통해 컬렉션의 불변성을 지키도록 변경되었다.
불변 변화를 이용한 리팩토링: sufferScoreFore
리팩토링
public static int sufferScoreFor(List<Journey> route) {
return sufferScore(
longestJourneysIn(route, 3),
getDepartsFrom(route)
);
}
- 불변성을 지켜 함수 호출의 순서가 상관없어졌기 때문에 함수호출 매개변수를 inline 형태로 변경해도 무관하다.
- 컬렉션을 변화시키는 함수를 사용하면, 해당 함수 호출 시점 전후로 컬렉션의 상태가 달라지기 때문에 코드 순서에 민감하다.
가변 컬렉션을 이용하는 메서드 수정하기: routesToShowFor
, removeUnbearableRoutes
public static List<List<Journey>> routesToShowFor(String itineraryId) {
var routes = routesFor(itineraryId);
removeUnbearableRoutes(routes);
return routes;
}
private static void removeUnbearableRoutes(List<List<Journey>> routes) {
routes.removeIf(route -> sufferScoreFor(route) > 10);
}
removeUnbearableRoutes()
메서드는void
를 반환함으로써, 컬렉션의 상태를 변환한다는 동작을 암시하고 있다.- 이전에 불변의 장점에 대해 배워보았으니, 이를 불변으로 리팩토링해보자.
private static List<List<Journey>> removeUnbearableRoutes(List<List<Journey>> routes) {
routes.removeIf(route -> sufferScoreFor(route) > 10);
return routes;
}
- 일단 컬렉션을 반환하도록 만들었다.
- 아직은 외부 컬렉션을 변경하는 코드가 그대로 남아있으니, 이를 차차 변경할 계획을 세우자.
private static List<List<Journey>> bearable(List<List<Journey>> routes) {
return routes
.stream()
.filter(route -> sufferScoreFor(route) <= 10)
.collect(toUnmodifiableList()); // 사실 그냥 toList() 쓰면, UnmodifiableList 반환된다.
}
filter
를 통해 수행되는 행위가 좀 더 쉽게 표현될 수 있고, 반환값이 명확하니 메서드명을 변경할 수 있다.removeUnbearableRoutes
->bearable
- 원본 컬렉션을 변경하지 않고, 불변 컬렉션을 반환하는 바람직한 함수로 변했다.
public static List<List<Journey>> routesToShowFor(String itineraryId) {
return bearable(
routesFor(itineraryId)
);
}
- 리팩터링 이후 외부 컬렉션을 변경하지도 않고, 코드 자체도 매우 짧아졌다.
- 지역변수가 필요 없어졌다.
코틀린 코드 리팩토링하기
자바에서 코틀린으로 넘어온 코드를 더 코틀린스럽게 변경하자.
longestJourneysIn
코틀린스럽게 변경하기
@JvmStatic
fun longestJourneysIn(
journeys: List<Journey>,
limit: Int
): List<Journey> {
val actualLimit = Math.min(journeys.size, limit)
return Collections.sorted(journeys, Comparator.comparing { obj: Journey -> obj.duration }
.reversed()
).subList(0, actualLimit)
}
@JvmStatic
fun List<Journey>.longestJourneys(limit: Int): List<Journey>
= sortedByDescending { it.duration }.take(limit)
- 첫번째 인자의 타입을 앞에 붙이고
.
을 이용해 함수를 선언할 수 있다.- 코틀린의 확장 함수를 이용하는 것이다.
- 불변 sort 메서드는 코틀린에서 이미 구현되어 있다.
take()
메서드를 이용하면,subList()
를 사용하기 위한 인자값도 따로 계산할 필요가 없다.
sufferScoreFor()
리팩토링
@JvmStatic
fun sufferScoreFor(route: List<Journey>): Int {
return sufferScore(
route.longestJourneys(limit = 3),
Routes.getDepartsFrom(route)
)
}
longestJourneys()
의 인터페이스가 바뀌었으므로, 맞추어주었다.
bearable
리팩토링
private fun bearable(routes: List<List<Journey>>): List<List<Journey>>
= routes.filter { sufferScoreFor(it) <= 10 }
- 코틀린의
filter
메서드는 리스트를 반환한다. - 반환 당시의 타입은
ArrayList
라 꼼수를 쓰면 리스트 변경이 가능하지만, 다운캐스트를 결코 하지 않기 때문에 크게 문제가 되진 않는다. - 이젠 자바에서도 공유된 컬렉션을 불변으로 취급하기 때문에 더더욱 문제가 없을 것이다.
리팩토링 과정 정리
- 자바 코드에서 먼저 외부 컬렉션을 수정하는 부분을 제거하고, 불변 리스트를 반환하도록 변경했다.
- 자바 코드를 코틀린으로 변환했다.
- 코틀린 코드를 더욱 코틀린스럽게 리팩토링 해보았다.
우리가 수행해야 하는 액션
- 코틀린에서 자바로 전달된 컬렉션이 변경될 수 있음을 인지하라.
- 자바 컬렉션을 사용하는 코드에서는 컬렉션 상태 변경을 제거하라.
- 상태 변경 제거가 힘든 경우에는 방어적 복사를 이용하라.
반응형
'코틀린 (Kotlin) > 자바에서 코틀린으로' 카테고리의 다른 글
자바에서 코틀린으로 5장 - 빈에서 값으로 요약 (0) | 2022.11.30 |
---|---|
자바에서 코틀린으로 4장 - 옵셔널에서 널이 될 수 있는 타입으로 요약 (0) | 2022.11.30 |
자바에서 코틀린으로 3장 - 자바클래스에서 코틀린 클래스로 요약 (0) | 2022.11.30 |