<개요>

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.

- 기존에 일반적인 내용들 (e.g RDB기준), 새로 추가되는 개념들 (e.g NoSQL)을 포함하여 정리

 

<내용>

1. Transactions

- read/write 가 일어날때 논리적인 하나의 단위로 묶어서 생각해본다.

- application에서 database에 접근할때 프로그래밍을 좀더 단순화하는데 목적이 있다고 본다. (추상화)

 

2. ACID

Atomicity, Consistency, Isolation, Durability

위와 같은 것은 일반적으로 트랜잭션의 특성으로 보고 있으며, 실제 각 DBMS에서 구현하는 방법들은 조금씩 차이가 있다.

특히, 이 책에서는 수년간 사용해왔던 이 단어들에 역시 모호한 부분들이 존재하며, 관점에 따라 차이가 있음을 소개하고 있다.

 

Atomicity

- 통상적으로 더이상 나눌수 없는 단위로 본다. 그러나 그 세부의 이해에는 약간의 차이가 있다.

- 예를 들어서 멀티쓰레드 환경에서는 하나의 쓰레드가 atomic operation을 수행할때 다른 쓰레드에서는 그 중간값을 참조할 수 없음을 의미한다. 그러나 ACID내에서 Atomicity는 concurrency의미가 아니다. 이 개념은 Isolation에 포한된다.

- ACID내에서 Atomicity는 write가 수행될때 (내부적으로 process, network, disk등의 여러 에러가 발생할 수 있는 요인이 있지만) 성공 혹은 실패가 하나로 묶이는 것을 의미한다. (committed / aborted)

 즉, "어디까지는 성공하고 어디까지는 실패하고" 가 발생하지 않도록 하는 것이 그 의미에 가까워서 이 책에서는 abortability가 좀 더 의미적으로 가깝지 않나라고 이야기한다.

 

Consistency

- 역시 다양한 영역에서 혼용되어 사용하고 있다. 예를 들어서 eventual consistency, consistent hashing, CAP, 그리고 ACID내에서 같은 단어들이지만 다른의미를 가지고 있다.

- ACID내 C의 경우 invariants가 항상 참인것에 집중한다고 볼 수 있다. 그 예로 여러가지 constraint를 이야기하고 있는데 이 책에서는 이러한 영역이 database보다는 application이 보장해야하는 것으로 보고 있다.

Data에 어떤 내용을 담을 것인가는 결국 application 에서 정해지며 A,I,D는 database의 속성이지만 consistency는 그렇게 볼수 없다는 것이다. 

- 나도 여기에 동의하는 것이 최근 NoSQL에서는 한 application의 기능에서 다루는 단위(set)를 기준으로 데이터를 구성하는 것이 일반적이며(JOIN X), 그에 따라서 각 데이터의 key값에 대한 정합성을 보장하는 것은 응답속도나 reliability에 따라서 타협하여 구현하는 모습을 통해서 체감할 수 있다고 생각한다.

 

Isolation

- 같은 record에 대해 동시에 여러 clients가 접근하여 경합이 발생했을 때 어떻게 해결할 것인가.

- 특히 우리가 일반적으로 알고 있는 Isolation level과 비슷하면서도 다른 용어, 추가적인 개념설명으로 재미있었던 부분이었다.

 (e.g dirty write, read write skew 등)

- 용어에서 설명하듯이 기본적으로 여러 transactions이 동시에 수행되더라도 각각은 별도로 격리 되어야 한다는 것이 기본적인 원칙이다. 이 때 발생할 수 있는 여러가지 데이터 이상현상이 있으며 (DIRTY READ, NON REPETABLE READ, PHANTOM READ등) 각 현상을 제거할 수 있도록 Level을 조정하여 성능과 정합성을 타협하는 것이다.

- 다만 각 database가 정의하는 isolation level는 차이가 있으며, 그것을 구현하는 방법도 다르다는 내용을 소개하고 있다.

 (e.g snapshot isolation, Inno DB consistent read 등)

 

Durability

- 결국 모든 저장소 (File, Database, Storage)는 안전하게 데이터를 저장하는 것이 목적이다.

- single-node database 에서는 이것을 보장하기 위해서 저장공간 (hdd,ssd 등)에 데이터를 기록하고, write-ahead log를 기록하고 recover event하는 형태로 접근했으며

- replicated database 에서는 복제하여 다른 노드에 저장하고, 각 transaction이 이루어질 때 write / replication을 수행하는 형태로 관리하고 있다.

 

특히 여기에는 개발자들이 놓치는 문제들이 간혹 있는데 간단히 살펴보면 다음과 같다.

- Disk에 기록한뒤 다운될 경우 Data는 잃어버리지 않으며 접근이 불가능한 것으로 접근 가능한 다른 시스템으로 대체할 수도 있다.

- Memory에 저장된 데이터는 휘발성이기 때문에 Disk가 필요하다.

- 비동기로 복제하는 시스템에서는 Leader의 상태에 따라서 최근 변경사항이 누락될 수 있다.

- Storage에도 버그나 문제는 존재하며 완벽은 없다. (e.g 정전, 펌웨어 버그 등)

- Disk는 생각보다 자주 망가진다. 갑자기 죽기도 하지만.. 천천히 망가지기도 한다. (bad sector!) 물리적인 매체가 망가진다는 것은 replicas, backup도 손상되는 것을 의미하고, 그래서 historical backup전략이 존재한다.

- 특히 SSD는 빠르지만 불량블록이 생각보다 자주 발생하고, HDD는 갑자기 죽기도 한다.
- SSD는 전원이 공급되지 않으면 온도에 따라서도 손실이 발생한다고 한다!?


결국 절대적인 보장은 이세상에 존재 하지않기 때문에 우리는 여러 기술을 복합적으로 사용하는 것이다.

 

<정리>

-  ACID는 과거부터 쭉 사용해온 용어이지만 Computing Area가 다양해지면서 비슷한 용어들의 모호함이 존재한다.

- Database에서 보장해야하는 개념들은 결국 각 벤더마다 다를 수도 있다. 

  즉 JDBC 드라이버의 실제 구현은 내가 생각하고 기대한 것과는 다를 수 있다. (과거에 update 쿼리실행 후 결과값이 변경된 건수로 나와야 하는데 0으로 나왔던 기억이 있다.)

- Consistency는 application의 영역으로 보는 것이 어떨까 라는 의견이 있다.

 (유효성을 위한 제약조건 같은 기능들은 Database가 제공할 수 있지만 결국 어떤값을 기록하는가는 Application에 달려있다.)

- 우리는 물리적인 세상에서 살고 있기 때문에 100% 보장이라는 것은 있을 수 없다.

 

Transcation에 대해서 대략적으로 살펴보았으며 다음 글에는 Atomicity와 Isolation에 대해서 좀 더 자세히 정리하도록 하겠다.

<개요>

- 기본생성자 유무

- Serializer / Deserializer 적용

- 클래스 패키지 위치

 

<내용>

Q1. 기본생성자 사용

Page<DataDto> findDataAll (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                               Pageable pageable)  {
        if(loginUserDetails.isAdmin()){
            return dataService.findAllDatas();
        }
        return new PageImpl(new ArrayList(),Pageable.unpaged(),0);
    }

- Paging처리를 위해서 JPA에서 제공되고 있는 Pageable를 사용하고 있었다.

- 여기에 Redis를 저장소로 하여 캐시를 적용하면 다음과 같은 형태로 사용이 가능하다.

@Cacheable(value="findDataAll", key="#pageable.pageSize.toString().concat('-').concat(#pageable.pageNumber)")

- 당연히 잘 될것으로 예상하고 수행하였으나 다음과 같은 Exception을 만나게 되었다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot 
 construct instance of `org.springframework.data.domain.PageImpl` (no 
 Creators, like default construct, exist): cannot deserialize from Object 
 value (no delegate- or property-based Creator)
 at [Source: (byte[])"

- 기본 생성자가 존재하지 않기 때문에 발생하는 오류로

 

A1. 기본 생성자를 가진 wrapper class를 활용하여 문제를 해결할 수 있다.

@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
public class RestPage<T> extends PageImpl<T> {
    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public RestPage(@JsonProperty("content") List<T> content,
                    @JsonProperty("number") int page,
                    @JsonProperty("size") int size,
                    @JsonProperty("totalElements") long total) {
        super(content, PageRequest.of(page, size), total);
    }

    public RestPage(Page<T> page) {
        super(page.getContent(), page.getPageable(), page.getTotalElements());
    }
}

 

Q2. Page클래스는 다음과 같은 형태로 해결하였으나 Java 8 부터 제공하는 LocalDateTime에 대해서도 비슷한 오류가 발생한다.

기본적으로 LocalDataTime을 저장하면 "yyyy-MM-dd'T'HH:mm:ss" 와 같은 형태로 처리되기를 원하지만 아래와 같은 형태로 저장이 된다. 그래서 json 역직렬화를 할때 문제가 발생하게 된다.

{ 
  "lastPartDate":{ 
    "year":2020,
    "month":"JANUARY",
    "dayOfMonth":1,
    "dayOfWeek":"WEDNESDAY",
    "dayOfYear":1,
    "monthValue":1,
    "hour":0,
    "minute":0,
    "second":0,
    "nano":0,
    "chronology":{ 
      "id":"ISO",
      "calendarType":"iso8601"
    }
  }
}

 

A2. 역직렬화 방안

- Spring boot 1.x 대에서는 JSR-310을 지원하지 않기 때문에 dependency 에 모듈 추가가 필요하다.

-  참조사이트에 나와있는 것처럼 Custom Serializer를 구현하는 방법도 있으며,

- Spring boot 2.x 대를 사용중이면 다음과 같이 쉽게 처리를 할 수도 있다.

@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime creationDateTime;

 

 

Q3. 혹시라도 Redis에 저장하는 Class의 패키지명등을 수정하면 아래 Exception 을 만나게 된다.

com.fasterxml.jackson.databind.exc.InvalidTypeIdException

이유는 우리가 Redis에 실제 저장된 값을 조회해보면 알 수 있는데 다음과 같다.

redis 저장정보

역직렬화에 필요한 클래스 정보가 같이 들어가있는데 기존 클래스와 다르기 때문에 에러가 발생한다.

 

A3. 해당 Cache를 evict해주도록 하자. 일반적으로 데이터가 변경되는 경우에 대해서는 Refresh를 해주지만 리팩토링작업중 클래스가 변경되었을 경우에 까먹는 케이스가 많다.

 

<정리>

- 정리를 해보면 결국 Serializer / Deserializer 에 대한 이야기이다. RedisCacheConfiguration설정을 보면 대부분 다음과 같다.

RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

 

- 특히 Json내부의 상세타입을 다시 객체화 하기 위해서는

 a. 저장되어 있는 클래스 정보가 일치하고,

 b. 기본 생성자 (혹은 그 역할을 수행할 수 있는 무언가) 가 존재해야 하며, 

 c. Serialize한것과 같은 방법으로 Deserialize를 수행해야 한다.

- Redis외의 기타 저장소를 사용할때에도 동일한 

 

<참조>

https://stackoverflow.com/questions/55965523/error-during-deserialization-of-pageimpl-cannot-construct-instance-of-org-spr

 

Error during Deserialization of PageImpl : Cannot construct instance of `org.springframework.data.domain.PageImpl`

Issue is when using Spring cache with redis cache manager, not able to deserializer Spring Pageable response due to no default constructor The spring boot version used is 2.1.4.RELEASE Redis con...

stackoverflow.com

https://github.com/HomoEfficio/dev-tips/blob/master/Java8-LocalDateTime-Jackson-%EC%A7%81%EB%A0%AC%ED%99%94-%EB%AC%B8%EC%A0%9C.md

1. maven repository 연결

- https 연결시 인증서 문제

- 인증서 추가

 

2. netty

java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled

--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
-Dio.netty.tryReflectionSetAccessible=true

 

<참조>

https://stackoverflow.com/questions/57885828/netty-cannot-access-class-jdk-internal-misc-unsafe

<개요>

https://icthuman.tistory.com/entry/Spring-EventListener-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Refactoring

 

Spring @EventListener 를 활용한 Refactoring

<개요> - 서비스 내에서 로직 처리후 타 서비스 연동을 위해서 Service Call을 다시 하는 구조 - Spring에서 제공하는 ApplicationEvent, ApplicationListener를 활용하여 리팩토링 <내용> - 현재 서비스내에서 다..

icthuman.tistory.com

- 이후 Slack API를 호출하는 중 오류가 간헐적으로 발생함

 

<내용>

2022-02-15 13:01:43.176 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Enabled protocols: [TLSv1.3, TLSv1.2, TLSv1.1, TLSv1]
2022-02-15 13:01:43.176 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Enabled cipher suites:[TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, TLS_RSA_WITH_AES_256_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384, TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDH_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_DSS_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA]
2022-02-15 13:01:43.176 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Starting handshake
2022-02-15 13:01:43.221 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Secure session established
2022-02-15 13:01:43.221 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   :  negotiated protocol: TLSv1.3
2022-02-15 13:01:43.221 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   :  negotiated cipher suite: TLS_AES_128_GCM_SHA256
2022-02-15 13:01:43.223 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.conn.ssl.DefaultHostnameVerifier   : peer not authenticated

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
	at java.base/sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:526) ~[na:na]
	at org.apache.http.conn.ssl.DefaultHostnameVerifier.verify(DefaultHostnameVerifier.java:97) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.verifyHostname(SSLConnectionSocketFactory.java:503) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:437) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.13.jar:4.5.13]
	at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87) ~[spring-web-5.3.7.jar:5.3.7]
	at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-5.3.7.jar:5.3.7]
	at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66) ~[spring-web-5.3.7.jar:5.3.7]
	at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:109) ~[spring-web-5.3.7.jar:5.3.7]

- 동기방식으로 순차처리할 경우 문제가 없으나 비동기식으로 대량 동시호출시 해당 오류가 간헐적으로 발생하고 있음

 

<정리>

- 관련이슈를 살펴보면 다음과 같다.

TLSv1.3 handshake is failing, but passes on TLSv1.2 on JDK 11. It also doesn't fail in TLSv1.2 on JDK 10

TLS 1.3 사용시 SSLSessionImpl 생성할때 PostHandshakeContext로 부터 값을 가져와서 생성하게 됨 (not ClientHandshakeContext)

PostHandshakeContext은 requestedServerNames 에 값을 할당하지 않고 있어서 빈값으로 문제가 발생함

TransportContext의 conSession필드에서 서버목록을 가져오도록 하여 해결할 수 있음

 

<참조>

https://github.com/slackapi/java-slack-sdk/issues/482

https://github.com/slackapi/java-slack-sdk/issues/465#issuecomment-632150000

https://github.com/slackapi/java-slack-sdk/pull/452

 

Add a workaround for OpenJDK 11's bug by seratch · Pull Request #452 · slackapi/java-slack-sdk

Summary Sending HTTP requests to Slack API server on OpenJDK 11 fails due to https://bugs.openjdk.java.net/browse/JDK-8211806 Requirements (place an x in each [ ]) I've read and understood th...

github.com

https://bugs.openjdk.java.net/browse/JDK-8211806

<개요>

- 서비스 내에서 로직 처리후 타 서비스 연동을 위해서 Service Call을 다시 하는 구조

- Spring에서 제공하는 ApplicationEvent, ApplicationListener를 활용하여 리팩토링

 

<내용>

- 현재 서비스내에서 다음과 같이 특정 상황이 발생했을 때 Noti하기 위해서 Slack으로 메시지를 보내는 로직들이 구성되어 있다.

 @Async("executorBean")
    public CompletableFuture<Boolean> refreshCount(){

        log.info("Before Current API Call remains {} ", countCallService.getCallCount());
        if(countCallService.getCallCount() > 0){
            sendSlackMessageInfo("");
        }
        List<MetaDto> metaList = filterService.findAllMetas();

        sendSlackMessageInfo( String.format("%s %s", metaList.size(), "metas") );
        log.info(...);
        ...
        
    }
    
    private void sendSlackMessageInfo(String text){
        log.info(text);
        SlackMessage message = SlackMessage
                .builder()
                .text(text)
                .color(SlackMessage.Color.GOOD)
                .build();
        slackMessageService.postMessage(message);
    }

 

* 문제점

- 비지니스와 관계없는 SlackService와 Dependency가 발생한다.

- Slack 이외에 다른 연동이나 로직이 추가될 수록 코드가 지저분해진다. 

- 실제 비지니스 로직과 관계없는 Message 조립에 대한 로직이 포함되고 있다.

 

<Refactoring #1>

1. Service

 @Async("executorBean")
    public CompletableFuture<Boolean> refreshCount(){

        log.info("Before Current API Call{} ", countCallService.getCallCount());
        if(countCallService.getCallCount() > 0){
             eventPublisher.publishEvent(new RefreshMetaErrorEvent(""));
        }
        List<MetaDto> metaList = filterService.findAllMetas();

        eventPublisher.publishEvent(new RefreshMetaStartEvent(metaList.size()));
        log.info(...);
        ...
        
    }

Biz Service내에서는 Event만 publish 하도록 변경하였다.

 

2. EventHandler

public class RefreshMetaEventHandler {

    private final SlackMessageService slackMessageService;

    @EventListener
    public void errorMessage(final RefreshMetaErrorEvent refreshMetaErrorEvent){
        final String message = refreshMetaErrorEvent.getMessage();

        sendSlackMessageInfo(message);
    }

    @EventListener
    public void startMessage(final RefreshMetaStartEvent refreshMetaStartEvent){
        final int count = refreshMetaStartEvent.getCount();
        final String message =  String.format("%s %s", count, "filters");
        sendSlackMessageInfo(message);
    }

    private void sendSlackMessageInfo(String text){
        log.info(text);
        SlackMessage message = SlackMessage
                .builder()
                .text(text)
                .color(SlackMessage.Color.GOOD)
                .build();
        slackMessageService.postMessage(message);
    }
}
@Getter
@RequiredArgsConstructor
public class RefreshMetaStartEvent {

    private int count;
}
@Getter
@RequiredArgsConstructor
public class RefreshMetaErrorEvent {

    private String message;
}

- 상황에 따라 Event를 수신하여 처리하는 로직은 모두 EventHandler로 모아서 구성하였다. (Service Dependency정리)

- slack연동외의 다른 로직을 추가하기 간결한 구조로 정리되었다.

 

* 남은 문제점

- Event가 많아질 수록 메소드가 늘어난다.

- EventHandler나 SlackMessageService의 경우 각 Message의 전달만을 책임져야 한다.

- Message조립에 대한 로직은 분리되는 것이 맞다.

 

<Refactoring #2>

1. Abstract Class 및 Concrete Class 정의

public abstract class AbstractRefreshMetaEvent {

    public abstract String getMessage();
}
public class RefreshMetaStartEvent extends AbstractRefreshMetaEvent{

    private final int count;

    @Override
    public String getMessage() {
        return String.format("%s %s", count, "metas");
    }
}
public class RefreshMetaDemoEvent extends AbstractRefreshMetaEvent {

    private final String svcColumnName;
    private final int count;

    @Override
    public String getMessage() {
        return String.format("%s has %s data",svcColumnName, count);
    }
}
public class RefreshMetaErrorEvent extends AbstractRefreshMetaEvent{

    private final String message;
}

- 각 Event별로 Message 조립에 대한 역할을 가져가도록 분리한다.

- 필요에 따라서 세부 Event를 추가로 정의하고 각 상황에 맞게 상세구현을 진행할 수 있다.

 

2. EventHandler

public class RefreshMetaEventHandler {

    private final SlackMessageService slackMessageService;

    @Async
    @EventListener
    public void sendMessage(final AbstractRefreshMetaEvent refreshMetaEvent){
        final String message = refreshMetaEvent.getMessage();

        sendSlackMessageInfo(message);
        // 연동 및 로직 추가구성 가능 
    }

    private void sendSlackMessageInfo(String text){
        log.info(text);
        SlackMessage message = SlackMessage
                .builder()
                .text(text)
                .color(SlackMessage.Color.GOOD)
                .build();
        slackMessageService.postMessage(message);
    }
}

- AbstractRefreshMetaEvent 타입으로 정리하고 Slack으로 전송하는 로직만 남게된다.

- 추가 연동이 발생하거나 로직 수정시 타 소스에 영향도가 낮다.

 

<정리>

- Event 발행구조 : 해당 Event발생에 따른 후속 상세로직은 Biz Service에서 알 필요가 없다.

- AbstractClass 를 활용하여 추상화하고, 세부 메세지 조립에 대한 로직은 상세클래스에서 구현한다.

- 후속 로직은 요건에 따라 비동기로 처리하여 Biz Logic에 영향이 없도록 한다.

 (비동기/넌블러킹 기술스택으로 변경하여 throughput 을 증가시키는 것은 덤!)

- 구조가 기술보다 우선이다.

 

<개요>

- 로컬 테스트코드 목적으로 data.sql 과 함께 h2 database 를 구성하여 사용 중
- 1.4.199 -> 2.0.206 버전으로 업그레이드 호환성 문제 발생

<내용>

1. 예약어 추가

- 사용중이던 테이블 중 USER 테이블이 존재하는데 h2 ver 2에서는 USER 가 예약어로 지정되어 사용할 수 없는 문제 발생
- datasource 설정 중 url 부분에 다음과 같이 추가

url: jdbc:h2:~/test;NON_KEYWORDS=USER

2. GeneratedValue

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)

- 기존에는 다음과 같이 설정시 ID가 자동으로 세팅되어 수행되었으나 ver2에서는 id가 null로 들어오는 현상발생
- github에 해당 이슈가 존재하며 Hibernate ORM 에서 아직 해당기능을 지원하지 않는 것으로 보임
1) 테스트코드에 id 세팅 부분추가
2) datasource 설정 중 url부분에 다음과 같이 추가

url: jdbc:h2:~/test;MODE=LEGACY



3. drop table의 경우 FK등의 제약사항에 따라서 순서가 필요하게 되는데 이부분 역시 잘 동작하지 않는것으로 보인다.
추가 확인이 필요하다.(22.03.24)

<참조>

https://github.com/h2database/h2database/issues/3302

H2 (version 2.0.202 ) auto_increment not working · Issue #3302 · h2database/h2database

Hello :) We are trying to bump the h2 dependency from 1.4.200 to 2.0.202 and the auto increment is not working anymore in my spring application. <dependency> <groupId>com.h2database<...

github.com

+ Recent posts