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

이전 글: 2022.02.08 - [서버 세팅, tool 사용법/postman] - [test] before you dive into api testing

 

[test] before you dive into api testing

포스트맨이란 API 개발을 보다 빠르고 쉽게 구현 할 수 있도록 도와주며, 개발된 API를 테스트하여 문서화 또는 공유 할 수 있도록 도와주는 플랫폼이다. 포스트맨으로는 api 요청을 하고 관련 스

bangpurin.tistory.com

환경: 포스트맨 Version 9.12.1

사전지식: 본문의 내용을 따라하기 전, 아래 사이트를 확인하여 각 변수에 대한 범위와 활용성에 대한 이해를 해야한다.

https://learning.postman.com/docs/sending-requests/variables/

 

Home

Postman Network Browse APIs, workspaces, and collections inside Postman. Explore Postman →

learning.postman.com

 

이제 api testing을 위한 세팅을 진행한다.

collection에 api-testing 이라는 콜랙션을 생성하고 api를 저장하였다.

이번 테스트에서는 free fake api 제공하는 jsonplaceholder의 샘플 api를 사용한다.

//GET or POST
https://jsonplaceholder.typicode.com/users

 

1. 환경변수 생성

url이 반복되니 url을 환경변수로 저장한다.

url을 환경변수로 생성하면 서버별로 콜랙션을 따로 만들 필요 없이 환경변수의 값만 바꿔주면 된다(ex. 개발서버/qa서버별로 콜랙션을 나눌 필요가 없고 환경변수에 값만 변경하여 돌릴 수 있다).

오른쪽 상단에 있는 눈 마크에서 설정할 수 있다. environment 와 global이 있는데 environment는 환경에 따라 전환하여 쓰는 값이고 global은 환경에 상관없이 공통으로 쓸 값이다. url은 environment와 성격이 더 맞기 때문에 environment의 add를 눌러 저장한다.

아래와 같이 환경에 맞게 url/기타 변수 등을 입력하고 환경변수의 이름을 지정한 후 save를 누른다.

그리고 다시 api를 쏘는 공간으로 돌아와서 주소 값을 방금 설정한 변수로 세팅해준다. 중괄호 두 개를 사용하면 변수를 불러올 수 있다.

여기서 처음에 잘 안될 수 있는데, 이는 환경 지정하지 않았기 때문이다. 오른쪽 상단에서 올바른 환경변수(real-server)를 선택해주자.

 

2. snippets 스크립스 설명

snippet을 활용한 기본 테스트를 해본다.

처음 보이는 부분은 변수 가져오기/저장하기/비우기에 관한 부분이다. 직접적인 테스팅은 아니고 테스트 스크립트에서 필요할 때 불러다가 사용하면 된다.

맨 위의 sene a request와 response body: convert xml body to a json을 제외한 나머지는 모두 테스팅 코드이다. 눌러보면 아래와 같은 형태를 하고 있음을 알 수 있다.

pm.test("테스트 이름/설명", function () {
  //어떻게 테스트 할지 로직
  pm.expect(); //테스트 기대값 확인하는 함수
});

 pm이라는 라이브러리에서 test라는 함수를 사용하고 있다. 첫 번째 변수는 테스트 이름이고 두 번째 매개변수인 함수가 실제 로직이 들어갈 곳이다. 보통 expect 함수를 써서 결과 값을 확인하게 되어 있다.

snippet에서 볼 수 있는 함수들은 아래와 같다.

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200); //status 가 200을 가지고 있는지
});
pm.test("Body matches string", function () {
    pm.expect(pm.response.text()).to.include("Bret"); //결과 어딘가에 Bret 이라는 단어를 포함하고(include) 있는지
    //pm.response.text() : 결과를 string화 함
});
pm.test("Your test name", function () {
    var jsonData = pm.response.json();   //결과 값을 json으로 만들고
    pm.expect(jsonData[0].id).to.eql(1); //array의 첫 번째 덩어리의 id 값이 1인지 확인
    //jsonData[0].id : 코드에서 대부분의 것은 0부터 시작이라 0이 첫 번째이고 결과 값이 object가 아닌 array여서 index([n])를 넣어줘야함
});
pm.test("Body is correct", function () {
    pm.response.to.have.body('[{"id":1}]'); //결과가 이것과 완전히 동일한지 확인
});
pm.test("Content-Type is present", function () {
    pm.response.to.have.header("Content-Type"); //특정 헤더가 있는지 확인
});
pm.test("Response time is less than 200ms", function () {
    pm.expect(pm.response.responseTime).to.be.below(200); //응답시간이 200ms 이하인지 확인
});
pm.test("Successful POST request", function () {
    pm.expect(pm.response.code).to.be.oneOf([201, 200]); //결과 http status code가 둘 중 하나인지 확인
});
pm.test("Status code name has string", function () {
    pm.response.to.have.status("OK"); //결과 http status code가 숫자가 아니라 문자로 올 때(대소문자 구분함에 주의)
});

/////

var schema = {
    "items": {
        "type": "boolean"
    }
};

var data1 = [true, false];
var data2 = [true, 123];

pm.test('Schema is valid', function () {
    pm.expect(tv4.validate(data1, schema)).to.be.true; //data1의 틀이 schema와 맞는지 확인
    pm.expect(tv4.validate(data2, schema)).to.be.false; //data2의 틀이 schema와 맞는지 확인
});

기본 snippet을 통하여 값 결과 값 검사, 상태 확인, 결과 format 확인(schema) 등이 가능하다.

schema에 대해서는 별도의 글로 설명한다.

2022.02.08 - [서버 세팅, tool 사용법/postman] - [test] schema validation

 

앞으로 자주 쓸 문법은 아무래도 결과 값을 json화 하여 변수를 다루는 부분일 것이다. 또한 eql과 have/include가 의미적으로 다르기에 헷갈리면 안된다.

앞.to.have.뒤       //앞 항목이 뒤를 가지고 있는지(명확)
앞.to.include.뒤    //앞 항목이 뒤를 포함하는지(존재성)
앞.to.not.have.뒤   //부정할 때 not의 위치 중요(to 다음; include 등에서도 활용가능)

앞.to.eql(뒤)     //앞이 뒤랑 값이 같은지 확인(데이터 값 확인)

pm.response...   // 여기 안에 결과가 들어있고
var jsonData = pm.response.json();   //결과 값을 json으로 만드는 부분; json화 해야 데이터를 가져오기가 쉽다.

 

다음 글에서 샘플 테스팅을 진행한다.


참고

pm object 설명: https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#the-pm-object

 

Home

Postman Network Browse APIs, workspaces, and collections inside Postman. Explore Postman →

learning.postman.com

 

포스트맨 테스트 원문: https://learning.postman.com/docs/writing-scripts/intro-to-scripts/

 

Home

Postman Network Browse APIs, workspaces, and collections inside Postman. Explore Postman →

learning.postman.com

728x90
반응형
반응형

포스트맨이란 API 개발을 보다 빠르고 쉽게 구현 할 수 있도록 도와주며, 개발된 API를 테스트하여 문서화 또는 공유 할 수 있도록 도와주는 플랫폼이다. 포스트맨으로는 api 요청을 하고 관련 스크립트를 저장해서 export/import 할 수 있을 뿐만 아니라, 다양한 환경/변수 등에서 테스트를 하고 그 결과를 분석하고, 소스코드와 연동하여 ci를 가능하게 한다. 

what you can do with postman from the perspective of api testing

포스트맨의 다양한 기능이 있지만 당분간 테스트 작성법에 대해 초점을 두고 알아보려고 한다.

앞으로 작성할 글을 읽기 전 전제사항은 다음과 같다.

  • 포스트맨으로 api 요청을 해 본 적이 있다.
  • 포스트맨의 다양한 기능을 사용해 본 적이 있다.
  • 포스트맨의 용어들과 친숙하다.

포스트맨과 친숙하지 않은 사람들을 위해 간략히 아래 참고 링크를 공유한다.

특히 포스트맨의 collection에 대해 알아둬야 할 필요가 있다.

포스트맨은 api를 관리하기 위한 묶음으로 collection을 제공하는데 이 묶음 별로 변수를 설정하고 테스트를 관리할 수 있다(또한 보기도 좋으니 일석이조 아닌가).

collection example

관련 사용법은 아래 링크로 대체한다.

https://gngsn.tistory.com/26

 

Postman, 어렵지 않게 사용하기 - 사용

안녕하세요 😆 이 번 포스팅의 주제는 서버 개발을 할 때 필수적인! POSTMAN을 '제대로' 사용하는 법에 대해 다룰 예정입니다. 서버뿐만 아니라, 클라이언트를 개발하는 분들도 도움이 되는 내용

gngsn.tistory.com

https://m.blog.naver.com/qbxlvnf11/222467061082

 

웹, 서버, API 테스트에 유용한 툴 POSTMAN 사용법

웹, 서버, API 개발 테스트 툴 포스트맨 "예비 개발자" 이번 포스팅에서는 Postman이라는 A...

blog.naver.com

https://kellis.tistory.com/17

 

[Postman 사용법] 1. 환경 설정 (Workspace와 Collection)

이 장에서는 Postman의 Workspace, Collection 그리고 작업한 Workspace를 다른 사람과 공유하는 방법에 대해 알아보겠습니다. Workspace란? Team Workspace 생성 Collection 이란? Collection 생성 Sharing Concl..

kellis.tistory.com


 

이제 테스트에 주로 사용할 화면을 맛보기로 만나본다.

 

1. pre-request script

콜렉션에 여러 api가 있고 collection runner을 통해 순차적으로 테스팅 할 때, 

두 번째 request가 첫 번째 request의 응답 값일 때, 

그래서 첫 번째 응답의 값을 test 에서 변수로 세팅해두고, 두 번째 요청을 하기 전 불러오는 작업이 필요할 때,

그럴 때 필요한 것이 pre-request script 화면이다.

위 그림에서 name이란 변수는 사전에 collection variable로 세팅이 된 것이고,

요청 시 필요한 name_detail값은 요청 전 pre-request script 화면에서 사전에 저장된 name을 불러와 가공되어 전달되는 것을 볼 수 있다.

 

2. test script

 

api를 요청하는 곳에 tests 라는 탭이 있고, 이곳에서 해당 api에 관한 테스트 스크립트를 작성할 수 있다.

해당 탭을 누르면 빈 칸이 보이고 이곳에 스크립트를 작성하면 되는데, 해당 스크립트는 자바스크립트 기반이며, 포스트맨이 제공하는 pm라이브러리를 활용하여 작성하면 된다.

여기서 좋은 점은 오른쪽에 snippets라고 자주 사용하는 코드를 바로 불러서 변형해서 사용할 수 있게 했는데, 코드를 다 외우지 않고 필요할 때마다 조합해서 사용할 수 있도록 해두었다.

또한 위 사진처럼 코드를 손으로 칠 경우 활용가능한 코드들의 리스트가 바로바로 나와 타 ide처럼 작성이 용이하다는 장점이 있다.

(이는 위에서 본 pre-request script에서도 동일하다).


포스트맨을 사용할 때 추천하는 사항은

  1.  계정 로그인을 해서 api 를 관리하기
    • 로그인을 하지 않으면 pc에 임시 저장되고 언제든지 히스토리가 날아갈 우려가 있음
    • 로그인을 하면 관련 히스토리/정보가 계정에 묶이게 되고 어디서 든 로그인만하면 테스트를 할 수 있는 환경이 됨
  2. collection/workspace 적극 활용
    • collection에 등록된 전체 api를 한번에 테스트 가능
    • 수 백건에 해당하는 api를 일회성으로 테스트하고 끝내지말고 할 때마다 제대로 정리하면 다음에 재활용하기가 쉬워짐

 

다음 글부터 본격적으로 알아본다!


참고

https://www.guru99.com/postman-tutorial.html

 

Postman Tutorial: How to Install and use Postman for API Testing

Postman tutorial for beginners: Learn What is Postman? Step by step guide on How to Download and Install POSTMAN, and Test API using Postman.

www.guru99.com

 

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
반응형
반응형

환경: springboot2.6.2, intellij2021.2.2

junit5기반의 테스트 코드를 돌릴 때 아래와 같은 에러가 나면서 테스트가 안 돌아갈 때가 있다.

Execution failed for task ':query:test'.
> No tests found for given includes: [com.cqrs.query.service.RetryServiceTest.retryTest_with_bean](filter.includeTestsMatching)
import org.junit.jupiter.api.Test;
...

@SpringBootTest
class RetryServiceTest {

    @Autowired
    private RetryService retryService;
    @MockBean
    private AccountRepository accountRepository;

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

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

아래와 같이 intellij의 run test using설정이 gradle(default)로 되어 있을텐데, 저 값을 gradle -> intellij로 바꾸면 실행된다..

intellij settings

 


추가)

아래와 같이 Mockito.when().thenThrow() 로 exception 발생 시 Exception.class로 지정하면 에러가 나면서 실행이 되지 않는다.

 Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(Exception.class);
 -----------
 //에러
Checked exception is invalid for this method!

 

Checked Exception 이란 위 그림에서 초록색 exception 인데, 이를 unchecked exception, 즉 RunTimeException이나 그 하위 exception 으로 바꿔주면 된다.

참고) exception 구분

728x90
반응형

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

[jmh] benchmark test  (0) 2022.08.04
[powermock] mock static method  (0) 2022.07.21
[java] jvm, java, gc  (0) 2022.02.24
[keyword] transient in serialization  (0) 2022.02.16
[junit] test runner  (0) 2022.01.03
반응형

이전 글:

2022.01.25 - [개발/spring] - [axon] query/replay 성능개선 - clone coding

 

[axon] query/replay 성능개선 - clone coding

이전 글: 2022.01.24 - [개발/spring] - [axon] query/replay - clone coding [axon] query/replay - clone coding 2022.01.19 - [개발/spring] - [axon] state stored aggregate - clone coding [axon] state sto..

bangpurin.tistory.com

클론 코딩 참고 블로그는 다음와 같다: https://cla9.tistory.com/18?category=814447 

 

14. Query 어플리케이션 구현(Event) - 4

1. 서론 Software 개발 및 유지보수 단계에서 요구사항에 의하여 데이터 모델은 변하기 마련입니다. 그리고 바뀌는 데이터모델에 맞춰 Event 또한 형태가 변합니다. 이때, 이전 발행된 Event와 앞으로

cla9.tistory.com

 

entity -> event의 항목이 바뀌어서 event 모델의 혼용이 생길 경우(ex. replay) 구 -> 신 모델을 어떻게 반영해야 할지에 대해 코드에 알려줘야 할 필요가 있다. 이번 장은 그런 것에 관한 내용이다.

event versioning 이라고 불리는 이 행위는 axon에서는 event upcasting 이라는 용어로 불린다.

 

위 블로그를 참고로 개발하던 와중 필요한 dependency 가 있었다. xml 분석을 위한 라이브러리를 추가해준다.

implementation 'org.dom4j:dom4j:2.1.3'

 

테스트: 아래와 같이 company가 있는 정보로 호출하였다.

POST http://localhost:8080/holder
{
  "holderName" : "ch13",
  "tel" : "02-1234-5678",
  "address" : "OO시 OO구 heheqq",
  "company" : "konai"
}

 

1. command application

DEBUG 8895 --- [mandProcessor-1] c.c.command.aggregate.HolderAggregate    : handling HolderCreationCommand(holderID=e5775054-4265-46ef-8116-297ac22f480d, holderName=ch13, tel=02-1234-5678, address=OO시 OO구 heheqq, company=konai)
...
Hibernate: insert into holder (address, company, holder_name, tel, holder_id) values (?, ?, ?, ?, ?)

command

DB에 잘 들어간 것 확인

2. query application

DEBUG 8893 --- [sor[accounts]-1] c.c.q.p.HolderAccountProjection          : >>> projecting HolderCreationEvent(holderID=e5775054-4265-46ef-8116-297ac22f480d, holderName=ch13, tel=02-1234-5678, address=OO시 OO구 heheqq, company=konai) , timestamp : 2022-02-03T06:52:26.203Z
...
Hibernate: insert into mv_account (account_cnt, address, name, tel, total_balance, holder_id) values (?, ?, ?, ?, ?, ?)

HolderAccountSummary 클래스에 company를 추가한 것은 아니라서 mv_account 테이블에 projection 할 때 추가로 넣는건 없다.

 

3. replay 실행

replay를 실행하는데 문제가 생겼다.. repository를 못 찾아 nullPointException이 뜬 것이다..

di not found...

@Component
@RequiredArgsConstructor
@Slf4j
@ProcessingGroup("accounts")
@EnableRetry
public class HolderAccountProjection {
    private final AccountRepository repository;

...

    @EventHandler
    @AllowReplay
    protected void on(DepositMoneyEvent event, @Timestamp Instant instant){
        log.debug(">>> projecting {} , timestamp : {}", event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() + event.getAmount());
        repository.save(holderAccount);
    }

...

    /**
     * 초기화 할 때 실행하는 부분
     */
    @ResetHandler
    private void resetHolderAccountInfo(){
        log.debug("reset triggered");
        repository.deleteAll();
    }

소스는 위와 같은데, 특이한 것은

  1. on 함수는 아무 문제 없이 실행된다(save 가능)
  2. @EnableRetry 어노테이션을 주석처리하면 reset도 에러없이 실행된다....
  3. on 함수 처럼 AllowReplay / DisallowReplay 어노테이션 등등을 넣어봤는데도 똑같다.

뭔가 ResetHandler랑 충돌난게 아닐까.. spring retry를 좀 더 파봐야 겠다..

++++ 해결... 왜인지는 아직 모르겠지만 public 함수로 바꾸니 정상적으로 된다... 공식문서 예시에는 모든 함수가 public으로 되어있긴 한데, 필수는 아닌 것 같은데.... 더 확인이 필요하다.

@ResetHandler
public void resetHolderAccountInfo(){
    log.debug("reset triggered");
    repository.deleteAll();
}

우선 enable retry 를 주석처리한 후 실행해본다. 

upcast bean(eventUpcasterChain 함수)을 등록 하기 전에 한 번, 후에 한 번 해보았다.

upcast bean 등록 전
DEBUG 9935 --- [sor[accounts]-0] c.c.q.p.HolderAccountProjection  : >>> projecting HolderCreationEvent(holderID=65cc77b7-6820-472d-ba72-dd942f801f21, holderName=melvin, tel=02-1234-5678, address=OO시 OO구 hehe, company=null) , timestamp : 2022-01-19T04:23:43.806Z
revision 1.0 version up
DEBUG 9935 --- [sor[accounts]-0] c.c.q.p.HolderAccountProjection  : >>> projecting HolderCreationEvent(holderID=4cafc63c-cdcb-4acf-8943-8dee7492383b, holderName=ch13, tel=02-1234-5678, address=OO시 OO구 heheqq, company=null) , timestamp : 2022-02-03T06:50:28.748Z
DEBUG 9935 --- [sor[accounts]-0] c.c.q.p.HolderAccountProjection  : >>> projecting HolderCreationEvent(holderID=e5775054-4265-46ef-8116-297ac22f480d, holderName=ch13, tel=02-1234-5678, address=OO시 OO구 heheqq, company=konai) , timestamp : 2022-02-03T06:52:26.203Z

upcast bean 등록 후
DEBUG 10126 --- [sor[accounts]-1] c.c.q.p.HolderAccountProjection  : >>> projecting HolderCreationEvent(holderID=65cc77b7-6820-472d-ba72-dd942f801f21, holderName=melvin, tel=02-1234-5678, address=OO시 OO구 hehe, company=N/A) , timestamp : 2022-01-19T04:23:43.806Z
revision 1.0 version up
DEBUG 10126 --- [sor[accounts]-1] c.c.q.p.HolderAccountProjection  : >>> projecting HolderCreationEvent(holderID=4cafc63c-cdcb-4acf-8943-8dee7492383b, holderName=ch13, tel=02-1234-5678, address=OO시 OO구 heheqq, company=null) , timestamp : 2022-02-03T06:50:28.748Z
DEBUG 10126 --- [sor[accounts]-1] c.c.q.p.HolderAccountProjection  : >>> projecting HolderCreationEvent(holderID=e5775054-4265-46ef-8116-297ac22f480d, holderName=ch13, tel=02-1234-5678, address=OO시 OO구 heheqq, company=konai) , timestamp : 2022-02-03T06:52:26.203Z

확인사항

  • 빈 등록 전에는 version up 전의 company 정보가 null로 나온다.
  • 빈 등록 후에는 version up 전의 company 정보가 N/A로 나온다.
  • event version up 후 실수로 company 정보를 넣지 않고 post api 를 요청했는데, 빈 등록 전/후 모두 null로 나왔다. 버전 업을 하고 나서 null로 보냈기 때문에 빈의 유무와 상관없이 null로 나오는 것이 맞다.

 


참고: event upcasting 원문, 기타 의미: https://docs.axoniq.io/reference-guide/axon-framework/events/event-versioning

 

Event Versioning - Axon Reference Guide

Axon's upcasters do not work with the EventMessage directly, but with an IntermediateEventRepresentation. The IntermediateEventRepresentation provides functionality to retrieve all necessary fields to construct an EventMessage (and thus a upcasted EventMes

docs.axoniq.io

 

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  (1) 2022.02.03
[jpa] OSIV란; spring.jpa.open-in-view  (0) 2022.01.27
[jpa] 영속성 컨텍스트 in spring  (1) 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
반응형

+ Recent posts