철지난 헥사고날 아키텍처 정리

2023년 헥사고날 아키텍처로 프로젝트를 진행하던 일이있었는데 어른들 사정으로 강제종료 되어버렸고, 그때 팀 내부 공유를 위해 정리했던 헥사고널 아키텍처 정리 문서, 그냥 버리기 아까워서 일단 블로그에 백업….


헥사고날 요점 정리

헥사고날 구조로 설계/개발하기 위한 요점정리 입니다 본 문서를 통해 프로젝트를 진행하기전 별도 서적을 통해 헥사고날 기본 지식을 습득하길 권장합니다.

1. 용어 및 기본 개념

1.1. Hexagonal

헥사고날 아키텍처는 소프트웨어 아키텍처 디자인 패턴 중 하나입니다. 이 아키텍처는 소프트웨어 시스템의 모듈성, 유연성 및 테스트 용이성을 향상시키기 위해 설계되었습니다. 헥사고날 아키텍처는 “헥스”라는 육각형의 모양으로 시스템을 구성하며, 세 가지 주요 원칙을 기반으로 합니다.

  • 내부-외부 분리 원칙: 헥사고날 아키텍처에서는 시스템을 “내부”와 “외부”로 구분합니다. 내부는 핵심 비즈니스 로직을 포함하고, 외부는 외부 인터페이스와 상호 작용하는 컴포넌트를 의미합니다. 이 분리는 내부의 변경이 외부에 영향을 미치지 않도록 하고, 내부와 외부를 독립적으로 개발하고 테스트할 수 있도록 합니다.
  • 의존성 역전 원칙: 헥사고날 아키텍처에서는 의존성 역전 원칙을 따릅니다. 즉, 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 모든 모듈은 추상화에 의존해야 합니다. 이는 시스템의 유연성을 높이고, 모듈 간의 결합도를 낮추는데 도움을 줍니다.
  • 인터페이스 분리 원칙: 헥사고날 아키텍처에서는 인터페이스 분리 원칙을 적용합니다. 인터페이스는 클라이언트와 서비스 사이의 계약으로 간주되며, 각 클라이언트는 필요한 인터페이스만 구현하도록 합니다. 이는 인터페이스의 명확성과 응집력을 높이며, 각 컴포넌트의 책임을 명확하게 분리합니다.

헥사고날 아키텍처는 특히 대규모 및 복잡한 소프트웨어 시스템에 유용하며, 비즈니스 로직의 변경이나 기술 스택의 변경에 유연하게 대처할 수 있는 아키텍처를 제공합니다.

from ChatGPT

1.2. domain

  • 헥사고날의 핵심
  • 헥사고날에서 시도하는 모든 노력은 결국 도메인을 encapsulcation하여 외부에 대한 종속성을 완전히 제거하는데 목적이 있다고 생각할수도 있다.
  • 이를통해 외부의 변화에 대한 도메인의 영향을 최소화하며 도메인 응집력이 강한 소프트웨어를 작성할 수 있도록 한다.
  • 따라서 도메인은 도메인간 참조만 가능하며 그외 다른 파트에 대한 참조가 발생해서는 안된다.

1.3. UseCase

  • 시스템의 실제기능과 1:1로 매핑되는 개념의 인터페이스, port.inbound 와 같은 개념이다.
  • 기존 controller-service-repository 아키텍쳐의 서비스를 대체한다는 개념으로 볼 수 있다.
  • 단, 기존 아키텍쳐의 service가 여러 기능의 집합체라면 헥사고날에서는 하나의 서비스가 하나의 UseCase 만 책임(구현)지도록 하는 것을 권장한다.

1.4. Port

  • Domain을 외부 시스템과 연결하는 개념의 인터페이스 집합
  • inbound port
    • 외부에서 도메인을 호출할때 사용되는 포트 interface
    • inbound port대신 usecase로 패키지명을 정하는 곳도 있다.
  • outbound port
    • 도메인 로직(inbound port 구현체)에서 외부 시스템을 호출할때 사용되는 포트 interface

1.5. Adapter

  • Port와 결합하여 Domain을 실제 외부 시스템과 연결 시켜주는 구현체들의 집합
  • inbound adapter
    • inbound port를 호출하는 어탭터
    • spring web controller, kafka consumer 가 이 패키지에 포함된다.
  • outbound adapter
    • domain이 outbound port를 통해 호출하는 어댑터 (port.outbound의 구현체)
    • JPA repository, Kafka Producer 가 이 패키지의 하부로 포함된다.

1.6 Domain, Port, Adapter 간의 참조 관계

  • domain은 다른 레이어를 참조할수 없다.
    • 원칙적으로 port도 참조 할 수 없다.
    • 단, 다른 모듈에 정의된 domain의 최소한 참조는 허용한다. 하지만 권정되지는 않는다.
  • port도 domain 영역에 포함되므로 다른 레이어를 참조할수 없다.
    • port.inbound의 구현체의 경우도 data 조회, 외부 api 호출 모두 외부 모듈을 참조할수 없다.
    • 모든 외부 요청(참조) 기능은 port.outbound interface 를 통해 정의되고 구현체를 주입받아 처리 되어야 한다.
  • adapter는 domain, port에 대한 간접 참조(interface 참조) 만 허용된다.
    • 상황에 따라 DTO 변환이 과하게 많아지는것을 막기 위해 domain 모델에 대한 참조는 일부 허용한다.
  • 레이어별로 호출/응답에서 사용되는 모든 쿼리(파라메터) 객체와 응답 객체들은 DTO를 통해서 각 레이어 독립적으로 정의/변환되어 사용되는 것이 원칙이다.
    • adapter.inbound => (query & response data mapping) => port.inbound, domain => port.outbound => (query & response data mapping) => adapter.outbound 의 형태가 기본 원칙이다.
    • 각 메핑 레이어를 통해서 데이터 모델에 대한 종속성도 제가하는게 원칙이다.
    • 다만 이구조에서는 무수한 DTO가 생성되는데, 이 수고를 줄이기 위해서는 어느정도 domain 모델의 공유를 허용하여 완화할 수 있다.

2. 통합 빌링의 헥사고날 패키지 구성

헥사고날은 각 모듈별로 분리된 패키지 안애서 헥사고날의 패키지 패턴(domain, application.port, adapter)을 반복하여 구성한다.

net
└─ abh0518
   └─ myapp
      ├─ configuration                 : 모든 설정들이 모어져 있는 패키지
      └─ module
         ├─ moduleA                    : 모듈 별 패키지
         │   ├─ domain                 : 각 모듈별 도메인 영역, 헥사고널의 핵심
         │   ├─ adapter
         │   │   ├─ inbound            : UseCase(port.inbound)를 외부 요청과 연결해주는 adapter를 모아놓는 패키지
         │   │   │   └─ web            : web 항목들(controller영역)을 모아놓은 패키지
         │   │   └─ outbound           : port.outbound의 구현체(adapter)를 모아놓는 패키지
         │   │       └─ persistence    : jpa 모듈들을 모아놓은 패키지
         │   ├─ application
         │       ├─ port
         │       │   ├─ inbound        : UseCase interface가 정의되는 패키지, 각 인터페이스 명은 XXXInPort 로 한다.
         │       │   └─ outbound       : Domain이 사용할 포트를 정의하는 패키지. 각 인터페이스 명은 XXXOutPort로 한다.
         │       └─ service            : port.inbound 구현체를 모아놓는 패키지. 각 클래스 명은 XXXService로 한다.
         └─ support                    : 프로젝트 모듈들이 공유하는 패키지
            ├─ domain                  : 도메인간 공유 영역
            ├─ adapter                 : adapter간 공유 영역
            └─ application             : application간 공유 영역

2.1. 패키지별 설명

net.abh0518.myapp

  • 개발하는 서비스의 최상위 패키지

net.abh0518.myapp.configuration

  • 서비스의 모든 설정을 모아놓는곳
  • 헥사고널의 기본 철학에 따라 module들이 목적한 기능 구현에만 충실하도록 하게 하기위해 외부 설정에 관련된 부분은 별개의 configuration을 통해 DI되도록한다.
  • 이런 구성으로 외부 시스템에 대한 직접적 참조를 회피한다.

net.abh0518.myapp.module.XXX

  • 독립된 모듈 단위로 패키지를 나누도록 한다.
  • 각 모듈들은 동일한 헥사고날 패키지 구조로 반복되어 생산된다.(domain, application, adapter)

net.abh0518.myapp.module.XXX.domain

  • 특정 모듈의 도메인 영역을 모아놓은 패키지
  • 이 패키지는 절대 외부를 참조하는 일이 있어서는 안된다.
  • 단, 도메인간 참조는 허용된다.
  • 도메인 내에서 발생하는 excetion 등 도 모두 이곳에 정의되어야한다.
  • 도메인의 모델 설계는 physical 영역과는 완전 무관하게 설계되어야한다.
  • 도메인 모델과 도메인 모델이 매핑될 entity는 대상이 jpa이든 mongo이든 상관없이 완전히 별개여야 한다.
  • physical 영역의 매핑은 모두 outbound.port 를통해 adapter에게 넘기는 구조이다.

net.abh0518.myapp.module.XXX.application.port

  • inbound
    • UseCase interface가 정의되는 패키지
    • 각 인터페이스 명은 XXXInPort 로 한다.
  • outbound
    • Domain이 사용할 포트를 정의하는 패키지.
    • 각 인터페이스 명은 XXXOutPort로 한다.

net.abh0518.myapp.module.XXX.application.service

  • port.inbound의 구현체를 모아 놓는 패키지
  • Class 명은 XXXService 로 한다.
  • UseCase의 단일책임을 보장하기 위해서 UseCase가 UseCase를 호출(참조)하는 일은 허용되지 않는다.
  • UseCase가 다른 UseCase를 참조하는 경우가 발생한다면 이것은 설계가 잘못된것이다.
  • UseCase간에 공유되는 로직이 있다면 이는 Domain에 정의되어 공유되어야 한다.
  • 외부에서 이 서비스가 직접 참조되는 일은 없어야 한다. (존재조차 모르는게 좋다.)
  • 이 서비스에 대한 모든 참조는 port.inbound 를 통해 주입 받아야 한다.

net.abh0518.myapp.module.XXX.adapter.inbound

  • port.inbound interface를 통해 UseCase를 호출하여 외부의 요청을 Domain 으로 연결해주는 adapter 들의 집합
  • adapter는 호출할 port.inbound interface를 참조하고 실제 service 구현체는 스프링을 통해 주입받아야 한다.
  • adapter.inbound가 어떻게 구현/변경 되든 domaindms port.inbound interface를 통해 호출되므로 아무런 영향이 없다.
  • inbound.web
    • 가장 대포적인 inbound adapter 이다.
    • 사용자의 http 요청을 port.inbound 통해 domain으로 전달하는 adapter

net.abh0518.myapp.module.XXX.adapter.outbound

  • port.outbound 를 통해 들어오는 domain 의 요청을 처리해주는 port.outbound의 구현체
  • outbound.persistence
    • 가장 대표적인 inbound adapter 이다.
    • XXXAdapter로 port.inbound 구현체를 정의하고, 내부 구현은 각 목적에 맞게 알아서(!) 구현하면 된다.
    • adapter.outbound 의 내부 구현/변경이 어떻게 되든 domain은 port.outbound interface로만 참조하므로 아무런 영향이 없다.

2.2. 개발 튜토리얼

2.2.1. 가장 먼저 UseCase(port.inbound)를 정의한다.

  • UseCase가 만들어지며 자연스럽게 domain 모델의 일부도 만들어 진다.
  • 시스템이 외부에 제공해야하는 기능과 1:1로 UseCase가 나오는것이 이상적이다.
  • UseCase가 UsaCase를 참조해야하는 상황이 발생하면 뭔가 설계가 잘못된것이다.

2.2.2. Domain 모델과 Service(port.inbound 구현체, 비지니스 로직)를 작성한다.

  • Domain 모델은 jpa 등 외부 시스템의 entity 모델은 무관하게 완전한 POJO로 작성되어야 한다
    • 예를 들어 기존 패러다엠에선 JPA Entity가 데이터의 중심이 되고 도메인 로직이 JPA Entity에 맞춰 작성이 되었다면, 헥사고날은 이 관계를 역전 시킨다.
    • 도메인 모델은 JPA Entity(혹은 다른 저장 시스템)와 상관없이 오직 도메인 관점으로 설계되어야 한다. JPA 변화에 도메인 모델이 영향을 받는 구조가 되어서는 안된다. 그렇게 된다면 더 이상 헥사고날이 아니다.
    • 따라서 jpa entity는 adapter를 통해 자신이 구현될때 알아서 domain model에 맞춰 자신을 매핑하던가 변경을 하던가 알아서 해야한다.
    • 프레임워크에 포함된 annotation 뿐 아니라 데이터 마샬링을 json annotation도 붙이지 않을 것을 권장한다. (이 항목들은 모두 adapter가 알아서 처리해야할 부분이다. => 기존 도메인이 고생했던 부분을 모두 adapter에게 넘기고 domain은 자신의 관심사만 깔꿈하게 구현되도록 하는 목적이다.)
  • 이 과정에서 자연스레 port.outbound가 정리가 된다.
  • 하나의 port.outbound를 여러 UseCase에서 사용하는건 문제가 되지 않는다.

2.2.3 adapter.outbound 을 구현한다.

  • domain에서 정의한 port.outbound의 구현체(Adapter) 클래스들을 만드는 단계
  • adapter에서 사용할 외부 구현체들(jpa, external api call service) 도 함께 개발한다.
  • physical 영역을 domain이 정의한 인터페이스 구조에 맞춰 개발 해야하는 곳이므로 헥사고날에서 가장 힘들고 더러운 일을 모두 맡아 하는 영역이다.
    • JPA repository가 대표적인데, repository adapter의 구현이 상당히 고통스러울수 있다.
    • 앞서 도메인 관점으로만 port.outbound와 도메인 모델이 정의되어 있기 때문에, Physical 영역(JPA Entity)과 domain 영역을 어떻게든 끼워 맞춰야 하는 adpter.outbound 는 상당히 고통스러운 작업을 동반한다.

2.2.4 adapter.inbound 를 구현한다.

  • domain의 port.inbound에 연결할 adapter.inbound의 연결점(Adapter) 클래스를 만드는 단계
  • web의 경우 해당 패키지 내에서 외부와 소통할때 사용할 데이터 모델, 마샬링 등을 직접 정의하거나 configuration쪽에 위임한다. (domain은 이부분에 대해서 알 필요가 없다.)
  • domain으로 전달되는 data 변환만 주로 처리하다보니 adapter.outbound 보다 고통이 좀 덜하다.

2.3. 테스트 코드의 범위

2.3.1. 모듈 테스트

  • Domain과 각 UseCase별로 모듈 테스트를 작성한다.
  • 각 UseCase에서 사용되는 port.outbound는 mocking하여 사용한다.
  • UsaCase 테스트가 충분하다면 Domain별 테스트가 필수는 아니다.
  • 도메인과 UseCase 영역만 테스트 하므로 테스트 코드에 framework 관련 코드가 들어가지 않는다. 테스트 코드에 framework 관련 설정이 들어가게 된다면 뭔가 잘못 설계 된 것이다.

2.3.2. 통합 테스트

  • adapter.inbound, adapter.outbound 별로 통합 테스트를 작성한다.
  • pyhsical layer들을 모두 연결하는 테스트이므로 framework 설정이 테스트코드에 들어간다.
  • adapter 테스트를 통해 관련된 web, kafka, jap repository 테스트가 자연스레 함께 진행된다.

3. 왜 헥사고날을 쓰지?

장점

  • 도메인이 완벽히 인캡슐레이션 되어 외부 변경에 대한 영향이 최소화 된다.
  • 도메인을 순수하게 도메인 과련 코드로만 응집시킬 수 있다.
  • 단위 기능(UseCase)에 대한 책임 소재가 명확해져 소스 분석 및 추적, 테스트 코드 작성이 수월해 진다.
  • 제대로 적용을 했다면 db쪽 스키마를 홀라당 뒤집어도 domain쪽에는 영향이 없다.
    • 다만, adapter 가 죽어나갈뿐이다. domain의 변경이 없는 대신 그만큼 변화된 환경에 맞춰 adapter의 변경량이 많아진다.

단점

  • Layer별(domain, port, )로 DTO, Interface들이 모두 분리해서 작성되어야 되다보니 기존 패러다임 대비해서 코드량이 상당히 늘어는다.
  • 좀 심하면 Domain개발을 깔짝 한 뒤에 DTO와 Adapter 작업하다 진이 다 빠질 정도다.
  • 모든게 domain 중심이다보니 domain을 중심으로 주변 모듈들의 변화량이 꽤 많아지는걸 각오해야한다.
  • 헥사고날은 코드량을 줄여주지 않는다. 핵심 domain을 외부 변화로부터 최대한 보호하는게 목적이고 그 목적 달성을 위해 domain외 관련 코드들의 변화나 테스트 코드의 증가는 감수하겠다는 각오(?)가 필요하다.

팰리세이드가 수리센터에 입고되서 신형 그랜져 렌트받아 타본 이야기

지난 이야기 참조 : http://abh0518.net/tok/?p=670

2020년 4월에 인도받은 팰리세이드의 좌측 사이드미러 카메라 영상에 조금씩 노이즈가 끼기 시작했다. 8월즈음 간헐적으로 증상이 나타나기 시작하더니 점점 심해져 요즘은 운전할때면 90%정도 확률로 노이즈가 발생한다. 4개월간 이리저리 환경을 바꿔가며 원인을 찾아보려 해도 좀처럼 찾아지지 않아 결국 현대자동차 남부서비스 센터를 방문했다.

코로나로 인해 아이들도 그냥 집에 머물고 있는 상황에서 아내랑 아이들만 집에 두고 갈 수 없어 그냥 모두 함께 남부서비스 센터를 방문했다. 다행히 센터 내부에 어린이 놀이방이 있어서 아내와 애들은 놀이방에 맡기고 차량 접수를 시작했다.

여튼 뭐 이래저래 대기시간 거치고 (중간에 너무 대기가 길어지고 안내 연락이 없어서 그냥 정비소로 곧바로 안내받았다.) 정비사 분과 상담을 진행했는데 결론은 3일 입고 였다. 젠장.

원인을 알면 고치는건 1시간이면 되는데 영상에 왜 노이즈가 끼는지 원인 파악과 수리후 테스트가 쉽지 않다고 한다. 게다가 간헐적으로 생기는 문제라 수리하고 난 뒤 하루 정도는 테스트 해봐야한다고 하니, 납득을 할 수 밖에 없어 입고하기로 결정 하고 대체 차량 렌트를 안내를 받았다.

입고가 결정 되자 마자 안내 데스크에서 렌트 담당자 분과 연결이 되었고(이건 빨라서 좋더라) 이래저래 필요한 옵션을 전달하니 현재 내 조건에 맞는 신형 그랜져 챠랑이 있다고 한다. SUV를 꼭 타고 다녀야 하는건 아니니 수락했다. 그리고 이 결정이 아이들에게는 최악의 선택이었음은 차량 인도 받고 알게 된다.

20분 정도 지나니 차량이 도착했다고 연락왔다. 렌트 직원분과 차량 상태를 확인하고 간단한 안내를 받았다.

KakaoTalk_Image_2020-12-02-23-49-35- 대체 차량으로 인도 2020 신형 그랜져, 어라운드뷰가 꼭 필요하다고 했더니 이녀석이 왔다. 
팰리세이드를 타와서 그런지 아담한 느낌이 들었다. 4000km 달린 놈이다. 새거 맞네!

이제 아래 내용들은 그랜저와 팰리세이드를 비교하는 글이 될텐데, 사실 이건 말이 안되는 비교이다. SUV와 승용은 비교대상이 될수 없다. 하지만, 난 할거다. 히히히

짐들을 그랜저로 옮기고 운전석에 탔다. 이제 비 본격적 2020 팰리세이드 vs 2020 그랜져 비교를 시작한다. 다만, 그랜져가 풀옵션은 아니다보니 옵션 관련 차이는 비교하지 않았다.

몸체

  1. 크기
    당연히 팰리세이드가 크다. 훨씬 크다. 그랜져가 아담하게 느껴질정도로 크기 차이감이 있다.
  2. 좌석
    그랜져가 준대형이지만 역시 팰리세이드에 익숙하다보니 공간도 아담하다고 느껴진다. 암레스트쪽 여유 공간도 당연 팰리세이드가 넓고 크다. 뒷좌석에 두 아이와 탄 아내도 팰리세이드와 비교하면 좁다고 한다.
  3. 내장 인테리어
    둘다 가격대가 비슷하다보니 내장 인테리어 차이는 크게 못느꼈다. 다만, 속도/RPM 계기판은 그랜져가 더 이뻤다. 같은 브랜드에서 나오다보니 편의 기능들이 대부분 비슷해더 버튼은 배치만 좀 다른 정도지 거의 동일하다.

주행 성능

  1. 핸들링과 운전석 승차감
    오! 부드럽다! 팰리세이드와는 확실히 다르다. 이게 고급 승용과 SUV차이인가? 생각될정도로 핸들링 느낌이 다르다. 팰리세이드의 핸들링이 결코 나쁜건 아닌데, 그랜져의 핸들링은 그래도 차이를 느낄정도로 부드럽게 쉭쉭 돌아간다. 승용차와 SUV 차인지는 모르겠지만 운전석도 낮게 깔리는 느낌인데 이게 승차감이 더 좋다고 하는건지는 모르겠다. 운전하는 입장에서의 승차감은 큰 차이를 못느꼈다.
  2. 가속감 및 브레이킹
    엑셀을 밟을때 가속감은 그랜져가 확실히 더 부드러웠다. 이전 아반떼 vs 팰리세이드 비교처럼 그냥 밟으면 죽죽 나간다의 차이가 아니라 같은 가속이어도 그랜저쪽이 좀 더 부드럽게 속도가 올라간다. 변속 충격도 잘 느껴지지 않았다. 변속 충격은 차량에 익숙해짐에 따라 느껴지는게 다를수 있어서 이건 고민이 좀 더 필요할거 같다. 나같은 경우 운전차량에 익숙해질 수록 변속 충격을 잘 느끼니 팰리세이드를 처음 탈때 변속 충격을 느꼈는지를 다시 더듬어 생각해 봐야 한다.
  3. 뒷좌석 승차감
    당연 그랜져가 좋을수 밖에 없는거 같다. 이것도 뒺좌석에 앉은 아내도 동의. 승용차가 SUV와 다르게 낮게 깔리는 차체와 승차감 위주로 세팅이 된다는걸 생각하면 당연 그랜져가 좋을수 밖에 없다.

결론
결론은 뻔하다. 승차감/가속감은 당연히 승용차인 그랜져가 좋고, 실내 공간과 편의 활용도는 팰리세이드가 좋다. 비교하지 말아야 할 걸 비교하니 이따위 뻔한 결론이 나온다.

추가로 그랜져 운전해보고 느낀 또 다른 점은 벤츠 E Class와는 비교하면 안된다는 거다.  간혹 인터넷 보면 그랜져랑 벤츠 E Class 비교하는 사람들 있는데, 그러면 안된다. 벤츠 입장에선 화난다. 그랜져가 좋은 차 이긴 하지만 벤츠 E Class랑 비교할 차는 아니다. 진짜다.

그외 

옵션 비교를 빼니 별로 비교할건 없다. 그외 부분은 그냥 쓰다 안쓰니 불편한 옵션이다.

  1. HUD
    이게 없다가 쓸때는 조금 편해졌다 느낌이었는데, 반대가 되니 사람 환장한다. HUD있으면 전방만 보는것 만으로 속도, 네비 방향, 좌우 차량 유무 확인 등이 쉽게 가능했는데, HUD갑자기 사라지니 눈이 바빠진다. HUD 옵션도 넣어달라고 할 걸 그랬다.
  2. 후방 자동 차양막
    운전하다 뭔가 팰리세이드에서는 보지 못한 버튼이 있길래 눌러보니 뒤에서 위잉 소리가 난다. 오오! 뒷창 차양막이 자동으로 올라온다. 우왕! 팰리세이드는 이거 없는데! 없다고!

두 아이 에게는 대참사

두 아이가 팰리세이드를 무척 좋아한다. 차 별명도 아이들 이름의 뒷글자를 따서 윤카라고 부르며 친구/동생 처럼 지낸다. 그리고 나는 이 사실을 잊지 말았어야 했다. 그 소중한 윤카가 입고되고 집에 함께 돌아가야할 차로 그랜져가 나타나니 첫째랑 둘째가 울기 시작한다. 우리 윤카 없어지고 이상한 차가 왔다고 운다. 젠장, 팰리세이드가 없으면 모양 비슷한 다른 SUV라도 골랐어야 했었다. 그래도 첫째는 울면서 타긴 하는데 둘째는 정비소 도로 한가운데서 안탄다고 떼쓰며 윤카 가져오라고 운다. 정말 미친듯이 당황했다.

결국 “입원한 윤카” 병문안 가자고 설득하여 첫째, 둘째 태우고 담당 정비팀에 방문(정비사 분이 짐 옮기러 차가지고 와도 된다고 해서 일단 안심하고 갔다.)하여 입고된 차에게 3일간의 작별인사를 하고 집에 왔다. -_-;;;

코로나로 폐쇄된 구청 어린이집 출입구에서 무개념 아저씨 만나 신고한 이야기

첫째가 다니고 있는 구청 1층 어린이집 옆에는 건물 외부로 통하는 출입구가 있는데, 이 출입구는 보건소 및 코로나 선별진료소로 바로 연결되는 구조이다. 때문에 구청에서는 이 출입구를 폐쇄 시켜놓은 상태이다. 그래서 구청에 용무가 있는 사람이거나 직원들은 모두 2층 검사소를 통해서 1층으로 내려와야 한다. 불편하긴 하지만 상황이 상황인지라 돌도 안지난 둘째를 등에 업고 첫째를 등원시키는 엄마나 손주들 등원 시키는 할머니, 할아버지 모두 군소리 하지 않고 1층과 2층을 오르락 내리락 하며 다니고 있다.

지난주 첫째를 구청 1층에 있는 어린이집에 등원 시키고 나오려는데 어린이집 옆 출입구 바깥쪽에서 50대로 보이는 아저씨가 문좀 열어달라고 부탁을 한다. 내가 “폐쇄된 문이니 안됩니다.” 라고 말했더니 “여기 직원이에요! 괜찮아요!” 라고 응답한다. 뭔 개소리인가 싶어서 그냥 무시하고 뒤돌아 2층으로 가려는데 딸깍 문열리는 소리가 난다. 아까 그 아저씨게 어떻게 폐쇄된 문을 따고 들어온거다. 아저씨에게 다가가 “여기로 들어오시면 안됩니다.” 라고 말을 건네니 “난 여기 직원이라 괜찮아요.” 하고 지나간다. 내가 큰소리로 “직원이 이러시면 더 안되죠!” 라고 목소리를 높여 말하니 “알았어요~ 알았어요~” 하며 손사레 치며 도망치듯 가버렸다. 뭐 이런 미친놈이 다 있나 싶었다. 그 자리에 서서 이걸 어떻게 해야하나 생각하다 보니 묘하게 빡이 올라왔다. 직원이면 공무원이란 건데 공무원이 이따위 짓거리를 당당하게 하고 다닌다고? 이대로는 안되겠다 싶어서 그대로 2층 민원 처리실로 갔다.

“직원 관련 항의는 어디서 하죠?” 2층 민원 처리실 안내소에 가서 물으니 9층 감사과로 가야한다고 한다. 곧장 9층으로 올라갔다. 9층도 코로나로 출입구가 통제되고 있어 전화를 하니 직원분이 오셔서 문을 열어주었다. 함께 감사실도 들어가 상담 테이블로 안내 받았다. 나를 안내해준 여자 직원분과 연배가 있어 보이는 남자 직원분이 내 맞은편에 앉았다.

“무슨 일로 오셨습니까?” 라는 물음에 “1층 폐쇄된 문에 직원이 임의로 문열고 다니는거 알고 계신가요?” 라고 그냥 단도직입적으로 이야기 했다. 남자 직원분 동공이 흔들리는게 보인다. 그냥 일반 민원 호소하러 왔나 했다가 코로나 신고가 들어오니 당황하신 느낌이다. 앞선 상황에 대해서 어린이집 아이 등원시키는 부모로서 느끼는 불안감과 구청 직원의 안일한 태도에 너무 화가남을 자세히 설명 드렸다. 내 이야기를 모두 들은 감사실 직원분들은 정말 죄송하다며 거듭 사과 했고 빨리 조치하고 상황 전파 하다록 하겠다고 했다. 그렇게 신고를 마치고 집으로 돌아왔다.

오후가 되어 아이들이 하원할 시간이 되었다. 먼저 둘째를 픽업하고(서로 다른 어린이집을 다닌다.) 첫째 픽업하러 갔다. 둘째와 함께 구청 2층을 거쳐 다시 1층으로 내려와 어린이집에 들어갔다. 아빠 왔다며 좋아하는 첫째와 언니 있다고 좋아하는 둘째를 부둥켜 안고 어린이집을 나왔다. 그런데 그때 아침의 그 아저씨가 보였다. 너무나도 자연스럽게 1층 폐쇄 출입구 문을 따고 밖으로 나간다. 뭔가 이성의 끈이 끊어지는 것을 느끼며 아저씨에게 말을 걸었다. “여기 직원이세요?”

밖으로 나가려던 아저씨가 뒤돌아 본다. “왜요?” 라며 나에게 되묻는다. “여기 직원이시냐고요!” 라고 목소리를 더 키워 물었다. 아무 말이 없다. 적당히 말해서 안될거 같아 “여기 직원이시냐고 묻잖아요!” 라며 크게 소리쳤다. 그러자 아저씨는 그냥 뒤돌아 나간다. “이런 식이면 신고 합니다!” 라고 소리치자 “할테면 하세요!” 라며 도망치듯 나간다. 뛰쳐 나가서 잡을까 했는데 애 둘을 두고 갈수 없어 그냥 보내 줬다. 그리고 곧바로 구청 감사실에 전화했다.

이번엔 아침과 다르게 언성을 높여 이야기 했다. 아침 일찍 신고를 한게 아직도 하나도 조치가 안되어 있고, 해당직원은 빨리 잡아서 조치를 취해야하는데 뭐하시는거냐고 라고 따졌다. 담당 여직원 분은 전파 속도가 늦어져서 그런거 같다고 거듭 사과를 했는데 난 상황이 이해가 가지 않았다. 이게 늦는다고 넘어갈 상황인가? 이건 좀 아니다 싶었다. 그래서 다시 다산콜센터 코로나 신고 센터로 전화했다. 내가 겪은 앞의 일들을 자세히 설명했고 해당 직원이 누군지 찾아내어 조치를 취해주길 바란다고 이야기했다. 콜센터 직원을 통해 신고가 접수 되었음을 확인하고 전화를 끊었다.

다산 콜센터에 신고를 한지 며칠이 지난 오늘 문자로 연락이 왔다. 내가 신고한 사람은 구청 상주 직원으로 확인 되어 관리 부서를 통해 교육하였고, 해당 출입문은 바리케이트를 추가 설치하여 보완하였다는 내용이었다. 감사실에 전화해서 어떻게 교육했고 징계는 한건지 물어볼까 하다가 이 정도 까지만 하기로 했다.

대체 그 상주 직원이란 인가는 뭔깡으로 그짓꺼리를 하며 당당하게 했던 걸까? 역시 철밥통이라?

팰리세이드 3주 사용 후기

작년 12월, 두 아이들과 아내가 뒷자석에 타기엔 이제 아반떼가 비좁다는 걸 깨닫고 차를 바꾸기로 했다. 새로 산 차는 현대 팰리세이드, 작년 12월에 말에 주문하고 올해 4월 3일날 받았으니 대략 4개월 걸린 셈이다. 작년 평균 6~7개월은 기다려야 받을수 있던데 비해 나는 매우 빨리 받은 경우다. 사촌형(현대차 영업맨인 사촌형을 통해 샀다)의 말로는내 순번 앞 사람들이 그냥 죽죽 빠져나갔다고 한다. 코로나 때문에 취소가 많아져서 그런걸까?

현대차의 장점이자 단점인 옵션질은 최대한 가성비를 뽑아보려 고민했으나 아내의 “어차피 10년은 쭉 탈건데 아쉬움 없이 지르시오.” 라는 조언에 그냥 선루프 빼고 넣고 싶은거 다 넣었다.

KakaoTalk_Photo_2020-04-23-15-53-03

가솔린 3.8 4WD Prestige 8인승 오토, 패밀리 + 라이프스타일 + 테크 옵션 + TUIX 옵션 추가 된 내 팰리세이드

여튼 이 글의 주요 목적인 3주 사용 평을 하자면

몸체

  1. 크기
    대형 SUV인 만큼 크긴 크다. 그만큼 처음 운전하는 사람에게 꽤 부담이 느껴진다. 하지만 운전 좀 해본 사람이면 알듯이 하루, 이틀 정도 몰면 금새 익숙해 지기 마련이고 후술할 서라운드 뷰 같은 편의기능을 사용하면 부담감이 확 줄어든다.
  2. 좌석
    차가 크니 좌석도 넉넉하다. 생각치 못했던 부분인데 1열 조수석에는 오토 on/off 에어백이 있다! 아이를 앞좌석에 태우게 될때 에어백이 항상 걱정되는 부분인데 (아동에겐 에어백이 오히려 위험하다.) 무게 감지로 에어백이 자동으로 on/off 된다. 2열은 아내와 두 아이가 모두 타도 꽤 넉넉함이 느껴진다. 넉넉한
 2열이 우리 가족의 최우선 순위 요건이었는데 이건 대만족이다. 3열은 3인석이라고 하지만 3인이 타면 비좁다. 2명이 타기 적당하다. (나중에 누나들이나 태워야지)
  3. 트렁크
    역시 크다. 아반떼 쓰다가 팰리세이드로 넘어오니 공간이 남아돈다. 아반떼에서 테트리스를 해야 간신히 들어가는 짐들이 팰리세이드에는 대충 던져넣어도 공간이 남는다. 드렁크 하부 손잡이를 당겨 올리면 공구 수납칸이 나오는데 이곳도 크기가 넉넉해서 삼각대, 우산, 공구류 등등 다 쑤셔 넣어도 꽤 자리가 남는다. TUIX옵션으로 제공되는 깔판이 의외로 좋다.
  4. 2, 3열 폴딩
    사실 이것 때문에 대형 SUV를 샀다. 제주도로 가족여행 갔을 때 렌트했던 산타페 트렁크 안에서 1시간 넘게 재미나게 노는 두 딸아이를 보는 순간 다음차는 무조건 SUV로 가기로 결심했었다. 그리고 아버지께서 운전을 싫어하셔서 고향에 내려가면 멀리 나가지 못하고 걸어서 갈 수 있는 식당만 골라서 갔어야 했는데 이제는 온가족 다 태우고 외곽까지 놀러 나갈수 있게 되었다.

    코로나 때문에 어디 놀러가지 못해서 동네 사람 없는 주차장에 가서 차박이 놀이 하는중

    코로나 때문에 어디 놀러갈 수가 없어 사람 드문 동네 주차장에 주차 해놓고 차소풍 놀이 하는중, 2열과 3열을 접으면 어른 둘과 애들 둘이 들어가서 놀기에 적당하다. 이거 하고 싶어서 대형 SUV 샀다.

주행 성능

  1. 엔진
    가솔린 3.8, 이전 차인 아반떼 가솔린 1.6과 비교하면 당연 압도적으로 잘나간다. 차가 큰데도 그냥 밟으면 밟는대로 죽죽 잘 나간다. 짐 잔뜩 싣고 가족 다 태우고 가도 붕붕 잘나간다. 하지만 그만큼 기름이 뚝뚝 떨어져 나간다. 붕붕 잘나가는 만큼 지갑에도 바람이 붕붕…..
  2. 연비
    1번과 모순된 이야기 같지만 연비가 은근 좋다. 아반떼 대비 기름이 뚝뚝 떨어져 나간다는거 뿐이지 동급 수준으로 연비가 매우 좋다. 양양-서울 구간 90~100km/h로 정속 운행하니 연비가12km/l 가 나왔다. (물론 아반떼 가솔린으로 이렇게 운전하면 19~20km/l 가 기본이다.). 가솔린인데도 엔진자동정지 기능이 적용되어 있다. (요즘 차는 다 달려 나오는거 같다.)
  3. 핸들링 및 운전석 승차감
    무난하다. 별 불편 없이 운전하고 있다. 다만 선회 반경은 좀 크다.
  4. 소음
    소음 차폐가 잘된다. 창문 열고 닫았을때 들리는 엔진 소리 크기 차이는 확실히 느껴진다. 자체 내부에 노이즈 캔슬링 기능이 있어서 외부 소음을 중화시킨다고 하는데 이 기능을 끌줄 몰라(끄는 기능이 없나?) 캔슬링 성능은 알 수 없다.
  5. 4륜 구동
    4가지 일반 주행 모드 외에 눈길/진흙/모래밭 등 4륜 구동 설정이 가능한데 내가 뭐 차 전문가나 매니아가 아니다보니 각각 주행시 어떤 차이가 있는지는 잘 느끼지 못한다. 평소 아반떼로 힘들게 올라가던 동네 오르막길도 쉽게 잘 올라가는데 이게 엔진때문인지 4륜 때문인지는 잘 모르겠다. 주행 모드를 다이알 돌리며 변경할 수 있는데 이게 좀 간지가 난다. 다이알 돌리면서 마음속으로 외쳐본다.
    아스라다! 스포츠 모드 체인지! 이제 터레인으로 전환! 스노우 모드 체인지!’
  6. 2열 승차감
    아내는 SUV라 그런지 승용차 대비 좀 불편하긴 하다고 한다. 난 눈에 꽁깍지가 씌인건지는 모르겠지만 별 불만 없이 잘 타는 중이다.

주행 보조

  1. HUD (Head Up Display)
    그냥 호기심에 달아봤다. 계기판 정보가 그냥 앞 유리에 표시되어서 전방주시에 조금 도움이 되는 정도의 보조 기구다. 그러나 HUD에 표시되는 정보가 대부분 눈만 살짝 아래로 내리면 볼수 있는 계기판 정보들이라 운전에 익숙한 사람은 HUD의 필요성을 크게 못느낄 확률이 높다. 미사일 조준할것도 아니고….. (뚜뚜뚜두~~~ 삐~~~~)
    하지만 네비게이션과 연동되는 순간 신세계가 펼쳐진다. 네비게이션 화면에 표시되는 길 안내 정보에 신경 쓰다 보면 전방주의를 소홀히 할 수 밖에 없는데 HUD와 네비게이션이 연동되는 순간 네비게이션 자체를 볼 필요가 없어진다. HUD에 교차길, 남은 거리, 주의사항들이 다 표시되니 전방에 집중하기 정말 좋다. 아쉬운건 차량의 순정 네비게이션 외에는 연동이 불가능하다. 그래서 Apple Carplay를 쓰는 사람에겐 실효성이 많이 떨어진다. 나는 HUD와 Car Play 사이에서 고민하다가 결국 HUD의 손을 들어주고 순정 네비게이션을 쓰고 있다.
  2. 후측방 알림 및 충돌 방지
    주행중 후측방에 차량이 존재하거나 고속으로 다가오는 차량을 적정 레벨 수준으로 알려주는 기능이다. 충돌 방지 기능까지 켜면 충돌할거 같을때 강제로 방향 전환을 한다. 처음 사용할때 많이 어색했다. 난 가만히 있는데 막 경고음 울리고 핸들 진동하고 급하다 싶으면 핸들이 혼자 꺽이고 막 이러니 히밤 이게 뭐야? 하다가 익숙해지면 음음….. 오거를 운전하는 카가의 심정이 이랬으려나? 하는 생각이든다.
  3. 차선 유지 보조/보정
    후측방 충돌 방지랑 비슷하다. 차가 차선을 벗어날거 같으면 경고음이 울리고 넘어가면 핸들을 자동으로 틀어 차선을 유지시켜준다. 이것도 처음엔 어색하다. 난 그냥 적당히 차선 아슬하게 밟으며 여유있게 턴하고 싶은데 차가 자꾸 차선 지키라고 핸들을 압박한다. 처음 사용할땐 차량의 핸들링 보정에 반감이 들다가 익숙해지면 핸들링 보정이 없으면 불안해진다.
  4. 크루즈 컨트롤
    속도 유지, 차량 간격 유지, 차선 유저 정도까지 된다. 차선 변경 기능은 없다. 서울-양양 고속도로 구간에서 매우 편리하게 써먹었다. 크루즈 컨트롤에 좀 회의적인 입장이었는데 실제 써보고 인식이 많이 바뀌었다.

기타 편의 기능

  1. 서라운드 뷰
    정말 잘 쓰고 있는 기능이다. 차량 전후좌우에 달린 카메라의 이미지를 조합하여 하늘에서 차량과 주변을 내려다 보는 스카이뷰를 만들어 준다. 대형차량에겐 이제 필수적인 기능이 아닐까 한다. 초기 차체에 익숙하지 않을 때부터 익숙해진 지금까지도 계속 잘 쓰고 있다. 후방주차, 측면주차, 넓이가 아슬아슬한 골목 지나갈 때 등등 정말 유용하다. 스카이뷰 외에도 각 카메라별로 시점을 변경해 볼수 있어서 좁은공간 주차할때 정말 유용하다.
  2. 원격 제어
    블루링크 가입을하면 (5년 무료고 이후엔 매월 돈을 내야한다고 한다.) 핸드폰 앱으로 차량을 원격 제어할수 있다. 차량 상태정보 확인과 잠금장지 및 시동 컨트롤 정도이다. 일반적으로 겨울에 원격으로 시동 걸어서 차 덮혀 놓는 용도로 쓰인다고 한다. 그외에는? 음?
  3. Auto Hold
    오토 홀드 모드를 켜면 차가 완전 정차된후에는 브레이크를 발에서 떼어도 정차가 유지된다. 호불호가 좀 갈릴거 같다. 난 쓰지 않고 있다.
  4. 버튼식 기어/추가 수납 공간
    버튼식 기어는 익숙해지는데 좀 시간이 걸렸다. 확실하게 호불호가 갈릴거 같다. 나는 잘 적응중이다. 버튼식 기어가 채택되면서 기어봉 부품들이 들어가야 할 자리가 추가 수납 공간이 되었다. 운전석 우측 수납공간 하부에 2층 구조로 추가 수납 공간이 있다. 껌, 선글라스 등 운전 보조 용품 넣기 좋다.
  5. 후석 승객 알림
    차에서 내릴때 후석에 사람이 있으면 후석에 승객이 있다고 한번 더 알려준다. 여름에 아이를 깜빡하고 후석에 두고 내려 발생하는 사고를 방지하는 기능이라고 한다. 차문을 잠글경우 후석에서 움직임이 느껴지면 요란하게 경고음을 울리고 블루링크가 연결되었을경우 핸드폰으로 문자를 보내준다. 아내와 애들 차에 두고 과자 사러 갈때 깜빡하고 차문 잠갔다가 미친듯이 경고음 울리고 핸드폰 울려대서 깜짝 놀랐던 적이 있다.
  6. 정차시 후측방 차량 알림
    정차하고 내릴때 후측방에 접근하는 차량이 있으면 경고가 울린다. 뭐 다들 알만한 안전기능이다.
  7. Apple Car Play/Android Auto
    지원은 되는데 무선으로되진 않는다. 충전잭으로 연결해야지만 사용 가능하다.
  8. 핸드폰 무선 충전
    핸드폰 무선 충전이 가능한데 Car Play랑 같이 쓰면 잭으로도 충전하고 무선 충전으로도 충전하는 사태가 벌어진다. 내 아이폰은 구형이라 무선충전이 되지 않아서 아내가 쓰고 있다.
  9. 통풍 시트
    1열 운전석/보조석이 통풍 시트다. 그냥 엉덩이에서 시원한 바람이 나오는 기능……
  10. 차일드 모드
    차일드 모드 켜면 2열 문과 창문을 내부에서 열수 없다. 깜빡하고 차일드 모드 해제 안하고 혼자 차에서 내렸다가 아내한테 혼난적 많다. “야아아아아! 문열어어어어어줘어어어어어!!”
  11. 그외 스마트키, 패들 쉬프트, 테일 게이트 등등
    요즘 이런거 없는 차량이 없으니…..생략…

결론

가성비가 정말 좋다. 대만족 중이다. (고장만 나지 마라.)

그외

나의 연애와 결혼생활과 육아를 함께했단 9년차 아방이를 떠나 보내기전에 찍은 한컷. 이제는 다른 주인을 섬기며 제 2의 차생을 보내고 있다.

나의 연애와 결혼생활과 육아를 함께했던 9년차 아방이를 떠나 보내기전에 찍은 한컷. 이제는 다른 주인을 섬기며 제 2의 차생을 보내고 있다. (얘는 썬루프가 있다.)

아빠의 속타는 마음을 모르고 나뭇가지로 새차 강도 테스트 중인 둘째

아빠의 속타는 마음을 모르고 나뭇가지로 새차 강도 테스트 중인 둘째

창피한 이야기 – Database Connection Timezone 설정

요즘 Kafka로 들어오는 메세지들을 여러 저장소이 Flush해주는 데몬형태의 Java Web Service를 만들고 있다. 이 놈의 주요 기능중 하나는 Kafka메세지를 선별해서 MySQL에 저장해주는 것이다.
(그럴거면 스파크를 쓰는게 낫지 않냐 할수 있지만 관리자님께서 이리 하라 시키셨다.)

내가 Java로 Web Application을 개발할 때 고민거리 중 하나는 Database의 Timezone 설정이다. Java의 Date가 Timezone을 원활히(?) 지원하지 않다보니 Database와 Web Application의 Timezone이 다르게 설정된 경우 날짜 데이터가 꼬이기 마련이다. 특히, 그 차이를 무시하고 그냥 사용할 경우 Database의 Timestamp 기능을 사용하여 create_time 정보를 남기면 헬게이트가 열린다. 어째 내가 Aplication찍은 시간과 Database에서 찍어준 시간이 안맞는 괴이한 현상이 발생한다. 물론 이건 괴이한게 아니라 당연한거다.

그래서 나는 Web Application과 Database의 Timezone을 항상 동일하게 맞춘다. 그럼 여러 Web Application이 동일하지 않은 Timezone을 사용할 경우 어떻게 할건데? 의문이 생기지만 그럴 경우는 Database 기준으로 시간을 조절하고 내부 로직에서 시간 변경을 하여 처리하면 된다는 식으로 넘어갔다. 아니면 Timestamp를 long type으로 Database에 저장하고 Application에서 자기 타임존에 맞춰 Date 객체로 변환해 쓰는 방식을 사용했다.

그러다가 최근 AWS로 넘어오면서 문제가 발생했다. AWS RDB서비스에서 MySQL을 사용하는데 Database의 Global Timezone 변경 권한을 주지 않는다. 그냥 UTC로 설정된 MqSQL이 나에게로 넘어왔다.

이걸 어쩌나 하고 심각하고 고민하고 있었는데, 알고보니 Database Timezone은 신경 끄고 DB Connection Session의  Timezone을 Application에 맞춰 설정하면 간단히 해결 되는 문제였다.

내가 이제 11년차 개발자인데 이걸 여태 몰랐었다니!

이 간단한걸 몰라 그동안 Database Timezone을 Wab Application과 맞추고 여러 Application에서 사용해야 할 시간 정보는 long type으로 저장해버렸던 지난 내 개뻘짓거리를 생각하니 자괴감이 밀려왔다.

어쨌든 아래와 같은 방법으로 모든 문제가 해결되었다. 그리고 머리 속 깊게 들어온 정신적 치명타는 아직 회복이 되지 않고 있다.

  1.  Spring Boot + Mysql 5.x 기준 DataSource 설정
    1. DB Table 과 JPA Entity 설정
      1. MySQL경우 날짜 컬럼의 Type은 TIMESTAMP여야 한다. DATETIME으로 하면 제대로 작동하지 않는것 같다. 초 이하 단위까지 남기고 싶으면 TIMESTAME(3) 으로 밀리세컨드 혹은 그 이하 까지 기록할 수 있다.
      2. JAP Entity경우 아래와 같이 컬럼 설정을 제대로 맞춰주자. 뺏을 경우 어떻게 되는지 테스트 안해봄. 닥치고 매뉴얼에서 하라는대로 하자!
        @Column(name = "log_time")
        @Temporal(TemporalType.TIMESTAMP)
        Date logTime;
    2. DataSource 설정
      @Configuration
      public class DataSourceConfig {
      
          @Bean
          @Primary
          public DataSource dataSource(){
              PoolProperties p = new PoolProperties();
              blabla...
      
              //tomcat datasource를 가져다 쓰는 이유는 묻지 말자......
              org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
              dataSource.setPoolProperties(p);
      
              //이거 두줄이면 모든 고민이 끝나는 거였다.
              Calendar cal = Calendar.getInstance();
              dataSource.setInitSQL("set @@session.time_zone = '$TIMEZONE'".replace("$TIMEZONE", cal.getTimeZone().getID()));
              return dataSource;
          }
      
      }
      

DataSource 등록시 setInitSQL로 세션 타임존만 한번 선언해 주면 모든것이 해결되는 일이었다. 이제 Database timezone이 뭐로 설정되어 있던 신경 안써도 된다.

Database마다 session timezone 설정 명령은 다르니 application.yml의 db className과 함께 initial Query도 프로퍼티로 관리하면 더 편리할거 같다.

2. 사용 결과

  • JPARepository : 당연히 잘된다. 한국 시간인 Application에서 현재 시간을 찍어보니면 Database에 UTC로 변환되에 들어가 있다.
    public interface JpaLogRepository extends JpaRepository<MyLog, Long> {
        List<MyLog> retrieveDigestList(@Param("ownerId") Long ownerId);
    }
  • Custom Query : 넣는 데이터가 너무 많다 보니 JPARepository로 한건 한건 넣다가는 답이 나오지 않아서 그냥 Custom Query로 한번에 수십개씩 넣고 있다. 이런 경우에도 문제없이 잘 변환 되어 들어간다.
    //이건 블로그를 위해 샘플로 만든거다. 내가 업무용으로 개발한거랑은 당연 다르다.
    @Repository
    public class BulkDataRepository {
        static final Logger logger = LoggerFactory.getLogger(BulkDataRepository.class);
    
        @PersistenceContext
        EntityManager entityManager;
    
        @Transactional
        public int bulkInsertInvalidLog(List<MyLog> logs){
            if(logs.size() < 1) return 0;
    
            StringBuilder sql = new StringBuilder();
            sql.append("insert into my_log (payload, log_time) values");
            for(int i = 0 ; i < logs.size(); i++){
                sql.append("(?,?)");
                if(i != logs.size()-1){
                    sql.append(", ");
                }
            }
    
            Query query = entityManager.createNativeQuery(sql.toString());
            int position = 0;
            for(int i = 0 ; i < logs.size(); i++){
                MyLog log = logs.get(i);
                query.setParameter(1+position, log.getPayload());
                query.setParameter(2+position, log.getLogTime());
                position += 2;
            }
            return query.executeUpdate();
        }
    }

 

아 씁쓸해……

Servlet filter code for Spring-Boot Gzip request

web-server를 별도로 사용하지 않고 부트로면 서비스 할때 Gzip request 지원을 하기 위한 서블릿 필터..

package net.abh0518.spring.boot.test.filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.zip.GZIPInputStream;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GzipRequestFilter implements Filter {

private static final Log logger = LogFactory.getLog(GzipRequestFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("Init GzipRequestFilter");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) request;
String encoding = httpReq.getHeader("Content-Encoding");
if(encoding != null){
if(encoding.toLowerCase().contains("gzip")){
request = new GZIPServletRequestWrapper(httpReq);
}
}

chain.doFilter(request, response);
}

@Override
public void destroy() {

}

private class GZIPServletRequestWrapper extends HttpServletRequestWrapper{

public GZIPServletRequestWrapper(HttpServletRequest request) {
super(request);
}

@Override
public ServletInputStream getInputStream() throws IOException {
return new GZIPServletInputStream(super.getInputStream());
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(new GZIPServletInputStream(super.getInputStream())));
}
}

private class GZIPServletInputStream extends ServletInputStream{
private InputStream input;

public GZIPServletInputStream(InputStream input) throws IOException {
this.input = new GZIPInputStream(input);
}

@Override
public int read() throws IOException {
return input.read();
}

@Override
public boolean isFinished() {
boolean finished = false;
try {
if(input.available() == 0){
finished = true;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return finished;
}

@Override
public boolean isReady() {
boolean ready = false;
try {
if(input.available() > 0){
ready = true;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return ready;
}

@Override
public void setReadListener(ReadListener listener) {}
}
}

Python, Django Model을 main 루틴에서 에서 사용하기(?), Django Model을 서버 구동 없이 사용하기(?)

django로 어플리케이션 만들면서 종종 귀찮은 DB 작업들을 해야할 떄가 있는데 (데이터 삭제, 변경 등등…) DBA가 아니다보니 SQL작성이 매우 귀찮다.
게다가 django덕분에 ORM에 익숙해져 버리니 가뜩이니 사이 안좋은 SQL과는 더욱 사이가 나빠져 있는 상태가 되어 있다.
그래서 난 python 코드로 Django Model을 돌려 DB작업을 하기로 했다.

Sample

import os
import django
from django.db import transaction

# django setting 파일 설정하기 및 장고 셋업
cur_dir = os.path.dirname(__file__)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_application.settings")
django.setup()

# 모델 임포트는 django setup이 끝난 후에 가능하다. 셋업 전에 import하면 에러난다. db connection 정보가 없어서......
from my_application.models import MyModel

@transaction.atomic
def update_my_model_data():
    datas = MyModel.objects.all()
    for data in datas:
        # 하고 싶은거 하고 
        data.save()

if __name__ == "__main__":
    update_my_model_data()

결론

SQL 안써도 되서 행복해요.

Python, datetime timezone 삽질 정리

java의 DateTime은 그냥 생성해도 로컬 타임존으로 설정이 되어있어 타임존 변경이 쉬운데 파이썬은 그렇지 않다. (python version 3.5.2 기준)
그냥 datetime을 생성하면 timezone 정보가 없어 astimezone 같은 메소드를 실행하면 에러가 난다.
장고 어플리케이션 만들다가 타임존 처리를 할게 있었는데 이러한 이유 때문에 은근 삽질을 많이 했다.
결론은 datetime 생성할떄 timezone과 timedeltal를 이용해서 필요한 타임존 설정을 해주면 모든게 해결된다. (근데 귀찮다)

Sampel Code

from datetime import timezone, timedelta, datetime

timestamp = time.time()
print("# timestamp를 찍어본다.")
print("time.time() => %f \n" % timestamp)

print("# timestamp를 datetime으로 변환한다.")
print("# 이 짓은 dt = datetime.now() 또는 dt = datetime.utcnow() 랑 똑같다.")
print("# 이경우 로컬 시간으로 출력 되긴 하는데 타임존 정보는 없다.")
dt = datetime.fromtimestamp(timestamp)
print("datetime.fromtimestamp(timestamp) => %s \n" % dt)

print("# datetime 생성시 timezone 정보를 넣어주면 timestamp를 해당 타임존에 맞는 시간으로 변환해 준다.")
print("# 이 짓은 dt = datetime.now(tz)랑 똑같다.")
utc_timezone = timezone.utc
dt_utc = datetime.fromtimestamp(timestamp, utc_timezone)
print("datetime.fromtimestamp(timestamp, utc_timezone) => %s " % dt_utc)
tz = timezone(timedelta(hours=9))
dt_9 = datetime.fromtimestamp(timestamp, tz)
print("datetime.fromtimestamp(timestamp, 9_timezone) => %s \n" % dt_9)

print("# 어쨋든 타임존 설정이 되어있는 datetime의 경우 쉽게 로컬 타임존을 찍을 수 있다.")
dttz = dt_utc = datetime.fromtimestamp(timestamp, timezone.utc)
print("datetime.astimezone() => %s \n" % dttz.astimezone())

print("# 결론 : datetime 생성시에 timezone 넣는걸 습관 화 하면 timezone 변환은 astimezone()으로 쉽게 사용할 수 있다.")
dt = datetime.now(timezone.utc)
print("dt = datetime.now(timezone.utc) => %s" % dt)
print("dt.astimezone() => %s" % dt.astimezone())
tz = timezone(timedelta(hours=7))
print("dt.astimezone(7_timezone) => %s" % dt.astimezone(tz))

Result

# timestamp를 찍어본다.
time.time() => 1488520605.554720 

# timestamp를 datetime으로 변환한다.
# 이 짓은 dt = datetime.now() 또는 dt = datetime.utcnow() 랑 똑같다.
# 이경우 로컬 시간으로 출력 되긴 하는데 타임존 정보는 없다.
datetime.fromtimestamp(timestamp) => 2017-03-03 14:56:45.554720 

# datetime 생성시 timezone 정보를 넣어주면 timestamp를 해당 타임존에 맞는 시간으로 변환해 준다.
# 이 짓은 dt = datetime.now(tz)랑 똑같다.
datetime.fromtimestamp(timestamp, utc_timezone) => 2017-03-03 05:56:45.554720+00:00 
datetime.fromtimestamp(timestamp, 9_timezone) => 2017-03-03 14:56:45.554720+09:00 

# 어쨋든 타임존 설정이 되어있는 datetime의 경우 쉽게 로컬 타임존을 찍을 수 있다.
datetime.astimezone() => 2017-03-03 14:56:45.554720+09:00 

# 결론 : datetime 생성시에 timezone 넣는걸 습관 화 하면 timezone 변환은 astimezone()으로 쉽게 사용할 수 있다.
dt = datetime.now(timezone.utc) => 2017-03-03 05:56:45.554877+00:00
dt.astimezone() => 2017-03-03 14:56:45.554877+09:00
dt.astimezone(7_timezone) => 2017-03-03 12:56:45.554877+07:00

Linux expect로 script나 command에 패스워드 자동 입력 처리 하기

Ansible로 배포 스크립트를 만드는데 일부 작업에서 사용하는 모듈과 스크립트들이 종종
prompt로 패스워드나 이런저런 의사를 물어보는 경우가 있다.
한두번이야 대충 적당히 입력해 주겠는데 배포해야할 서버가 늘어날수록 여간 귀찮은게 아니다.
그래서 자동입력을 어떻게 해야하나 찾아보니 expect란 애를 쓰면 해결이 된다고 한다.

설치

$ apt-get install expect # 간단하네! (맥은 brew….)

작성 예

패스워드 프롬프트가 뜨면 파라메터로 넘긴 패스워드를 입력하고
ssh connect 연결 yes/not를 물어보는 프롬프트가 뜨면 yes를 입력해주는 예제

$ vi auto_password.exp
--------------------------
#!/usr/bin/expect

set timeout -1
set password [lindex $argv 0]
spawn ansible-playbook -k -i $host $playbook

expect {
      "password: " { # 프롬프트에 password: 항목이 뜨는 경우 
          send "$password\r"
          exp_continue # expect 가 반복되서 처리된다.
      }
      "connecting (yes/no)?" { # 서버에 연결 할지 물어보는 경우
          send "yes\r"
          exp_continue
      }
}
--------------------------
$ chmod 755 auto_password.exp

실행

./auto_password.exp $my_password

터미널 작업시

ssh 자동 접속 후 terminal 작업을 해야할때는 마지막에 interact를 붙인다.

#!/usr/bin/expect

set timeout -1
set password [lindex $argv 0]
spawn ssh $target_host

expect {
      "password: " { 
          send "$password\r"
      }
}

interact

끝!

— 추가 분 —