728x90

분산 락

회사에서 스크래핑 작업을 수행하다가 분산락을 구현하게 되어 너무 잦은 야근으로 인해 이제야 포스팅을 끄적여본다.

한번의 스크래핑요청은 곧바로 비용 + 시간이 요구된다.

비용이 요청때마다 들고, 내가 구현했던 정부기관의 스크래핑 시간은 길면 15초까지 걸리는 작업도 있다. (의존도가 너무 높은것도 사실이다)

이런 비용과 시간이 드는 스크래핑 작업을 동시 다발적으로 같은 작업을 수행해준다?

이것도 문제가 있을 것이다. 한번의 스크래핑 데이터를 계속해서 돌려쓰면 되지않을까?

이러한 스크래핑 데이터를 공유자원이라 생각했고 이미 요청중인 작업이 있다면 이미 작업중인 스크래핑이 있다고 오류를 내려주는게 오히려 적합하다고 생각했다.

Synchronized

우선 Java에서 제공해주는 synchronized 키워드를 사용하여 동시성을 제어하는 것은 어떨까 하고 생각해봤다.

자바 내부의 monitor를 기반으로 상호 배제 기능을 제공해준다.

Monitor란?

3. JVM Synchronization이란?

해당부분에서 책 스터디를 하며 monitor에 대해 정리했었다.

여기까지만 보면 오! 동시성 제어 완벽하게 할 수 있겠지만, monitor는 1개니까 비용이 비쌀것으로 예상해 적재적소에 꼭 사용해야 하는 곳만 사용해야겠다! 라고 처음 생각했다.

근데 생각해보니 문제점은 아래와 같았다.

정상 개발환경 - 이 환경은 서버 1개로만 운영되어도 문제없다

문제 운영환경 - 이 환경은 이중화 구성이 되어있고 부하분산 되어있다.

분산락

그래서 등장한게 바로 이 분산락이다.

Redis를 사용하여 구현을 했는데 락이라는 단어가 나왔다고 해서 DB에서의 락을 생각하면 다른 개념이므로 주의하자.

DB의 락을 이용하여 동시성 제어를 할 수는 있지만, 분산락으로 풀어내는게 더 많을 것 같다.

그럼 왜?

왜냐면 일단 DB의 락을 건다는 것 자체가 결국 DB에 대한 I/O가 한번은 일어나야 락상태인지 어떤지 알 수 있기 때문에 DB 조회 1회를 무조건 1번은 한다는 전제를 깔고 간다 생각해서 동시성을 컨트롤 해주려면 그 앞단에서부터 끊어줘야 될 것이라고 생각했다.

그래서 대표적으로 많이 쓰는 Redis를 사용하여 분산락을 구현했다.

결국 이 개념은 키 하나를 갖고 있어서 이 키가 있다면 락이 잡혀있다고 생각하면 된다.

그래서 이중화 구성이 된 서버들이 하나의 Redis를 바라보며 임계영역에 대해 접근을 할 수 있는지를 체크한다.

이득은?

이렇게되면 맨 위에서 언급했던 비용 + 시간을 절약할 수 있는 구성이 되었다.

여러 사업자 등록번호로 스크래핑하는 것은 가능하나, 같은 법인의 경우 스크래핑을 요청하는 순간에는 이런 분산락을 구현해주어 요청을 차단하는 효과를 가지게 되었다.

 

이전엔 이런 기능 없이 무조건 요청이 들어온다면 그냥 계속 스크래핑을 수행했으며 스크래핑 이력, 받아온 데이터또한 남기지 않았다.

 

회사의 코드를 구성할 순 없으니 최대한 비슷한 구성으로 코드를 구성해보려 한다.

항상 말하지만 모든 코드는 나의 깃허브에 있다.

 

scrapeSomething 메소드를 실행하는 경우 synchronized옵션을 주어 설정한다면

각 프로세스마다의 실행이 1회씩 수행될 것이다.

한 프로세스에서 실행됐을때 이중화 이상이 구성된 프로세스에서도 실행되지 않게 하려면 레디스로 아래 이미지와 같이 수행해주자!

코틀린으로 구현해본 분산락 로직

결론

결국 이 분산락을 통해 새로고침하고 다시 스크래핑 수행하는 요청을 막아서 비용이 비싼 스크래핑 로직을 안태울 수 있게 로직을 변경했다.

728x90

'Spring' 카테고리의 다른 글

Slack Slash Commands(슬랙 슬래시 커맨드) 사용하기  (1) 2023.07.06
@ModelAttribute, @RequestBody 커맨드 객체  (3) 2022.09.15
@Valid, @Validated 차이  (0) 2022.08.10
AOP  (0) 2022.08.10
728x90

예제는 깃허브에 있다.

트랜잭션을 공부했다고 생각하고 업무에 임했던 나였는데, 도저히 풀리지않는 느낌으로 예외를 받았던게 있다.
이전에 한번 포스팅했던 Stream Closed 에러였는데 이게 또 한번 나를 붙잡았다.
처음 개발환경에서 OpenFeign을 사용하면서 또 logging level에서 IOException이 나는줄알고 이부분으로 삽질을 했다.

근데 그게 아니어서 이 포스팅을 작성했다.

  • HelloController.java
@RestController
@RequiredArgsConstructor
public class TestController {

    private final TransactionParentService transactionParentService;

    @GetMapping("/transaction/test")
    public ResponseEntity<?> test() {
        transactionParentService.size();
        return ResponseEntity.ok().build();
    }
}
  • TransactionParentService.java
@Service
@RequiredArgsConstructor
public class TransactionParentService {

    private final MemberService memberService;
    private final TransactionErrorService transactionErrorService;

    @Transactional
    public int size() {
        transactionErrorService.throwExceptionLog();
        return memberService.allSize();
    }
}
  • TransactionErrorService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class TransactionErrorService {

    private final MemberService memberService;

    @Transactional
    public void throwExceptionLog() {
        try {
            memberService.saveAndException();
        } catch (RuntimeException e) {
            log.error("error : {}", e.getMessage());
        }
        System.out.println("끝");
    }
}
  • MemberService.java
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public void save() {
        memberRepository.save(new Member(1L, "홍길동"));
    }

    @Transactional
    public void saveAndException() {
        memberRepository.save(new Member(null, "홍길동"));
        throw new RuntimeException();
    }

    public int allSize() {
        return memberRepository.findAll().size();
    }

}

TransactionParentService -> TransactionErrorService -> MemberService 순서로 서비스는 동작하게 되고,
기본적으로 Service에는 메소드마다 @Transactional 어노테이션이 붙어있다.

아래는 application.yaml에

logging:
  level:
    org.springframework.transaction: trace

이와 같은 로그설정을 해주고 찍어본 결과이다.

TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.github.lsj8367.service.TransactionParentService.size]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.github.lsj8367.service.TransactionErrorService.throwExceptionLog]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.github.lsj8367.service.MemberService.saveAndException]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]

TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.github.lsj8367.service.MemberService.saveAndException] after exception: java.lang.RuntimeException

TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.github.lsj8367.service.TransactionErrorService.throwExceptionLog]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.github.lsj8367.service.MemberService.allSize]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]

TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.github.lsj8367.service.MemberService.allSize]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.github.lsj8367.service.TransactionParentService.size]

여기서 보면 정상적으로 트랜잭션을 처음엔 쭉 획득한다.
그리고나서 memberService.save()를 통해 SimpleJpaRepository까지 하기위한 transaction을 얻게된다.

에러가 나는 부분

우리의 코드에서는 save 이후 throw new RuntimeException()이 있다.
여기서 부터 after Exception : java.lang ...으로 보이는 rollback 마킹이 존재한다.

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752) ~[spring-tx-5.3.22.jar:5.3.22]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) ~[spring-tx-5.3.22.jar:5.3.22]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654) ~[spring-tx-5.3.22.jar:5.3.22]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407) ~[spring-tx-5.3.22.jar:5.3.22]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.22.jar:5.3.22]
    ...생략
    at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

이게 도대체 어떻게 동작하는것인가?
추상클래스인 TransactionAspectSupport.invokeWithinTransaction 메소드를 사용하는 TransactionInterceptor의 invoke를 호출해서 안쪽에 추상클래스의 메소드를 사용한다.

스크린샷 2023-02-27 오후 10 37 23

해당 @Transactional이 붙은 메소드에서 던져진 exception을 Throwable 객체로 catch절에서 받고있다.

스크린샷 2023-02-27 오후 10 40 03

  • DefaultTransactionAttribute

스크린샷 2023-02-27 오후 10 43 31

  • rollbackOn 메소드

스크린샷 2023-02-27 오후 11 04 58

이 스프링 기본 구성인 DefaultTransactionAttribute구현에서 rollbackOn 메소드를 사용하게 되는데
이 rollbackOn에서 그토록 얘기했던 말들이 나온다.

RuntimeException이 여기서 채택되어 instanceof로 체크하고 있다!!!

그래서 rollback 마크를 하나를 진행해두고 나머지 트랜잭션 어노테이션에 대해서도 commit을 할거냐 rollback을 할거냐에 대한 작업을 이 해당 aop를 통해 동작한다.

TransactionManager

각 트랜잭션 매니저들은 AbstractPlatformTransactionManager 추상클래스를 상속하여 사용한다.

스크린샷 2023-02-27 오후 10 53 18

그래서 트랜잭션 매니저가 전부 commit을 수행하는데,
rollback 전용 마크가 하나라도 붙게되면 UnexpectedRollbackException 이 에러가 나타나게 된다.
그래서 보이게되는 예외 메시지가 Transaction silently rolled back because it has been marked as rollback-only로 나오게 된다.

그럼 Stream Closed 는?

ㅋㅋㅋㅋ.. 그러게 말이다. 이거 왜뜬건지 도저히 이해가 되지 않는데, dev환경에선 이 로그만 보였지 위의 로그가 안보였었다.
근데 로컬에서 이 부분을 수정하니 말끔히 해결됐다.
아무튼 원초적인 문제는 이 무지성 붙이기 @Transactional이었다.
막판에 이게 떠올라서 지웠더니 해결되서 꿀잠각이다.

아무튼 @Transactional에 대한 디버깅 정리를 해본다.

728x90

'디버깅' 카테고리의 다른 글

Kafka가 내 로직을 9번이나 재시도를 했다  (0) 2023.11.02
Stream 오류 제거  (2) 2023.04.21
FeignClient Logging level 디버깅  (0) 2022.12.17
@Async 사용시 에러 해결  (0) 2022.11.04
728x90

1일1커밋 회고나 다른 회고는 작성했으면서 정작 연말에 한번씩 작성하는 회고는 이번이 처음인 것 같다.

어제는 회사에서 백엔드 팀끼리 회고를 진행했다!!!

KPT 방식으로 진행을 했었는데, 바빠서 못했던 것들 그리고 문제적으로 좀 느끼고 있던 것들 위주로 Try 에서 3가지가 꼽혔다.

1. 테스트코드 2. 레플리카 DB 활용 3. 컨벤션 확립

 

테스트코드는 나는 단위테스트를 짜면서 개발을 진행했었는데, 다른 분들도 이제 유지보수가 점점 어려워지면서 테스트코드를 지금이라도 늦지 않았으니 꼭 짜자 라는 방식으로 안건이 나와 채택되었다.

레플리카 DB는 구성은 되어있으나 제대로 활용하지 않고 있었는데, 이번 기회에 내가 건의하여 이슈를 수면위로 드러냈다.

다들 의견이 동일하셔서 안건이 채택됐다.

3은 협업한다면 당연히 확립되어야 하는게 맞지 않나 해서 채택되었다. (얘기는 많이 나왔지만 실패했었다고 한다)

 

자, 이제 내 회고로 좀 돌아와보자!

 

올 초반에는 내가 다른회사에서 일을 진행했다.

일단 내가 블로그를 올해 많이 옮겼다.

 

초반에는 깃허브 블로그, 중간에 벨로그로 옮겼으며, 마지막인 지금은 티스토리에 정착한 상태다.

그래서 깃 커밋 로그를 좀 뒤져봤다 ㅋㅋㅋㅋㅋㅋ

 

1월

https://lsj8367.tistory.com/entry/Mock-%EC%82%AC%EC%9A%A9%EA%B8%B0 이 글이 1월에 작성했던 글이다.

이 때 당시에 테스트 코드에 대한 관심을 지속적으로 가졌고,

계속 진행했던 사이드 프로젝트 팀에서 HTTP 완벽 가이드 스터디를 진행했다.

 

2월

2월에는 자료구조, 알고리즘 그리고 디자인패턴에 관심을 많이 가졌던 한 때이다.

회사에서 테스트코드를 먼저 작성한 기반들을 토대로 리팩토링을 진행하면서,

디자인패턴을 적용시키거나, 아키텍처 관점에서 분리할 수 있는 방법들을 계속해서 생각해냈던것 같다.

그때 만들었던게 벨로그로 옮기면서 퍼사드 패턴 적용기

 

Facade Pattern 적용기

퍼사드 패턴에 대한 설명은 깃허브 에 있다.이직하고 회사를 옮기면서 지금의 커머스 서비스 회사에 입사했다.리팩토링 진행도중에 좀 좋은 구조를 구성해서 적어본다.우선 Service 레이어에서 Re

velog.io

이걸 작성했는데, 이 때 당시에 나보다 늦게 들어오셨던 신입분께서 아는 지인이 이 글을 봤다고 했었다...!!! 😱

그래서 되게 신났던게 아직 내 기억에 남아있다 ㅋㅋㅋㅋ 더 열심히 글을 작성해야겠다고 생각했던 날이었다.

 

3월 ~ 4월

사실 이부분엔 되게 뭘 한게 없는것같다 라고 생각했는데,

이 때 친구의 권유로 스프링 스터디 모임에 나가게됐다.

김영한님의 스프링 고급편 + 토비의 스프링 1권을 읽는 스터디를 나가면서 많은 대화들을 나눴다.

다양한 회사에 근무하는 분들과 대화해서 좋았던 자리다.

이때 스프링이 끝난 후에도 Real Mysql 8.0 서적을 읽는 스터디도 진행했었다.

이 때 DB에 대해 좀 더 깊게 공부했던 때가 아닐까 한다.

책정리 는 노션에서 주로 했다.

 

책 읽기

책 추가하기

www.notion.so

계속해서 책을 읽었던것 같다. DDD... ㅋㅋㅋ

 

5월

5월은 이제 삽질을 많이했던 시기였다.

사내에서 배치 세미나를 열었었다. 이때 부터였다 세미나를 내가 해보고 싶다고 관심 가진것이. ㅋㅋㅋ

세미나 자료는 이때 DDD, 스프링 배치, 단위테스트 였다.

 

GitHub - lsj8367/semina-list

Contribute to lsj8367/semina-list development by creating an account on GitHub.

github.com

 

 

그리고 배치에서 나는 에러를 개선했다!!!

 

[Spring Batch] 배치 에러 개선기

업무에서 Spring Batch로 세미나를 진행하고, 앱 푸시 기능을 배치로 전환하는 작업을 진행했다.

velog.io

 

그리고 쿼리 속도 개선했던 작업도 있었고, 이게 바로 Real Mysql 서적을 같이 공부하면서 인덱스를 내가 적용했던 시점이다.

 

6월엔 운영체제 강의를 들었다!

더불어서 블로그를 이제 티스토리로 옮기게 된다.

하나씩 옮겼던 이유는 벨로그에선 전체 통계를 볼 수 없었던게 너무 아쉬웠다.

그래서 티스토리로 옮겨왔는데, 벨로그에 있는 글이 훨씬 사람들 방문이 많은 것 같다.

다시 옮겨가고 싶은 느낌도 들기는 한다...ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

그리고 7월에 공백기간을 좀 거친다.

이 때 했던것은 넥스트 스텝의 ATDD과정이다.

전에 TDD, Clean Code With Java 과정을 할 때에는 바빠서 마지막 볼링구간을 이수하지 못했다!

그래도 얻어가는 것은 많았는데 ATDD는 전부 완주를 했다.

ATDD회고

 

ATDD, 클린 코드 with Spring 5기 수료 회고

이전에 TDD, Clean Code with Java 12기를 이수하면서, 테스트 코드에대한 중요성 그리고 단위 테스트는 어떻게 해야겠다! 라고 깨달음을 얻었었다. 그렇게 하면서 업무에도 테스트코드를 적용하려고

lsj8367.tistory.com

 

이 때의 나는 퇴사를 결심한다.

그러면서 핀테크로 도메인을 옮기게된다.

8월

8월에 드디어 모든 글을 티스토리로 옮기고, 

근황을 쓴다.

 

블로그를 옮기고 최신 근황

블로그를 기존 벨로그에서 티스토리로 옮기게 되었다. 그래서 거기서 포스팅 했던 글들을 전부 지금 이 티스토리로 옮기고 드디어!! 진짜 날짜에 맞는 첫글을 써본다. 이직 우선, 전 직장에서 6

lsj8367.tistory.com

여기에 그동안의 근황이 다들어있어서 이걸 열어보신다면 좋을것 같다 👍

 

8월에 인프콘을 다녀왔었는데, 응모한 것에선 당첨이 되지 않았고,

지금 다니는 회사에서 발행하는 카드를 인프런이 사용하고 있어서 그걸 통해 내가 표를 받았었다!

본의 아니게 내가 친구한테 받은 라즈베리파이로 서버를 구축하면서 짧게 인프콘을 올렸었다.

 

라즈베리파이 사용

시작하기 내가 클라우드 요금은 견딜 수 없어서 상당히 겁이 난 상태였다. 하지만 뭔가 내가 만들어서 써보고 싶은게 최근에 생기게 됐다. 그게 바로 북마크인데, 크롬에 의존해서 북마크를 하

lsj8367.tistory.com

 

컨퍼런스를 오프라인으로 접하게 되면서 다른 개발자분들은 어떤것에 관심을 가지고 있는지를 알아볼 수 있었던 시간이 좋았다.

 

9, 10, 11월

9월에 기억나는건 이펙티브 자바 스터디를 참여하게 된다.

회고를 쓰는 지금 이제 2주차 정도만 더 스터디를 하면 이펙티브 자바는 끝나는 상태가 된다.

2회독을 스터디로 하게 되면서 공유한게 많은 것 같다.

이 부분은 책정리에도 있다.

 

이 때는 내가 처음으로 Kafka 를 사용하게 되는 날이 온다!

막상 얘기만 들었지 사용해본 경험은 없었기에 새로운 시도라서 굉장히 좋았던 달이다.

그리고 입사를 하면서 적응하는 시기였고, 해당 금융 도메인 지식이 없던 상태에서 시니어 개발자분이 메시지 플랫폼을 구축해보는게 좋을 것 같다 하시면서 이 부분을 개발하게 되었다.

 

면접을 봤던 것보다 빡세게 설계 질문, 코드 질문, 기술에 대한 허점 같은 것들을 많이 질문해주셨다.

등에서 식은땀이 났던게 아직도 기억이 난다 ㅋㅋㅋㅋㅋㅋ

 

지금 회사에선 백엔드 개발자 분들이 나를 포함해 8명이 있다.

한분은 프로그래밍 동아리를 통해 마켓컬리로 이직하셨다. 👏👏👏

그러면서 회고를 쓰는 지금 신입 백엔드 개발자분이 한분 입사하여 8명이 변함없는 상태가 되었다 ㅋㅋㅋ

 

월별로 무언가를 정리해보니 꽤 많은 일들이 있었다.

11월엔 NHN Forward 컨퍼런스도 다녀왔고, 여기서 인상깊었던건 아무래도 DDD + Clean Architecture

이 세션을 너무 재미있게 들었었다. 아는 내용이기도 하고 지금 관심갖고 있던 터였기 때문이다.

MSA 구조로 회사가 전환을 하면서 기존의 계층형 아키텍처를 탈피하며 도메인 단위로 뭔가 구성을 해보고 싶어서 많이 들여다 본게 컸다.

 

12월

이펙티브 자바 스터디는 계속 유지하면서 알고리즘을 지속적으로 월, 목 마다 2문제씩 풀이하는 스터디를 한분과 진행중이다.

덕분에 설계하는데에 조금 더 생각하는게 깊어진 효과를 보는중이다.

아 참, 이때부터 까치산의 모각코를 나갔다 ㅋㅋㅋㅋㅋ 잘하시는 고수분들 많이 만났다.

업무에 많이 힘을 쏟았던 월이고, 오늘은 마지막날이라 휴가를 쓰고 쉬고있다 !!

 

이 12월에 좀 내가 회사에 정말 적응을 했다고 느끼는건 이제 사람들과 많이 대화하고 있다는것. 장난도 치며 웃기도 하고 소통도 활발해진것.

공유하고 소통하는 문화를 지향했던 나지만, 낯가림이 심해 적응이 힘들어서 본격적으로 공유를 활발하게 했던 월이라 가장 좋은 월인것 같다.

 

끝으로

한 해를 다 정리해버렸다. 이번 년도에는 6개월마다 이력서를 써보는 것을 많이 못해봤다.

시간을 내서 따로 지원해보고 했어야되는데, 업무가 많이 바빠 그럴 여력이 못됐던것 같다.

내년 상반기부터 다시 해서 실력 체크를 주기적으로 해봐야겠다 라고 느낀다!

 

요즘은 일일 회고를 작성한다. 업무가 됐든, 주말이 됐든 하루에 뭘 했는지 간략하게 쓰는 작업을 하고 있는데,

 

일일 Todo List

일일 데일리 해야할 일 그리고 공부할 것을 적어놓는 공간

www.notion.so

이것도 나중에 1년치를 작성하면 회고를 해보는것도 좋은것같다.

일단 지금은 링크로써만 공유를 해본다.

 

올 한해는 한 단어로 보면 과도기 인것 같다.

뭔가 하나에 잘 정착하지 못했다. 블로그든, 회사든

이제 좋은 개발자분들에게 많이 배우고 공부해야될 것도 늘어난 지금 많이 노력해서 내년엔 좀 더 많이 성장을 했으면 좋겠다!!

728x90

'Diary' 카테고리의 다른 글

NHN Forward  (1) 2022.11.24
라즈베리파이 사용  (0) 2022.08.31
ATDD, 클린 코드 with Spring 5기 수료 회고  (0) 2022.08.14
블로그를 옮기고 최신 근황  (0) 2022.08.13
728x90

회사의 서비스들이 여러개로 쪼개져있다.

그래서 우리는 주로 FeignClient를 사용하는데, 애를 먹었던 로깅레벨에 대해 포스팅한다.

업무중에 삽질을 진행했었으며, 해당 내용으로 자바스럽게 고쳤던 경험을 좀 풀어본다..

 

스프링 프레임워크를 사용하고 있기에 여기서 같이 제공해주는 Spring Cloud의 OpenFeign을 사용하였다.

 

아래는 사용하는 예시이다.

출처 -&nbsp;https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/

 

해당 예시처럼 나는 FeignClient를 구현했었다.

물론 @EnableFeignClients 는 별도의 Configuration 클래스 파일에 설정을 해주었었다!

 

여기까지는 일단 기본적인 설정이지만, 아래의 레벨 설명이 진짜다.

 

Feing Logging Level은 총 4단계로 이루어져있다.

  • NONE:  로깅 없음(Default)
  • BASIC:  요청 방법 및 URL, 응답 상태 코드 및 실행 시간 기록
  • HEADERS:  요청 및 응답 헤더와 함께 기본 정보를 기록
  • FULL : 요청과 응답 모두에 대한 헤더, 본문 및 메타데이터를 기록

기본적으로 BASIC, HEADERS에는 정상작동을 했던 우리의 소스였다.

 

일단 소스가 어떤 구성이냐면...

 

회사의 정보를 많이 노출할 수는 없기에 간략하게 설명한다.

 

일명 스크래핑이라고 하는 기술, 흔히 크롤링이라고도 할 수 있을것이다.

해당 작업을 외부 API로 연동하여 응답값을 받아 파싱해주는 작업이 있다.

근데 우리의 규칙상 해당 스크래핑의 원문 데이터라는 아예 크롤링의 전체에 해당하는 xml양식의 데이터도 같이 받아오는 방식을 정책으로 정했기 때문에 우리는 Google Cloud 를 사용하기에 Google Cloud Storage에 원문 데이터를 저장하고, 나머지 데이터를 데이터베이스에 저장하는 구조로 구성이 되어있다.

 

여기서 문제가 발생한다.

 

일단 원문 데이터가 포함이되기에 응답값의 길이가 상당히 길다.

길지않고 정상적인 응답을 받는 스크래핑 로직의 경우에는 이런 디버깅이 해당되지 않는다.

 

근데 글자가 길었고, 내가 핸드폰에서 슬랙으로 알림을 받았던 에러메시지는 다음과 같았다.

에러 로그.... 정보유출되는 것이 너무 많아 다 지워버렸다 ㅋㅋㅋ...

아무튼 이러한 예외가 발생했다.

에러 메시지를 보면 해당 응답값의 타입이 application/json이 아니라는데

디버깅을 찍어보면 정상 json응답값이 파싱이 되고 있었다.

 

아래부분이 좀 더 의심스러웠었는데, buffer Length : 8192 

그러니까... 버퍼길이가 8192 제한인데 길이가 더 커서 담기지 않아서 에러가 발생한다는 것이다.

 

이 부분이 왜 나왔는지를 보니까

 

해당 스크래핑 작업을 하는 로직에서 우리는 로그를 찍는다.

 

스크래핑 작업 전후로 시작했고 완료되었다는 외부의 로직 실행결과를 로그로 담고 있다.

 

그런데 이 담는 로직에서 예외를 이렇게 뱉는다는 것이다.

 

feign 의 기본 클라이언트는 어떤것을 쓰냐면 바로 ApacheHttpClient를 사용한다.

 

아래는 구현체의 내용 일부분이며, 빨간 네모의 asInputStream으로 body를 출력하고 있다.

 

httpResponse에서 받는 해당 entity의 content를 찍어내는 것인데, 이 부분에서 에러가 발생한다.

기본 maxContentLength 가 8192이기 때문에 정상적으로 스트림 반환을 하지 않고 저렇게 예외를 띄우게 되는 것이다!

이부분이 TCP 연결에서의 최대 길이이기 때문에 HttpClient를 사용하면 이런 예외가 발생된다.

 

아래는 ByteArrayBody이다.

해당 부분에서는 ByteArrayInputStream을 사용하기 때문에 TCP length 제한보다 더 쓸수 있는 것 같다.

이것이 바로 ByteArrayInputStream....?

아무든 이 두 asInputStream()을 보는 이유는 아래에서 설명하겠다.

아무튼 이 ByteArrayInputStream은 tcp max content length와 관계없이 스트림을 출력하기에 길이제한이 없다.

 

이 두개의 차이를 가르는 것은 Log 였다!!!

 

그래서 소스를 파고들었다.

 

해당 로그 부분이 BASIC레벨 보다 수준이 낮은 경우 수행되는 로직인데, 

그래서 FULL로직에서만 정상적으로 데이터 파싱이 된다.

 

왜? - response.toBuilder().body(bodyData).build() 를 통해 새로운 ByteArrayInputStream으로 생성되기 때문이다.

 

그래서 이 부분을 재정의를 해주어 BASIC 레벨에서도 길이가 아무리 길더라도 데이터를 파싱할 수 있게 설정해주었다.

 

로직이 정상적으로 수행되며 아래 로그부분에서 객체를 다시 만들어주게 되는걸 보고나서 넘 행복했다는것...😇😇😇

 

해결방안

해당 Feign의 Logger를 상속받아서 log찍는 부분을 override 시켜주었다.

그래서 기존 로직을 또 건드리고 싶지는 않았다. <- 로그는 이대로 찍어주는게 마음 편했으니까?

그래서 분기처리 하는 부분을 제거하고 정말로 필요없는 부분만을 제거해주고 나머지 로직은 유지했다.

 

아무튼 데이터가 길었기 때문에 로그레벨에 따라 값이 파싱이 되고 안되고 났던게 좀 신기했다.

길이가 짧은 응답을 사용했다면 오히려 이런것도 모르고 그냥 설정따라 파싱해주고 안해주고로 넘어갔을 것 같다!!!!

728x90

'디버깅' 카테고리의 다른 글

Stream 오류 제거  (2) 2023.04.21
@Transactional 제대로 알고쓰기  (4) 2023.02.27
@Async 사용시 에러 해결  (0) 2022.11.04
Jenkins 에러  (0) 2022.08.11
728x90

개발자 삶의 두번째 컨퍼런스인 NHN Forward에 다녀왔다.

인프콘 때와는 다르게 한 공간에서 쭉 들었다.

백엔드 세션만 들어야겠다 라고 생각했는데 그 세션들이 전부 한곳에 모여있었다 ㅋㅋㅋㅋ

들어가자마자 체크인을 QR로 진행하고 키노트를 진행하는데 계속 서서 컨퍼런스 장소까지 가니 다리가 아팠는데 또 내내 서있었다.

점심먹고 들었던 세션인 분산 시스템에서 데이터를 전달하는 효율적인 방법 세션에서

RDB, RabbitMQ, Kafka를 통한 서로 다른 마이크로 서비스들의 트랜잭션을 어떻게 관리할 것인가에 대해 되게 재밌게 봤다.

RDB로 사용하는 방법을 이번에 처음 보게되어 예전에 면접에서 받았던 질문이 이런걸 물어본 거였구나 하면서 아~ 하고 깨닫는 순간이었다.

메시지 큐를 사용하는 방법은 현재 내가 담당하고 있는 메시지 플랫폼에서의 데드레터 처리와 같았다.

자세하게 정리하는건 내 노션에서 해야겠다.

사람들이 엄청많았고, 아침에 점심 조금 늦게먹고 자리를 안잡았으면 밖에서 그냥 발만 동동 굴렀을 것 같다.

앉지 못하면 자리가 없어서 못들어간다고 밖에서 얘기하시는 스탭분을 봤다. 그래서 다른분들이 참여 못하시는걸 봤었다...

혼자만 뽑히게되어 아는사람 없이 혼자가서 얌전하게 세션들 다 듣고 바로 와버렸다.

중간에 다른 부스인 커리어리에서 앱 다운하면 뭔가 물품을 준다해서 이미 나는 앱이 있어서 저는 이미 앱이 있어요!

하니까 귀하신 회원님이라면서... ㅋㅋㅋ 바로 챙겨주셨다.

아무튼 너무 좋은 시간이었고, 지속적으로 이런 컨퍼런스도 참여하면서 많이 교류하고 정보도 얻어야겠다.

그리고 컨퍼런스를 다녀옴으로써 한번 더 자극을 받았고 다시 한번 열심히 달려봐야겠다!

728x90

'Diary' 카테고리의 다른 글

2022년 회고  (8) 2022.12.30
라즈베리파이 사용  (0) 2022.08.31
ATDD, 클린 코드 with Spring 5기 수료 회고  (0) 2022.08.14
블로그를 옮기고 최신 근황  (0) 2022.08.13
728x90

오랜만에 포스팅하는데 회사에서 그동안 앱 2.0 버전을 출시한다고 이래저래 바빴던 나날을 보냈다.

결과적으로는 만족스러운 출시..? 였던것 같다 ㅋㅋㅋ

버그도 많았고, QA 엔지니어께서 고생을 많이 하셨을 수도 있고 내가 구현한 메시지 플랫폼도 테스트하기가 정말 까다로웠다.

각설하고..

해당 에러사항을 구현하는건 Kafka를 이용하지 않아도 되기 때문에 RestAPI로 구현했다. (+ 테스트코드로만)

모든 코드는 깃허브에 있다.

이번엔 무슨 버그였냐?

이런 에러가 쏟아져나왔다. 기존 레거시 푸시는 NHN Toast를 이용한 푸시서비스로 구성되어 있었다.

변경한다고 해도 과도기가 존재하기 때문에 바로 지울수는 없고 이전 앱을 사용하는 사용자들에게는 해당 푸시로 알림은 계속 받아야되기 때문이다.

왜 났던 에러였는지 더듬어봤더니

Async 설정

기본적으로 @Async 를 사용하기 위해서는 Configuration 클래스를 하나 만들어주고 비동기 실행기에 대한 설정을 해주어야 한다.

스크린샷 2022-11-04 오후 11 40 12

이런식으로 에러를 일부러 내기 위해서 큐 사이즈를 5개 기본 스레드 1, 최대 스레드 2, 그리고 비동기로 처리할 큐 사이즈를 5로 구성했다.

문제상황 1

nhn 푸시를 보내기 위해선 Kafka에 Producer를 통해 메세지를 넣어주면, 내가 구현한 메시지 플랫폼에서 해당 메시지를 Consume하여 동작하는 방식으로 구성되어있다.

그런데 나는 비동기로 구현하겠다고만 생각하고 사이즈를 제한해두지는 않았었다.

그래서 Consume은 계속해서 KafkaListener를 통해 무제한으로 하고 있는 와중에 이 컨슘 속도가 너무 빠르니까

위에서 설정한 비동기 처리 큐 사이즈를 200으로 잡아놓아도 예를 들어 1000건이 한방에 producer를 통해 적재된 후 바로 consume을 해버리니까

100개 큐를 훌쩍 넘어버려서도 막는게 아니라 지속적 컨슘이 일어나기에 큐 사이즈를 넘어서는 순간에도 이 메소드 실행을 밀어 넣는 오류였다.

그래서 맨위의 이미지처럼 Exception이 발생하게 되는 것이다.

에러 직접 구현

정확한 코드는 깃허브에서 보면 되지만 여기서는 간략하게 대충 구현하도록 하겠다.

JDK 17버전이기 때문에 record class를 사용함. (record를 사용하면 클래스는 final로 선언되며 equals & hashCode를 재정의하여 갖고있음)

/hello 를 호출하면 printAsync라는 비동기 메소드를 100번호출하게끔 만드는 로직이 있다.

HelloController.java

@RestController
public record HelloController(HelloService helloService) {

    @GetMapping("/hello")
    public ResponseEntity<?> hello() {

        int i = 0;
        while (true) {
            helloService.printAsync();
            i++;

            if (i == 100) {
                break;
            }
        }

        return ResponseEntity.ok(Map.of("data", "success"));
    }
}

HelloService.java

@Slf4j
@Service
public class HelloService {

    @Async("threadPoolTaskExecutor")
    public void printAsync() {
        try {
            Thread.sleep(5000);
            log.info(Thread.currentThread().getName());
            log.info("hello service print!!!");
        } catch (InterruptedException e) {
            log.error("error : {}", e.getMessage());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

}

printAsync()는 비동기로 호출하며 5초를 기다리고 log를 찍게 되는데, 우리가 처음 전역적으로 설정해준 스레드 설정은

요청이 들어오면 maxQueue 사이즈인 5까지만 담을 수 있게 되어있고 작업이 느려서 큐 데이터를 하나 소진하지 못한다면 에러가 발생하는데

그 에러는 이렇다.

이미지로 한방에 찍어지지 않아 글자로 첨부한다.

Caused by: org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@265c1a7c[Running, pool size = 2, active threads = 2, queued tasks = 5, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$967/0x00000008010a7dc0@740a0d5e
    at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:391)
    at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:292)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor.invoke(AsyncExecutionInterceptor.java:129)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
    at com.github.lsj8367.application.HelloService$$EnhancerBySpringCGLIB$$7321912.printAsync(<generated>)
    at com.github.lsj8367.presentation.HelloController.hello(HelloController.java:17)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1070)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    ... 87 more
Caused by: java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@29013ef2[Not completed, task = org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$967/0x00000008010a7dc0@740a0d5e] rejected from java.util.concurrent.ThreadPoolExecutor@265c1a7c[Running, pool size = 2, active threads = 2, queued tasks = 5, completed tasks = 0]
    at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065)
    at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833)
    at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
    at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:388)
    ... 107 more

처음 봤던 이미지와 같은 예외가 발생하는걸 볼 수 있다.

그러니까 큐 공간이 가득차기 전에 계속해서 비동기를 호출하며 작업큐에 메소드를 태우려고 하다보니 이런 예외가 발생한 것이다.

해결은?

그래서 나는 일단 푸시 발송이 그렇게 오래걸리는 로직이 아니라서 일단은 동기 상태로 바꿔놓았지만,

지속적인 모니터링을 하여 트래픽이 많아지게 되면 그때는 batchListener를 적용하여 비동기로 여러개를 한방에 처리하는 방법을 고려중이다.

-> 이 부분은 카프카의 리밸런싱을 고려해야한다.

아무튼 내가 생각했던 여러 추측중에 하나가 걸리게 되니까 좀 재밌었던 것 같다.

그래도 오늘도 해결했다!

728x90

'디버깅' 카테고리의 다른 글

@Transactional 제대로 알고쓰기  (4) 2023.02.27
FeignClient Logging level 디버깅  (0) 2022.12.17
Jenkins 에러  (0) 2022.08.11
AWS SNS 토큰 에러  (0) 2022.08.10

+ Recent posts