소감

시간이 무척 빠르다. 올해 2월은 예상보다 추웠고 흐린 날이 많았다. 특별히 기온이 영하이면서도 습기가 가득한 날이 종종 있었는데 그런 음산한 날씨 탓에 기분이 좋지 못한 때가 있었다. 나는 날씨에 영향을 많이 받는 것 같다. 날씨가 좋은 곳에서 살고 싶다는 생각을 한다. 그래서 호주나 캘리포니아 같은 곳에서 개발자로 일하고 싶다. 회사가 끝나자 마자 강이며 들이며 좋은 바람을 맞으며 뛰어 다니고 싶다.

나는 근 2개월간 점심시간에 러닝을 하지 못했다. 그러나 하루 20분의 그 짧은 운동의 부재는 연쇄적으로 나의 습관이나 식습관에도 영향을 미치기엔 충분했다. 과도한 휴대폰 사용으로 인해 수면의 질이 떨어졌고 식습관은 다소 자극적으로 변하고 말았다. 브레인 포그도 심해졌으니 말이다.

재밌는 점은 그런 회고란 지금처럼 맑아진 정신에서만 가능하다는 것이다. 나는 지난주부터 점심시간에 러닝을 다시 시작했다. 기온이 영상 2도 이상 오르기 시작했고 그간 내리고 쌓여 도로의 일부분이었던 눈이 녹았기 때문이다. 봄을 일찍 준비하는 마음으로 나는 옷을 갈아 입고 올림픽 공원으로 뛰어 나갔다. 2개월간 굳은 몸이 내 마음처럼 움직여주지 않을 것을 알고 있기에 매일 천천히 강도를 늘렸다. 국기 계양대까지 1키로 남짓 뛰던 거리는 이제 막 호수 한바퀴는 돌아오는 거리까지 늘어났다. 작년 최대 거리가 올림픽공원 북서편의 출구까지 돌아와 4km 정도 였던 것 같은데 지금은 그 절반정도를 뛰는 것이다.

운동을 해서 그런가? 피곤해서 요즘 일찍 잠에 들었고 머리가 맑아진 느낌이 든다. 그간 무너진 식습관도 균형잡히게 변하고 있다. 그리고 이렇게 2월 회고에 이르렀다.

나비효과라는 말을 한다. 복잡계에서 특정한 사건이 예상치 못한 사이드 이팩트를 발생시키는 것. 보통 인과관계가 뚜렷할 때는 나비효과라는 말을 쓰지 않지만 인간은 복잡계라고 생각하고 또한 관찰자이자 주체이기 때문에 ‘알아차림’ 상태가 아닌 이상 사는대로 생각하고 사고가 막히게 된다. 따라서 우리는 스스로에게 또는 나와 함께 사는 사람들에게 나의 이데아를 남겨두면 좋다. 몸에게는 작년에 주었던 규칙적인 러닝의 흔적으로 인해 내 몸이 근질거리게 했고 회사 사람들에게는 내가 점심시간에 러닝하는 사람이라고 각인 되었기 때문에. 내가 흘러가는대로 살고 또한 생각하던 순간에 나를 돌아보고 회상케 하는 것이다.

사내 개발자 스터디 : 쉬어가는 시기?

사내 개발자 스터디를 작년 11월부터 하고 있다. 이제는 참여도가 줄고해서 ㅎㅎ 뭔가 조용해졌다. 그냥 스터디원을 만나면 요즘 근황은 어떤지 묻고 얘기하는 것으로 만족하고 있다.

개발자 스터디의 템포 낮추기. 후기

개발자 스터디의 강도를 낮추었는데 다들 의지를 가지고 디스코드 채널에 참여했지만 월말까지 포스트를 제출하는 사람은 없었다. ㅎㅎ 이것도 길게 봐야할듯..

독서모임 운영

독서모임. 2월 책은 정지아 작가의 ‘아버지의 해방일지’ 이다. 나는 잘 읽히지 않았다. 3월 책은 내가 선정했다. 나잔 카잔차키스의 그리스인 조르바 이다.

밸리데이션 로직에 대한 고민

나는 임상시험데이터를 c-disc SDTM 표준에 맞도록 밸리데이션 하는 API를 개발하게 되었다. Web 기반이어서 Json을 입출력 데이터 포맷으로 정했고 서버인 자바 스프링부트로 밸리데이션을 실시하고, 밸리데이션의 결과(실패여부, 상세한 로그)를 응답해야 했다.

처음 고민했던 것은 Request DTO의 스키마였다. 도메인 모델은 SDTM 에서 정하는 60개가 있어서 그것을 그대로 입력시 DTO로 둘까 생각을 했었지만 그것은 좋지 않은 방법이었다. 검증 대상 데이터가 SDTM에 정해준 변수명이나 타입을 지켜서 온다는 보장이 없기 때문이다. SDTM 밸리데이션의 범위가 변수의 존재여부, 변수 값의 타입까지 검증한다는 것을 생각했을 때, 스프링 웹이 지원하는 요청 DTO의 자동 매핑은 Argument Resolver에서 처리하기 때문에 커스텀이 어렵다. 따라서 정말 최소한의 스키마만 정의하고 가변적인 데이터는 모두 Map<String,Object>로 받게 했다.

다음 고민했던 것은 성능이었다.
이제 해당 데이터의 검증을 수행해야한다. 임상데이터 중에 “문자열로 받으면서 특정 코드리스트 안에 포함해야한다”는 변수가 많은 편이다. 코드 리스트를 구현하기 위해서 처음에 DB로부터 코드리스트를 로딩 하려고 했으나 필요할 때마다 로딩하는데 있어서 시간이 많이 걸려 성능이 떨어졌다. 코드의 개수만해도 1만개는 가볍게 넘었다. 스케쥴 작업을 걸어 특정 시간마다 미리 로딩해두고 도중 접근이 가능하게 두는 것도 좋은 방법이지만 동시성 문제도 고려야했고 우선 SDTM 표준은 표준처럼 개정주기 최소 1년 단위이기 때문에 굳이 동적으로 구성할 필요가 없었다. 따라서 초기화 로직으로 수정 불가능한 HashSet 필드를 가지는 코드 리스트 클래스를 싱글턴으로 두고 코드 검증에 그것을 사용하게끔 했다. 스프링 빈으로는 굳이 두지 않았다. DI를 받을 필요도 없고 사용하는 입장에서도 굳이 생성자로 초기화를 할 필요가 없기 때문이다.

밸리데이터(검증기)또한 싱글턴으로 구성했고 검증 처리 속도를 높이기 위해서 Stream API ParallelStream 으로 처리하게끔 했다. 해당 도메인 모델을 검증하는데 필요한 시간은 기대값으로 ec2 t3.Large 기준으로 60개 도메인을 종합한 3만개 레코드에 대해 1초 미만이다. 대신 입력시 JSON을 DTO로 읽어오는 과정에서 1초 이상이 걸리긴 하지만 우선 검증에 대한 성능은 확보했다. 입력시 변환시간을 줄일 수 있는 방법은 여러가지가 있겠지만 당장은 이 정도 수준에서 만족했다.

벨리데이터 로직의 추상화, 체이닝

밸리데이터 가장 공을 들인 부분은 추상화였다. 상세한 구현은 숨기고 API를 조합하는 형태로 밸리데이터를 만드는 것이 목표였다. 우선 Map<String, Object>를 인자로 받는 Validate(Map<String, Object> data) 메소드를 가지는 함수형 인터페이스(RecordValidator)를 선언했다. 리턴값으로는 ValidationResult 클래스의 인스턴스 해당 클래스는 자기참조적 관계가 가능하도록 만들었다.

이제 이 RecordValidator 를 필요할 때마다 실체적인 익명객체로 만들어줄 수 있는 팩토리 메서드가 필요하다. 그 역할을 KeyBaseRecordValidatorBuilder가 수행한다. 해당 팩토리 메소드는 빌더패턴으로 여러 RecordValidator을 일렬로 조합한 파이프라인으로 합성하고 이것을 래핑한 RecordValidator 익명객체를 반환할 수 있는 빌더를 제공한다. 빌더는 구체적인 구현은 숨기고 아래와 같이 선언적으로 사용할 수 있다.

// 실제 밸리데이터 하나를 만드는 메소드 체이닝 빌더 코드
RecordValidator customKeyValidator = 
        KeyBaseRecordValidatorBuilder // 키 기반의 검증
                .forKey("원하는 Map의 Key이름") // 검증 대상 Map의 키를 명시
                .isPresent() // 값이 있어야 다음 검증 진행 없으면 실패
                .isChar() // 문자열이면 다음 검증 진행 아니면 실패
                .build(); // 도중에 하나라도 실패하면 실패결과를 반환하는 체이닝된 검증 객체를 반환

또한 중요한 점은 빌더는 기본적으로 자주 사용되고 검증된 단위 로직을 제공하는 한편 RecordValidator는 곧 함수형 인터페이스이기 때문에 필요할 때마다 커스텀하여 로컬메소드 내에서도 람다로 구현할 수 있다.

이제 쉽게 밸리데이터를 만들 수 있다. SpecificDomainValidator와 같은 구체화된 밸리데이터는 DomainRecordValidator를 상속하며 앞서 만든 RecordValidator들을 여러개 가지고 Validate(Map<String, Object> data) 메소드로 데이터를 검증하고 ValidationResult 반환한다. 앞서 ValidationResult가 자기 참조적 관계가 가능하도록 설계했기 때문에 SpecificDomainValidator 는 자신이 가지고 있던 RecordValidator 에서 각각 리턴한 ValidationResult를 종합하거나 래핑한 ValidationResult 객체를 리턴할 수 있다.

추가적으로 검증에 성공할 경우 성공한 결과를 반환할 때가 있다. 그러나 보통 검증에 있어서 성공결과는 별로 중요하지 않고 필터 아웃 대상인 경우가 많다. 이럴 때도 성공시 반환하는 별다른 메세지가 없는 성공상태의 ValidationResult를 싱글턴 인스턴스로 관리하면 객체 생성 비용이 없기 때문에 경제적이다. 밸리데이터가 null을 반환하지 않아도 된다는 점에서도 타입안정성도 보장된다. 단 성공상태의 고정 상태값을 가져야하기에 불변 클래스로 설계해야한다.