728x90

현재 회사에서 spring-kafka 를 이용해서 특정 서비스들의 푸시 메세지 이벤트를 받아서

 

전송해주는 서버를 구현하고 있다. (오늘 쿠버네티스에 배포까지 했다!! 모르는게 너무많은...)

 

로컬에서 내 맘대로 메세지 토픽을 발행해서 쏴도 잘 맞게 역직렬화를 수행을 해주길래 그냥 그런가보다.

 

하고 잘 넘어갔던 찰나에!!!

Class Not Found Exception

왜? 이 에러가 났을까?

일단 나는 구독하는쪽 그러니까 Kafka에서는 Consumer 쪽 만을 구현해주었다.

 

내 지식이 부족했던 탓인지는 모르겠지만, 어쨌든 같은 JSON 형태라고 생각해서 클래스가 무엇이던 간에

 

JSON형식만 같다면 Consume해도 괜찮을 것이라고 처음 생각했었다.

 

그래서 발행 모델인 Producer쪽에서는 예를 들면 MessageReq로 Producer가 보내고

 

받은 부분인 Consumer에서는 PushReq라고 받는다고 하고 데이터는 둘다 똑같은 형식으로 매칭이 되어있다고 가정한다.

 

이랬을 때 Consumer 서버를 키면???!!!

 

바로 Class Not Found Exception이 떠버린다.

Caused by: org.springframework.messaging.converter.MessageConversionException: failed to resolve class name. Class not found [com.github.lsj8367.message.PushReq]; nested exception is java.lang.ClassNotFoundException: com.github.lsj8367.message.MessageReq
  at org.springframework.kafka.support.converter.DefaultJackson2JavaTypeMapper.getClassIdType(DefaultJackson2JavaTypeMapper.java:138)
  at org.springframework.kafka.support.converter.DefaultJackson2JavaTypeMapper.toJavaType(DefaultJackson2JavaTypeMapper.java:99)
  at org.springframework.kafka.support.serializer.JsonDeserializer.deserialize(JsonDeserializer.java:342)
  at org.apache.kafka.clients.consumer.internals.Fetcher.parseRecord(Fetcher.java:1030)
  at org.apache.kafka.clients.consumer.internals.Fetcher.access$3300(Fetcher.java:110)
  at org.apache.kafka.clients.consumer.internals.Fetcher$PartitionRecords.fetchRecords(Fetcher.java:1250)
  at org.apache.kafka.clients.consumer.internals.Fetcher$PartitionRecords.access$1400(Fetcher.java:1099)
  at org.apache.kafka.clients.consumer.internals.Fetcher.fetchRecords(Fetcher.java:545)
  at org.apache.kafka.clients.consumer.internals.Fetcher.fetchedRecords(Fetcher.java:506)
  at org.apache.kafka.clients.consumer.KafkaConsumer.pollForFetches(KafkaConsumer.java:1269)
  at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1200)
  at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1176)
  at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:741)
  at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:698)
  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
  at java.lang.Thread.run(Thread.java:748)

예제코드는 깃허브에 있지만, producer와 consumer는 각기 다른 서버로 만들어서 진행시켜주어야 한다!! 안그러면 에러 안나고 받지도 않는다.

왜? Consumer쪽엔 직렬화한 MessageReq클래스가 없기 때문이다.

그래서 보내는 쪽에서 미리 받는 Consumer쪽의 객체 형식을 Header에 바인딩 해주거나,

받는 입장에서 Header가 아닌 Method로 받게 설정해줄 수 있다.

해결책

그래서 해결책이 무엇이냐!

위에서 잠깐 말했지만 producer에서 header를 설정해주거나 consumer에서 설정을 해주는 방법이 있다고 했다.

물론 서로 정의가 잘 되어있고, 동시에 구현했다면 나는 전자의 방법을 택했을 것이다.

그렇지만, 각자 개발하는 속도가 있었고, 또 다른 작업들을 계속해서 진행해야 하고 이미 돌아가고 있던 배치서버에서 그걸 바꿔서

다시 배포하기엔 쉽지 않았었다. (사실 말씀드리기도 좀 그랬다.)

디버깅

그래서

맨끝부분에 false를 넣어주었는데, 이는 들어가보면...

생성자를 오버로딩하기에 쭉 들어오니 이 생성자가 나온다.

initialize에서 해당 boolean값을 사용하고 있기 때문에

boolean값에 의해서 어떤 타입으로 결정하는지가 나온다.

그 설명은

위에서 앞서 설명했던 것과 똑같은 설명이 자바독으로 쓰여져있다.

공부해야될거 참 많다...

정리

그래도 내가 혼자 힘으로 이 메세지 서버를 만들면서 정말 재밌게 개발했다.

 

물론 오늘 배포하는데 쿠버네티스 지식이 없어서 애를 먹었다. ~~ㅋㅋㅋㅋㅋㅋ~~

 

그래도 백엔드 개발자분들이 도와주셔서 꿀 플러그인도 전수받고 기본 개념을 정리해가는 것 같다.

 

뭐하나 건들면 바로 공부해야되는게 너무 어렵지만 재밌어서 좋다.

 

조만간 카프카 기본개념과 쿠버네티스도 정리해야겠다 🔥🔥🔥🔥🔥

728x90

'Spring > Kafka' 카테고리의 다른 글

Kafka Offset Commit의 중요성  (0) 2022.10.06
Spring Kafka 좀 더 공통 설정하기  (0) 2022.10.06
728x90

ModelAttribute와 RequestBody의 커맨드 객체 파싱이 다른것을 확인했다.
한번 알아보자! ModelAttribute 동작과정은 덤이다.

ModelAttribute

이 포스팅을 하는 이유는 인자가 많을 경우에 post방식으로 조회를 하는 식으로 구성을 했었는데,

코드리뷰중에 이런말이 나왔었다.

get방식으로 다른 객체로 묶어서 한번에 받아보는건 어떤가요?

변수가 많아지면 많아질 수록 수정점이 늘어날것 같아요! 라고 받았다.

그래서 무의식적으로 평소에 하던방식처럼 post로 수정하여 커밋하고 수정했었다.

근데 post로 안바꾸고 get에서 @ModelAttribute 사용하면 객체로 파싱이 된다는것을 듣고 내가 부족했구나 싶었다.

이 글은 그 부분에서 나와 집에와서 따로 정리하여 포스팅한다.

코드는 깃허브에 있다.

컨트롤러와 dto 그리고 컨트롤러 테스트 코드를 간략하게 작성했다.

Debugging

그리고 디버깅을 돌리면 이런 순서로 진행이 된다.

무조건 DisPatcherServlet이 모든 작업을 분산하여 처리 위임을 진행해주는데,

여기서의 핵심은 이 아랫부분이다.

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter로 시작

핸들러 메소드를 처리할 수 있는 ArgumentResolver 들과
returnValueHandler들을 넣어준다.

이렇게 넣어준다!

ServletInvocableHandlerMethod

다음은

ServletInvocableHandlerMethod가 returnValue를 할 수 있는 애로 채택되는데

InvocableHandlerMethod

InvocableHandlerMethod를 통해 HandlerMethodArgumentResolver를 찾는다.

쭉 반복해서 알맞는 것을 탐색중....

완료되면? 여기서 찾게되는 인스턴스가 바로 ModelAttributeMethodProcessor

ModelAttributeMethodProcessor가 처리해주게끔 반환해준다.

ModelAttributeMethodProcessor

ModelAttribute어노테이션을 읽어들이고

ServletModelAttributeMethodProcessor가
어노테이션이 붙은 변수의 타입이 url 변수와 일치하는 속성이 있는지

이렇게 찾는다.

찾고 없으면 null을 반환하여 객체 생성부분으로 간주하고

같은 객체내에 오버로딩된 메소드에서 리플렉션을 이용해서
해당 ModelAttribute 어노테이션이 붙은 객체의 타입을 가져온다.

BeanUtils 클래스를 이용하여
이때 이 타입의 생성자를 찾는데
제일 먼저 기본생성자를 찾고 있다면 기본생성자를 반환하고 아니라면 구현된 생성자를 전부 가져오는데
이때 길이가 1개이면 해당 생성자를 생성해준다.

이후에 ModelAttributeMethodProcessor에서 파라미터와 매칭되는 것을 넣어준다.

커맨드 객체의 차이?

개인적으로 생각했을 때 @ModelAttribute@RequestBody를 읽는 커맨드 객체의 차이점은

RequestBody는 Jackson 라이브러리를 통해서 읽어와 ObjectMapper가 사용되서 조금 생성부분에서 차이가 나지않나 생각한다.

@RequestBody

전에 RequestBody 동작이 어떻게 되는지를 보며 작성한 메세지 컨버터 정리

중간 부분에 보면 해당 컨버팅 작업을 MappingJackson2HttpMessageConverter가 수행해주는데

read() 메소드에서 ObjectMapper를 사용하여 매핑해준다.

그래서 @RequestBody는 getter메소드와 기본생성자가 있어도 ObjectMapper가 해주기에 setter를 쓰거나 해주지 않아도 주입된다.

@ModelAttribute

ModelAttribute는 이와는 다르게 커스텀하게 어노테이션을 만들어서 우리가 MethodArgumentResolver를 구현해서 파라미터에 값을 넣어주듯,

어노테이션 + 리플렉션을 이용해서 값을 넣어주는 것이다. 물론 ServletRequest에서 읽어와서 값들을 가지고 있는건 둘다 공통이지만 말이다.

그래서 기본생성자와 getter만 있으면 값을 넣어줄 수가 없기에

기본생성자에 setter를 전부 생성해주거나 전체 필드를 할당할 수 있는 생성자를 만들어 주어야 한다!!

728x90

'Spring' 카테고리의 다른 글

Slack Slash Commands(슬랙 슬래시 커맨드) 사용하기  (1) 2023.07.06
분산 락  (0) 2023.04.15
@Valid, @Validated 차이  (0) 2022.08.10
AOP  (0) 2022.08.10
728x90

이펙티브 자바를 읽다가 약한참조에 대한 이야기가 나와서 포스팅한다.

참조에는 아래 4가지가 존재한다.

  • Strong References (강한 참조)
  • Soft References (소프트 참조)
  • Weak References (약한 참조)
  • Phantom References (팬텀 참조)

이 해당 참조 유형에 따라 GC 실행 대상여부, 시점이 달라진다.

강한참조

new 연산자를 사용하여 객체를 인스턴스화 하고 참조하는 방식.

참조가 해제되지 않으면 GC의 대상이 되지 않는다.

Test test = new Test();

해당 test라는 변수가 참조를 가지고 있다면 GC의 대상이 되지 않는다.

test = null이 되는 순간 GC의 대상이 된다.

 

소프트참조

대상 객체의 참조가 SoftReference만 있다면 GC의 대상이 된다.

단, JVM 메모리가 부족한 경우에만 Heap에서 제거된다.

메모리가 부족하지 않은경우에는 제거하지 않는다.

 

public static void main(String[] args) {
        String ss = "문자열";
        SoftReference<String> reference = new SoftReference<>(ss);

        // 이 시점에 GC의 실행 대상이 가능
        ss = null;

        System.gc();

        // JVM의 메모리가 부족하지 않아서 GC 실행 대상이 되지 않은 경우
        // 그대로 유지한다.
        ss = reference.get();
        System.out.println(ss);
}

약한참조

위에서 봤던 소프트참조와 비슷하게 

대상 객체의 참조가 WeakReference만 있다면 GC의 대상이 된다.

다른점은, 메모리가 부족한경우가 아니라 다음 GC가 일어나게 되면 바로 힙에서 제거된다.

 

public static void main(String[] args) {
        String ss = "문자열";
        WeakReference<String> reference = new WeakReference<>(ss);

        // 이 시점에 GC의 실행 대상이 가능
        ss = null;

        System.gc();

        // gc를 명시적으로 호출했지만 컬렉션이 동작하지 않을수도 있음
        // 그래도 무조건 동작한다고 가정
        ss = reference.get();

        // null 로 비어있게 된다.
        System.out.println(ss);
}

팬텀참조

생성시 ReferenceQueue가 필요하며, PhantomReference의 참조값을 수동으로 clear() 메서드를 실행해야 하고, PhantomReference.get() 메서드는 항상 null을 반환한다는 특징이 있다.

 

PhantomReference는 객체 내부의 참조를 null로 설정하지 않고 참조된 객체를 phantomly reachable 객체로 만든 이후에 ReferenceQueue에 enqueue 된다.

 

두가지에서 사용한다.

  1. 자원 정리 (finalizer 보다는 조금 나은 방법) 그렇지만 try-with-resources를 사용하자.
  2. 생성 비용이 비싼 객체가 언제 메모리에서 해제되는지 알 수 있음.

깃허브 바로가기

 

GitHub - lsj8367/laboratory: 뭔가를 연습해보기 위한 연구 저장소

뭔가를 연습해보기 위한 연구 저장소. Contribute to lsj8367/laboratory development by creating an account on GitHub.

github.com

 

728x90

'Java' 카테고리의 다른 글

effectively final 및 lambda capturing에 대해 톺아보기  (2) 2023.12.08
Checked Exception, Unchecked Exception  (0) 2022.09.07
변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09
728x90

자바 변성 (Variance)

자바의 가변성에는 크게 공변, 무공변, 반공변이 존재한다.
제네릭을 잘 사용하려면 이 가변성에 대한 이해가 필요하다.

변성을 제대로 이해하려면 "타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입인가?" 라는 질문에서 시작하는게 좋다.

배열은 공변, 제네릭은 무공변이 기본이라고 다들 알고 있을 것이다.

무공변 (Invariance) or 불공변

기본적으로 제네릭은 무공변이다.

무공변이라고 하니 헷갈리는것 같다. 사전적으로 번역해보면 불공변으로 나오게 된다.

타입 S가 T의 하위 타입일 때, Box[S]와 Box[T] 사이에 상속 관계가 없는 것

쉽게 말하면 너는너, 나는 나 인 느낌이다.
그래서 선언한 유형만 들어갈 수 있게 코드를 구성할 수 있다.

Object에는 Object만, String에는 String만 들어갈 수 있단 얘기이다.

void invariance() {
    // 제네릭은 기본적으로 무공변
    List<Object> objectList = new ArrayList<>();
    List<String> stringList = new ArrayList<>();

    objectList.add(1);
    objectList.add(1.0);

    stringList.add("aaaaa");
}

공변

공변(covariance)는 타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입 임을 나타내는 개념

@Test
void arrayTest() {
    Object[] arr = new Long[5]; //배열에서는 공변이고, Long은 Object의 하위타입이기에 할당이 가능하다.
    arr[0] = "arr"; //공변으로 인해 선언한 arr은 Object로 참조가 된상태라 String도 할당 가능.
    // 여기서 런타임에 ArrayStoreException 발생
}

자바에서 이 배열을 공변으로 열어두지 않았다면, 다형성의 이점을 살릴 수 없게 됐을 수 있다.

Arrays.swap()

Arrays의 메소드를 하나를 가져와봤는데,
만약 공변이 아니었다면, 이 배열 스왑 메소드는 객체별로 전부 구현해주어야 했을 것이다.
제네릭이 있기전엔 형변환에 대한 에러가 나더라도,
다형성의 장점으로 얻을 수 있는 이득이 많았을 것 같다.

리스트의 공변

void variance() {
    List<? extends Object> list = new ArrayList<Number>();
    list.add(1); //컴파일 에러
    list.add(1.0); //컴파일 에러
    list.get(0); // 정상 로직
}

이처럼 선언을 했을때 add는 선언된 제네릭으로 변수를 넣게 되어있는데,

무공변으로 만들었을 경우

공변인 경우

위와 같은 경우에는 capture of ? extends Object e Object의 하위타입은 맞지만,

어떤 타입인지는 모른다? 라는 뜻이라고 생각된다.

그래서 list.get(0)이 Object로 형변환은 가능하지만, 반대로 add()를 통해 null을 제외한 무언가를 추가해줄 수는 없다는 소리이다. 안에 들어가는 객체가 정확하게 뭔지 모르기 때문이다.

그래서 정확한 타입이 어떤건지는 모르기 때문에 개발자가 null을 제외하고는 아무것도 추가하지 못하게 막을 수 있다라고 봐도 될 것 같다.
그래서 자주쓰던 Collections 클래스의 UnmodifiableList를 찾아보게 되었다.

생성자에 이런식으로 공변을 이용해서 막아주고 있는것을 볼 수 있었다.
그러면서 List의 구현체이기 때문에 밖에서는 add에 어떤 값을 넣어줄 수는 있기에, 그대로

Override로 재정의 한 뒤에 Exception을 던져주게 만든것을 확인할 수 있었다.

반공변(Contravariance)

반공변 처음 봤을때 반만 된다 이런생각을 했었다.ㅋㅋㅋㅋㅋㅋㅋ

그게 아니라 공변의 반대

타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 상위 타입 임을 나타내는 개념입니다.

@Test
void contravariance() {
    List<? super Number> list = new ArrayList<>();
    list.add(1.0);

    final Number number = (Number) list.get(0);
    final Object object = list.get(0);
}

Number를 포함한 Number의 상위 타입들만 들어갈 수 있게 설정한 상태이다.

아까는 하위타입이 뭔지 알 수가 없다는 것이었는데,

이 코드는 Number 상위인건 알겠는데 상위 누구인지를 알 수 없는 상태이다.

super키워드 다음에 붙은 클래스까지의 형은 전부 넣을 수 있다는 소리와도 같다.

다시말하면, 최소 Number 타입은 보장이 된다는 소리와 같다.
그래서 list.get(0); 에서 최상 타입인 Object로 꺼내서

형에 맞는 캐스팅 or instanceof를 통해 값을 읽어오는게 가능하다.

마무리

이렇게 자바의 가변성에 대해 알아보았다.
얼추 정리되면서 감은 잡은것 같다.
PECS(Producer Extends Consumer Super)를 보면서,
일반적으로 소비(Consume)라는게 스타크래프트의 디파일러가 저글링을 컨슘해서 저글링을 잡아먹기때문에,

스타크래프트의 컨슘

어떤 컬렉션이 만들어지는 과정이 컨슘이라고 생각하고 값을 빼내는 과정(get)이 동작한다고 알고 있었다.
반대로 생산자(Producer)는 말그대로 생산이기에 값을 생성해주는(new) or 더해주는(add) 것이 생산자로 알고 있었다.
반대로 알고있던 것이다.

올바른 내용

컬렉션을 뒤져서 어떤 작업들을 처리 해주어야 한다면 그게 바로 컬렉션 값을 빼내(get) 뭔가를 만들기 때문에 생산자가 되어 extends를 사용해야 한다는 것이고,

컬렉션에 값을 추가해야되면 매개변수로 주어진 값이 소비되어 컬렉션에 들어가니(add) 소비자 관점이라고 보는것 같다.

그래서 이 경우에는 super를 사용해주면 되겠다.

휴..되게 어렵다 😇😇😇

아무튼 읽기전용으로 만들고 싶을때에는 extends를 사용하는것.

좀더 안전하게 데이터 삽입을 하고싶다면 super를 사용하는 것만 기억하면 될 것 같다.

728x90

'Java' 카테고리의 다른 글

참조 유형  (0) 2022.09.12
Checked Exception, Unchecked Exception  (0) 2022.09.07
일급 컬렉션  (0) 2022.08.09
변수  (0) 2022.08.07
728x90

업무에서 Spring Batch로 세미나를 진행하고, 앱 푸시 기능을 배치로 전환하는 작업을 진행했다.
여기에 저장하면서 글로만 보던 것들을 직접 경험해보면서 겪었던 일들을 기록하려고한다.

첫번째 에러

우리 푸시 배치 서버의 구조는 스프링 스케줄러 서버에서
푸시 서버의 api를 호출해서 해당 job들을 돌려주는 방식으로 구성이 되어있다.

물론 이 부분을 새롭게 개편해야 하는것은 맞다ㅋㅋㅋ

그래서 특정 시간이 되면 해당 job api로 호출을 하는데
여기서 대략 총 데이터가 100,000건 정도 되는데 전부 동기 + 블록킹처리로 진행했다.
그래서 스케줄러가 api를 쏘고 요청값이 최대 오래걸려도 limit을 30분을 잡았었다.
그런데 100,000건의 데이터를 여러 로그를 쌓고, 푸시를 하는데까지 1시간이 넘게 걸렸었다.
그래서 1시간이 지나도 받지 못하는 상황에 에러가 나서 해당 스케줄러가 돌다가 실패가 되었다. (근데 뒤에서의 푸시 서버는 계속 돌고있었다)

그러니 다시 정리해보면 1시간이 넘는 시간동안 작업을 하고 있던것이다.

-> 이부분에서 나도 대기중인 데이터만 뽑는 쿼리를 작성해야했는데 실수로 보낸 데이터까지 조회하게끔 만들었다.

Mono 객체를 block()을 사용해서 푸시를 진행했기 때문에, 블록킹 방식으로 동작하여

제어권도 아예 넘겨버려서 끝이나야 다음 작업을 수행하는 형태로 진행되서 굉장히 느렸었는데,

이방식을 subscribe() 방식으로 바꿔서 논블록킹으로 푸시 발송 명령만주고 다음 작업을 진행하게끔 해서

많이 속도를 줄일 수 있었다.
-> 이부분은 조만간 다른 포스팅에서 자세하게 다룰 예정이다.

찾은 부분

SEND, WAIT 두 상태의 데이터를 모두 가져오고 나서 SEND로 업데이트를 치고 있던 것이다.

이부분이 일단 성능 저하의 첫번째 원인이라 생각했고,
그리고 스케줄러는 다른 스케줄링들도 가지고 있으니, 푸시 서버에서 요청을 바로 돌려주고 해당 Job은 뒷편에서 실행해주는 것이 맞다고 생각했다.
그래서 이 두부분을 고쳐보았다.

  • 쿼리는 WAIT상태만 추출하여 갖고 있는다.
  • 푸시서버는 기본적으로 제공해주는 JobLauncher를 배제한다.
@RestController
@RequiredArgsConstructor
public class Demo {
    private final BasicBatchConfigurer basicBatchConfigurer;

    @PostMapping("/demo/push")
    public String appPushJobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = (SimpleJobLauncher) basicBatchConfigurer.getJobLauncher();
        jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());

        // jobLauncher 실행 로직....
    }
}

이렇게해서 요청을 바로 수행후 return값을 먼저 돌려주었다.
이렇게 해서 스케줄러의 동기처리로 늦어졌던 것에 대해서 일단락 짓게 되었고,
1시간 이내로 처리가 되게 되었다.

두번째 에러

두번째 에러는 페이징 처리에 대한 부분이었다.
참고 블로그 링크
예를 들어서, 총 10페이지로 구성되어있고, 1페이지당 10개의 데이터가 있다고 가정한다.
총 100개의 데이터가 업데이트가 되어야 한다.
근데 커밋이 한번 일어나게 되면,

1페이지 10 -> 2페이지 10 -> 3페이지 10 -> ... 이 될줄 알았었다.

애초에 도입하기 이전부터 해당 참고 블로그를 보면서 이런 에러가 있구나 하면서 개발을 진행했다.
인덱스 기준으로
1 ~ 10 번까지의 데이터가 작업 완료되면 해당 데이터들은 Update가 진행이 될 것이다.
그래서 11 ~ 20을 원하던 다음 데이터는 21 ~ 30을 조회하게 되는것.
이것은 배치의 문제가 아니라 그냥 페이징 쿼리 자체의 문제라고 한다.

그래서 유저들에게 푸시를 보낼때 50%의 유저만 푸시를 받았을 것이다.😇

이렇게 해서는 안됐다.
방법은 우선 2가지가 있었다.
그렇지만 나는 2번째 방법을 사용했다.

그렇다면 왜?

우리 회사의 배치 서버는 JPA로 구성하기로 했었고, 그래서 JPA를 사용한 배치 동작을 구현해서
Cursor대신 JpaPaging을 사용했다.

커서 사용

커서(Cursor)란??

쿼리문에 의해서 반환되는 결과값들을 저장하는 메모리공간
Fetch => 커서에서 원하는 결과값을 추출하는 것
커서는 한번 커넥션을 맺은 후 커서만을 다음으로 가기 때문에 조회하고 Update되어도 갱신되는 일이 없이 적용 가능하다.

@Bean
@StepScope
public JpaPagingItemReader<Pay> payPagingReader() {

    private final int chunkSize = 1000;

    JpaPagingItemReader<PushAlarm> reader = new JpaPagingItemReader<PushAlarm>() {
        @Override
        public int getPage() {
            return 0;
        }
    };

    reader.setQueryString("SELECT p FROM PushAlarm p WHERE p.sendStatus = :sendStatus");
    reader.setParameterValues(Map.of("sendStatus", "WAIT"));
    reader.setPageSize(chunkSize);
    reader.setEntityManagerFactory(entityManagerFactory);
    reader.setName("payPagingReader");

    return reader;
}

이런식으로 page를 0으로 고정시켜줘서 update가 일어나도 다시 0페이지만 계속 조회하는 것이다.
이렇게 해서 문제를 해결했다.

마무리

배치 세미나를 진행하면서 공식문서를 읽고 참고 블로그까지 더해서 학습해서
적용해본 결과 그래도 역시 글로 보는것보다 맞으면서 배우는게 좀 더 빠르게 습득이 가능하다는걸 느낀다.
지금은 데이터가 작아서 내가 일을 제대로 처리했을지 모르겠다.
그래서 강의를 하나 더 들으면서 좀더 뿌리를 깊게 내려야겠다...

728x90
728x90

도메인

도메인은 구현해야할 소프트웨어의 대상이다.
쇼핑몰을 생각해보면 쇼핑몰은 대상, 그리고 상품조회, 주문, 배송, 결제 등등이
하위 도메인이 된다.
그리고 도메인이라고 해서 고정된 하위 도메인이 존재하는건 아니다.
결제같은것을 PG사에 위임하니까 말이다.

도메인 모델 패턴

기존의 나는 흔히 말하는 MVC 레이어 아키텍처를 사용했다.

이미지 출처 바로가기

단순히 Controller, Service, Repository세개를 사용해서 구현했었다.

이것은 내가 넥스트스텝에서 TDD와 클린 코드를 수강했어도 변하지 못했다.
근데 이 방식이 아니라 DDD책을 보면서 주워들었던 도메인 모델 패턴을 봤다.
그래도 말만 바뀌었지 느낌은 전과 같았다.

의존은 화살표 방향으로 주입이 된다.

  • Presentation
    • 사용자의 요청을 처리하고 사용자에게 정보를 보여준다.
    • 사용자는 사람이 아니라 외부 시스템일 수 있다.
  • Application
    • 사용자가 요청한 기능을 실행
    • 비즈니스 로직을 구현하지는 않고 도메인의 계층을 조합해주기만 함
  • Domain
    • 이 설계의 가장 핵심
    • 시스템이 제공하는 도메인들의 규칙을 구현
  • InfraStructure
    • DB나 메시징 시스템 같은 외부 시스템의 연동을 처리함

핵심 규칙들은 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 확장해야 된다면
다른 코드에 영향을 끼치지않고 모델들에 반영해주어야 한다.
도출한 모델들은 엔티티와 값 타입으로 구분할 수 있다.

엔티티와 값타입을 제대로 구분하여야 도메인을 잘 설계하고 구현할 수 있다. 👍

엔티티

엔티티는 식별자를 가진다. 이 식별자는 엔티티 객체마다 고유해야 한다.

그래서 회원의 경우 회원번호가 서로 다르다.
이것이 바로 식별자가 되는것!!
다른 값이 바뀌어도 이 엔티티 객체가 생성되거나 삭제되기 전까지 유지된다.

그렇기 때문에 이 식별자 값이 같다면? 같은 객체❗️

그렇다면 이 식별자 값으로 equals(), hashCode()를 재정의 할 수 있다.

식별자 생성

그러면 식별자를 생성해야 하는데
엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.

  1. 특정 규칙에 따라 생성
  2. UUID같은 고유 식별자 사용
  3. 값 직접 입력 (개인적으로 제일 안좋다 생각)
  4. 일련번호 사용 (시퀀스나 DB의 auto-increment)

모델

배송지의 집주소가 우편번호 + 도로명주소 + 상세주소 이렇게 같이 사용하는게 대부분인데,

세 가지 데이터는 다르지만 개념적으로는 하나로 일맥상통한다. 바로 주소

값 타입은 바로 이럴때 사용하는 것이다.
이 값타입의 장점은 이 값들만의 기능을 추가할 수 있다.
이 값객체의 값을 바꾸는건 기존 데이터를 두기보단 변경이 되면 새로 생성하는
불변 객체 를 생성하는 방식을 선호한다.
불변으로 만드는 이유는 안전한 코드를 작성할 수 있다는 곳에 있다.
setter 메소드로 값을 바꾼다면 다른 로직에서 잘못 반영이 되는 불상사를 초래할 수 있다.
그래서 두 값 객체를 비교할 때에는 모든 인자가 같은지를 체크해주어야 한다.

무지성 Getter/Setter 남용금지

getter는 어쩔수 없는 경우가 있다고 할지라도,
setter는 값이 깨지는것을 막을수는 없다. + 핵심 개념이나 로직을 망가뜨릴수 있다.

728x90

'아키텍처' 카테고리의 다른 글

DDD 표현 영역과 응용 영역  (0) 2022.08.11
Monolithic vs MSA  (0) 2022.08.10

+ Recent posts