반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 러스트
  • 느린 쿼리
  • 팩터리 메서드 패턴
  • 이펙티브 자바
  • next js app
  • 작업기억공간
  • 외래키 제약조건
  • 싱글톤 패턴
  • try-with-resources
  • 자바스크립트
  • 메이븐 골
  • pnpm
  • item8
  • 메이븐 라이프사이클
  • 디자인패턴
  • 자바
  • 플라이웨이트패턴
  • serverless computing
  • 객체복사
  • Pre-rendering
  • 토비의 스프링
  • 이펙티브자바
  • 싱글톤
  • 슬로우 쿼리
  • Javadoc 자바독 자바주석 주석 Comment
  • Java
  • 도커공식문서
  • item9
  • 자료구조
  • 메이븐 페이즈
  • 서버리스 컴퓨팅
  • prerendering
  • 프로그래머의 뇌
  • NEXT JS
  • 알고리즘
  • 싱글턴
  • bean Validation
  • MSSQL
  • 자바 디자인패턴
  • 스프링 검증
  • 자바스크립트 면접
  • 빈 검증
  • 참조 해제
  • rust
  • 이펙티브 자바 item9
  • 자바 검증
  • 자바스크립트 인터뷰
  • Next.js
  • 추상 팩터리 패턴
  • item7

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

코틀린 (Kotlin)/자바에서 코틀린으로

자바에서 코틀린으로 6장 - 자바에서 코틀린 컬렉션으로 요약

2022. 12. 6. 21:30

주안점

자바에서 출발한 컬렉션은 코틀린으로 오며 왜 지금의 형태를 띄게 되었는가?

자바 컬렉션

  • 자바 컬렉션의 가변성은 처음에는 혁신적인 무기였음.
  • 자바 컬렉션은 극단적 가변 컬렉션
    • 컬렉션을 자유롭게 변경시켜 이용하며, 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
    '코틀린 (Kotlin)/자바에서 코틀린으로' 카테고리의 다른 글
    • 자바에서 코틀린으로 5장 - 빈에서 값으로 요약
    • 자바에서 코틀린으로 4장 - 옵셔널에서 널이 될 수 있는 타입으로 요약
    • 자바에서 코틀린으로 3장 - 자바클래스에서 코틀린 클래스로 요약
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바