레벨0 - 2주차 회고, 그리고 회고를 회고하기

2026. 2. 10. 01:05·우아한테크코스/레벨0

 

글을 쓰는 방식에 대하여

1주차 회고를 다시 읽어봤습니다. "~~했다. 일까?" "~~이다." 같은 문체로 쓰면서 읽고 나니까 별로 와닿지 않았어요. 디컴파일 결과를 나열하고 "이게 이렇게 작동합니다"라고 설명하는 건 맞는데 정작 내가 그 과정에서 뭘 느꼈는지 뭘 고민했는지는 잘 안 보였어요.

그래서 이번 주부터는 평소에 아티클 쓰듯이 문체를 쓰려고 합니다. 항상 자연스러운 문체를 써보려고 하는데 쉽지 않네요. 🥲

이번 주에 한 것들

2주차 계획은 상속과 인터페이스였습니다. 평소 개발하면서 인터페이스는 자주 썼지만 코틀린의 프로퍼티 선언이 내부적으로 어떻게 처리되는지는 제대로 확인해본 적이 없었어요. 그리고 코틀린이 왜 모든 클래스를 기본적으로 final로 만들었는지도 원리를 알고 싶었습니다.

일단 간단한 인터페이스부터 만들어봤습니다.

interface User {
    val name: String
    val greeting: String
        get() = "Hello, $name"
}

이걸 디컴파일하면 자바 인터페이스에 getter만 생겨요. name도 getter고 greeting도 getter입니다. 상태를 저장하는 필드가 없어요. 인터페이스는 구현을 위임받는 쪽에서 어떻게든 값을 제공해야 합니다.

1주차에 backing field를 공부했을 때 "커스텀 getter만 있으면 backing field가 생성 안 된다"는 걸 배웠는데 인터페이스도 정확히 같은 원리였습니다. 인터페이스의 프로퍼티는 항상 backing field가 없는 프로퍼티인데요. getter만 있거나 구현 클래스가 오버라이드해서 제공해야 해요. 추상 클래스랑 다른 지점이 여기예요.

abstract class BaseUser {
    abstract val name: String
    val createdAt: Long = System.currentTimeMillis()  // 상태를 가질 수 있음
}

추상 클래스는 상태를 가질 수 있습니다. createdAt은 실제로 필드로 저장돼요. 디컴파일해보니 private final long createdAt 필드가 생성된점이 인터페이스와의 결정적 차이였어요.

"인터페이스는 계약이고 추상 클래스는 부분 구현"이라는 말을 자주 들었는데 바이트코드로 보니까 그 의미가 더 명확해졌습니다. 인터페이스는 "이런 기능을 제공해야 한다"는 약속만 하고 추상 클래스는 "이런 상태를 가지면서 이 기능들을 제공한다"는 베이스를 제공하는 느낌..?

open과 final 사이에서

코틀린은 모든 클래스가 기본적으로 final입니다. 상속받으려면 명시적으로 open을 붙여야 해요. 사실 안드로이드 개발을 하면서 오히려 편할 때가 많았는데, 왜 이런 설계를 택했는지 공식 문서에서 확인하고 싶었어요.

공식 문서를 읽다가 Effective Java의 "상속을 위한 설계와 문서를 갖추거나 그럴 수 없다면 상속을 금지하라"는 원칙이 언급된 걸 봤습니다. 결국 언어 차원에서 이 원칙을 강제하고 있었습니다.

상속은 강한 결합을 만들게 됩니다. 부모 클래스를 변경하면 자식 클래스가 깨질 수 있습니다. 그래서 상속을 허용할 거면 "어떤 메서드를 오버라이드할 수 있고 어떤 순서로 호출되고 무엇을 가정해도 되는지" 같은 걸 문서화하거나 정확하게 알고 있어야합니다.

코틀린은 그래서 기본값을 final로 가져가요. "상속 가능하게 만들려면 의도적으로 open을 붙이세요"라고 강제합니다. 이게 "상속보다 조합"이라는 원칙을 언어 차원에서 밀어주는 방식이구나 싶었어요. (저는 자바를 싫어해서 상속이 잘 와닿지 않습니다🤔)

실제로 data class도 final이라서 상속이 불가능해요. 불변성을 보장하기 위한 설계예요. copy() 메서드가 새 인스턴스를 만드는데 만약 상속을 허용하면 하위 클래스에서 예상 못 한 상태를 추가할 수 있고 그럼 copy()가 제대로 작동한다는 보장이 불가능 하기 때문입니다.

object의 내부 구현

object로 싱글톤을 만들 수 있다는 건 당연히 알고 있었는데 이것도 내부적으로 어떻게 구현되는지는 바이트코드로 확인해본 적이 없었어요.

object DatabaseConfig {
    val maxConnections = 10
    fun connect() {
        println("Connecting...")
    }
}

이걸 디컴파일하면 꽤 흥미로운 구조가 나옵니다.

public final class DatabaseConfig {
   public static final DatabaseConfig INSTANCE;
   private static final int maxConnections = 10;

   static {
      DatabaseConfig var0 = new DatabaseConfig();
      INSTANCE = var0;
   }

   private DatabaseConfig() {
   }
}

static 블록에서 인스턴스를 생성하고 생성자를 private으로 막아놨어요. 이게 자바의 싱글톤 패턴인걸 또하나 배웠습니다. Bill Pugh Singleton이라고 부르는 그 패턴입니다.

근데 코틀린에서는 그냥 object 키워드 하나로 이게 다 처리됩니다. 스레드 안전성도 보장되고 lazy initialization도 알아서 됩니다(만세). JVM의 클래스 로더가 static 블록을 실행할 때 동기화를 보장하기 때문이에요.

안드로이드에서 Hilt를 쓸 때 @InstallIn(SingletonComponent::class) 같은 걸 붙이는데 그게 결국 이런 싱글톤 패턴을 DI 컨테이너가 관리해주는 거라는 게 이제 좀 이해가 가요.

이번 주 잘된 점

디컴파일을 계속 돌려보면서 "코틀린 문법 → 자바 코드 → 실제 동작"의 연결고리가 점점 명확해지고 있습니다. 1주차에는 생성자와 프로퍼티의 실행 순서를 봤고, 이번 주에는 인터페이스와 추상 클래스의 차이를 바이트코드 레벨에서 확인했어요.

그리고 공식 문서를 읽는 습관이 생겼습니다. 평소 개발하면서 쓰던 기능도 공부할 시간이 적다는 핑계로 평균적으로 중간까지만 파봤다면 "왜 이렇게 설계했는지"까지 찾아보는 과정에 시간이 짧아졌습니다.

계획한 대로 data class의 copy() 구현과 object의 싱글톤 패턴도 모두 확인했습니다. 특히 copy() 메서드가 생성자를 호출하면서 지정된 파라미터만 바꾸고 나머지는 그대로 복사하는 방식이라는 걸 바이트코드로 직접 보니까 더 확실하게 이해됐어요.

이번 주 아쉬운 점

1주차에서 배운 개념을 2주차 내용에 바로 연결하지 못한 게 아쉬웠습니다. "커스텀 getter만 있으면 backing field가 안 생긴다"는 걸 배웠으면 인터페이스에도 당연히 적용될 거라고 생각했어야 하는데 막상 인터페이스를 디컴파일할 때는 따로 확인해봐야 했네요. 개념을 알고 있는 것과 다른 맥락에 적용하는 건 역시 다른 문제같습니다.

그리고 추상 클래스와 인터페이스의 선택 기준을 이론으로는 알아도 실제 설계에서 판단하기는 또 다를 것 같아요. "이 설계는 상태가 필요한가?"를 판단하는 게 생각보다 미묘한 경우가 많다고 생각해요. 그리고 아직까지 추상 클래스의 필요성을 잘 느끼지 못하고 있어요. 인터페이스와 구현체 분리를 통한 DI가 훨씬 좋지 않나…이 생각도 구체화 해봐야 겠습니다.

회고를 쓰는 방식도 계속 고민이 돼요. 1주차처럼 기술 내용을 자세히 쓸 것인가, 이번 주처럼 과정과 고민을 더 담을 것인가. 둘 다 필요한 것 같은데 균형을 어떻게 맞출지 아직 잘 모르겠어요. 일단은 이번 주 방식으로 계속 써보고 나중에 다시 조정해봐야겠습니다.

다음 주 계획

3주차는 제네릭과 널 안전성입니다. 사실 제일 머리아픈 주차라고 생각해요.

특히 reified 키워드가 궁금합니다. 자바는 제네릭 타입 소거 때문에 런타임에 T의 타입을 알 수 없는데, 코틀린의 inline 함수는 어떻게 is T 검사가 가능한지 디컴파일로 확인해볼 생각이에요.

그리고 String?이 컴파일 타임에만 존재하는 건지, 런타임에도 뭔가 차이가 있는지 바이트코드를 통해 보고 싶습니다. 아마 @Nullable 어노테이션이랑 null 체크 로직이 추가될 것 같은데 정확히 어떤 형태로 변환되는지 확인해보려고 해요.

이번주도 이렇게 마무리를 합니다. 긴 글 읽어 주셔서 감사합니다.👍🏻

'우아한테크코스 > 레벨0' 카테고리의 다른 글

레벨0 - 지연 초기화와 위임, 그리고 4주간의 마무리  (0) 2026.02.24
레벨0 - 제네릭과 널, 그리고 3주간의 흐름  (0) 2026.02.17
레벨0 - 생성자와 프로퍼티, 그리고 컴파일 과정  (2) 2026.02.02
'우아한테크코스/레벨0' 카테고리의 다른 글
  • 레벨0 - 지연 초기화와 위임, 그리고 4주간의 마무리
  • 레벨0 - 제네릭과 널, 그리고 3주간의 흐름
  • 레벨0 - 생성자와 프로퍼티, 그리고 컴파일 과정
아키001
아키001
Android 개발자가 되기까지.
  • 아키001
    미래 가젯 연구소
    아키001
  • 전체
    오늘
    어제
    • 분류 전체보기 (37)
      • Android (27)
        • Compose (13)
        • Jetpack (2)
      • Kotlin (2)
      • 우아한테크코스 (8)
        • 일상, 회고 (0)
        • 레벨1 (0)
        • 레벨0 (4)
        • 프리코스 (4)
  • 블로그 메뉴

    • 홈
    • 안드로이드
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    Gradle
    runcatching
    Kotlin
    compose
    레벨0
    우테코
    jetpack
    build-logic
    Android
    coroutine
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
아키001
레벨0 - 2주차 회고, 그리고 회고를 회고하기
상단으로

티스토리툴바