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

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외 관련 코드들의 변화나 테스트 코드의 증가는 감수하겠다는 각오(?)가 필요하다.

창피한 이야기 – 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

끝!

— 추가 분 —

Python, Decorator 써본 이야기

어쩌다 python-django 로 웹 어플리케이션 하나를 만들게 되었는데 이거 은근 신세계다. django 같은 프레임워크야 뭐 많으니까 놀랄게 없더라도 python의 decorator는 나에게 뭔가 신세계를 보여줬다. java-spring에서 그렇게 복잡했던 AOP가 decorator를 쓰면 그냥 별 고민 없이 끝나버린다. 이걸 AOP라고 해도 될런지 모르겠지만 말이다. 여튼 decorator에 감탄한 나머지 까먹기 전에 decorator 파트만 정리해본다.

일반 데코레이터

  1. 이렇게 코딩하고 실행하면
    # 얘가 데코레이터
    def decorator(func):
        def decorator(*args, **kwargs):
            print("%s %s" % (func.__name__, "before"))
            result = func(*args, **kwargs)
            print("%s %s" % (func.__name__ , "after"))
            return result
        return decorator
    
    # 함수에 데코레이터를 붙여준다.
    @decorator
    def func(x, y):
        print(x + y)
        return x + y
    
    func(1,2)
  2. 이런 결과가 나온다. 아! 신통방통 하다!
    func before
    3
    func after
  3. @데코레이터는 사실 이거랑 같은 의미라고 한다
    def decorator(func):
        def decorator(*args, **kwargs):
            print("%s %s" % (func.__name__, "before"))
            result = func(*args, **kwargs)
            print("%s %s" % (func.__name__ , "after"))
            return result
        return decorator
    
    def func(x, y):
        print(x + y)
        return x + y
    
    func2 = decorator(func)
    func2(1,2)

파라메터를 가지는 데코레이터

  1. 데코레이터에 뭔가 파라메터를 전달하고 싶을데는 약간 복잡하긴 하지만 역시 다 된다! function을 감싸는 decorator를 다시 감싸주면 된다.
    # 얘가 파라메터도 붙는 데코레이터
    def decorator_with_param(param):
        def wrapper(func):
            def decorator(*args, **kwargs):
                print(param)
                print("%s %s" % (func.__name__, "before"))
                result = func(*args, **kwargs)
                print("%s %s" % (func.__name__ , "after"))
                return result
            return decorator
        return wrapper
    
    @decorator_with_param("hello, decorator!")
    def func(x, y):
        print(x + y)
        return x + y
    
    func(1,2)
  2. 결과는 이렇게 나온다!
    hello, decorator!
    func before
    3
    func after

그런데 func.__doc__이 나오지 않는다. 아,  망했다!

  1. 원랜 이렇게 나와야한다. 그래야 Swagger UI 같은애랑 붙일때 자동으로 문서화가 된다.
    def func(x, y):
        """
        x와 y를 더합니다.
        :param x:
        :param y:
        :return:
        """
        print(x + y)
        return x + y
    
    print(func.__doc__)
    
    ---- 출력 ----
    
     x와 y를 더합니다.
     :param x:
     :param y:
     :return:
  2. 그런데 데코레이션을 붙이는 순강 망한다. __doc__이 안나온다. 실제로 django api application을 만들면서 api endpoint 메소드들을 decorator로 신나게 감쌌더니 Sweager UI에서 doc 처리하지 못해 공백 API 가이드만 한가득 나왔다.
    def decorator(func):
        def decorator(*args, **kwargs):
            print("%s %s" % (func.__name__, "before"))
            result = func(*args, **kwargs)
            print("%s %s" % (func.__name__ , "after"))
            return result
        return decorator
    
    @decorator
    def func(x, y):
        """
        x와 y를 더합니다.
        :param x:
        :param y:
        :return:
        """
        print(x + y)
        return x + y
    
    print(func.__doc__)
    
    ---- 출력 ----
    None # 아, 망했어요!
  3. 생각해보면 당연한 일이다. 실행시간에 실제로 접근하는 메타데이터는 func가 아니라 데코레이터가 만들어준 wrapper의 메터데이터니 제대로 나올리가 없다. 그렇다. 우린 망했다.
  4. 그렇다고 진짜 망한건 아니다. decorator에 @wraps 달아주면 모든것이 해결된다. 모든 decorator에는 반드시 @wraps를 달아주자. 그것이 모두가 행복해지는 길이다. 이유는 찾아보기 귀찮아서 생략. (대충 소스 보니 func의 __doc__ 같은 meta 정보를 wrapper에 복사해 넣는거 같은데 확실한건 아님!)
    from functools import wraps
    
    # 파라메터 없는 데코레이터에도 @wraps 붙여주고
    def decorator(func):
        @wraps(func)
        def decorator(*args, **kwargs):
            print("%s %s" % (func.__name__, "before"))
            result = func(*args, **kwargs)
            print("%s %s" % (func.__name__, "after"))
            return result
        return decorator
    
    
    # 파라메터 있는 데코레이터에도 @wraps 붙여주고
    def decorator_with_param(param):
        def wrapper(func):
            @wraps(func)
            def decorator(*args, **kwargs):
                print(param)
                print("%s %s" % (func.__name__, "before"))
                result = func(*args, **kwargs)
                print("%s %s" % (func.__name__ , "after"))
                return result
            return decorator
        return wrapper
    
    
    @decorator
    def func(x, y):
        """
        x와 y를 더합니다.
        :param x:
        :param y:
        :return:
        """
        print(x + y)
        return x + y
    
    
    @decorator_with_param("hello, decorator!")
    def func2(x, y):
        """
        x와 y를 더합니다.
        :param x:
        :param y:
        :return:
        """
        print(x + y)
        return x + y
    
    
    print(func.__doc__)
    func(1, 2)
    print(func2.__doc__)
    func2(1, 2)
  5. 실행하니 잘 나오네!
      x와 y를 더합니다.
     :param x:
     :param y:
     :return:
     
    func before
    3
    func after
    
     x와 y를 더합니다.
     :param x:
     :param y:
     :return:
     
    hello, decorator!
    func2 before
    3
    func2 after
    
    

class로도 decorator 선언이 가능하다고도 합니다.

class로 만드는게 뭔가 낙타표기도 되고 그래서 뭔가 그 뭔가 멋져보이는거 같은데 여기엔 치명적인 단점이 있다. @wraps를 붙일수가 없다. 그나마 parameter를 가지는 데코레이터의 경우 __call__ 시점에서 wrapper를 만들면서 @wraps를 붙여줄수 있는데 parameter가 없는 데코레이터의 경우 wraps를 붙일 방법이 보이질 않는다. 이거 저거 찾아보니 결국 __doc__, __name__들을 복사해 넣는데 이럴거면 그냥 function으로 데코레이터 만들란다.

from functools import wraps

# 그냥 Class 데코레이터, @wraps를 붙일만한데가 보이지 않는다.
class Decorator:
    def __init__ (self, func):
        self.func = func

    def __call__ (self, *args, **kwargs):
        print("%s %s" % (self.func.__name__, "before"))
        result = self.func(*args, **kwargs)
        print("%s %s" % (self.func.__name__, "after"))
        return result


# 파레매터를 가지는 Class 데코레이터, __call__에서 @wraps를 넣어준다.
class DecoratorWithParam:
    def __init__ (self, param):
        self.param = param

    def __call__ (self, func):
        @wraps(func)
        def decorator(*args, **kwargs):
            print(self.param)
            print("%s %s" % (func.__name__, "before"))
            result = func(*args, **kwargs)
            print("%s %s" % (func.__name__, "after"))
        return decorator


@Decorator
def func(x, y):
    """
    x와 y를 더합니다.
    :param x:
    :param y:
    :return:
    """
    print(x + y)
    return x + y

@DecoratorWithParam("hello, decorator!")
def func2(x, y):
    """
    x와 y를 더합니다.
    :param x:
    :param y:
    :return:
    """
    print(x + y)
    return x + y


print(func.__doc__)
func(1,2)
print(func2.__doc__)
func2(1,2)

---- 출력 ----
None # 아, 이거 짜증나네
func before
3
func after

 x와 y를 더합니다.
 :param x:
 :param y:
 :return:
 
hello, decorator!
func2 before
3
func2 after

끝!

git log graph 설정

vi ~/.gitconfig

[alias]
lg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all
lg2 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n''          %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all
lg = !"git lg1"

인터넷 뒤져서 나온 제일 맘에 드는 설정……

Android – Intent extras size limit (android.os.TransactionTooLargeException) 문제 회피한 이야기

안드로이드 어플리케이션 개발을 하다보면 종종 Activity나 Service에 Intent의 Extra로 데이터를 전달하는데 뭔가 애매한 경우가 있다. 그냥 Intent Extra로 ID나 쿼리 값만 전달하고 각 화면에서 필요한 시기에 데이터를 불러와도 되긴 하지만 어플리케이션 특성상 서버에서 데이터를 가져오는 경우가 많아 좋지 상황이 발생하는 경우도 있다. 특히 Activity에서 데이터를 불러오는 동안 UI들이 공백 상태로 상태로 노출되어 영 보기가 좋지 않고 불러오기가 실패했을 경우의 처리는 역시 애매하다 사실 실패의 경우는 화면마다 재시도 UI 만들기가 귀찮다.

그래서 나는 이전 화면이나 로딩화면으로 잠깐 나왔다 사라지는 Fake Activity에서 데이터를 불러온 뒤 Intent Extra로 데이터 자체를 넘겨 주는 방식을 선호한다. 불러오기를 실패했을 경우엔 그냥 다시 시도해주세요 메세지 한번 띄워주면 그만이니 실패 처리도 간단해진다. 그래서 내가 만든 어플리케이션에서는 Intent의 Extra에 데이터 객체를 담아 던져주는 경우가 많다. 물론 Java premetive type 밖에 담지 못하는 Intent의 특성상 데이터 객체는 반드시 Binary나 json으로 Serialize해야한다. serialize/deserialize 하는 비용 문제가 있긴 하겠지만 뭐 서버 사이드도 아니고 클라이언트에서 크게 문제될 일이 없었다.

한동안은 별 무리 없이 이 방법을 잘 썼다. 한가지 예외사항에 부딪히기 전까지는 말이다. 특정 상황에 데이터 객체의 크기가 생각 이상인 경우가 있었고 결국 Intent가 전달할 수 있는 사이즈를 넘어 버려 ‘android.os.TransactionTooLargeException’ 발생하며 앱이 죽는 경우가 생겼다. 이걸 어찌하나 하고 구글링을 좀 하다가 그냥 DataHolder를 만들어 쓰면 괜춘하다는 글을 봤다.

나름 어떻게 적용해 볼까 고민하다가 아래처럼 해봤다.

  1. DataHolder 클래스 생성 (둘중 뭐가 좋을지는 아직 잘 모르겠음)
    1. Application 클래스에 추가하거나….
      public class MyApp extends Application {
          private Map<String, Object> mDataHolder = new ConcurrentHashMap<>();
          
          public String putDataHolder(Object data){
              //중복되지 않는 홀더 아이디를 생성해서 요청자에게 돌려준다.
              String dataHolderId = UUID.randomUUID().toString();
              mDataHolder.put(dataHolderId, data);
              return dataHolderId;
          }
      
          public Object popDataHolder(String key){
              Object obj = mDataHolder.get(key);
              //pop된 데이터는 홀더에서 제거
              mDataHolder.remove(key);
              return obj;
          }
      }
    2. 별도의 DataHolder 스태틱 클래스를 추가하거나….
      public class DataHolder {
          private static Map<String, Object> mDataHolder = new ConcurrentHashMap<>();
          
          public static String putDataHolder(Object data){
              //중복되지 않는 홀더 아이디를 생성해서 요청자에게 돌려준다.
              String dataHolderId = UUID.randomUUID().toString();
              mDataHolder.put(dataHolderId, data);
              return dataHolderId;
          }
      
          public static Object popDataHolder(String key){
              Object obj = mDataHolder.get(key);
              //pop된 데이터는 홀더를 제거
              mDataHolder.remove(key);
              return obj;
          }
      }
  2. 데이터를 보내는 측(대충 이런느낌으로?)
    BigData bigData = .......;
    Intent intent = new Intent(....);
    String holderId = DataHolder.putDataHoler(bigData);
    intent.putExtra("holderId", hoderId);
    startActivity(intent);
  3. 데이터를 받는 Activity (도 대충 이런 느낌으로)
    String holderId = getIntent().getStringExtra("hoderId");
    BigData bigData = (BigData)DataHolder.popDataHolder(hoderId);

매우 간단하고 별거 아닌 꼼수인데 매우 잘 동작하고 편하고 결과도 좋다.

치명적인 단점은 당연하겠지만 Application 내에서만 작동한다는 것이다. Applicatoin간의 Intent 호출을 할 경우에는 당연히 망한다. 받는 측에서 Null Pointer Exception이 뙇! 하기도 전에 요청하는 측에서 DataHolder가 뭔지도 모르겠지……. 그러려면 DataHolder를 외부 서비스로 만들어 볼까? …근데 그 서비스로는 데이터 객체를 어떻게 전달하나….. Intent? 그게 안되서 이렇게 한건데? 역시 꼼수는 꼼수일 뿐……

외부 전달시의 꼼수는 file로 저장한 다음 uri만 던져주는 방식으로 하면 된다고 한다. 근데 뭐 지금은 그럴일 없으니 그런 구현은 생략!

참고 사이트 : https://www.neotechsoftware.com/blog/android-intent-size-limit

Android Gradle Build Setting 정리

안드로이드 프로젝트 진행하며 사용한 그래들 빌드 세팅 정리

Application Bulid 설정

  1. 프로젝트의 gradle 버전 업데이트
    1. 프로젝트 root디렉토리의 build.gradle 파일 수정하여 gradle 버전을 1.5 이상으로 올려준다. 프로젝트를 module 단위로 관리할것 아니면 상관 없지만 그냥 기분으로라도 올려준다.
      buildscript {
          repositories {
              jcenter()
          }
          dependencies {
              //그래들 버전이 1.5 이상이어야 module의 aar 파일명 변경(baseName) 옵션이 정상 동작 한다
              classpath 'com.android.tools.build:gradle:1.5.0'
          }
      }
  2. app/build.gradle파일 수정 및 Flavor 리소스 분리
    1. defaultConfig에 setProperty로 ouput 파일의 이름을 설정해준다. 파일 이름이 versionName과 versionCode가 추가되도록 하면 빌드 후 apk별 버전 관리가 쉬워진다.
    2. Flavor를 추가한다. dev, staging, product 세개의 Flavor로 나누가 각각 개발용, QA용, 상용 배포용 으로 나누어 빌드 할 수 있도록 한다. 대충 아래같은 느낌적 느낌.
      apply plugin: 'com.android.application'
      
      android {
          compileSdkVersion 23
          buildToolsVersion "23.0.2"
      
          defaultConfig {
              applicationId "net.abh0518.androidbuildsetting"
              minSdkVersion 16
              targetSdkVersion 16
              versionCode 1
              versionName "1.0"
              //apk파일 이름에 version 정보가 들어가도록 내용 추가
              setProperty("archivesBaseName", "app-v${versionName}c${versionCode}")
          }
          buildTypes {
              release {
                  minifyEnabled false
                  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
              }
          }
      
          //빌드별 Flavor 설정을 해준다.
          productFlavors{
              dev{
                  applicationId "net.abh0518.androidbuildsetting.dev"
              }
              staging{
                  applicationId "net.abh0518.androidbuildsetting.staging"
              }
              product{
                  applicationId "net.abh0518.androidbuildsetting.product"
              }
          }
      }
    3. Flavor에 맞게 리소스 파일 분리
      app/src 디렉토리 아래에 flavor과 동일한 이름의 디렉토리들을 생성하고 실행 환경에 따른 리소스 파일을 분리한다. main이 기본이고 동일한 구성으로 변경 사항들만 dev, staging, product쪽에 추가해준다. 리소스들은 빌드시 선택된 Flavor값들이 main리소스에 override된다. 대충 아래와 같은 느낌. Flavor 관련 자세한 설명은 구글 문서의 Flavor 항목을 참고하자. (https://developer.android.com/studio/build/build-variants.html?hl=ko)
      app - AndroidBuildSetting - [~:AndroidStudioProjects:AndroidBuildSetting] 2016-06-09 10-35-32
  3. 빌드
    1. Android Studio를 사용한다면 Build Variant 메뉴를 사용해서 빌드될 Flavor를 선택할 수 있다.
    2. Termianal사용시는 프로젝트 root에서 gradlew assemble${Flaver name}${BuildType} 명령으로 빌드할 수 있다.
      ./gradlew assembledevDebug //dev를 Debug로 빌드
      ./gradlew assemblestagingDebug //staging을 Debug로 빌드 
      ./gradlew assembleproductDebug //product를 Debug로 빌드
      ./gradlew assembleproductRelease // pruduct를 Release로 빌드, 릴리즈의 경우 gradle에 signing 설정이 되어있지 않아면 unsigned apk가 나온다.

Library Module Bulid 설정

다른 파트들과 협업을 하면서 우리의 코드를 안드로이드 라이브러리(aar)파일로 만들어 배포하기 위한 빌드 설정이다.

  1. 프로젝트에 Android Library 모듈을 추가한다.
  2. 버전 관리를 위해 library module의 java source에 Version.java 를 추가해 준다. 이유는 Android Module을 빌드할때는 Application빌드와 달리 Gradle의 versionCode와 versionName 항목들이 무시되기 때문이다. 따라서 별도로 Version정보 클래스를 제공하지 않으면 Library를 사용하는 사람 입장에서 버전을 확일할 방법이 없어진다. 적당한 위치에 아래처럼 만들어주면 된다.
    1. 대충 이런 느낌
      package net.abh0518.applibrary;
      
      public class Version {
          public final static int versionCode = 1;
          public final static String versionName = "1.0.0";
      }
  3. Library 모듈의 build.gradle 파일을 수정 (여기서는 app_library/build.gradle 파일)
    1. Version.java코드에서 버전정보를 읽어들여 output파일 명에 버전 정보를 추가한다.
    2. Application 의 설정과 동일하게 Flavor를 나누어서 관리하면 좋다. 사용자에게 개발버전, QA버전, 상용버전을 나누어서 제공할 수 있으면 좋지 않을까? 단 Flavor를 추가한 이후 defaultPublishConfig를 반드시 설정해야하는데 이것은 같은 프로젝트 내에서 모듈을 include할때 선택할 Flavor가 된다. Android Studio버그인지 빌드 시스템의 문제인지는 모르겠는데 Studio의 Build Vriant에서 선택해 놓은 Module의 Flavor가 참조하는 쪽의 Flaovr와 연결되진 않고 그냥defaultPublishConfig 설정에 맞춰 빌드되어 참조된다. 근데 뭐 난 Library모듈을 프로젝트 내부에서 쓰진 않는 상황이라 무시하고 그냥 dev로 잡아버렸다.
    3. Flavor를 설정했다면 module의 resource 파일 설정도 Application때와 동일하게 설정해 준다.
    4. Library Module의 build.gradle은 대충 이런 느낌….
      apply plugin: 'com.android.library'
      
      // Android Library 프로젝트는 그래들에서 제공하는 버전 코드와 버전 네임 항목을 무시하므로 버전 관리를 위해서 별도 파일 처리를 해준다.
      def moduleVersionCode  = 0
      def moduleVersionName = "0.9.0"
      
      // Java 소스 파일에서 버전 정보를 읽어온다.
      File versionFile = file('src/main/java/net/abh0518/applibrary/Version.java')
      versionFile.eachLine { line ->
          //버전 코드 가져오기
          def group = (line =~ /versionCode( )*=( )*[0-9]+/)
          if(group.hasGroup() && group.size() > 0){
              moduleVersionCode = group[0][0].toString().replaceAll("versionCode( )*=( )*", "")
          }
      
          //버전 네임 가져오기
          group = (line =~ /versionName( )*=( )*\".*\"/)
          if(group.hasGroup() && group.size() > 0){
              moduleVersionName = group[0][0].toString().replaceAll("(versionName( )*=( )*)|\"", "")
          }
      }
      
      android {
          compileSdkVersion 23
          buildToolsVersion "23.0.2"
      
          defaultConfig {
              minSdkVersion 16
              targetSdkVersion 16
              //output 파일 이름 설정
              setProperty("archivesBaseName", "library-v${moduleVersionName}c${moduleVersionCode}")
          }
      
          buildTypes {
              release {
      //            minifyEnabled false
      //            debuggable false
      //            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
              }
              debug{
      //            minifyEnabled false
      //            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                  debuggable true
              }
          }
          productFlavors{
              dev{
              }
              staging{
              }
              product{
              }
          }
          //라이브러리 모듈에 Flavor를 추가했을 경우 아래 옵션으로 기본 Flavor를 설정해 주어야 Android Studio에서 Run이 정상 동작을 한다.
          defaultPublishConfig "devDebug"
      }
      
      dependencies {
          compile fileTree(dir: 'libs', include: ['*.jar'])
      }
  4. 빌드해 봅시다.
    1. Studio에서 별도로 aar로 빌드하는 방법은 찾지 못했다. terminal에서 application을 빌드하면 aar은 무더기로 함께 빌드 되어 있었다. module만 따로 빌드하는 방법은 찾지 못했다. 빌드된 aar파일은  “projectRoot/$moduleDir/build/outputs/aar” 에 있다.
    2. Terminal에서 aar만 따로 빌드 할수 있긴 하다. “./gradlew :${moduleName}:aR” 명령으로 가능하다. aar파일 위치는 A와 동일.

      ./gradleW :app_library:aR
  5. 배포된 aar 사용하기, 사용하는 application의 gradle 설정을 아래처럼 하면 된다고 한다. (귀찮아서 테스트 안해봄)
    dependencies {
        compile 'package.name.of.your.aar:myaar@aar'
    }
    
    repositories{
        flatDir{
            dirs 'libs'
        }
    }

귀찮으니까 그냥 한번에 빌드

  1. 고객쪽에 Application dev, staging, product판과 Library Module dev, staging, product 판을 자주 전달해야하는데 매번 flavor 별로 따로 빌드하는게 귀찮다. project rootdptj shell로 그냥 한번에 끝내자. 대충 아래 느낌으로….
    #!/bin/bash
    
    //ouput 파일을 모아놓을 장소
    package_app_dir="release_app_package"
    package_module_dir="release_module_package"
    
    # Application의 build.gradle 파일에서 버전 정보를 가져온다.
    appVersionCode=$(echo "$fullName"  | sed -n '/versionCode /p' ./app/build.gradle)
    appVersionName=$(echo "$fullName"  | sed -n '/versionName /p' ./app/build.gradle)
    [[ $appVersionCode =~ [0-9]+ ]]
    appVersionCode=${BASH_REMATCH[0]}
    [[ $appVersionName =~ [0-9]+.[0-9+.[0-9]+ ]]
    appVersionName=${BASH_REMATCH[0]}
    
    # 패키지 디렉토리 및 그래들 빌드 환경을 초기화
    rm -rf $package_app_dir
    mkdir $package_app_dir
    rm -rf $package_module_dir
    mkdir $package_module_dir
    ./gradlew clean
    
    # 스테이징 디버그, 상용 디버그, 배포용 빌드
    ./gradlew assembledevDebug
    find ./app -name '*-debug.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradlew assemblestagingDebug
    find ./app -name '*-debug.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradlew assembleproductDebug
    find ./app -name '*-debug.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradlew assembleproductRelease
    find ./app -name '*-unsigned.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradleW :app_library:aR
    find ./app_library -name '*.aar' -exec mv {} ./$package_module_dir/ \;

GitHub Link : https://github.com/abh0518/android_build_setting