스프링 버전 5.3.18 기준으로 작성됨

 

이 글에서는 용어를 다음과 같이 줄여서 표현한다.

OpenEntityManagerInViewFilter => 필터

WebApplicationContext => wac

TransactionSynchronizationManager => tsm

EntityManagerFactory => emf

EntityManager => em

EntityManagerHolder => emh

AsyncRequestInterceptor => ari

 

Spring-orm 에서 OSIV 처리를 담당하는 주요 클래스는 org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter 이다. 이 클래스는 다음과 같이 작동한다.

 

이 클래스는 OncePerRequestFilter 타입이다. request가 들어오면 doFilterInternal 에서 다음을 수행한다.

 

step 1. 현재 필터 자신의 인스턴스에 emf 인스턴스가 존재하는지(초기화되어 있는지) 확인한다. 없으면 wac 에서 emf를 찾아서 세팅한다.

- 이 동작은 필터가 받는 첫 요청 처리에서 이루어질 것이다.

 

step 2. tsm에 emf를 키로 리소스 존재 여부를 확인해서, 존재할 경우 participate = true 를 실행한다.

- tsm에 이 리소스가 존재한다는 것은 이미 이 요청에 대해 생성된 em이 존재한다는 뜻이다. 때문에 글자 그대로 기존 요청에 '참여' 함을 표시한다.

 

step 3. 이 요청이 async request이면서 AsyncManager가 ari 를 가지고 있다면, ari 에 em 을 바인딩한다. (bindEntityManager) 그리고 step 6 으로 넘어간다.

 

step 4. 이 요청이 async request 가 아니거나, async 라도 AsyncManager가 ari 를 가지고 있지 않다면, 다음 과정을 수행한다. (일반적인 mvc요청은 이 방식으로 이루어진다.)

1. emf 로 em을 생성한다.

2. 생성한 em 을 가지는 emh 를 생성한다.

3. tsm 에 bindResource(emf, emh) 를 호출한다.

- tsm의 bindResource 는 내부 쓰레드로컬에 맵을 세팅하여 그곳에 리소스를 저장해둔다. 때문에 이미 request에 대해 생성된 em이 존재하는 경우, tsm를 통해 이를 확인할 수 있도록 한다. (step 2)

4. emf, emh 를 가지는 ari를 생성하고, AsyncManager에 ari를 등록한다.

- 이렇게 등록된 ari 가 다음번 async 요청에서 바로 ari 에 em을 바인딩할 수 있게 된다. (step 3)

 

step 5. filterChain.doFilter

 

step 6. finally 에서, participate = false 인 경우 tsm에서 em을 제거하고(unbindResource(emf)), async요청이 아닐 경우 EntityManagerFactoryUtils.closeEntityManager 로 em을 완전히 닫는다.

- '참여' 요청이라면 기존 요청이 존재하므로 여기서 em을 unbind 해서는 안 된다. 때문에 participate를 체크한다.

 

카프카는 왜 빠른가

프로그램 구현 레벨까지 가면 여러가지 이유를 댈 수 있겠지만, 카프카를 고성능으로 만드는 결정적인 이유를 말하라면 네 가지를 들 수 있겠다.

 

1. 시퀀셜 IO

일반적으로 RAM은 랜덤 엑세스를 지원하지만, 디스크는 그렇지 않다. 디스크는 원하는 데이터의 위치한 블록을 찾기 위한 시간(seek time), 블록을 메모리에 카피하는 시간 등의 오버헤드가 존재한다. 데이터가 캐시나 램에 이미 존재하면 이 과정은 생략될 수 있지만, 그렇지 않을 경우(page fault) 이러한 동작을 반복적으로 필요로 한다. 이런 점으로 인해 디스크는 일반적으로 느리다고 인식되곤 한다.

 

용량을 기준으로 볼 때 RAM은 디스크보다 훨씬 비싸다. 대용량 데이터를 핸들링하는 카프카가 그 모든 데이터를 RAM에 올리기는 현실적으로 어려운 일이다. 때문에 카프카는 디스크를 저장소로 사용하여 작동하는데, 대신 시퀀셜 디스크 엑세스를 통해 seek time의 최소화를 도모한다.

 

이는 카프카가 데이터를 로그(segments)로 저장하기 때문에 가능한 일이다. 로그는 한 번 쓰여지면 변하지 않는 성질을 지닌다(immutable). 특히 카프카의 데이터는 컨슈머가 읽어도 지워지지 않는다. 데이터는 오로지 맨 끝에 추가될 뿐이다(append-only). 이로 인해 카프카의 데이터는 디스크에 조각(fragments)으로 나뉘어 저장되지 않고, 가능한 연속적인 블록에 저장된다. 때문에 카프카는 원하는 데이터에 순차적으로 접근할 수 있다. 즉 디스크 seek time이 최소화된다.

 

이 시퀀셜 엑세스가 항상 보장되는 것은 아니다. 카프카의 데이터가 위치하는 파일 시스템을 다른 어플리케이션이 함께 사용하는 경우, 다른 어플리케이션으로 인해 디스크 단편화가 발생할 소지는 존재한다. 카프카의 데이터를 가급적 독립된 파일시스템에 유지할 것을 권장하는 이유가 여기에 있다. 하나의 파일 시스템을 카프카가 온전히 홀로 사용하도록 하여 카프카의 시퀀셜 엑세스를 보장해야 한다.

 

또, 디스크 엑세스를 더 빠르게 하기 위해 reading and writing은 OS 페이지 캐시를 최대한 활용하도록 해야 한다. 그래서 카프카가 구동되는 머신은 커널 swapiness를 최대한 작게 설정하여 OS 페이지 캐시가 swap out됨을 최대한 방지하는 것이 좋다.

 

 

2. Zero-Copy

예로부터 데이터를 디스크에서 읽어 네트워크로 전송하는 작업은 아래와 같이 4번의 카피와 4번의 컨텍스트 스위칭을 요구한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer 에서 application buffer로 (CPU)

    -> application buffer 에서 socket buffer로 (CPU)

      -> socket buffer에서 nic buffer로 (DMA)

 

Zero copy는 이 과정을 대폭 축소한다. 위 과정에서 application 영역은 디스크(read buffer)에서 읽은 데이터를 그대로 socket buffer로 보내기만 할 뿐, 다른 하는 일이 없다. 즉 데이터를 read buffer에서 socket buffer로 곧바로 보낼 수 있으면 좋을 것이다. zero copy는 그대로 탄생했다. 그리고 다음과 같이 동작한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer에서 socket buffer로 (CPU)

    -> socket buffer에서 nic로 (DMA)

카피 횟수도 줄었지만, 컨텍스트 스위칭이 처음 call에서 한 번, 리턴에서 한 번, 총 2번으로 줄었다.

 

Zero copy는 OS의 sendfile() syscall를 통해 이루어지는데, 이는 OS에서 zero copy를 지원해야 함을 의미한다. 유닉스나 요즘의 웬만한 리눅스 버전은 대부분 지원한다고 한다.

 

그런데 여기서 한 번 더 나아간다. 위 과정에서, read buffer에서 nic로 바로 데이터를 보낼 수 있다면 더욱 좋을 것이다. 이 기능은 nic라 gather operation을 지원해야 한다. 리눅스는 커널 2.4 이후로 이 기능을 지원하기 위해 socker buffer descriptor에 변경이 있었다. 이렇게 하여 최종적으로 zero copy는 다음과 같이 동작한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer에서 파일 디스크립터 정보(파일 위치, 크기 등)만 socket buffer로 (CPU)

    -> read buffer에서 nic로 (DMA)

Socket buffer는 파일 메타데이터만 가지며, 실제 데이터 카피는 발생하지 않는다. Read buffer에서 nic로의 데이터 카피는 DMA를 통해서만 이루어진다.

 

 

3. Data Batch & Compression

카프카는 데이터 batching과 compression을 지원하여 네트워킹 횟수를 대폭 줄여 성능을 향상한다. 추가로 로그 데이터 디스크 사용량도 크게 감소한다. 이 기능은 카프카 설정에 따라 작동 여부와 그 정도가 달라지지만, 대부분의 경우 사용할 것을 권장한다.

 

 

4. Topic Partitioning

카프카의 데이터는 로그 세그먼트로 저장되는데, 이 데이터는 순차적이기 때문에 다수의 쓰레드가 동시에 접근하는 일은 의미가 없다. 데이터는 최근의 것만 한 번 읽으면 되기 때문에, 아무리 많은 쓰레드가 있어도 경합만 발생할 뿐, 이득이 없다고 할 수 있다. 즉 병렬 처리가 불가능하다.

 

카프카는 로그 파티셔닝을 통해 이를 개선했다. 같은 토픽이라도 데이터를 나누어(sharding) 저장하고, 각 파티션에는 독립된 쓰레드가 붙어 작업할 수 있도록 함으로써 결과적으로 하나의 토픽을 여러 쓰레드가 나누어 처리할 수 있도록 병렬성을 구현했다.

 

 

@개인적으로 1번과 4번이 카프카의 특이점이라고 생각된다. 나머지는 다른 메시징 어플리케이션에서도 얼마든지 구현할 수 있을 것 같은 느낌이다. 카프카가 결정적으로 차별화를 두는 점은 4번이 결정적이고, 다음이 1번이라고 할 수 있지 않을까. 이 두 특성으로 인해 카프카가 메시징 어플리케이션을 넘어 데이터 스트리밍 플랫폼으로 군림할 수 있게 되었다고 생각된다.

'Kafka' 카테고리의 다른 글

Kafka 설정 listeners vs. advertised.listeners  (1) 2020.09.08

두 설정 모두 모두 카프카 브로커 서버가 바인딩하는 주소를 나타낸다.

 

listeners

- 카프카 브로커가 내부적으로 바인딩하는 주소.

 

advertised.listeners

- 카프카 프로듀서, 컨슈머에게 노출할 주소. 설정하지 않을 경우 디폴트로 listners 설정이 적용된다.

 

카프카는 분산 시스템이다. 클라이언트(프로듀서, 컨슈머)는 분산된 파티션에 접근하여 write/read 를 수행한다. 카프카가 클러스터로 묶인 경우, 카프카 리더만이 write/read 요청을 받는데, 클라이언트는 클러스터의 브로커 중 누가 리더인지 알아야 하기 때문에 write/read 요청에 앞서 해당 파티션의 리더가 누구인지 알 수 있는 메타데이터를 요청한다. 이 메타데이터 요청은 클러스터의 브로커 중 아무나 받아서 응답할 수 있다. 메타데이터 요청을 받은 브로커는 요청된 파티션의 리더가 어떤 브로커인지와 그 브로커에게 접근할 수 있는 엔드포인트를 반환한다. 그러면 클라이언트는 이 반환된 메타데이터를 가지고 실제 요청을 수행한다.

 

머신 OS에 직접 설치되어 구동되는 카프카의 경우 이 엔드포인트는 단순히 호스트 주소가 되는데, 그렇지 않고 가상 환경이나 클라우드와 같이 복잡한 네트워크 환경에서 구동되는 경우 상황이 달라진다.

 

이 경우, 카프카는 내부 브로커들 간의 통신을 위한 엔드포인트와 외부 클라이언트를 위한 엔드포인트를 구분할 필요가 생긴다. 네트워크 환경에 따라 내부의 서버 간 접근에 사용되는 호스트 주소와 외부에서 접근하기 위한 주소가 달리지기 때문이다. 그리고 성능 및 기타 비용을 고려할 때, 내부에서는 plaintext로, 외부에서는 SSL로 통신하도록 하는 등의 구분이 필요할 수 있다.

 

lisnters 와 advertised.listeners 는 그 구분을 위해 사용되는 설정이다.

 

카프카 공식 사이트에서 가져온 아래 설정 예시는 다양하게 구분된 엔드포인트를 보여준다:

listener.security.protocol.map=CLIENT:SASL_PLAINTEXT,REPLICATION:PLAINTEXT,INTERNAL_PLAINTEXT:PLAINTEXT,INTERNAL_SASL:SASL_PLAINTEXT
advertised.listeners=CLIENT://cluster1.foo.com:9092,REPLICATION://broker1.replication.local:9093,INTERNAL_PLAINTEXT://broker1.local:9094,INTERNAL_SASL://broker1.local:9095
listeners=CLIENT://192.1.1.8:9092,REPLICATION://10.1.1.5:9093,INTERNAL_PLAINTEXT://10.1.1.5:9094,INTERNAL_SASL://10.1.1.5:9095

 

 

모든 설정은 key-value 로 매핑된다. CLIENT 라는 key 의 내부 바인딩은 192.1.1.8:9092 로, 외부에서 접근하기 위한 엔드포인트는 cluster1.foo.com:9092 가 되며, 사용되는 프로토콜은 SASL_PLAINTEXT 를 사용한다.

 

내부 사용을 위한 엔드포인트로 보이는 INTERNAL_PLAINTEXT 는 내부 바인딩으로 10.1.1.5:9094 를, 외부에서의 접근은 broker1.local:9094 를 사용하며 PLAINTEXT 로 통신한다.

 

출처: 

cwiki.apache.org/confluence/display/KAFKA/KIP-103%3A+Separation+of+Internal+and+External+traffic

 

'Kafka' 카테고리의 다른 글

카프카는 왜 빠른가  (0) 2021.11.26

spring-data-redis의 LettuceConnectionFactory에 setEnableTransactionSupport(boolean) 메서드가 있다.

 

기본값은 false인데, true로 설정하면 트랜잭션을 사용할 수 있게 된다. 그런데 이는 트랜잭션을 지원한다는 의미이지, JDBC처럼 트랜잭션 처리를 알아서 해준다는 의미가 아니다. 

 

레디스에서 트랜잭션을 처리는 MULTI-EXEC 를 사용하여 이루어지는데, 당연하게도 한 트랜잭션은 하나의 커넥션이 MULTI 에서 EXEC 까지 모두 수행해야 한다. setEnableTransactionSupport 는 그것을 돕는다.

 

이 값이 false 이면, MULTI-SET-EXEC 를 호출하면 다음과 같이 동작한다:

getConnection: 커넥션A 획득

MULTI

close

getConnection: 커넥션 B 획득

SET

close

getConnection: 커넥션 C 획득

EXEC

close

 

MULTI 를 포함하여 모든 오퍼레이션이 다른 커넥션에서 호출된다. 마지막 EXEC 에서는 'EXEC without MULTI' 에러가 떨어진다. MULTI 없이 EXEC를 호출했다는 의미이다. 다른 커넥션에서 호출했었으니 하나의 트랜잭션으로 이어지지 않는다.

 

setEnableTransactionSupport 이 true 이면 위 오퍼레이션은 다음과 같이 동작한다:

getConnection: 커넥션A 획득

MULTI

getConnection: 커넥션A 획득

SET

getConnection: 커넥션A 획득

EXEC

 

이와 같이 MULTI에서부터 EXEC 까지의 오퍼레이션을 한 커넥션에 묶어준다. 위 동작의 배경은 LettuceConnection을 가지고 있는 RedisConnectionHolder를 생성하여 스프링의 TransactionSynchronizationManager에 리소스로 바인딩하는 방식을 취한다. JDBC 트랜잭션 관리와 마찬가지로 쓰레드에 자신이 사용할 커넥션을 묶어두는 것이다.

'Redis' 카테고리의 다른 글

Spring-Lettuce 커넥션 풀링 시 shareNativeConnection  (3) 2020.09.02
Lettuce 센티넬 failover 테스트  (0) 2020.09.01

spring-data-redis, Lettuce 사용 시, non-blocking, non-transactional 오퍼레이션은 커넥션 풀을 설정해도 하나의 포트로만 통신한다.

 

이 동작은 LettuceConnectionFactory 의 shareNativeConnection 값에 따라 달라진다. 이 값은 네이티브 커넥션을 공유해서 사용할지 여부를 가리킨다. 기본값이 true 이기에 non-blocking, non-transactional 오퍼레이션은 하나의 커넥션으로만 통신한다. MULTI 를 통해 트랜잭션을 시작하면 새로운 dedicated 커넥션이 할당되어 이것으로 통신한다.

 

언뜻 보기에 이 값을 false로 두고 커넥션 풀을 사용하여 오퍼레이션에 사용되는 커넥션을 분산하면 레디스 처리량이 올라가 성능이 좋아질 것 같지만, 레디스 자체가 싱글 쓰레드로 작동하기 때문에 실제로는 그렇지 않다고 한다. 스프링 부트 github에 부트 레벨에서 이 값을 false 로 설정할 수 있도록 해달라는 issue가 있었는데, 이와 같은 이유로 거부되었다:

 

github.com/spring-projects/spring-boot/issues/14196

 

Allow the ability to set shareNativeConnection to false for LettuceConnectionFactory · Issue #14196 · spring-projects/spring-b

By default, the LettuceConnection will use the sharedConnection for almost all non-tx or non-blocking operations. I have found the exclusive use of the shared connection to not allow very high thro...

github.com

 

이런 이유로, 스탠드얼론 레디스를 사용할 때 트랜잭션을 사용하지 않는다면 커넥션 풀을 사용할 이유가 없다.

 

 

출처:

docs.spring.io/spring-data/data-redis/docs/current/reference/html/#reference

'Redis' 카테고리의 다른 글

Spring-Lettuce setEnableTransactionSupport  (0) 2020.09.02
Lettuce 센티넬 failover 테스트  (0) 2020.09.01

spring-data-redis 1.7.111RELEASE

lettuce-3.5.0.Final

Redis 5.0

 

1 master - port 6379

3 replicas - port 6380, 6381, 6382

3 sentinels - port 26379, 26380, 26381

 

빈 설정

 

첫 연결 수립

로그 상단을 보면 아래와 같이, 3 개의 센티넬 주소 중 하나로 접근한다고 하는데, 여러 번 시도했지만 항상 첫 번째 주소인 26379 포트로만 접근했다. 순서대로 접근 시도하면서 센티넬이 응답하지 않는 경우 다음 센티넬로 넘어간다. 정상적으로 응답하는 센티넬을 찾으면 나머지 센티넬로는 요청을 보내지 않았다.

Trying to get a Sentinel connection for one of: [RedisURI [host='127.0.0.1', port=26379], RedisURI [host='127.0.0.1', port=26380], RedisURI [host='127.0.0.1', port=26381]]

 

로그 하단에서는 SENTINEL 커맨드를 통해 마스터 호스트를 얻는다:

Decoded Command [type=SENTINEL, output=ValueListOutput [output=[127.0.0.1, 6379], error='null']] ...

 

마스터로의 연결 수립

 

마스터 fails

마스터와 연결이 끊어져도 failover가 수행되기 전까지는 계속 연결 시도한다.

 

마스터와 연결이 끊어지자, 다시 센티넬로 연결하여 새로운 마스터 정보를 조회하는데, failover가 완료되기 전까지는 기존 마스터 정보를 반환한다.

 

Failover 완료 후

센티넬이 failover를 완료하고 새로운 마스터 주소(포트 6381)를 반환한다.

 

새 마스터로의 연결 정상 수립.

 

 

NOTE:

- 센티넬이 죽으면 다음 센티넬로 연결한다.

- quorum 값을 2로 하여 3 개의 센티넬이 구동했다가 2 개의 센티넬이 죽으면 failover가 수행되지 못한다. 1 개의 센티넬만 죽으면 failover는 수행되었다. 센티넬의 상태와 상관없이 vote 수를 채우면 failover에는 문제가 없는 것 같다.

1. 디폴트

정적 자원 핸들링에는 디폴트로 ResourcehttpRequestHandler가 사용된다.

디폴트로, 아래 경로의 자원을 '/' URI 패턴으로 제공한다.

classpath:/static

classpath:/public

classpath:/resources

classpath:/META-INF/resources

 

예로, classpath:/static/myface.html 이 있을 때,

http://example.com/myface.html 경로로 접근할 수 있다.

 

 

2. 커스터마이징

주의: 설정 커스터마이징 시 디폴트 설정은 사라진다. 따로 설정하지 않으면 더이상 '/' 패턴으로 정적 자원 접근이 불가능하다.

 

프로퍼티 방식

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=classpath:/mystatic/,file:/shared/mystatic/

 

URI 패턴, 자원 위치 모두 콤마로 구분하여 2개 이상 설정할 수 있다.

 

위 설정은 아래와 같이 매핑된다

http://example.com/mystatic/myface.html => classpath:/mystatic/myface.html (없으면 파일시스템 /shared/mystatic/myface.html)

 

 

위와 같이, 클래스패스가 아닌 일반 파일시스템 접근에는 'classpath' 대신 'file:' 을 사용한다. (윈도우는 'file://')

 

자바 방식

위 설정을 자바 방식으로 하면 아래와 같다.

@Configuration 
public class WebConfig implements WebMvcConfigurer {

	@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { 
    		registry.addResourceHandler("/mystatic/**")
        		.addResourceLocations("classpath:/mystatic/", "file:/shared/mystatic/");
	}    
}

 

WAR 파일의 경우, webapp/mystatic/ 에 자원이 있다면, 아래와 같이 'classpath:'를 빼고 설정한다.

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=/mystatic/

 

 

Windows 10

IntelliJ IC2019.3 기준

 

1. 프로젝트 루트\.idea\

1.1. workspace.xml

- 최근 변경된 파일, 열려있는 파일, 사용자가 지정한 각종 윈도우의 위치, 사이즈 등 사용자의 세션 정보

- 설정파일에서 쓰이는 프로젝트ID (예: 1g1MbsIX7nO62asWlrlyedlPXqr)

- RunManager (un configuration) 정보

 

1.2. gradle.xml

- 그레이들 home, 버전, 그레이들 구동 jvm 버전 및 기타 그레이들 정보

 

1.3. jarRepositories.xml

- maven central 등 원격 jar 저장소 정보

 

1.4. misc.xml

- 프로젝트의 JDK 이름, 타입, 언어 레벨, 빌드 output path 등

 

 

2. user path\.Idea{인텔리J 버전}\ (예: C:\Users\user\.IdeaIC2019.3\config\workspace)

2.1. config\workspace\{1.1. workspace.xml 의 프로젝트ID }.xml (예: 1g1MbsIX7nO62asWlrlyedlPXqr.xml)

- 최근 변경 파일, 레이아웃 등 1.1. workspace.xml 과 비슷한 정보가 있는 것 같으나, 정확한 역할은 모르겠음

- 프로젝트 뷰, 모듈 UI 구조 설정 정보

 

2.2. system\workspace\{프로젝트명.{해시값}.xml (예: remote-file-transfer.938a031e.xml)

- 프로젝트(모듈) 빌드 클래스패스, 그레이들 설정 등

 

 

'Env' 카테고리의 다른 글

IntelliJ 스프링부트 app 실행  (0) 2020.08.14

1. Project Structure -> Project -> Project SDK 설정 (원하는 런타임 Java)

2. Project Structure -> Modules -> main/resources 영역 Resoueces 마킹

3. Settings -> Build Tools -> Gradle -> Build and run 에서 'Build and run using' - IntelliJ IDEA 로 변경

 

3번 설정은 그레이들 프로젝트의 경우 디폴트로 Gradle 로 맞추어져 있다. 때문에 스프링부트 어플리케이션의 main 메서드 실행 시 그레이들이 사용되는데, 이를 IntelliJ IDEA 로 바꾸면 인텔리J의 자체 최적화를 거쳐 더 빠른 실행 속도를 누릴 수 있다.

 

허나 이 설정의 단점은, 로컬 테스트 환경이 인텔리J에 의존하게 된다는 점이다. Gradle은 로컬 뿐만 아니라 다른 런타임에서도 동일한 실행 결과를 보장하겠지만. 인텔리J는 아닐 수 있다.

'Env' 카테고리의 다른 글

IntelliJ 프로젝트 설정 저장파일 (Windows)  (0) 2020.08.14

사용자는 build.gradle, settings.gradle, init.gradle ... 과 같은 그레이들 스크립트를 작성한다. 사용자가 작성한 스크립트 안에는 빌드 과정에서 실행되어야 할 동작이 기술되어 있다.

 

그레이들은 미리 정해진 단계(lifecycle phases)에 따라 빌드를 수행하는데, 각 단계에 맞는 스크립트를 읽어들이고, 각 파일의 성격에 해당하는 그루비 인스턴스를 생성한다. 이 인스턴스는 사용자가 스크립트에 작성한 내용을 알고 있어, 이 내용에 해당하는 메서드를 실행한다.

 

Lifecycle Phases

그레이들의 빌드 라이프싸이클에는 다음 단계가 존재한다.

1. Initialization

그레이들 자체의 설정, 전역 프로퍼티 설정 등 현재 빌드 대상 프로젝트에 종속되지는 않지만 공통으로 적용되어야 할 동작을 초기화한다. 그레이들이 빌드할 대상 프로젝트가 이 시점에 결정된다.

 

구체적인 동작은, .gradle/init/ 경로의 init.gradle + *.gradle 파일을 읽어들여 Gradle 인스턴스를 생성한다. 그리고 프로젝트의 settings.gradle 파일로 Settings 인스턴스를 생성한다. 그레이들이 빌드할 대상 프로젝트가 결정되면 build.gradle 을 참조하여 각 프로젝트의 Project 인스턴스를 생성하여 빌드 수행을 준비한다.

 

2. Configuration

현재 빌드 대상 프로젝트를 위한 설정을 적용한다. build.gradle 파일을 바탕으로, Initialization 단계에서 생성된 Project 인스턴스들에 빌드 설정을 적용한다. build.gradle 파일이 이 단계에서 실행되어 Task, Action 인스턴스들을 생성하고, Task들이 실행될 순서 등을 정리한다. (calculating task graph)

 

3. Execution

Configuration 단계에서 준비된 내용을 바탕으로 실제 Task의 Actions를 실행한다.

 

 

Interfaces

그레이들에는 6가지 메인 인터페이스가 존재한다. (설명에는 '인스턴스' 라 칭했는데, 실제 생성된 실체는 인스턴스는 인터페이스를 구현한 객체의 인스턴스이기 때문이다.)

1. Script

그레이들은 사용자가 작성한 *.gradle 을 읽어들여 Script 인스턴스를 생성한다. 이 인스턴스는 빌드 스크립트를 실행하는 기반이 된다.

 

2. Gradle

그레이들은 .gradle/init 경로의 init.gradle + *.gradle 파일로 Gradle 인스턴스를 생성한다. 그레이들 설정, 전역 프로퍼티 설정 등 프로젝트에 종속되지 않는, 그레이들의 글로벌한 영역을 담당한다.

 

3. Settings

그레이들은 프로젝트의 settings.gradle 파일로 Settings 인스턴스를 생성한다. settings.gradle은 싱글 프로젝트 빌드에서는 옵셔널이지만, 멀티 프로젝트 빌드에서는 필수이다. 즉 여러 프로젝트 간의 관계 구성 정보를 담고 있고, Settings 인스턴스는 이 영역을 담당한다.

 

4. Project

그레이들은 프로젝트의 build.gradle 파일로 Project 인스턴스를 생성한다. build.gradle 파일이 한 프로젝트를 위한 빌드 스크립트이듯, Project 인스턴스는 그 프로젝트의 빌드 과정에 실행되어야 할 Tasks 인스턴스들을 참조하고 있다. Project 인스턴스는 Task 인스턴스들의 컬렉션이라고도 말할 수 있다.

 

5. Task

build.gradle 파일에 선언된 하나의 task 는 하나의 Task 인스턴스가 된다. Task 인스턴스는 자신이 선언된 Project 인스턴스에 종속된다. 하나의 Task 인스턴스는 java 파일 컴파일, java doc 생성과 같이, 빌드 과정 중 수행되어야 할 하나의 아토믹한 과정을 의미한다. 하나의 Task는 자신이 실행할 Action 인스턴스들을 참조하고 있다. Project 인스턴스가 Task 인스턴스들의 컬렉션이듯, Task 인스턴스는 Action 인스턴스들의 컬렉션이라고 할 수 있다.

 

6. Action

build.gradle 파일로 생성된 Task 인스턴스가 실제 실행할 동작을 의미하는 인스턴스이다. Action 인스턴스는 자신이 선언된 Task 인스턴스에 종속된다.

 

 

Lifecycle Phases / 생성 Interfaces / Script Files 매핑

Initialization / Gradle, Settings, Project / init.gradle + *.gradle, settings.gradle, build.gradle

Configuration / Task, Action / build.gradle

Execution / - / build.gradle

※ Script 인스턴스는 각 gradle 파일을 읽는 단계에서 생성.

 

 

기술적 작동 원리

그레이들은 각 gradle 파일에 해당하는 Script 인스턴스를 생성한다. init.gradle 파일은 이에 해당하는 Script 인스턴스가, build.gradle 파일은 이에 해당하는 Script 인스턴스가 독립적으로 생성된다.

 

이 Script 인스턴스는 빌드 phase에 매핑되는 인스턴스에 delegating하는 방식으로 빌드 과정을 수행한다. 이 delegating은 그루비의 Closure 인스턴스의 delegate = <context> 를 지정하는 방법으로 이루어진다. 이런 방식으로, 그레이들 글로벌 정보를 담은 Gradle 인스턴스에 무언가를 설정하려면 init.gradle 파일에 그 내용을 기술하고, 프로젝트 실제 빌드에 무언가를 설정하려면 build.gradle 파일에 그 내용을 기술하여 그레이들이 delegating을 통해 그 내용을 빌드 과정에서 실행하도록 한다.

 

phases / delegating 대상 인스턴스 / Script Files 매핑

Initiliazation / Gradle, Settings / init.gradle + *.gradle, settings.gradle

Configuration / Project / build.gradle

 

정리하자면, 하나의 *.gradle 파일은 인스턴스가 된다. 그리고 이 인스턴스는 빌드 과정 중 자신에게 해당하는 단계에 delegating되어 실행된다. 즉 *.gradle 파일을 작성함은 하나의 Closure code block을 작성하는 것이라 말할 수 있을 것이다.

+ Recent posts