public abstract class AbstractCommonTable {
public abstract Integer getPrimaryKey();
public boolean checkAvailablePrimaryKey(){
return //condition;
}
public boolean checkNotAvailablePrimaryKey(){
return !checkAvailablePrimaryKey();
}
}
- 여러 비지니스 Dto 에 필요한 PK의 필드명은 각각 다르지만, 이를 활용한 공통로직을 구성할때 필요하여 작성했다.
- 일반적으로 Controller 레벨에서 혹은 이를 테스트하는 코드를 작성할때 우리는 객체를 Json string으로 변환하여 사용한다.
- 여러 종류의 오픈소스가 존재하는 데 여기서는 jackson을 사용하였다. (Spring default)
ObjectMapper mapper = new ObjectMapper();
String str = mapper.writeValueAsString(productInsightDto);
<현상>
- 그러나 Controller Testcase를 수행하였을때 org.springframework.http.converter.HttpMessageNotReadableException 을 만나게 된다.
- 구글링으로 통해서 다양한 원인을 찾을 수 있는데 정리해보면 Json 형식 오류이다.
a. body형식 -> JSON
b. LocalDateTime -> module등록, @JsonSerialize, @JsonFormat/@DateTimeFormat 등등
- 내가 발생했던 오류는 getPrimaryKey()라는 메소드가 실제 Json Data Format과 불일치하기 때문에 발생하는 것으로
@JsonIgnoreProperties({"primaryKey"})
를 설정해주면 간단히 해결된다.
- 추가 : 간혹 application.yml에 설정해둔 내용이나 나의 의도와는 관계없이 동작하는 경우가 있는데 ObjectMapper가 여러개 만들어있지는 않은지 확인한다.
applicatoin.yml에 설정하는 내용은 Spring에서 자동생성하는 ObjectMapper의 설정으로, 본인이 별도의 Bean으로 생성하였다면 따로 관리해주어야 한다.
- 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에 대해서 좀 더 자세히 정리하도록 하겠다.
- 당연히 잘 될것으로 예상하고 수행하였으나 다음과 같은 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 역직렬화를 할때 문제가 발생하게 된다.
@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);
}
}
- 기존에는 다음과 같이 설정시 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)