많은 시간이 지나고 끄적이는 회고

유우선·2026년 4월 12일

회고록 모음.zip

목록 보기
7/7

1달동안 뭐했니? (7주차 회고)

처음 벨로그를 개설할 때만 해도 "주마다 한번씩 회고록 써야지!"라며 당차게 시작했었는데...

빡빡한 일정, 올라가는 난이도, 점점 부족해지는 체력 등 이런저런 핑계를 대며

"미션할 시간도 없는데 회고록을 어떻게 써?"라고 스스로를 달래며 회피하다가

이대로는 안된다는 생각이 들어 간만에 끄적이게 된 회고록입니다.

성장했다고 생각하는 부분

다형성이 어떤 녀석인지 조금 알 것 같다.

대학교 4년동안 컴퓨터 공학 전공으로 대학을 다니며 수도 없이 들어왔던 interface, 상속.

언제, 어떻게, 왜 써야 하는지를 몰라 작업할 때 스스로 떠올려내 써 본 기억이 없는 친구입니다.

(마치 이름은 많이 들어봤는데 대화는 해본 적 없어 서먹서먹한 고등학교 동창같은 느낌...)

이 동창 녀석에 대해 이해하게 된 계기는 지난 6주차에 진행한 "칸반 보드 관리"미션에서

레아가 수업해준 다형성을 통한 조건문 줄이기 라이브 코딩의 도움이 컸습니다.

갓.레.아

동일한 관심사를 갖고 비슷한 역할을 하지만 내부에서 가져야 할 상태가 다른 녀석을 구현할 때

다형성을 사용하면 유용하다는 걸 수업을 통해 알 수 있었고,

테코톡 일정으로 남들보다 미션 진행이 뒤쳐져 있었던 저는

다른 크루들은 각자의 기준과 고민을 통해 요구사항을 구현하고 있을 때

조금 치사하지만 레아의 수업을 듣고 이 규칙들을 어떻게 구현할지 고민하는 시간을 건너 뛰었습니다.

하지만 수업에서 라이브 코딩으로 보여준 예시를 완전히 그대로 쓰는 건 의미가 없을 것 같아 구조를 조금 수정해서 제 코드에 적용했습니다.

interface TaskRules {
    val isDeletable: Boolean

    fun moveTo(targetStatus: TaskStatus): TaskRules

    val requireAssignee: Boolean
}

class Todo : TaskRules {
    override val isDeletable: Boolean = true

    override fun moveTo(targetStatus: TaskStatus): TaskRules {
        return when (targetStatus) {
            TaskStatus.TO_DO -> this
            TaskStatus.IN_PROGRESS -> InProgress()
            TaskStatus.REVIEW, TaskStatus.DONE -> throw IllegalStateException()
        }
    }

    override val requireAssignee: Boolean = false
}
.
.
.

이 구현체 까지는 별 차이가 없습니다.

라이브 코딩에선 이 규칙의 구현체가 task 객체를 들고 있었는데

"규칙이 task 객체를 들고 있다." 라는 문장에서 기시감을 느껴

저는 이 규칙들을 task 객체가 들고 있도록 구조를 변경했습니다.

data class KanbanTask(val data: TaskData, val status: TaskStatus) {
    private var taskRules: TaskRules = when (status) {
        TaskStatus.TO_DO -> Todo()
        TaskStatus.IN_PROGRESS -> InProgress()
        TaskStatus.REVIEW -> Review()
        TaskStatus.DONE -> Done()
    }

    val isDeletable get() = taskRules.isDeletable

    init {
        validateAssignee(taskRules, data)
    }
    
    fun changeStatus(targetStatue: TaskStatus): KanbanTask {
        val nextRules = taskRules.moveTo(targetStatue)

        validateAssignee(nextRules, data)
        taskRules = nextRules

        return copy(status = targetStatue)
    }

이렇게 구조를 수정해도 괜찮을까? 라는 걱정도 있었지만 일단 해봤고 미션을 제출했습니다.

물론 엄청나게 많은 피드백을 받고, 고쳐야할 부분도 많았습니다.

그런데 리뷰어한테서

상태전이에 대한 규칙을 상태패턴으로 적용한 점이 좋았습니다 👍
덕분에 도메인과 UI로직이 잘 분리되어 있네요.

라는 피드백을 받았습니다.

... 정말 기분이 좋았습니다.

상태 전이 규칙 관련한 대부분의 코드가 레아의 라이브 코딩에서 따온 코드이긴 해도

스스로 고민하고 구조를 바꿔서 적용하기로 한 판단이 좋게 받아 들여졌다는 사실에 입가에 미소가 번졌습니다.

이렇게 얻은 다형성에 대한 자신감으로 다음 미션에도 다형성을 활용 했습니다.

영화 예매 미션에서 배운 점

다음 미션인 영화 예매 미션은 총 4단계로 구성되어 있었습니다.

1단계: 도메인 설계
2단계: view와 controller 구현
3단계: DB에 연결
4단계: Spring boot를 통한 REST API 구현

3, 4단계 마감이 얼마 남지 않았지만 아직 1, 2단계 병합도 안된 막막한 미션...

이 미션에는 여러 할인 정책, 좌석 별 가격에 대한 요구사항이 있었고,

얼핏보기에 interface-class 구조로 구현하기 좋아 보이는 녀석들 이었습니다.

그래서 저는 이 녀석들을 다형성으로 제작하자고 페어에게 적극 주장했고 페어가 이를 받아줘

해보고 싶은데로 코드를 작성 할 수 있었습니다. (고마워요 신밧드)

이렇게 그렇게 두 요구사항 모두 interface-class 구조로 구현하고 미션을 제출했습니다.

다형성도 너무 남발하면 좋지 않다

interface SeatGrade {
    val price: Money
}

class GradeS : SeatGrade {
    override val price: Money = Money(SeatPrice.PRICE_S)
}

class GradeA : SeatGrade {
    override val price: Money = Money(SeatPrice.PRICE_A)
}

class GradeB : SeatGrade {
    override val price: Money = Money(SeatPrice.PRICE_B)
}

이 코드는 저와 페어가 작성한 좌석별 금액에 대한 interface와 그 구현체입니다.

겉보기엔 깔끔하고 이뻐보였습니다. 하지만 리뷰어는 이 코드에 대해

interface - class 구조로 만들 필요가 있었을까요?
다른 구현의 선택지는 없을지 고민해보시면 좋겠어요.

라고 피드백을 받았습니다.

이 피드백을 읽고 "다형성을 쓰기엔 과한 부분"이라는 생각이 들었습니다.

의자의 등급은 가격을 들고만 있지, 그 가격으로 어떤 행위를 하는 녀석이 아니었습니다.

할인 정책의 경우 할인률을 갖고 입력 받은 가격에 할인을 "적용해주는" 행위를 들고 있기라도 하지

이 녀석은 상태만 들고 있는 녀석이라 interface-class로 구현하는게 과했다고 생각합니다.

OCP도 조금은 알 것 같다,,?!

Open Close Principle? 확장에는 열려있고 수정에는 닫혀있다?? 그게 뭔데;;;

라고 생각하며 OCP라는 게 있다~ 정도만 생각하고 있다가 정말 생각치 못하게 깨달음을 었었습니다.

controller를 구현하는 과정에서 생성된 예약들을 controller에서 들고 있었고,

저는 이게 정말 맘에 들지 않아 어떻게 수정할지 고민하다가 크루에게 도움을 청했습니다.

"이거 너무 맘에 안드는데 어떻게 생각하세요??"

그 크루도 controller에서 상태를 직접 들고 있는건 좋지 않다고 동의 해줬고 어떤 해결 방법이 있는지 힌트도 주었습니다.

그 힌트 중 domain에 대한 피드백도 있었습니다.

class PayCalculator(
    private val reservations: List<Reservation>,
) {
    private var totalPrice: Money = Money(0)
    private val timeDiscountPolicy = TimeDiscountPolicy()
    private val movieDayDiscountPolicy = MovieDayDiscountPolicy()
    private val cardDiscountPolicy = CardDiscountPolicy()
    private val cashDiscountPolicy = CashDiscountPolicy()

가격을 계산해 주는 객체에서 할인 정책들을 프로퍼티로 직접 들고 있었는데 이 부분을 짚어주며

유연하지 않다고 해주었습니다.

리뷰어에게도 같은 부분에 같은 리뷰를 받았었기 때문에 이를 어떻게 해결해야 할지 조언을 구했습니다.

크루가 해준 조언은 "외부에서 파라미터로 주입 받아라"였습니다.

이 말을 듣고 OCP에 대해 조금은 감을 잡은 것 같습니다.

OCP는 뭐다! 라고 명쾌하게 정의하지 못하지만 정책같은 것들을 객체에 직접 선언하여 고정 하지 말자 라는 건 알 것 같습니다.

class PayCalculator {
    fun calculateInitPrice(reservations: List<Reservation>): Money {
        var initPrice = Money(0)

        for (reservation in reservations) {
            val info = reservation.getReservationInfo()
            initPrice += info.price
        }

        return initPrice
    }

    fun calculateTimeDiscountedPrice(
        price: Money,
        reservations: List<Reservation>,
        discountPolicies: List<DiscountPolicy>,
    ): Money {
        var discountedPrice = price
        for (reservation in reservations) {
            for (discountPolicy in discountPolicies) {
                discountedPrice = discountPolicy.applyDiscount(discountedPrice, reservation)
            }
        }

        return discountedPrice
    }

피드백과 조언들을 반영한 코드입니다.

할인 정책은 같은 interface를 공유한다는 점을 이용해 정책들을 list 형태로 주입받아 적용하도록 코드를 수정했습니다.

하지만 여기서 걸리는 점은 list로 받으면 할인을 적용하는 순서가 list에 저장된 순서에 의해 결정된다는 것입니다.

이 문제에 대해선 아직 더 고민을 해봐야 할 것 같습니다.

아무튼 "확장에는 열려있고 변경에는 닫혀있다" 라는 문장이 코드에 어떻게 반영되는지

느낌을 알 것 같습니다. 한문장으로 정의하기엔 아직 깨달음이 부족합니다...

마무리

시골에서 올라와 바쁜 도시 생활에 차차 적응되면서도

한편으론 지치기도 하고 잘 해내고 있는지 의문이 들기도 하는 한달 이었습니다.

아직 공부해야할 것도 산더미고 해야할 일도 많고 신경써야 하는 것도 많아 정신이 없지만

레벨 1의 마지막을 잘 마무리 할 수 있기를 바라며

1달을 건너뛰고 쓰는 회고록을 마칩니다.

(회고 쓰면서 발견 했는데 PayCalculator에 오류가...)

0개의 댓글