sydney-harbour-bridge

1월 1일부터 11일까지 호주여행을 다녀왔다. 새해 첫날부터 해외여행을 다녀오다보니 뭔가 새해라는 느낌은 크게 와닿지 않았던것 같지만 새해부터 공항에 가는 기분은 다른때와 달리 색다른 기분이 들었다.

작년에 호주에 살고있는 조카가 처가댁에서 지내면서 거의 일주일마다 처가댁에 가서 조카와 놀아주었었다. 그러다 11월쯤 장인어른과 장모님께서 조카를 다시 집으로 데려다 주기 위해 호주로 떠나셨고 우리도 이번기회에 형님네도 가보고 호주여행도 해볼겸 호주 여행을 가기로 결정하여 다녀왔다.

호주는 1월 초까지도 휴가를 쓰는 사람들이 많아서 시내가 생각보다 한산했다. 오히려 휴양지에 사람이 훨씬 많았다. 긴 휴가를 가지는게 너무 오랜만이라 어색하긴 하지만 사람들이 여유를 즐기고 있는 모습들을 보니 문득 우리나라도 외국처럼 휴가기간이 좀더 늘어났으면 좋겠다라는 생각이 들었다.

호주의 주택단지는 영화에서 보았던 딱 그 모습이었다. 잔디밭이 있고 개인 풀장이 있는 전원주택들이 넓은 땅에 퍼져있었다. 하루는 새벽에 산책겸 동네 한바퀴를 돌아보았는데, 새들 울음소리와 새벽의 고요함, 그리고 맑은 공기가 서울 도시의 삭막한 느낌과 차이가 있어 이런 동네에서 살면 좋겠다라는 생각이 들었다.

호주에서 지내는 동안 버스를 타고 시내로 놀러가곤 하였는데, 종종 자전거 라이딩을 하시는 분들을 볼 수 있었다. 유튜브에서 보던 외국인들이 멋진 자전거를 타고 라이딩을 하는 모습을 직접 보니 나도 그들처럼 호주 풍경을 구경하며 라이딩을 하고싶다라는 생각이 들었다. 올해는 자전거도 샀겠다 자전거를 더욱 많이 타기로 결심했으니 원없이 자전거를 타야겠다.

호주는 팁문화가 없다. 환율도 나쁘지 않아서 그런지 물가가 우리나라와 비슷하거나 오히려 더 저렴한 경우도 있었다. 하지만 최저임금이 우리나라 2배라고 하니 우리나라는 아직 갈길이 멀구나 라는 생각이 들기도 하였다. 하지만 한편으로는 인건비가 비싸다보니 가게들이 5시가 넘어가면 대부분 문을 닫았고 지인 이야기로는 코로나때 많은 사람이 해고를 당했다고하니 최저임금이 높다고 마냥 좋은것만은 아닐 수 있겠다라는 생각도 들었다.

호주는 시라즈 와인이 유명하다. 와인도 좋아하겠다 호주에 간김에 시라즈 와인을 원없이 마시자해서 거의 매일 마셨던것 같다. 다행히 형님도, 장인어른께서도 와인을 즐기셔서 자주 마실 수 있었던 것 같다. 우리나라와 다르게 호주는 크고작은 Bottle Shop이 많았는데 맛있는 와인을 저렴하게 편리하게 살 수 있어서 너무 좋았다. 다행히 우리나라도 점점 Bottle Shop들이 생기고 있는데 좀 더 활성화 되면 좋겠다.

호주의 날씨는 한국과 반대로 1월에는 여름이었다. 캘리포니아 날씨와 유사하게 습도는 낮지만 햇볓이 뜨거웠다. 그래서 그늘에서는 시원하고 그늘이 아닌 곳에서는 엄청 덥고 살이 금방 타는 느낌이 들었다. 몇일은 비가 왔는데 비가오는날엔 외투를 입어야할 정도로 추웠다. 한국의 여름을 생각하면 덥고 습해 불쾌지수가 상당히 높은데 호주는 그렇지 않아서 부러웠다.

호주 음식은 일본이나 태국 등과 같이 특별히 유명한 음식이 있지는 않았다. 호주사람들이 영국에서 와서 그런가…싶기도 하다. 그래서 호주에서 가장 많이 먹은 음식이 바로 Cheeps(감자튀김)였다. 어떤 음식을 사먹어도 항상 Cheeps는 사이드로 있었다. 호주 소고기도 유명해서 먹어보았는데 문제는 우리나라사람들이 좋아하는 소고기와 호주사람들이 좋아하는 소고기가 조금 차이가 있다는 것이었다. 한국사람은 한우처럼 기름기가 많고 부드러운 부위를 선호하지만 호주 소고기는 기름 비중이 낮은 경우가 많았다. 그래서 조금 퍽퍽하다고 느껴지는 경우가 많았다. 하루는 형님께서 비싼 와규를 구워주셨는데, 그게 호주에서 먹은 고기중에 제일 맛있었다.


new-bike

KPI달성으로 받은 성과금으로 새로운 자전거를 장만했다. 그동안 살까말까 많이 고민도하고 좋은 자전거 매물이 나와도 여유자금이 없어 망설였는데, 이번에 큰마음먹고 하나 장만했다. 자전거 기종은 자이언트 프로펠 어드밴스 1 Disc 2021이다. 최신 기종은 전동 구동계이지만 2021년형은 전동 구동계가 아니라 조금 아쉬움이 있지만 그만큼 가격이 저렴하고 무엇보다 색상이 최신기종보다 마음에 들어서 좋았다.

아직 날씨가 추워서 라이딩을 못하고 있지만 날씨가 풀리면 자전거로 출퇴근도 하고 주말엔 여기저기 돌아다녀봐야겠다. 일단 피팅부터 해야지.


여행을 다녀오는 기간동안 테스트 주도 개발을 다시 읽었다. 이 책을 처음 읽은게 3년전인가 그랬던것 같다. 당시에는 TDD를 시작한지 얼마 되지 않은 상태에서 이 책을 읽게 되었는데 솔직히 크게 좋았다는 느낌이 없었다. 그래서 누군가 이 책에 대해 물어보면 추천을 하진 않았던 것 같다.

하지만 이번에 이 책을 다시 읽으니 지금까지 TDD를 하면서 고민해오던 것과 동료가 테스트에 대한 질문을 하였을 때 명확하게 답변하지 못했던 것들에 대한 답을 찾을 수 있었다. 책의 저자도 나와 같은 고민들을 해왔고 나름대로의 결론을 내렸다는 것에 있어 내가 TDD를 잘못된 방향으로 수행하고 있진 않구나 하는 안도감도 들었다.

어쩌면 몇년 뒤 다시 읽게 되었을 때 느끼는 바가 또 다를 수 있겠다는 생각이 들었다. 오래전에 읽었던 책들중에 마음에 드는 책을 다시한번 꺼내보아야겠다.


새해가 되면서 신년회때 회사의 전반적인 복지제도가 개편되었다. 팀별로 팀장님 재량에 의해 제공해주던 간식을 전사차원에서 지원해주고, 도서 및 운동을 위한 지원이 추가되었다. 그리고 근무 방식이 변경되었다. 재택근무제도를 전사적으로 확대한다는 내용이었다. 하지만 신년회 이후 구체적인 시행방법을 발표했을때 제품팀 모두가 변경되는 방법에 대해 불만을 가질수 밖에 없었다. 왜냐하면 매주 수요일과 목요일 자유롭게 사용할 수 있었던 재택근무를 월 2회로 제한하였기 때문이다. 그리고 유연근무의 시간도 기존의 11시~5시에서 10시~5시로 변경되면서 출근시간에 대한 자유로움도 줄어들었다.

회사의 복지제도는 상황에 따라 경영진의 의지에 따라 언제든지 바뀔 수 있다고 생각한다. 현재 코로나 유행이 어느정도 잦아듬에 따라 많은 회사들이 재택근무제도를 없애고 있으니 재택의 축소도 어느정도 이해할 수 있는 부분이다. 다만 모두가 아쉬워 했던것은 과정이었다. 신년회에서는 두루뭉실하게 재택근무제도를 확대하겠다는 말로써 모두가 오해할 수 있을만한 이야기만 하였는데 당장 2월부터 시행하는 구체적인 시행항목들은 1월 말이 되어서야 발표되었기 때문이다. 더군다나 팀원들에게 의견을 수렴하는 절차또한 없었기에 모두가 갑작스레 변경되는 근무방식에 대해 혼란스러워했다. 심지어 팀원중 한명은 얼마뒤에 거리가 먼곳으로 거주지를 옮기기로 결정하였는데 갑작스레 재택근무가 축소됨에 따라 당장 출퇴근을 고민해야하는 사람도 생겼다.

최근 어려운 상황에서도 KPI 달성으로 인해 성과금도 나오고 업무방식도 적응해 감에 따라 모두가 열심히 일할 수 있는 조건들을 어느정도 갖추어가고 있는 와중에 갑자기 찬물을 끼얹은 격이 되버린것이다. 개인적으로는 재택이 축소되고 출퇴근시간이 변경되는것에 대한 불만은 크지 않으나 소통방식은 큰 아쉬움으로 남아있다. 물론 시간이 지남에 따라 해결될 이슈라고 생각하지만 이런 사건들이 쌓여가면 지금까지 열심히 하던 사람들이 동기를 상실하여 수동적인 조직으로 변할까 우려가 된다.

모쪼록 이번 이슈가 잘 넘어가길 기대해본다.


아래는 1월동안 정리한 이슈 내용들이다.

Shell로 문자열 Hash 값 얻기

나이스페이 API는 암호화 키로 특정 문자열에 SHA-256으로 해싱해서 보내주는것을 요구한다. 그래서 프로그램을 개발하기전에 로컬환경에서 간단하게 테스트하기 위해서 해시값을 생성해야할 필요가 있었는데 아래와 같이 명령어를 사용하면 간단하게 해시된 문자열을 얻을 수 있다.

echo -n any string | shasum -a 256 | awk '{ print $1 }'

CircleCI incident report

휴가를 가있는동안 CircleCI에서 사용하는 암호화 키가 유출되는 보안사고가 발생하였다. 그래서 당시 개발팀에서도 사용키들을 부랴부랴 교체하는 헤프닝이 있었다고한다. 그로부터 몇일 후 CircleCI의 Blog에 보안사고에 대한 보고서가 발간되었다.

CircleCI incident report for January 4, 2023 security incident

내용을 보면 어떤일이 발생하였고 어떻게 조치하였는지, 그리고 이번 사건이 어떤 영향범위가 있었는지, 앞으로 어떻게 할것인지 등이 적혀있다. 지금까지 여러 장애 대응 보고서들을 작성도 해보고 다른 회사의 글을 보기도 하였지만 CircleCI의 장애 보고서는 두고두고 참고해도 좋을만큼 정석대로 잘 작성했다고 느껴진다.

내용도 명확하고 원인과 해결방법 그리고 CircleCI를 사용하는 사용자들이 우려할만한 사항들까지 아주 친절하고 간결하게 설명해주고 있다. 앞으로 장애대응 보고서를 작성할 일이 있을때 두고두고 참고해야겠다.

cellular-automaton

cellular-automaton는 실행시 작성중이 코드를 난독화 시키는 코드이다. 저장소에서도 설명하다시피 전혀 쓸모는 없지만 재미삼아 실행해봄직하기는 하다.

그냥 재밌어서 가져온것이긴 하지만 세상에는 참 다양한 사람들이 있다는걸 다시한번 느꼈다.

JDK 메모리 누수

작년부터 간헐적으로 서버에 메모리 부족으로 pod이 죽는 현상이 발생하고 있다. 짬이날때마다 엑셀 다운로드 시 메모리 누수를 방지하기 위한 조치와 같은 의심되는 원인을 해결하려고 노력하고 있다. 하지만 여전히 지금도 간헐적으로 발생하고 있고 최근에 발견한 의심되는 원인중 하나가 바로 JDK 17의 특정 버전에서 발생하는 메모리 누수 이슈였다.

Spring boot, Java 17 and Native memory leak

요약하면 도커에서 사용하는 OpenJDK 17.0.1버전에 버그가 있어 Native Memory에 누수가 발생하고 있으며 17.0.2버전부터 해결되었으니 업그레이드를 하면된다고 한다.

Entity 생성 트릭

예를 들어 Parent라는 Entity가 있고 Child라는 Entity가 있다고 가정해보자. ParentChild는 1:N 관계이고 단어에서 유추할 수 있다시피 부모 자식관계를 가진다. 이때 Parent는 생성시점에 Child를 매개변수로 받아 함께 생성되며 Child도 부모인 Parent를 필수 매개변수로 받고 있다.

Parent는 생성시점에 Child를 필요로하고 Child도 생성시점에 Parent를 필요로 한다. 양방향 관계의 가장큰 문제점이 이부분이다. 바로 순환참조문제이다. 이미 영속화된 Entity를 조회하는 것은 JPA의 도움을 받아 어찌저찌 해결할 수 있다고 치지만 지금 위 요구사항대로라면 생성자에서는 서로 참조를 해야하기에 어려움이 있다.

이런 상황에서 Parent는 어떻게 생성할 수 있을까?

해결방법 1 - Setter

자바를 사용한다면 아마 가장 익숙하게 발견할 수 있는 패턴이다. 바로 생성시점에는 자식들을 빈값으로 초기화하고 setter로 밀어넣는 방법이다.

@Entity
class Parent {
    @Id
    val id: UUID = UUID.randomUUID()

    @OneToMany(mappedBy = "parent", cascade = [CascadeType.ALL], orphanRemoval = true)
    val children: MutableList<Child> = mutableListOf()

    fun addChild(child: Child) {
        children.add(child)
    }
}

@Entity
class Child(
    parent: Parent,
    name: String,
) {
    @Id
    val id: UUID = UUID.randomUUID()

    @Column(nullable = false)
    val name: String = name

    @ManyToOne(optional = false)
    @JoinColumn(name = "parent_id", nullable = false)
    val parent: Parent
}

fun create() {
    val parent = Parent()
    val child = Child(parent, "홍길동")
    parent.add(child)
}

하지만 이렇게하면 Parent의 생명주기상 Child가 존재하지 않는 상태가 존재하게 된다. 이렇게되면 시스템이 불필요하게 복잡해지고 불필요한 상태로인해 제약조건을 체크하는 비지니스 로직이 많아지거나 오히려 비지니스 로직을 넣을 수 없는 경우가 발생하기도 한다.

해결방벙 2 - DTO

첫번째 해결방벙이 좋지 않다면 Child를 미리 생성하지 않는 방법은 없을까? 바로 DTO를 매개변수로 넘기면 된다.

@Entity
class Parent(
    childrenDtoList: List<ChildDto>,
) {
    @Id
    val id: UUID = UUID.randomUUID()

    @OneToMany(mappedBy = "parent", cascade = [CascadeType.ALL], orphanRemoval = true)
    val children: MutableList<Child> = mutableListOf()

    init {
        val childrenEntity = childrenDtoList.map { 
            Child(this, it.name)
        }
        children.add(childrenEntity)
    }
}

@Entity
class Child(
    parent: Parent,
    name: String,
) {
    @Id
    val id: UUID = UUID.randomUUID()

    @Column(nullable = false)
    val name: String = name

    @ManyToOne(optional = false)
    @JoinColumn(name = "parent_id", nullable = false)
    val parent: Parent
}

data class ChildDto(
    val name: String,
)

fun create() {
    val childrenDtoList = listOf(
        ChildDto("홍길동"),
        ChildDto("아무개"),
    )
    val parent = Parent(childrenDtoList)
}

이렇게 하면 첫번째 해결방법의 문제점인 Parent가 불필요한 상태를 가지는 문제점을 해결할 수 있다. 하지만 코드를 보면 알겠지만 DTO가 생기면서 Parent의 코드를 보면 너무 장황해지는것을 볼 수 있다. 변수명 중목을 막기위한 의미없는 변수들과 꼭 필요하지 않을 수도 있는 DTO를 정의해야한다는 등과 같은 부분은 큰 문제가 아닐 순 있지만 코드의 가독성을 많이 해친다는 생각이다.

해결방법 3 - Lambda

두번째 해결방법의 불편함을 해소하기 위한 방법으로 Lambda를 사용하는 방법이 있다. 먼저 코드부터 보자.

@Entity
class Parent(
    children: (parent: Parent) -> List<Child>,
) {
    @Id
    val id: UUID = UUID.randomUUID()

    @OneToMany(mappedBy = "parent", cascade = [CascadeType.ALL], orphanRemoval = true)
    val children: MutableList<Child> = mutableListOf()

    init {
        this.children.add(children(this))
    }
}

@Entity
class Child(
    parent: Parent,
    name: String,
) {
    @Id
    val id: UUID = UUID.randomUUID()

    @Column(nullable = false)
    val name: String = name

    @ManyToOne(optional = false)
    @JoinColumn(name = "parent_id", nullable = false)
    val parent: Parent
}

fun create() {
    val parent = Parent {
        listOf(
            Child(it, "홍길동"),
            Child(it, "아무개"),
        )
    }
}

Parent의 매개변수로 Lambda를 받도록 함으로써 Lambda의 매개변수로 Parent를 넘긴다면 Child를 생성하는 시점에 Parent를 가져와서 생성자에 넣어줄 수 있게된다. 코틀린의 문법으로 생성 코드를 우아하게 표현할 수 있는것은 덤이다.

처음에는 사파(?)같아서 고민을 하였는데, 두번째 해결방법보다는 간결해져서 좋은 방법이라고 생각한다. 해당 방법을 알려준 회사 동료에게 감사의 인사를 드린다.

여기서 하나 아쉬운것은 여전히 init에서 this를 넘기는 행위는 IDEA에서 아래와 같은 경고를 노출한다는 것인데… 이부분은 아직 딱히 명쾌한 해결방법을 찾지 못하고있다. 일단 경고니까…주의할수밖에.

Leaking 'this' in constructor of non-final class Parent 

쿠키런 장애 대응

최근에 CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기 글을 보게되었다. 쿠키런에 대단한 개발자분들이 많다고 들었지만 이렇게까지 장애대응을 할 수 있다는 부분에서 멋지다는 생각이 든다. 만약 내가 당시 상황이되었을 때 저런 결정을 내릴 수 있을까? 싶기도 하다. 당시에는 지옥같았겠지만 아마 장애 대응을 했던 모든 분들이 값진 경험을 가져갔지 않을까 싶다.

오픈렌즈

이제 Lens는 유료서비스로 바뀌었다. 그래서 우리는 OpenLens를 사용하기로 했다. 편하게 잘 사용하던 도구가 유료화 되는것은 아쉽지만 개발하고 운영하는 사람들도 먹고 살아야하니 이해는 간다.

OpenLens를 설치하는 방법은 간단하다.

brew install --cask openlens

다만 설치 후 환경설정을하여 Lens를 접속한 후 Pod에 접근하면 Pod 콘솔창에 접근할 수 없는 이슈가 있다. 이를 해결하려면 Extensions 메뉴에서 아래 패키지를 설치하면 된다.

@alebcay/openlens-node-pod-menu

Jackson - kotlin에서 enum의 특정값을 직렬화 하고 싶으면?

kotlin에서 enum의 특정값을 직렬화 하고 싶으면 아래와 같이 @JsonValue를 지정해주면된다.

enum class Bank(@JsonValue val code: String) {
    KAKAO_BANK("03"),
}

data class Foo(
    val bank: Bank
)

그럼 아래와 같이 직렬화한다.

{
    "bank": "03"
}

jackson-module-kogera

jackson-module-kogera는 직렬화 시 Jackson보다 3배나 빠른 속도를 보인다고 한다.

이렇게 빠른 직렬화 속도를 보여주는 라이브러리를 볼때마다 Jackson을 버리고 다른 라이브러리로 갈아타고 싶다는 생각이 들지만…스프링 MVC에서 어떤 난리가 날지 상상이 되지 않되기에… 가슴에만 담아놔야겠다. 개인 프로젝트 때 한번 설정해봐야겠다.

Feign encoder/decoder 정의 시 주의할점

Feign encoder 또는 decoder를 설정할때 아래와 같이 설정하면 메모리 누수가 있을 수 있다.

fun feignEncoder(objectMapper: ObjectMapper): SpringEncoder {
    val converter = MappingJackson2HttpMessageConverter(objectMapper)
    val objectFactory: () -> HttpMessageConverters = { HttpMessageConverters(converter) }

    return SpringEncoder(objectFactory)
}

왜냐하면 objectFactory 생성 시 매번 SpringEncoder 생성자를 호출할 수 있고 이때 매번 HttpMessageConverters가 생성될 수 있기 때문이다. 대신 아래와같이 코드를 작성하자.

@Bean
fun encoder(objectMapper: ObjectMapper): Encoder {
    return JacksonEncoder(objectMapper)
}

@Bean
fun decoder(objectMapper: ObjectMapper): Decoder {
    return JacksonDecoder(objectMapper)
}