728x90
반응형
728x90
반응형
반응형

매번 보고 또 봐도 까먹고 또 까먹고 헷갈리고 당황하는 것이 이 쪽 친구들인 것 같다.

interceptor, filter, aop, servlet, servlet container, dispatcher servlet...

반복학습을 하면 할수록 점점 마스터할 수 있을 것이라 믿으며..! 

필요한 내용에 대해 간략히 정리해 본다.

 

Servlet container 란?

  • 서블렛의 생성부터 소멸까지의 life cycle을 관리
  • 요청이 올 때마다 새로운 자바 스레드 생성하고 HttpServletRequest, HttpServletResponse 두 객체를 생성
  • 톰캣/제티 등

was와 servlet container의 차이가 궁금해서 찾아봤는데 정확히는 모르겠고 was가 더 큰 개념 같다.

application server > servlet container

 

interceptor vs filter

request flow

필터란(서블릿 필터)

  • 필터는 Web Application에 등록되며, 요청 스레드가 Servlet Container에 도착하기 전에 수행(필터가 호출되고 서블릿이 호출)
  • 체인으로 구성
  • 유일하게 ServletRequest, ServletResponse의 객체를 변환할 수 있다. 주로 전역적으로 처리해야 하는 일에 사용됨
  • Springboot에서 특정 urlPattern 에만 필터를 적용하거나 세부적인 설정(우선순위, 필터 이름 등) 시에는 FilterRegistrationBean 빈을 사용하면 되고 아니면 Component로 전역 사용 가능

제공 함수

  • init() 메서드: 필터 초기화 메서드이며 서블릿 컨테이너가 생성될 때 호출
  • doFilter() 메서드: 고객의 요청이 올 때마다 해당 메서드가 호출되며 메인 로직을 이 메서드에 구현하면 됨
  • destory() 메서드: 필터 종료 메서드이며 서블릿 컨테이너가 소멸될 때 호출

사용 예

  • 오류 처리
  • 인코딩 처리
  • 보안 관련 기능
  • 데이터 압축이나 변환 기능
  • 요청이나 응답에 대한 로그
  • 권한 검사

인터셉터란

  • 인터셉터는 스프링 콘텍스트에 등록되며, 서블릿 컨테이너를 통과한 후 dispatcher servlet과 컨트롤러 호출 직전/후에 수행됨
  • 필터는 서블릿이 제공하는 기술이지만 스프링 인터셉터는 스프링 MVC가 제공하는 기술
  • 스프링 컨텍스트 내에 존재하기 때문에 모든 Bean 객체에 접근 가능
  • 체인으로 구성
  • 필터와 비슷하지만 호출 시점이 다르고 인터셉터가 더 많은 기능을 제공함

제공 함수

  • preHandle() 메서드: 컨트롤러 호출 전(HandlerAdapter 호출 전)에 호출. preHandle의 반환 값이 true일 경우 다음 체인으로 진행이 되고, false일 경우 더 이상 진행이 되지 않으며 핸들러 어댑터 또한 호출이 되지 않음
  • postHandle() 메서드: 컨트롤러 호출 후(HandlerAdapter 호출 후)에 호출. 컨트롤러의 예외가 발생하면 호출되지 않음
  • afterCompletion() 메서드: 뷰가 렌더링 된 이후에 호출되며 컨트롤러의 성공/실패와 무관하게 무조건 호출됨

사용 예

  • 보안(spring security)

ContentCachingRequestWrapper

사실 이 모든 것은 애플리케이션의 incoming/outgoing 로깅을 개발하려다가 어디에다 할지, 요즘 추세나 더 좋은 건 없는지 찾아보다가 시작하였다.
필터에서 body까지 확인하기 위해서는 request body를 읽을 수 있어야 하지만 기본적으로 사용되는 HttpServletRequest 같은 경우에는 InputStream은 한 번만 읽어올 수 있고, 두 번째 읽기 시도를 하는 순간 IOException이 발생한다. 그래서 보통 wrapper를 사용하여 구현하는데, 이번에는 아래 두 wrapper를 이용해서 구현해 보았다.

ContentCachingRequestWrapper extends HttpServletRequestWrapper
ContentCachingResponseWrapper extends HttpServletResponseWrapper

주요 로직은 생략하고 request body 가져오는 부분만 보자면 아래와 같다.

//래핑 해제
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
byte[] buf = wrapper.getContentAsByteArray();  //byte로 본문 가져오기
String body = new String(buf, 0, buf.length, "UTF-8"); //byte to string

response body도 동일한 로직으로 사용하면 된다.

우선 필터를 통해 HttpServletResponse, HttpServletRequest 클래스로 들어온 request와 response를 ContentCachingRequestWrapper와 ContentCachingResponseWrapper로 래핑 해주어야 한다.

왜냐하면 HttpServletRequest 그대로 request.getReader 함수를 호출하거나 안에 있는 데이터를 읽으려고 하면, 단 한 번만 읽을 수 있도록 톰캣에서 만들어두었기 때문에 이걸 다시 읽을 수 있는 클래스로 래핑해주어야 하기 때문이다.

Response도 동일하게, 안에 있는 Body값을 한번만 읽을 수 있게 해 두었기 때문에 필터로 다시 읽을 수 있는 클래스로 래핑 하지 않으면 사용자가 response값을 받지 못하는 참사가 일어날 수 있다.

wrappingResponse.copyBodyToResponse(); 이 부분이 핵심이다. 이걸 통해 body값을 copy 해서 캐시로 저장해 두기 때문에 다시 읽을 수 있다.

https://hirlawldo.tistory.com/44


++ 기존에 web-flux(reactor 기반)만 사용하다가 spring-web을 추가하면서 servlet 기반으로 변경 시

implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-webflux"

아래와 같이 두 종류의 api가 있다.

/// mono리턴 -> controller 밖에 단에서 block이 걸려 body 내용물 추출 가능
@GetMapping
public Mono<BaseResponse> getConfig() {
...
    return Mono.just(new BaseResponse(result));
}

/// 이미 block되어 내용물이 있는 상태에서 return
@GetMapping("/configuration")
public BaseResponse<MoneyExchangeConfigurationResponse> configuration() {
...
    return new BaseResponse<>(moneyExchangeService.getConfiguration());
}

아래와 같이 필터를 걸다간 mono 쪽에서 아무 응답을 받지 못하는 경우가 생긴다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	...
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

    chain.doFilter(requestWrapper, responseWrapper); ///
    ...
}

mono 쪽에서 되려면 reponseWrapper -> reponse로 바꾸면 되긴 하는데..

이러면 body log가 안 남는 문제가 있다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	...
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

    chain.doFilter(requestWrapper, response); ///
    ...
}

 

이리저리 구글링 해봐도 mono에 대한 body logging은 webFilter를 사용하라고 밖에 안 나오고

여러 방식을 잘 모르고 혼용하는 것은 후에 관리하기 힘들 듯하여 우선은 mono를 들어내고 block 방식으로 할까 한다..


참고: https://www.sollabs.tech/ContentCachingRequestWrapper

 

ContentCachingRequestWrapper

Sollabs Main Page

www.sollabs.tech

 

차이 설명 with 코드: https://velog.io/@ansalstmd/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EB%8B%A4%EC%96%91%ED%95%9C-%EA%B8%B0%EB%8A%A5-5.-Spring-Boot-Filter%EC%99%80-Interceptor

 

스프링부트 다양한 기능 5. Spring Boot Filter와 Interceptor

Filter란 Web Application에서 관리되는 영역으로써 Spring Boot Framework에서 Client로 부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 요청/응답의 정보를 변경하거나, Spring

velog.io

 

로깅 예시 https://vkuzel.com/log-requests-and-responses-including-body-in-spring-boot

728x90
반응형
반응형

이전글: 2022.02.04 - [개발/spring] - [retry] spring-retry for auto retry

 

[retry] spring-retry for auto retry

spring-retry? exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다. 일시적인 connection error 등에 유용하게 사용할 수 있다. 환경: springboot2.6.2 implementation 'org.springframewo..

bangpurin.tistory.com

 

저번 시간에는 실전 코딩을 해보았다면 이번에는 spring-retry 관련 테스트 코드를 작성해본다.

가. 어노테이션 이용 - @SpringBootTest

1. retry

우선 아래는 service 코드 이다.

@Retryable(
        value = RuntimeException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 3000)
)
public HolderAccountSummary getHolderAccountSummary(String holderID) {
    log.debug(">>> getHolder : {} ", holderID);
    return repository.findByHolderId(holderID)
            .orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
}

테스트 코드

@Test
void retryTest_with_bean(){
    //when
    Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(RuntimeException.class);
    assertThrows(RuntimeException.class, () -> retryService.getHolderAccountSummary(ArgumentMatchers.anyString()));

    //then
    Mockito.verify(accountRepository, Mockito.times(3)).findByHolderId(ArgumentMatchers.anyString());
}

>> 성공; 3번 재시도해도 실패가 나서 최종적으로 RuntimeException이 난다.

 

2. recovery

역시 service 코드 -> 테스트 코드 순이다.

@Retryable(
        value = RuntimeException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 3000)
)
public HolderAccountSummary getHolderAccountSummary(String holderID) {
    log.debug(">>> getHolder : {} ", holderID);
    return repository.findByHolderId(holderID)
            .orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
}

@Recover
private HolderAccountSummary recover(RuntimeException e, String holderID) {
    return HolderAccountSummary.builder()
            .name("recovery")
            .build();
}
@Test
void retry_with_bean(){
    //when
    Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(RuntimeException.class);
    assertThrows(RuntimeException.class, () -> retryService.getHolderAccountSummary(ArgumentMatchers.anyString()));

    //then
    Mockito.verify(accountRepository, Mockito.times(3)).findByHolderId(ArgumentMatchers.anyString());
}

@Test
void recovery_with_bean(){
    //when
    Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(RuntimeException.class);
    HolderAccountSummary result = retryService.getHolderAccountSummary(ArgumentMatchers.anyString());
    
    //then
    Mockito.verify(accountRepository, Mockito.times(3)).findByHolderId(ArgumentMatchers.anyString());
    Assertions.assertThat(result.getName()).isEqualTo("recovery");
}

이렇게 실행하면 recovery test는 성공하나 retry test가 실패한다.

되던게 안 된다고? 라고 당황해하며 검색을 해봤으나 도무지 말이 안되서 생각해보니..ㅎ

recovery가 생기고 나서는 3번 실패 이후 recovery함수를 타기 때문에 exception이 날 일이 없기 때문이었다. 하하하..

@Recovery 어노테이션을 주석으로 처리하고 다시 돌리면 성공한다.

 

나. retryTemplate 이용 - @SpringBootTest

0. retryConfig 생성

@Configuration
@EnableRetry
public class RetryConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3); //횟수
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.registerListener(new RetryListener());

        return retryTemplate;
    }

    @Slf4j
    public static class RetryListener extends RetryListenerSupport {
        @Override
        public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
            log.info(">>>> before retry");
            return super.open(context, callback);
        }

        @Override
        public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
            log.info(">>>> after retry");
        }

        @Override
        public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
            log.info(">>>> error on retry");
        }
    }
}

 

1. retry

public HolderAccountSummary getHolderAccountSummaryTemplate(String holderID) {
    log.info("retry going on");
    return retryTemplate.execute(context -> {
         return repository.findByHolderId(holderID)
                .orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
    });
}

기존 테스트 코드에서 위와 같이 서비스 함수만 바꿔주고 실행하면, 아래와 같이 로그가 지나가면서 retryConfig에 설정한대로 적용된 것을 볼 수 있다.(2초 간격으로 3번 재실행, 에러날 때마다 onError 실행 등)

13:04:30.883  INFO 5638 --- [main] com.cqrs.query.service.RetryService      : retry going on
13:04:30.884  INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener   : >>>> before retry
13:04:30.887  INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener   : >>>> error on retry
13:04:32.892  INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener   : >>>> error on retry
13:04:34.895  INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener   : >>>> error on retry
13:04:34.895  INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener   : >>>> after retry

 

2. recovery

가. 에서는 retry와 recovery를 완벽히 분리하여 작성하였지만, retryTemplate을 사용 할 시에는 execute 함수에 recovery context도 같이 실어 넘기는 방법으로 작성해야한다.

public HolderAccountSummary getHolderAccountSummaryTemplateAndRecovery(String holderID){
    log.info("retry going on");
    return retryTemplate.execute(context -> {
        return repository.findByHolderId(holderID)
                .orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
    }, recovery -> {
        log.info("recovery start");
        return HolderAccountSummary.builder()
                .name("recovery")
                .build();
    });
}

기존 recovery 테스트 코드에서 함수명만 바꿔주고 실행하면, 아래와 같이 로그가 지나가면서 3회 시도 후 recovery가 진행된 것을 알 수 있다.

14:35:32.460  INFO 7166 --- [   main] com.cqrs.query.service.RetryService      : retry going on
14:35:32.462  INFO 7166 --- [   main] c.c.q.config.RetryConfig$RetryListener   : >>>> before retry
14:35:32.465  INFO 7166 --- [   main] c.c.q.config.RetryConfig$RetryListener   : >>>> error on retry
14:35:34.471  INFO 7166 --- [   main] c.c.q.config.RetryConfig$RetryListener   : >>>> error on retry
14:35:36.474  INFO 7166 --- [   main] c.c.q.config.RetryConfig$RetryListener   : >>>> error on retry
14:35:36.475  INFO 7166 --- [   main] com.cqrs.query.service.RetryService      : recover start
14:35:36.475  INFO 7166 --- [   main] c.c.q.config.RetryConfig$RetryListener   : >>>> after retry

 

다. retryTemplate 이용 - @MockitoExtension

위 가/나 방법 모두 @SpringBootTest 어노테이션을 사용하기 때문에 테스트를 하기 위하여 모든 spring context불러와 테스트가 무겁고 느리다는 단점이 있다. retryTemplate를 사용했을 때에도 @Configuration 빈을 활용하였기 때문에 @SpringBootTest 어노테이션을 사용했는데 이를 아래와 같이 수정하면  @SpringBootTest 어노테이션을 사용하지 않고도 테스트 코드를 작성할 수 있다.

@Service
//@RequiredArgsConstructor
public class RetryService {
    private final AccountRepository repository;
    private final RetryTemplate retryTemplate = new RetryTemplate();

    public RetryService(AccountRepository repository) {
        this.repository = repository;
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3); //횟수
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.registerListener(new RetryConfig.RetryListener());
    }
    
...
}

기존에 retryTemplate 빈을 통해 자동 주입하던 것을 수동 주입으로 바꾸고 설정 값을 세팅한다.

@ExtendWith(MockitoExtension.class)
public class RetryServiceTest_template {
    @InjectMocks
    RetryService retryService;
    @Mock
    AccountRepository accountRepository;
    
//@SpringBootTest
//class RetryServiceTest_template {
//
//    @Autowired
//    private RetryService retryService;
//    @MockBean
//    private AccountRepository accountRepository;

테스트 코드에서도 @SpringBootTest 어노테이션을 mockito extension으로 바꾸면 전체 spring context를 불러오지 않고도 테스트가 정상적으로 실행되는 것을 볼 수 있다.

허나 이럴 경우 테스트는 가벼워졌지만, retryTemplate을 매 서비스마다 수동 주입해줘야 하기 때문에 장단점이 있다는 것을 인지해야한다(물론 서비스마다 서로 다른 설정을 해야한다면 오히려 더 나은 선택일 수도 있다).

728x90
반응형
반응형

spring-retry?

exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다.

일시적인 connection error 등에 유용하게 사용할 수 있다.

 

환경: springboot2.6.2

implementation 'org.springframework.retry:spring-retry'

다른 출처에서는 아래 의존성도 필요하다고 하는데, 2.6.2 에서는 없어도 돌아간다.

implementation 'org.springframework:spring-aspects'

확인해보니 이미 관련 라이브러리들이 불러와져 있었다.

 

사용법은 쉽다. @Configuration 쪽 클래스에 아래 어노테이션을 추가하면 사용가능하다.

@EnableRetry

 

관련 어노테이션을 각 함수 위에 달아두면 그 함수가 설정한 조건에 맞게 실행된다.

1. @Retryable

@Retryable(value = {NoSuchElementException.class}, //언제 실행되는가
            maxAttempts = 5,                       //최대 몇 번 재시도 하는가
            backoff = @Backoff(delay = 1000))      //재시도 전 딜레이 시간(ms)
void retryServiceWithRecovery(String value) {
}

NoSuchElementException이 났을 경우 최대 5번 실행하고 각 실행 전 1초씩 쉬도록 설정하였다.

5번이 넘어가도록 실패한다면..? 아래 @Recovery 어노테이션으로 fallback을 구현할 수 있다.

 

2. @Recovery

@Recover
void recover(NoSuchElementException e, String value){
}

여기서 특이점은, input으로 @Retryable 함수의 input 변수와 Retryable의 exception을 받도록 구현해야 한다는 점.

그리고 output은 @Retryable 함수의 return type과 동일해야한다.

 

3. 개별 설정 말고 글로벌 설정

1) properties에 설정한 후 클래스로 불러와서 세팅하기(스프링 기본설정 아님)

properties 파일에 대략 아래와 같이 설정하고(변수 이름 아무거나 가능)

retry.maxAttempts=2
retry.maxDelay=100

어노테이션을 아래와 같이 수정한다.

  @Retryable( value = SQLException.class, 
      		maxAttemptsExpression = "${retry.maxAttempts}",
            backoff = @Backoff(delayExpression = "${retry.maxDelay}")) 
  void retryServiceWithExternalizedConfiguration(String sql) throws SQLException{
  }

이 때 주의해야 하는 것은 maxAttempts -> maxAttemptsExpression, delay -> delayExpression 로 항목명이 바뀌었다는 점!

 

2) RetryTemplate 사용

전반적으로 사용하는 설정을 RetryTemplate으로 만들어 빈으로 등록해놓고 필요한 곳에서 불러다가 함수 내부에서 호출한다.

@Configuration
@EnableRetry
public class RetryConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2); //횟수
        retryTemplate.setRetryPolicy(retryPolicy);

        return retryTemplate;
    }
}
  • bean 적용(before)
@Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000))
protected void on(AccountCreationEvent event, @Timestamp Instant instant){
    log.debug(">>> projecting {} , timestamp : {}", event, instant.toString());
    HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
    holderAccount.setAccountCnt(holderAccount.getAccountCnt() + 1);
    repository.save(holderAccount);
}
  • retryTemplate 적용(after)
@RequiredArgsConstructor
...
private final RetryTemplate retryTemplate; 
...
protected void on(AccountCreationEvent event, @Timestamp Instant instant){
    log.debug(">>> projecting {} , timestamp : {}", event, instant.toString());
    retryTemplate.execute(context -> {
        log.info("retry going on");
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setAccountCnt(holderAccount.getAccountCnt() + 1);
        repository.save(holderAccount);
        return null;
    });
}

참고로 retryTemplate 의 execute 함수는 다음과 같이 4종류가 있다. 상황에 맞게 구현하면 된다. 아래는 관련 용어 설명이다.

  • retryCallback은 exception 시 재시도 할 로직
  • retryContext는 재시도 시 이전 시도 작업물(실행 간 공유해야 할 데이터가 있을 경우) 들고 실행
  • recoveryCalback은 재시도 횟수만큼 전부 실패할 경우 호출되는 로직
  • retryState 는 retryContext에 든 상태를 가지고 재시도를 하면 stateful; 아니면 stateless라고 한다. 일반적으로는 stateless가 많고 주로 개별적으로 설정한다.
//retryCallback 만 사용; stateless
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E {
    return doExecute(retryCallback, null, null);
}

//retryCallback, reoveryCallback 사용; stateless
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
        RecoveryCallback<T> recoveryCallback) throws E {
    return doExecute(retryCallback, recoveryCallback, null);
}

//retryCallback, statefulRetry
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState)
        throws E, ExhaustedRetryException {
    return doExecute(retryCallback, null, retryState);
}

//retryCallback, reoveryCallback 사용하는 statefulRetry
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
        RecoveryCallback<T> recoveryCallback, RetryState retryState) throws E, ExhaustedRetryException {
    return doExecute(retryCallback, recoveryCallback, retryState);
}

 

어노테이션과 retryTemplate을 사용하는 것에 각각 장단점이 있는데,

어노테이션은 간편하지만 매번 설정해야하는 불편함이 있고(반면 각각 설정에 대해 가시적으로 알 수 있어 좋을 수도 있다)

retryTemplate은 di 주입방식이라 테스트코드 짜기가 간편, 세부 설정이나 콜백 로직을 작성할 수 있지만 소스가 길어진다는 단점이 있다.

뭐든 캐바캐, 상황에 맞게 사용하면 될 듯 하다.

 

2-1) 재시도 전/후 콜백 Listener

재시도를 할 때마다 호출되는 콜백 로직을 추가할 수 있다(로그 등). RetryListener를 구현한 구현체를 만든 후 retryTemplate에 등록하면 된다.

//RetryListener.class
//모든 재시도 전
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);

//모든 재시도 후
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);

//개별 retryCallback 에러 시 
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
@Slf4j
public class RetryListener extends RetryListenerSupport {
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        log.info(">>>> before retry");
        return super.open(context, callback);
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info(">>>> after retry");
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info(">>>> error on retry");
    }
}
@Configuration
@EnableRetry
public class RetryConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2); //횟수
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.registerListener(new RetryListener());  //listener

        return retryTemplate;
    }
}

 

+ 근데 자동으로 재시도하는게 항상 이점만 있을까?

예를 들어 '타 컴포넌트로 정보 조회 -> 결과를 가지고 나의 데이터 수정 작업 -> 타 컴포넌트 데이터 수정' 이러한 로직이 있다고 가정해보자.

이 경우 정보 조회 시 네트워크 에러가 나 전체 작업이 중지되는 경우 이와 같은 재시도가 의미 있을 수 있다. 허나 타 컴포넌트로 저장 작업이 실패해서 계속 재시도를 할 경우.. 제대로된 timeout 설정 등 뭔가가 맞지 않는다면 중복으로 데이터가 들어가지 않거나 다른 장애를 가져올 수 있기 때문에 MSA일 경우 더더욱 심사숙고해야 할 듯 하다.

 

아 그리고 비슷한 라이브러리로 Resilience4j 가 있다고 하는데, 참고하길 바람!!

다음 시간에는 spring-retry 관련, 간단한 테스트 코드를 짜본다.

2022.02.07 - [개발/spring] - [retry] spring-retry test code

 

[retry] spring-retry test code

이전글: 2022.02.04 - [개발/spring] - [retry] spring-retry for auto retry [retry] spring-retry for auto retry spring-retry? exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다. 일..

bangpurin.tistory.com


참고

원문: https://docs.spring.io/spring-batch/docs/current/reference/html/retry.html

 

Retry

Sometimes, there is some business processing that you know you want to retry every time it happens. The classic example of this is the remote service call. Spring Batch provides an AOP interceptor that wraps a method call in a RetryOperations implementatio

docs.spring.io

재시도에 대한 고찰: https://brunch.co.kr/@springboot/580

 

Spring Retry

Resilience4j, Spring Retry 재시도 패턴 구현 | Overview 이 글에서는, Resilience4j 및 Spring Retry 라이브러리를 사용해서 "재시도 패턴"을 구현한다. 목차 1. Resilience4j 를 사용해서 구현2. Spring Retry 를 사용해

brunch.co.kr

 

728x90
반응형
반응형
As of Spring Boot 2.3, Spring Boot now supports the graceful shutdown feature for all four embedded web servers (Tomcat, Jetty, Undertow, and Netty) on both servlet and reactive platforms.

springboot 2.1.8을 상용화 버전으로 사용할 때에는 graceful shutdown을 사용하기 위해서 TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> 를 구현하여 어플리케이션이 종료 시 이벤트를 설정했어야 했는데,

springboot 2.3이후 버전에서는 기본으로 탑재되어 application.properties에 아래 설정만으로도 graceful shutdown 이 가능해졌다.

server.shutdown=graceful     //default: immediate
spring.lifecycle.timeout-per-shutdown-phase=30s      //default: 30s

 

graceful shutdown?

  • 어플리케이션 종료 시 사용하던 자원을 반납하고 현재 처리 중인 작업을 정리하는 과정
  • 어플리케이션 종료 요청을 받았을 시 바로 종료하지 않고, 신규 유입은 받지 않으면서 특정 시간을 기다려 처리 중인 작업이 마무리 되기를 기다림
  • 지정된 시간이 지나도록 작업이 마무리 되지 않는다면 그 때 강제 종료

graceful process

Q1. /actuator/shutdown으로 종료해야만 graceful이 작동하는가?

Q2. kill -9 로 프로세스를 종료시킬때에도 graceful한가?

Q3. kill -15일 경우에는?

 

A. /actuator/shutdown & kill -15 로 프로세스를 킬 할 경우 아래와 같은 로그가 지나가면서 graceful이 적용된 것을 알 수 있다.

11:09:07 INFO  [c.n.g.controller.TestController         .sleeeeeep           :  30] start sleep for 10 secs
11:09:11 INFO  [o.s.b.w.e.tomcat.GracefulShutdown       .shutDownGracefully  :  53] Commencing graceful shutdown. Waiting for active requests to complete
11:09:17 INFO  [c.n.g.controller.TestController         .sleeeeeep           :  32] sleep done
11:09:17 INFO  [o.s.b.w.e.tomcat.GracefulShutdown       .doShutdown          :  78] Graceful shutdown complete

허나 kill -9로 킬하면 graceful이 적용되지 않고 바로 종료된다.

INFO  [c.n.g.controller.TestController         .sleeeeeep           :  30] start sleep for 10 secs

Process finished with exit code 137 (interrupted by signal 9: SIGKILL)

 

공식적인 종료(배포 등)를 할 경우 kill -9를 사용하지 않고 actuator나 kill -15를 사용하여 진행 중인 작업이 안전하게 종료되도록 해야한다. 다만, 이 때 graceful time 이후에 health를 날려 잘 죽었는지 확인 후 재시작을 하도록 해야지 아니면 프로세스가 두 개 뜰 수 있으니 주의해아 한다.

728x90
반응형
반응형

환경: java11, springboot2.6.2, gradle7.1

spring actuator?

  • 스프링 부트 기반의 애플리케이션에서 제공하는 여러가지 정보를 쉽게 모니터링하게 하는 라이브러리
  • DB연결상태, disk space, rabbit, reddis, 커스텀 정보 등 표현 가능

 

예전(spring2.1.8, gradle4)에 spring actuator에 깃 인포를 담기 위해, gradle task로 git정보를 담은 파일을 특정 장소에 생성하는 task를 작성했었는데 자동으로 생성해주는 플러그인이 있어 사용해보기로 한다.

https://github.com/n0mer/gradle-git-properties

 

GitHub - n0mer/gradle-git-properties: Gradle plugin for `git.properties` file generation

Gradle plugin for `git.properties` file generation - GitHub - n0mer/gradle-git-properties: Gradle plugin for `git.properties` file generation

github.com

plugins {
	...
    id "com.gorylenko.gradle-git-properties" version "2.4.0-rc2"
}

build.gradle에 위와 같이 한 줄을 추가하고 gradle refresh 하면 generateGitProperties task가 생긴다.

실행하면 build 경로에 아래와 같은 파일이 생긴다.

클릭해보면 깃 정보가 들어있는 것을 알 수 있다.

 

더 놀라운 것은 이 플러그인/파일이 공식적으로 spring-boot-actuator와 연동 가능하다는 것인데, actuator와 자동 연동을 하려면 src/main/resources 경로에 git.properties 가 있어야 한다.

build.gradle에 아래 항목을 추가한다.

gitProperties {
    //필요한 항목만 나열가능, keys 없으면 전체 다 나옴
    keys = ['git.branch','git.commit.id','git.commit.time', 'git.commit.message.full', 'git.commit.id.abbrev', 'git.build.version','git.build.user.name']
    //날짜 포맷
    dateFormat = "yyyy-MM-dd HH:mm:ss"
    //커스텀 값 넣을 수 있고 기존 항목 override도 가능
    customProperty 'git.build.version', '3.4.5'

    // The directory in this config key is also added as a classpath entry
    //복사할 경로 지정
    gitPropertiesResourceDir = file("${project.rootDir}/src/main/resources")
}

빌드하면 아래와 같은 에러가 나면서 빌드가 실패한다..ㅎㅎ 

> Task :compileJava
> Task :generateGitProperties UP-TO-DATE
> Task :processResources FAILED


Execution failed for task ':processResources'.
> Entry application-common.properties is a duplicate but no duplicate handling strategy has been set.

우선 빌드 순서를 보면 generateGitProperties를 하고 processResources를 하는데,

gitProperties.gitPropertiesResourceDir 의 설명에 적힌 것 처럼,

  1. generateGitProperties 단계에서 src/main/resources 에 파일을 생성 후 src/main/resources -> build/resources/main 로 복사를 하고
  2. processResources 단계에서 (또) src/main/resources -> build/resources/main 으로 옮기는 듯 하다.

그래서 두 번의 복사로 인해 충돌되는 파일(application-common.properties / git.properties)에 대해 설정하라는 에러인 듯 하다.

 

해결책은

1. 아래와 같이 build.gradle에 duplicatesStrategy과 관련된 설정을 추가하거나..

gitProperties {
    //필요한 항목만 나열가능, keys 없으면 전체 다 나옴
    keys = ['git.branch','git.commit.id','git.commit.time', 'git.commit.message.full', 'git.commit.id.abbrev', 'git.build.version','git.build.user.name']
    //날짜 포맷
    dateFormat = "yyyy-MM-dd HH:mm:ss"
    //커스텀 값 넣을 수 있고 기존 항목 override도 가능
    customProperty 'git.build.version', '3.4.5'

    // The directory in this config key is also added as a classpath entry
    //복사할 경로 지정
    gitPropertiesResourceDir = file("${project.rootDir}/src/main/resources")
}

processResources{
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

 

2. 아래와 같이 설정을 바꾸면 해결은 된다..

gitProperties {
    //필요한 항목만 나열가능, keys 없으면 전체 다 나옴
    keys = ['git.branch','git.commit.id','git.commit.time', 'git.commit.message.full', 'git.commit.id.abbrev', 'git.build.version','git.build.user.name']
    //날짜 포맷
    dateFormat = "yyyy-MM-dd HH:mm:ss"
    //커스텀 값 넣을 수 있고 기존 항목 override도 가능
    customProperty 'git.build.version', '3.4.5'

    // Customize file name (could be a file name or a relative file path below gitPropertiesResourceDir dir)
    gitPropertiesName = "${project.rootDir}/src/main/resources/git.properties"
}

 

허나 좀 찝찝한게 있는데, 여전히 아래와 같은 경고 메세지가 나오기 때문이다. 

> Task :processResources
Execution optimizations have been disabled for task ':processResources' to ensure correctness due to the following reasons:
Reason: Task ':processResources' uses this output of task ':generateGitProperties' without declaring an explicit or implicit dependency.

 

위 경고에 대한 설명은 여기에 있다. 간단히 설명하자면 두 task 가 같은 경로에서 작업을 하기 때문에 순서에 영향을 받는다는 의미다.

+ 순서를 명시적으로 잡아주면 해결된다. 깃 정보 파일을 만들고 프로퍼티 파일 옮기는 작업을 하도록 설정했다.

processResources{
    dependsOn(generateGitProperties)
}

 

빌드를 하면 src/main/resources에 git.properties가 생성되어있고, 파일을 열어보면 아래와 같이 위 설정(keys)대로 값이 들어있다.

git.properties

실행하고 info를 띄워본다.

/actuator/info

이렇게 별다른 코딩없이 그래들 설정으로 info 정보를 보여줄 수 있다.

추가적으로 application.properties에 아래 설정들을 추가해서 더 보거나 덜 보거나 하는 사항들을 조절할 수 있다.

management.info.git.mode=full  //or simple
management.info.env.enabled=true  //or false
//등등 많음

참고로 exclude 속성은 include 속성보다 우위에 있기 때문에 exclude로 막아놓으면 include로 추가해도 안 보임


actuator를 넣는 목적은 배포 후에 배포가 잘되었는지 확인하기 위해서기 때문에 아래 목적을 달성할 수 있어야한다.

  1. 서버가 잘 떴는지(유관 서버들 포함; DB, rabbit, reddis, etc) 확인
  2. 최종 버전으로 잘 배포 되었는지 확인

사실 1번을 알기 위해서는 /actuator/health 를 사용하면 되고 2번을 알기 위해서는 /actuator/info 를 이용하면 된다.

허나 이를 위해 api를 두 번 날리는게 번거로웠고(게다가 관리하려는 서버가 많으면 두 번씩 날리는게 매우 힘들고 한 눈에 보기도 어려워진다), 어떻게 보면 info에 나오는 정보에서 딱히 쓸만한게 깃 해쉬정도밖에 없는 듯해 info를 health와 합치고 싶었다.

그래서 health 를 커스텀하기로 한다. 아래와 같이 HealthIndicator를 구현하면 된다.

@Component
@PropertySource("classpath:git.properties")
public class HealthConfig implements HealthIndicator {
    @Value("${git.commit.id.abbrev}")
    private String hash;
    @Value("${git.branch}")
    private String branch;
    @Value("${git.build.time}")
    private String buildTime;
    @Value("${git.build.version}")
    private String buildVer;

    @Override
    public Health health() {
        return Health.up()
                .withDetail("version", buildVer)
                .withDetail("hash", hash)
                .withDetail("branch", branch)
                .withDetail("buildTime", buildTime)
                .build();
    }
}

그 외 health 에서 보고싶은 정보가 있으면 추가하면 된다.

최종 결과는 아래와 같다.

/actuator/health

 


참고

스프링 설정: https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html

 

Production-ready Features

You can enable HTTP Tracing by providing a bean of type HttpTraceRepository in your application’s configuration. For convenience, Spring Boot offers InMemoryHttpTraceRepository, which stores traces for the last 100 (the default) request-response exchange

docs.spring.io

 

728x90
반응형

'개발 > spring' 카테고리의 다른 글

[retry] spring-retry for auto retry  (0) 2022.02.04
[spring] graceful shutdown default as of 2.3  (0) 2022.02.03
[jpa] OSIV란; spring.jpa.open-in-view  (0) 2022.01.27
[jpa] 영속성 컨텍스트 in spring  (0) 2022.01.27
[jpa] one-indexed pageable  (0) 2022.01.27
반응형

OSIV에 대한 이해를 하려면 영속성 컨텍스트가 뭔지 알아야한다. 이전 글을 참고하자.

2022.01.27 - [개발/spring] - [jpa] 영속성 컨텍스트 in spring

 

[jpa] 영속성 컨텍스트 in spring

영속성 컨텍스트? 엔티티를 영구 저장하는 환경으로 애플리케이션과 DB 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하

bangpurin.tistory.com

 

springboot2.0 이 후 springboot 프로젝트를 시작하면 아래와 같은 warning이 지나갔는데, 딱히 별 일 없어서 그냥 넘어갔었다.

WARN  [JpaBaseConfiguration$JpaWebConfiguration.openEntityManagerInV: 219] spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning

warning을 지우는 방법은 프로퍼티(application.properties)에 아래 설정값을 주면 되긴하지만,

spring.jpa.open-in-view=false

단순히 저 warning을 지우는 건 의미가 없으므로.. 저 설정 값의 의미를 생각해보도록 한다.

참고로 default는 true로 사용하는 것임

 

OSIV란?

Open Session In View의 줄임말로 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지된다. (따라서 뷰에서도 지연 로딩을 사용할 수 있다.)

참고로 Open Session In View는하이버네이트에서 사용하는 단어이며, JPA에서는 Open EntityManager In View 라고 하지만 보통 OSIV라고 한다.

 

기존 방식의 OSIV?

라고 하면 프레젠테이션 단부터의 영속성 유지 방식을 말한다.

old OSIV

이렇게 될 경우 저번 시간에 살펴 본 지연로딩의 문제는 사라지겠지만, 컨트롤러 단에서 수정한 값이 별도의 로직이 없이도 그대로 DB에 반영되어 위험하다. 

 

스프링의 OSIV는 비즈니스 계층 중심의 OSIV이다. 

위 설정 값에 따라 두 가지 버전을 제공하는데, 하나씩 알아보자.

OSIV ON / spring.jpa.open-in-view=true

OSIV on

영속성 컨텍스트의 범위와 트랜젝션의 범위를 다르게 가져가는 것이다. 이전 글에서 '스프링 컨테이너는 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다'고 했는데 그걸 깰 수 있게 되는 것이다.

  • 장점
    • API 요청부터 응답이 끝날 때 까지 영속성 컨텍스트와 DB 커넥션을 유지
    • 지연로딩 등의 처리가 가능
  • 단점
    • DB 커넥션 리소스를 비교적 장시간 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자를 수 있음
    • 예를 들어 외부API가 오래걸려서 생기는 지연동안에도 DB커넥션을 들고있어 비효율적임

 

OSIV OFF / spring.jpa.open-in-view=false

OSIV off

저번 시간에 본 스프링 컨테이너의 기본전략과 동일하다.

  • 장점
    • 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, DB 커넥션도 반환하기에 커넥션 리소스를 낭비하지 않음
  • 단점
    • 준영속 시 생기는 문제 그대로 나옴
    • 모든 지연로딩 코드를 트랜잭션 안으로 넣거나 강제 호출해야 함

해결법?

요즘 뜨는 cqrs기법이 제일인 듯 하다.

2022.01.11 - [architecture/micro service] - [arch] what is cqrs - 조회와 비조회의 엄연한 분리

 

[arch] what is cqrs - 조회와 비조회의 엄연한 분리

CQRS는 Command and Query Responsibility Segregation(명령과 조회의 책임 분리)의 약자이다. 즉 쉽게 말해서 data에 대한 read와 write는 분리되어 개발되어야 한다는 것이다. 1. Before 설명을 하기 전에 그림..

bangpurin.tistory.com

 


그래서 OSIV 를 끄라는 건지, 켜라는 건지...

  • 실시간 트레픽/성능이 중요한 경우 OSIV를 사용하지 말고(off), DTO로 직접 조회하도록 하자.
  • 어드민 페이지같이 실시간 트레픽이 중요하지 않는 경우 OSIV를 사용해도 좋다(on)!

참고

영속성과 OSIV: https://ttl-blog.tistory.com/183

 

[JPA] OSIV란? (feat. 스프링 JPA의 작동원리, 퍼사드 패턴 등)

OSIV에 다가가기까지 조금 서론이 길다. (JPA의 작동원리, FACADE 계층 등) OSIV에 대해서만 궁금하다면 그쪽 부분만 찾아서 보길 바란다. 스프링에서 JPA를 사용하게 되면, 스프링 컨테이너가 트랜잭

ttl-blog.tistory.com

 

728x90
반응형
반응형

영속성 컨텍스트?

엔티티를 영구 저장하는 환경으로 애플리케이션과 DB 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 DB에 바로 작업하지 않고 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

영속성 컨텍스트는 엔티티/@Id를 식별자 값으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.

그리고 포인트는 flush 와 commit이 다르다는 것, flush는 DB에 적용은 하지만 최종 commit은 안 한 상태이다.

영속성 생명주기

 

영속성에 대한 다른 내용들은 타 블로그에 잘 적혀있으니 참고 링크를 아래에 남기는걸로 대체하고, 보통 spring-data-jpa를 사용하기 때문에, spring 컨테이너의 기본 전략을 알아본다.

스프링 컨테이너의 기본 전략?

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 말 그대로 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻인데, 즉 트랜잭션이 시작하는 순간 영속성 컨텍스트도 생성되고, 트랜잭션이 끝나는 순간에 영속성 컨텍스트가 종료되는 것이다.

 

우리는 보통 비즈니스 로직을 서비스에서 짜고, 함수에 @Transaction 어노테이션을 사용하여 트랜잭션을 시작한다. 

내부적으로 보자면, @Transaction 이 있으면 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다.

@Transactional

 

트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.

즉, 위 그림과 같이 한 서비스에서 서로 다른 repository의 함수를 사용한다고 했을 때 각각의 entityManager는 다르지만 한 트랜젝션 내에서는 항상 같은 영속성 컨텍스트를 사용한다. 

그리고 해당 영속성 컨텍스트는 트랜잭션과 생명주기가 똑같다.

 

그렇다면 위 그림처럼 컨트롤러나 뷰 같은 프리젠테이션 계층에서는? 준영속 상태가 된다.

준영속 상태는 영속 상태가 아니기 때문에 영속성 컨텍스트에서 제공하는 기능을 사용하지 못한다(지연로딩, 변경감지 등).

컨트롤러에서 업데이트를 하는건 아니라고 생각하여 변경 감지 기능이 굳이 필요하나 싶었는데, 지연로딩은 종종 컨트롤러에서 필요할 때가 있었던 기억이 있다..(1번 서비스로 어떤 엔티티를 구해온 후, 그 엔티티의 내용물을 2번 서비스로 넘길 때..)

영속성 컨텍스트를 뷰까지 살아있게...? 이 개념과 이어서 다음 시간에는 OSIV에 대해 살펴본다.


참고

영속성 컨텍스트: https://dev-monkey-dugi.tistory.com/72

 

JPA 영속성 컨텍스트가 뭘까?

JPA 영속성 컨텍스트에 관하여 클라이언트 요청부터 DB까지 동작하는 로직 웹 애플리케이션에 클라이언트 요청이 들어오면 EntityMangagerFactory 는 EntityManager 객체를 생성하게 됩니다. 각각 생성된 En

dev-monkey-dugi.tistory.com

 

준영속성과 지연로딩 등: https://www.nowwatersblog.com/jpa/ch13/13-2

 

만렙 개발자 키우기

개발 경험치를 쌓아가며 성장하는 개발자의 기록 일지입니다.

www.nowwatersblog.com

 

transactional in read: https://willseungh0.tistory.com/75?category=880297 

 

[JPA] @Transaction(readOnly=true) 성능 향상 이유?

개요 스프링 프레임워크에서 어노테이션으로 트랜잭션을 읽기 전용 모드로 설정할 수 있다. @Transactional(readOnly = true) 예상치 못한 엔티티의 등록, 변경, 삭제를 예방할 수 있고, 또한 성능을 최적

willseungh0.tistory.com

 

728x90
반응형
반응형

springboot 2 사용 시 application.properties에 아래 값을 설정 할 경우,

 spring.data.web.pageable.one-indexed-parameters=true

 

스웨거에서 page = 23으로 요청

결과에서 pageNumber = 22 로 내려오니 주의해야한다.

 

jpa는 기본적으로 zero-base 라서 위 설정값을 true로 설정하면 page-1의 값을 컨트롤러에 넘겨 작업을 하고 그대로 result를 뱉는 듯 하다.

 


참고: https://treasurebear.tistory.com/59

 

[Spring boot] page 1부터 시작하기

Spring data jpa를 사용하면 paging 하기 쉽게 Pageable 인터페이스를 제공해준다. https://docs.spring.io/spring-data/jpa/docs/2.2.7.RELEASE/reference/html/#core.web.basic Spring Data JPA - Reference Doc..

treasurebear.tistory.com

 

728x90
반응형
반응형

초기 환경: swagger 2.9.2, springboot 2.6.2

@Configuration
@EnableSwagger2
@RequiredArgsConstructor
public class SwaggerConfig {

    private final TypeResolver typeResolver;

    @Bean
    public Docket restAPI() {
        return new Docket(DocumentationType.SWAGGER_2)
                .alternateTypeRules(AlternateTypeRules.newRule(
                        typeResolver.resolve(Pageable.class),
                        typeResolver.resolve(SwaggerPage.class)))
                .apiInfo(apiInfo())
                .directModelSubstitute(LocalDateTime.class, String.class)
                .select()
                .apis(RequestHandlerSelectors.basePackage("package.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("test_backend")
                .version("1.0.0")
                .description("This is swagger!")
                .build();
    }

    @Getter
    @Setter
    @ApiModel
    public static class SwaggerPage {
        @ApiParam(example = "1", defaultValue = "1")
        @ApiModelProperty(value = "페이지 번호(1~N)")
        private int page;

        @ApiParam(example = "20", defaultValue = "20")
        @ApiModelProperty(value = "페이지 크기", allowableValues = "range[0, 100]")
        private int size;

        @ApiModelProperty(value = "정렬(사용법: 컬럼명,ASC|DESC)")
        private List<String> sort;
    }
}

 

위 빈 설정이 springboot2.5.6에서는 잘 작동하였는데, 부트 버전을 2.6.2로 올리고 나서 부터는 아래와 같은 에러가 발생한다.

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException

방안 1) 찾아보니 springboot2.6.x 대와 설정 값이 맞지 않는 부분(해답2에 언급)이 있기 때문이며 boot 버전을 2.5.x로 내리면 된다.

방안 2) 부트2.6.2에서 아래 설정 값을 추가하니 잘 실행되었다. 아래 값은 부트2.5.x에서는 기본값이었는데 2.6.x로 오면서 기본값이 바뀌었고 아래 값으로 변경 값을 override 해주면 된다. 

spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

 

허나 스프링 진영에서 이 default값을 변경하게 된 이유가 있을 터, 이 설정값이 어떤 이슈를 가져올지 모르므로 단순히 설정 값 변경으로 해결하고 싶지 않았다.

방안 3) 스웨거 변경 springfox -> springdoc

springfox가 2.9 버전 이후로 한동안 업데이트를 안 하다가 2년 만에 2.10, 3.0 버전을 만들었는데, 그 사이 springdoc 진영에서 새로 만든 swagger가 요새 뜨고 있는 듯하다.

https://www.libhunt.com/compare-springfox-vs-springdoc-openapi?ref=compare 

 

springfox vs springdoc-openapi - compare differences and reviews? | LibHunt

springfox Posts with mentions or reviews of springfox. We have used some of these posts to build our list of alternatives and similar projects. The last one was on 2020-12-22.

www.libhunt.com

 

springdoc swagger

springdoc이 더 다양한 옵션(보안 관련/ webclient 비동기 등)을 먼저 지원하여 빠른 안정화를 하고 있고, 설정만으로도 금세 만들 수 있다는 장점, 스프링에서 밀어주는 느낌, springfox의 게으름 및 버그 등등의 이유로 springdoc의 스웨거를 사용해보고자 한다.

심지어 springdoc에서는 springfox에서 어떻게 바꿀 수 있는지 가이드도 해준다..ㅋㅋ

https://springdoc.org/#migrating-from-springfox

 

OpenAPI 3 Library for spring-boot

Library for OpenAPI 3 with spring boot projects. Is based on swagger-ui, to display the OpenAPI description.Generates automatically the OpenAPI file.

springdoc.org

 

위 설정대로 진행하니 기본적인 화면은 잘 나온다. 허나 나는 pageable 내용이 swagger에 나오는 걸 중시해서(페이징 테스트를 스웨거로 하고 싶다) 샘플 api를 작성했는데 자꾸 오브젝트로 나오는 것이었다..?!

pageable object... 못마땅..

 

implementation "org.springdoc:springdoc-openapi-data-rest:${swaggerVersion}"

위 dependency 넣으면 된다고 에 쓰여있었는데.. 그래도 안돼서 더 찾아봤더니

@ParameterObject

위 어노테이션을 변수에 넣어야 한다고 한다..  기본값을 주려면 @PageableDefault랑 같이 쓰면 된다. 아래처럼 말이다.

@GetMapping("game")
@Operation(description = "게임기록조회(일반/토너먼트)")
public BaseResponse<Page<?>> gameLogByTypeAndGid(@ParameterObject @PageableDefault(size = 20, sort = "seq", direction = Sort.Direction.DESC) Pageable pageable){
    return new BaseResponse<>();
}

 

게다가 1.6.4 버전부터는 org.springdoc:springdoc-openapi-data-rest 의존성도 필요 없다(코어에 포함시켰다고 한다)고 하니 완전 땡큐다. 바로 지우고 실행했는데 아래와 같이 잘 나온다.

 

추가사항.. POST 요청 시 request body를 위 사진처럼 input 타입으로 주고 싶은데 지원이 안 되는 것 같다. GET / query param만 저렇게 가능한 듯하다... 이러면 직접 json을 손대야 해서 실수가 날까 봐 별로 안 좋아하는데,, 이 때문에 springfox로 돌아가야 하나 살짝 고민된다.

 


참고

나랑 같은 고민을 한 사람들: https://stackoverflow.com/questions/70178343/springfox-3-0-0-is-not-working-with-spring-boot-2-6-0

 

Springfox 3.0.0 is not working with Spring Boot 2.6.0

Springfox 3.0.0 is not working with Spring Boot 2.6.0, after upgrading I am getting the following error org.springframework.context.ApplicationContextException: Failed to start bean '

stackoverflow.com

 

openapi 적용 법: https://www.baeldung.com/spring-rest-openapi-documentation

 

migration, jwt: https://deepak-shinde.medium.com/migrating-from-springfox-swagger-2-to-springdoc-openapi-3-79a79757b8d1

 

Migrating from Springfox Swagger 2 to Springdoc OpenAPI 3

As we know documentation is an essential part of building REST APIs. I have been using Springfox for my Spring boot projects for quite a…

deepak-shinde.medium.com

 

springdoc pageable included in 1.6.4

https://github.com/springdoc/springdoc-openapi/issues/1415

 

Moving PageableDefault support to springdoc-openapi-common · Issue #1415 · springdoc/springdoc-openapi

Describe the bug Pageable is documented to be used with Parameter(hidden) and PageableAsQueryParam. When we add PageableDefault, the default page size should be 10 but is reported as 20. To Reprodu...

github.com

 

728x90
반응형
반응형

2022.01.20 - [architecture/micro service] - [gRPC] what is gRPC?

 

[gRPC] what is gRPC?

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. RPC(Remote Procedure Call)? 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서..

bangpurin.tistory.com

이전 글에서 gRPC에 대한 개념을 간략히 살펴보았다. 이번 글에서는 간단히 구현해본다.

목표: java11 / gradle 7.3.3 multi project / springboot 2.6.2 / grpc server & client 개발

 

참고 블로그: https://velog.io/@chb1828/Spring-boot%EB%A1%9C-Grpc%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90

 

Spring boot로 Grpc를 사용해보자

오늘은 Grpc에 대해서 글을 써보려고한다. Grpc를 간단하게 설명하면 서로 다른 위치에 존재하는 공간에서 동일한 객체를 가져다 와서 사용하는 것이다.

velog.io

 

위 블로그는 서버 프로젝트 / 클라이언트 프로젝트 / interface용 jar 를 만드는 프로젝트를 각각 구성하였다면, 여기서는 그래들 멀티 프로젝트로 구성한다. 

깃허브 주소: https://github.com/haileyjhbang/grpc-test.git

 

GitHub - haileyjhbang/grpc-test

Contribute to haileyjhbang/grpc-test development by creating an account on GitHub.

github.com

프로젝트 구조는 아래와 같다.

각 소스 내용물은 위 깃허브에서 확인가능하며 특이사항은 아래와 같다.

  • 공통 사항은 common 폴더 안에 있고, server / client 프로젝트 빌드 전 common을 먼저 빌드해 common-plain.jar를 생산해야함.
  • 각 프로젝트 build.gradle 에 보면 해당 jar을 import하게 되어있음.
        - common 의 내용물은 spring/java 에 관련된 내용은 없고 단순히 proto만 빌드함
        - server / client 프로젝트에서 common 의 컴파일된 java 파일을 사용해야하기 때문에 common을 멀티 프로젝트로 구성하는 것은 불가능하다고 판단(멀티 프로젝트로 import 시 src/main 아래의 폴더를 바라보나 common에는 src/proto만 있음)
  • client stub은 blocking stub 사용, unary 방식으로 통신으로 구현
  • client의 컨트롤러에서 request param으로 name을 넘기면 서버로 전달되어 추가 작업 후 화면에 표시
  • server 프로젝트와 client 프로젝트는 별도의 각각 jar을 생산하며 서로 다른 포트로 뜨게 되어 있음
  • [GET] http://localhost:9091/test?name=jhbang 로 확인

 

여기서는 구현하면서 만났던 문제와 해결방법에 대해 기술한다.

1. 멀티 프로젝트 구성 시

Execution failed for task ':common:bootJar'.
> Error while evaluating property 'mainClass' of task ':common:bootJar'
   > Failed to calculate the value of task ':common:bootJar' property 'mainClass'.
      > Main class name has not been configured and it could not be resolved

common/build.gradle 스크립트에 아래 추가

//common 프로젝트는 proto 번환만 한다.
//실행할 메인 클래스가 없을때는 아래와 같이 한다.

bootJar.enabled = false

 

완성하고 실행하면 아래와 같이 나온다.


참고

proto3 rule: https://developers.google.com/protocol-buffers/docs/proto3

 

Language Guide (proto3)  |  Protocol Buffers  |  Google Developers

Language Guide (proto3) This guide describes how to use the protocol buffer language to structure your protocol buffer data, including .proto file syntax and how to generate data access classes from your .proto files. It covers the proto3 version of the pr

developers.google.com

 

proto java guide: https://developers.google.com/protocol-buffers/docs/javatutorial

 

Protocol Buffer Basics: Java  |  Protocol Buffers  |  Google Developers

Protocol Buffer Basics: Java This tutorial provides a basic Java programmer's introduction to working with protocol buffers. By walking through creating a simple example application, it shows you how to Define message formats in a .proto file. Use the prot

developers.google.com

 

gradle multiproject setting: https://clack2933.tistory.com/15

 

Gradle Multi Module 프로젝트

머리말 하나의 단일 모듈로는 관리에 어려움을 겪게 되었고 이문제를 해결하기 위해서 모듈을 분류하는 multi module을 공부하게 되었습니다. 프로젝트 생성 Spring Boot 2.4.xx Gradle 6.8 Intellij Java11 초

clack2933.tistory.com

 

728x90
반응형

+ Recent posts