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

환경: spring batch 5.0.3, java 17

스프링 배치의 fault tolerant에는 크게 retry와 skip이 있다.

  • skip(): ItemReader, ItemProcessor, ItemWriter 모두에서 예외 발생 시 적용할 수 있으며, 예외를 스킵하고 다음 아이템으로 넘어감
  • retry(): ItemProcessor와 ItemWriter에만 적용되며, 예외 발생 시 설정된 횟수만큼 재시도. ItemReader에서는 retry()를 사용할 수 없음
    • ItemReader의 read() 메서드는 한 번 호출될 때마다 단일 항목을 반환함. 만약 재시도하면 여러 번 호출해야 하며, 이는 ItemReader의 기본 설계 원칙에 맞지 않고 재시도는 데이터 읽기와는 별개의 책임이므로, ItemReader 내부에서 이를 처리하는 것은 설계 원칙에 어긋남

이번 글에서는 writer의 retry에 대해 집중해 본다.


에러로 인한 재시도를 하고 싶을 경우. faultTolerant()와 함께. retry()와. retryLimit() 설정을 사용하면 Spring Batch에서 Step 또는 Chunk 단위로 처리 중 발생하는 예외에 대해 재시도 처리를 할 수 있다. 

private Step getCommonStep(
    JobRepository jobRepository,
    PlatformTransactionManager transactionManager,
    String stepName,
    MyBatisCursorItemReader<GmahjongRankingRat> itemReader,
    ItemWriter<GmahjongRankingRat> itemWriter) {
  return new StepBuilder(stepName, jobRepository)
      .listener(deleteAllBeforeStep)
      .<GmahjongRankingRat, GmahjongRankingRat>chunk(CHUNK_SIZE, transactionManager)
      .reader(itemReader)
      .writer(itemWriter) ///-> 에러 발생
      .faultTolerant()
      .retry(RuntimeException.class)
      .retryLimit(2)   // -> 재시도 두번
      .listener(retryListener())
      .build();
}

 

1. .faultTolerant()

  • Fault Tolerant Step을 설정
  • Step이나 Chunk 처리 중 예외가 발생했을 때, 해당 예외를 허용하거나 재시도할 수 있도록 구성
  • retry, skip, noRollback, noRetry 등의 다양한 옵션을 적용할 수 있는 시작점

2. .retry(Class<? extends Throwable> exceptionClass)

  • 재시도 대상 예외 타입을 지정
  • 예외 타입은 특정 예외 클래스(예: RuntimeException.class) 또는 그 하위 클래스

예를 들어, .retry(RuntimeException.class)를 설정하면 RuntimeException과 그 하위 클래스에서 예외가 발생할 때마다 재시도가 이루어짐

3. .retryLimit(int retryLimit)

  • 최대 재시도 횟수를 설정
  • retryLimit 값은 예외가 발생했을 때 최대 재시도 가능 횟수를 의미(총 횟수가 아닌 재시도 횟수)
    • 예를 들어, retryLimit(2)로 설정하면 최대 2번 재시도함
  • retryLimit에 설정된 횟수는 최초 시도에는 영향을 주지 않으며, 최초 시도 후 추가로 재시도할 수 있는 횟수를 의미

4. .faultTolerant().retry(RuntimeException.class).retryLimit(2)의 의미

  • faultTolerant():
    • 예외가 발생했을 때, 예외를 처리하거나 재시도할 수 있는 내구성 있는 단계로 설정
  • 재시도 대상 예외 설정 (retry(RuntimeException.class)):
    • RuntimeException과 그 하위 클래스에서 예외가 발생했을 때 재시도하도록 설정
  • 최대 재시도 횟수 설정 (retryLimit(2)):
    • 최초 시도 외에 최대 2번의 재시도를 허용
    • 즉, 총 3번의 시도(초기 시도 1번 + 재시도 2번) <<<<<< 3일간 나를 고민에 빠트린 오늘의 주제...

재시도 테스트를 위해 3번 실행까지 에러를 발생시킨다.

  @Bean
  @StepScope
  public ItemWriter<GmahjongRankingRat> insertTotalRank(
      @Qualifier(DataSourceConfig.SESSION_FACTORY) SqlSessionFactory casualDb) {
    System.out.println(">>>>>>>>> insertTotalRank");
    return new ItemWriter<RankingRat>() {
      private final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(casualDb);
      private int attempt = 0;

      @Override
      public void write(Chunk<? extends RankingRat> items) {
        attempt++;
        System.out.println("Attempt " + attempt + ": Writing items " + items);
        if (attempt < 3) { // 2번 이하로는 예외 발생
          throw new RuntimeException("Intentional error on attempt " + attempt);
        }
        for (RankingRat item : items) {
          sqlSessionTemplate.insert(
              Constant.GAME_MAPPER + "insertTotalRank", item);
        }
      }
    };
  }
  •  

retry 할 때 로그를 보기 위해 retryListener도 만들어서 달아준다.

  @Bean
  public RetryListener retryListener() {
    return new RetryListener() {
      @Override
      public <T, E extends Throwable> void onError(
          RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        // 재시도 중 발생한 예외를 로깅
        System.err.println(
            "Retry attempt "
                + context.getRetryCount()
                + " failed with exception: "
                + throwable.getMessage());
      }
    };
  }

 

예상 시나리오

최초 시도 -> 에러1 -> 재시도1 -> 에러2 -> 재시도2 -> 에러3 -> 재시도 횟수 2번이 지나서 종료처리

그래서 에러가 3번까지 발생하고, 재시도 로그 2번 남을 것이라 기대

10:54:24.210 [main] DEBUG o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write:   ////최초 시도
10:54:24.221 [main] DEBUG o.s.batch.core.scope.StepScope - Creating object in scope=step, name=scopedTarget.insertTotalRank
>>>>>>>>> insertTotalRank
Attempt 1: Writing items... //최초 적재
...
10:54:24.247 [main] DEBUG o.s.b.core.step.tasklet.TaskletStep - Rollback for RuntimeException: java.lang.RuntimeException: Intentional error on attempt 1
10:54:24.247 [main] DEBUG o.s.t.support.TransactionTemplate - Initiating transaction rollback on application exception
java.lang.RuntimeException: Intentional error on attempt 1
...
10:54:24.250 [main] DEBUG c.a.i.imp.CompositeTransactionImp - rollback() done of transaction 10.78.130.172.tm172740206400000010
...
10:54:24.250 [main] DEBUG o.s.b.c.s.i.SimpleRetryExceptionHandler - Handled non-fatal exception
java.lang.RuntimeException: Intentional error on attempt 1
...
10:54:24.251 [main] DEBUG o.s.b.repeat.support.RepeatTemplate - Repeat operation about to start at count=2
...
Retry attempt 1 failed with exception: Intentional error on attempt 1


10:54:24.265 [main] DEBUG o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write: ////재시도 1
...
Attempt 2: Writing items... //재시도1 적재
10:54:24.282 [main] DEBUG o.s.b.core.step.tasklet.TaskletStep - Rollback for RuntimeException: java.lang.RuntimeException: Intentional error on attempt 2
10:54:24.282 [main] DEBUG o.s.t.support.TransactionTemplate - Initiating transaction rollback on application exception
java.lang.RuntimeException: Intentional error on attempt 2
...
10:54:24.282 [main] DEBUG c.a.i.imp.CompositeTransactionImp - rollback() done of transaction 10.78.130.172.tm172740206425100011
...
10:54:24.283 [main] DEBUG o.s.b.c.s.i.SimpleRetryExceptionHandler - Handled non-fatal exception
java.lang.RuntimeException: Intentional error on attempt 2
...
10:54:24.283 [main] DEBUG o.s.b.repeat.support.RepeatTemplate - Repeat operation about to start at count=3
10:54:24.288 [main] DEBUG o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write: ////재시도 2
...
Retry attempt 2 failed with exception: Intentional error on attempt 2


...
10:54:24.295 [main] DEBUG o.s.b.core.step.tasklet.TaskletStep - Rollback for RuntimeException: org.springframework.retry.ExhaustedRetryException: Retry exhausted after last attempt in recovery path, but exception is not skippable.
10:54:24.295 [main] DEBUG o.s.t.support.TransactionTemplate - Initiating transaction rollback on application exception
org.springframework.retry.ExhaustedRetryException: Retry exhausted after last attempt in recovery path, but exception is not skippable.
...
10:54:24.295 [main] DEBUG c.a.i.imp.CompositeTransactionImp - rollback() done of transaction 10.78.130.172.tm172740206428300012
...
10:54:24.298 [main] DEBUG o.s.b.repeat.support.RepeatTemplate - Handling fatal exception explicitly (rethrowing first of 1): org.springframework.retry.ExhaustedRetryException: Retry exhausted after last attempt in recovery path, but exception is not skippable.
10:54:24.299 [main] ERROR o.s.batch.core.step.AbstractStep - Encountered an error executing step gmahjongTotalRankingStep in job gmahjongDailyRankingJob

writer.write() 함수 두 번 호출됨. 3번째 시도를.. 하는 것 같긴 한데.. write 호출은 안 하고.. 횟수 초과로 전체 롤백을 하는 듯한 로그만 남음..

정확하게 세 번 시도를 하는 건지는 모르겠으나..(10:54:24.288과 10:54:24.295 사이에 실행 로그가 있어야 하지 않나)

"Rollback for RuntimeExceptioin", "rollback() done"의 로그가 세 번씩 남으므로 프로그램의 입장에선 에러를 세 번 감지한 것 같긴 하다..

 

참고로 rollback을 한다고 했지만 해당 chunk대한 부분만 rollback이라, beforeStep에서 한 deleteAll은 이미 적용되어 있고(테이블이 비어 있고) writer의 첫 chunk부터 에러 발생이라 결국 빈 테이블이 유지된다.

 

이 상태에서(retryLimit = 2; writer에서 첫 2번만 에러 발생)

retryLimit만 2 -> 3으로 올리면, 에러는 그대로 두 번, 총 시도는 최초 + 재시도 3번 = 4번이라 마지막에 성공해야 한다.

실행해보면, 위와 같은 에러가 두 번나고 마지막 쪽 로그가 아래처럼 바뀌면서 insert가 된다..

14:38:12 DEBUG [main] o.s.b.repeat.support.RepeatTemplate - Repeat operation about to start at count=3
14:38:12 DEBUG [main] o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write: ...
14:38:12 DEBUG [main] org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
...// insert logs

뜻대로 되긴 하는데.. 아직도 왜 마지막 시도에 대한 로그가 제대로 안 찍혔는지 모르겠다.. write을 세 번 부르는 게 아닌가?

 

테스트 코드로 확인

@Test
  void testRetryLimit() throws Exception {
    // 첫 번째, 두 번째, 세 번째 호출에서 예외 발생
    JobParameters jobParameters = getJobParameters();
    given(totalRankingReader.read()).willReturn(userRats().get(0), userRats().get(1), null);
    doNothing().doNothing().doNothing().when(deleteAllBeforeStep).beforeStep(any());
    doAnswer(
            invocation -> {
              System.out.println("First write attempt");
              throw new RuntimeException("error! 1");
            })
        .doAnswer(
            invocation -> {
              System.out.println("Second write attempt");
              throw new RuntimeException("error! 2");
            })
        .doAnswer(
            invocation -> {
              System.out.println("Third write attempt");
              throw new RuntimeException("error! 3");
            })
        .when(insertTotalRank)
        .write(any(Chunk.class));

    // Step 실행
    JobExecution jobExecution =
        jobTestUtils
            .getJobTester(GmahjongRankingJobConfig.JOB_NAME)
            .launchJob(jobTestUtils.makeJobParameters(jobParameters));

    verify(totalRankingReader, times(3)).read();
    // write 메서드가 총 3번 호출되었는지 확인
    verify(insertTotalRank, times(3)).write(any(Chunk.class));

    // Job이 실패했는지 확인 (최대 재시도 후 실패)
    assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.FAILED);
  }

retryLimit = 2 일 때 writer를 3번 부르는지 테스트 코드이다. 시도한 로그를 남기기 위해 doThrow가 아닌 doAnswer을 사용하였다.

위 테스트는 실패한다. 아래 부분에서 실제로 2번 호출했다고 검증된다.

verify(insertTotalRank, times(3)).write(any(Chunk.class));

관련 로그.. 본 로그와 크게 다르지 않다 세 번째 writer를 호출하는지 잘 모르겠는.. 로그다.

14:58:57.449 [Test worker] DEBUG o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write:
14:58:57.449 [Test worker] DEBUG o.s.retry.support.RetryTemplate - Retry: count=0
First write attempt
Retry attempt 1 failed with exception: error! 1
14:58:57.451 [Test worker] DEBUG o.s.retry.support.RetryTemplate - Checking for rethrow: count=1

14:58:57.459 [Test worker] DEBUG o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write:
14:58:57.460 [Test worker] DEBUG o.s.retry.support.RetryTemplate - Retry: count=1
Second write attempt
Retry attempt 2 failed with exception: error! 2
14:58:57.460 [Test worker] DEBUG o.s.retry.support.RetryTemplate - Checking for rethrow: count=2

14:58:57.466 [Test worker] DEBUG o.s.b.c.s.i.FaultTolerantChunkProcessor - Attempting to write:
14:58:57.466 [Test worker] DEBUG o.s.b.core.step.tasklet.TaskletStep - Rollback for RuntimeException: org.springframework.retry.ExhaustedRetryException: Retry exhausted after last attempt in recovery path, but exception is not skippable.
14:58:57.466 [Test worker] DEBUG o.s.t.support.TransactionTemplate - Initiating transaction rollback on application exception

 

파고들기..(w GPT)

1. retryLimit의 동작 원리

  • retryLimit은 최대 재시도 가능 횟수를 의미합니다. 이는 "초기 시도 횟수를 제외한" 재시도 횟수입니다.
  • 예를 들어, retryLimit = 2로 설정한 경우, 최초 시도 1번 + 재시도 2번 = 총 3번의 시도가 이루어집니다.
    • 시도 1: 최초 시도
    • 재시도 1: retryLimit에 의해 허용된 첫 번째 재시도
    • 재시도 2: retryLimit에 의해 허용된 마지막 재시도

2. 로그 분석

2.1. 첫 번째 시도 (count=0)

  • First write attempt 로그가 나타난 후 RuntimeException이 발생하여 재시도가 필요합니다.
  • retry()가 적용되어 재시도 1이 시작됩니다.

2.2. 첫 번째 재시도 (count=1)

  • Second write attempt 로그가 나타난 후 두 번째 예외가 발생합니다.
  • retry()에 의해 마지막 재시도가 필요합니다.

2.3. 두 번째 재시도 (count=2)

  • 이 시도에서도 실패하여 RetryTemplate의 ExhaustedRetryException이 발생합니다.
  • 세 번째 재시도에서 예외 발생 후, 더 이상 재시도가 이루어지지 않습니다.

3. 실제 호출과 예외 발생 횟수 차이

위 로그에서 write() 메서드가 총 3번 호출된 것을 확인할 수 있습니다. 하지만 예외가 명시적으로 두 번만 발생하는 이유는 다음과 같습니다:

  1. 첫 번째 시도와 첫 번째 재시도: 첫 번째 시도와 첫 번째 재시도에서 예외가 발생하여 retry()가 작동했습니다. 이때 retryLimit에 따라 재시도가 시도됩니다.
  2. 두 번째 재시도: 두 번째 재시도에서 예외가 발생하면 retryLimit을 모두 소진하게 됩니다. 이 시점에서 ExhaustedRetryException이 발생합니다.
  3. 재시도 실패 후 처리: ExhaustedRetryException이 발생하면 더 이상 재시도가 이루어지지 않으며, 마지막 재시도 실패 후에는 예외가 발생한 상태로 처리됩니다.

4. ExhaustedRetryException의 의미

  • ExhaustedRetryException은 설정된 재시도 한도(retryLimit)를 모두 소진한 후에도 예외가 해결되지 않았다는 의미입니다. 이 예외는 마지막 시도에서 예외가 발생했다는 것을 의미하며, 재시도가 더 이상 이루어지지 않음을 나타냅니다.

 

: 말이 애매하다. 총 3번의 시도를 하지만 writer를 부르는 시도는 아니고,, writer를 2번 부르고 재시도 횟수가 고갈되면 마지막 시도를 하기 전에 예외가 발생한다고 이해해야 할 것 같다.

 

참고로 첫 번째, 두 번째는 실패 세 번째에서 성공시키는 테스트 코드를 짜도 writer는 2번 불리고 전체 배치는 실패처리 된다.

최초 시도 -> 에러1 -> 재시도1 -> 에러2 -> 재시도2 -> 성공 -> ..전체 성공?이라고 생각하기 쉬운데..

이미 에러2에서 재시도 횟수(2)가 소비되어 에러가 발생하는 플로우다..

 @Test
  void testRetryLimit() throws Exception {
    JobParameters jobParameters = getJobParameters();
    given(totalRankingReader.read()).willReturn(userRats().get(0), userRats().get(1), null);
    doNothing().doNothing().doNothing().when(deleteAllBeforeStep).beforeStep(any());
    // 첫 번째, 두 번째 호출에서 예외 발생
	doAnswer(
            invocation -> {
              System.out.println("First write attempt");
              throw new RuntimeException("error! 1");
            })
        .doAnswer(
            invocation -> {
              System.out.println("Second write attempt");
              throw new RuntimeException("error! 2");
            })
        .doNothing()
        .when(insertTotalRank)
        .write(any(Chunk.class));

    // Step 실행
    JobExecution jobExecution =
        jobTestUtils
            .getJobTester(GmahjongRankingJobConfig.JOB_NAME)
            .launchJob(jobTestUtils.makeJobParameters(jobParameters));

    verify(totalRankingReader, times(3)).read();
    // write 메서드는 2번 호출
    verify(insertTotalRank, times(2)).write(any(Chunk.class));

    // Job이 실패했는지 확인 (최대 재시도 후 실패)
    assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.FAILED);

흐름 설명:

  1. 첫 번째 시도 (Write attempt 1):
    • write() 호출 → RuntimeException 발생 → 재시도 1로 진입.
  2. 첫 번째 재시도 (Write attempt 2):
    • write() 호출 → 다시 RuntimeException 발생 → 재시도 2로 진입.
  3. 두 번째 재시도 (Write attempt 3):
    • write() 호출 → 성공적으로 처리됨.
  4. 결과 처리:
    • 비록 세 번째 시도에서 성공했더라도, 재시도 한도인 retryLimit 2번을 모두 소진했으므로, 전체 Step이 실패로 처리됩니다.
    • ExhaustedRetryException이 발생하여, 더 이상의 재시도 없이 실패로 간주됩니다.

재시도 한도 소진과 처리 방식

  1. 재시도 한도 소진:
    • retryLimit에 따라 재시도 횟수가 소진되면 RetryTemplate은 더 이상 ItemWriter를 호출하지 않습니다.
    • 재시도 중 실패한 예외를 처리할 수 없는 상태가 되면 ExhaustedRetryException을 발생시킵니다.
  2. 최종 예외 처리:
    • 최종적으로 발생한 예외가 스킵 가능하지 않거나, 재시도 후에도 처리되지 않은 경우 전체 Step이 실패로 종료됩니다.
  3. 마지막 시도가 성공하더라도:
    • 마지막 재시도에서 성공하더라도 재시도 한도가 모두 소진된 상태에서는 더 이상 재시도 없이 Step이 실패로 처리됩니다.

이.. 말싸움 때문에 3일 정도 매진한 것 같다..ㅠㅠ

결국, 재시도 횟수 차감되는 시점이 중요.....!

 

728x90
반응형
반응형

테스트 코드 작성 시 mocking을 위한 객체를 보통은 테스트 코드 아래에 private로 선언해서 사용한다.

이게 한두 개면 그냥 쓰는데 케이스가 다양하거나 테스트 양이 많아 테스트 객체를 생성하는 것만으로도 몇백 줄이 되게 되면 점점 테스트코드가 뭐고 객체가 뭔지 가독성을 잃게 된다.

그래서 보통 객체 생성부분을 다 발라서 별도 클래스를 두고 extend 해서 쓰곤 했었다.

class TournamentServiceTest extends TournamentServiceTestArguments {
..


//
public class TournamentServiceTestArguments {
  protected static TournamentRecordRequest getExistSearchValueTournamentRecordRequest(TournamentType tournamentType) {
    return TournamentRecordRequest.builder()
        .searchType(tournamentType)
        .searchValue(TEST_TOURNAMENT_NAME)
        .startDateTime(TEST_START_DATETIME)
        .endDateTime(TEST_END_DATETIME)
        .build();
  }
  ...
  }

오늘 타 회사 기술블로그 글을 읽다가 이걸 명칭한다는게 있다는걸 알고.. 기록해본다.

나만 고민한게 아니구나..ㅋㅋ

 

Object Mother

"Object Mother"는 소프트웨어 개발, 특히 테스트 코드 작성 시 자주 사용되는 디자인 패턴 중 하나. 이 패턴은 테스트에서 사용할 복잡한 객체 인스턴스를 생성하는 방법을 제공한다. 객체를 생성하는 로직을 별도의 클래스나 메서드로 분리하여 테스트 코드의 중복을 줄이고, 객체 생성에 관련된 코드의 가독성과 유지보수성을 높이는 것이 목적이다.

주요 특징:

  1. 객체 생성의 분리:
    • 테스트 코드에서 직접 객체를 생성하지 않고, Object Mother 클래스를 통해 필요한 객체를 생성한다.
    • 테스트 코드와 객체 생성 로직을 분리하여 테스트의 가독성을 높이고, 테스트에서 필요로 하는 객체를 일관되게 생성할 수 있다.
  2. 복잡한 객체 생성:
    • 기본적으로 생성자가 복잡하거나 여러 설정이 필요한 객체를 손쉽게 생성할 수 있도록 한다.
    • 객체의 기본 설정이나 상태를 지정하여, 테스트에서 필요한 특정 상태를 가진 객체를 제공할 수 있다.
  3. 테스트의 유지보수성 증가:
    • 객체 생성 로직이 한 곳에 모여 있어, 객체 생성 로직이 변경되더라도 한 곳에서만 수정을 하면 된다.
    • 테스트 코드에서 중복된 객체 생성 로직을 제거하여, 테스트 코드의 간결함과 유지보수성을 높다.
// User 클래스 예제
public class User {
    private String name;
    private int age;
    private String email;

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    // getter, setter 생략
}

// Object Mother 클래스
public class UserMother {
    public static User createDefaultUser() {
        return new User("John Doe", 30, "john.doe@example.com");
    }

    public static User createUserWithAge(int age) {
        return new User("John Doe", age, "john.doe@example.com");
    }

    public static User createUserWithName(String name) {
        return new User(name, 30, "john.doe@example.com");
    }

    // 다양한 생성 메서드 추가 가능
}

// 테스트 코드
public class UserTest {
    @Test
    public void testDefaultUser() {
        User user = UserMother.createDefaultUser();
        assertEquals("John Doe", user.getName());
        assertEquals(30, user.getAge());
    }

    @Test
    public void testUserWithSpecificAge() {
        User user = UserMother.createUserWithAge(25);
        assertEquals(25, user.getAge());
    }
}

장점:

  • 객체 생성 코드의 중복 제거.
  • 테스트 코드의 가독성 및 유지보수성 향상.
  • 다양한 상태를 가지는 테스트 객체를 쉽게 생성 가능.

단점:

  • Object Mother 클래스가 복잡해질 수 있음.
  • 여러 테스트에서 사용될 경우, 특정 테스트에 의존적인 설정이 들어갈 수 있음.

이 패턴은 주로 테스트 코드에서 객체 생성이 자주 필요하거나 복잡할 때 사용되며, 객체의 일관된 상태를 보장하고 중복 코드를 줄이는 데 도움을 준다.

++ 랜덤한 값을 생성하기 위한 라이브러리도 있다.

Naver에서 관리하고 있는 FixtureMonkey도 있지만 쓰기 편하고 효율적인 프로젝트로 알려진 EasyRandom도 있다.
EasyRandom은 github star 수도 가장 많았고, 이름 값 하는 프로젝트다.

EasyRandom은 굉장히 강력한데 다음과 같은 특징들이 있다.

  1. setter가 없어도 된다.
  2. contructor가 없어도 된다. (private contructor only인 경우)
  3. 자동으로 sub class들의 값도 random하게 채워준다.
  4. test object list 생성이 간단하다.

setter와 constructor가 없어도 된다는 점이 굉장히 좋아보인다! 조만간 도입 예정

728x90
반응형
반응형

OpenFeign은 Spring Cloud에서 제공하는 HTTP 클라이언트로, RESTful 서비스 간의 통신을 간편하게 처리할 수 있게 해준다.

1. OpenFeign의 기본 에러 처리 방식

OpenFeign은 기본적으로 400번대, 500번대의 HTTP 상태 코드를 에러로 간주하며, feign.FeignException을 발생시킨다.

  • 4xx (클라이언트 에러): 잘못된 요청, 인증 실패 등.
  • 5xx (서버 에러): 서버 내부 오류, 서비스 불가 등.

2. 에러 처리(httpCode != 200)의 경우

2-1. Feign Client에 ErrorDecoder 설정

ErrorDecoder의 역할

  • ErrorDecoder는 HTTP 상태 코드가 200번대가 아닐 때 호출됨
  • ErrorDecoder는 주로 4xx(클라이언트 에러)나 5xx(서버 에러)에 대한 예외 처리를 담당
  • OpenFeign은 기본적으로 ErrorDecoder.Default를 사용하여 FeignException을 던지며, 이를 커스터마이징하여 예외를 처리할 수 있음.

클래스 레벨 @FeignClient -> configuration에 커스텀 에러 디코더 달아줄 수 있음

import org.springframework.cloud.openfeign.FeignClient;

@FeignClient(name = "example-client", url = "http://example.com", configuration = FeignConfig.class)
public interface ExampleClient {

    @GetMapping("/resource")
    String getResource();
}
///////////

import org.springframework.context.annotation.Bean;

public class FeignConfig {
    @Bean
    public ErrorDecoder errorDecoder() { //////FeignException이 발생한 경우
        return new CustomErrorDecoder();
    }
}
////////////

import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class CustomErrorDecoder implements ErrorDecoder {

	//에러 처리
    @Override
    public Exception decode(String methodKey, Response response) {
        String message = null;
        try {
            if (response.body() != null) {
                message = StreamUtils.copyToString(response.body().asInputStream(), StandardCharsets.UTF_8);
            }
        } catch (IOException e) {
            return new Exception("Failed to process response body", e);
        }

        HttpStatus status = HttpStatus.resolve(response.status());
        switch (status) {
            case BAD_REQUEST:
                return new BadRequestException("Bad Request: " + message);
            case NOT_FOUND:
                return new NotFoundException("Resource Not Found: " + message);
            case INTERNAL_SERVER_ERROR:
                return new InternalServerErrorException("Internal Server Error: " + message);
            default:
                return new Exception("Generic error: " + status + " " + message);
        }
    }
}

위 예제는 http status가 200이 아닐 경우이다. 200이 아닌 400, 500일 경우 Exception으로 처리되고 자동으로 저기에 걸린다.

3. 성공 처리(httpCode == 200)의 경우 

3-1. Decoder 사용해서 처리

Decoder는 OpenFeign에서 HTTP 응답을 처리할 때 사용되는 인터페이스로, 주로 성공적인 HTTP 응답(200번대)을 파싱하여 객체로 변환한다. 기본적으로 Decoder는 예외 처리와는 관계없이 모든 응답을 처리할 수 있다. 그러나 응답이 성공적이지 않은 경우, 즉 HTTP 상태 코드가 200번대가 아닐 경우에는 OpenFeign이 기본적으로 ErrorDecoder를 사용하여 예외를 던지기 때문에 Decoder가 호출되지 않는다.

만약 200인데 body에 있는 값으로 에러를 처리해야한다면 Decoder를 사용할 수 있다.

import feign.Response;
import feign.Util;
import feign.codec.Decoder;

import java.io.IOException;
import java.lang.reflect.Type;

public class CustomDecoder implements Decoder {
    
    private final Decoder defaultDecoder;
    
    public CustomDecoder(Decoder defaultDecoder) {
        this.defaultDecoder = defaultDecoder;
    }
    
    @Override
    public Object decode(Response response, Type type) throws IOException {
        // 상태 코드가 200번대일 때만 호출됨
        if (response.status() == 200) {
            // 응답 바디를 읽고 필요한 처리를 수행
            String body = Util.toString(response.body().asReader());
            System.out.println("Response Body: " + body);
            
            // 기본 디코더를 사용하여 바디를 객체로 변환
            return defaultDecoder.decode(response, type);
        }
        
        // 상태 코드가 200번대가 아닌 경우 예외를 던지거나 기본 처리를 수행
        throw new RuntimeException("Unexpected status code: " + response.status());
    }
}

Decoder와 ErrorDecoder의 비교

  • Decoder: 성공적인 HTTP 응답(200번대)에 대한 처리. 응답을 객체로 변환(파싱).
    • 상태 코드가 200이더라도 응답 바디에 따라 예외 처리가 필요하다면, Decoder에서 바디를 확인하고 사용자 정의 예외를 던질 수 있다.
  • ErrorDecoder: 4xx, 5xx 상태 코드에 대한 예외 처리. 예외를 던짐.

 

3-2. feign 의 interceptor사용

@FeignClient -> configuration에 feign의 interceptor를 사용할 수 있다.

이 때 interceptor는 모든 처리를 잡기 때문에 조건을 잘 줘야한다.

import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

    @Bean
    public ResponseInterceptor responseInterceptor() {
        return new ResponseInterceptor();
    }
}
///////////////

import feign.Request;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ResponseInterceptor implements feign.ResponseInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(ResponseInterceptor.class);

    @Override
    public void apply(Response response) {
        // HTTP 상태 코드가 200인 경우만 처리
        if (response.status() == 200 && response.body() != null) {
            try {
                // 응답 바디를 읽어온다.
                String body = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
                logger.info("Response Body: {}", body);

                // 응답 바디의 특정 조건을 확인
                if (body.contains("error") || body.contains("INVALID")) {
                    // 응답 바디에 에러 메시지가 포함된 경우 예외를 발생시킴
                    throw new CustomException("Error found in response body");
                }
            } catch (IOException e) {
                logger.error("Failed to read response body", e);
                throw new RuntimeException("Failed to read response body", e);
            }
        }
    }
}

 

3-3. AOP로 처리

@FeignClient(name = "gameserver-client", url = "${external.gameserver.url}")
@ExternalGameServerAdminHeaderValid
public interface ExternalGameServerFeignService {
@Slf4j
@Component
@Aspect
public class ExternalGameServerAdminHeaderValidAspect {

  @Around(
      "@within(com.annotation.ExternalGameServerAdminHeaderValid) || @annotation(com.annotation.ExternalGameServerAdminHeaderValid)")
  public Object validationAdminHeader(ProceedingJoinPoint joinPoint) throws Throwable {
    Object response = joinPoint.proceed(); //메소드 실행

    if (response instanceof ExternalAdminResponseHeader) {
      ExternalAdminResponseHeader header = (ExternalAdminResponseHeader) response;
      String errorMessage = header.getHeader().getErrMsg();
      if (StringUtils.isNotEmpty(errorMessage)) {
        MethodSignature method = (MethodSignature) joinPoint.getSignature();
        String methodName = method.getMethod().getName();
        log.warn("invalid admin header {}, {}", methodName, errorMessage);
        throw new AException(AExceptionCode.ADMIN_SUPPORT_API_ERROR, methodName, errorMessage);
      }
    } else {
      log.warn("response is not ExternalAdminResponseHeader");
    }

    return response;
  }
}

 

클래스/메소드에 사용된 어노테이션 함수 전/후로 실행되도록 짜져 있으나 실질적으로는 후에만 실행하면 되니까 아래처럼 수정해도 될 것 같다.

@AfterReturning(
    pointcut = "@within(com.annotation.ExternalGameServerAdminHeaderValid) || @annotation(com.annotation.ExternalGameServerAdminHeaderValid)",
    returning = "response")
public void validationAdminHeader(JoinPoint joinPoint, Object response) {
    // 메서드가 정상적으로 실행된 후 반환된 response 객체를 이용해 로직 수행
    if (response instanceof ExternalAdminResponseHeader) {
        ExternalAdminResponseHeader header = (ExternalAdminResponseHeader) response;
        String errorMessage = header.getHeader().getErrMsg();
        if (StringUtils.isNotEmpty(errorMessage)) {
            MethodSignature method = (MethodSignature) joinPoint.getSignature();
            String methodName = method.getMethod().getName();
            log.warn("Invalid admin header in method: {}, error message: {}", methodName, errorMessage);
        }
    } else {
        log.warn("Response is not of type ExternalAdminResponseHeader");
    }
}
  • pointcut: 타겟 메서드를 지정하는 포인트컷 표현식을 정의. @within 및 @annotation을 사용하여 특정 어노테이션이 적용된 클래스나 메서드에 대해 어드바이스를 적용
  • returning = "response": 반환 값을 response라는 매개변수로 받아옴. 반환 값이 없거나, void인 경우에는 @AfterReturning 어드바이스가 실행되지 않음

 

1. 포인트컷 표현식의 의미

1.1. @within(com.annotation.ExternalGameServerAdminHeaderValid)

  • 의미: 클래스 레벨에 @ExternalGameServerAdminHeaderValid 어노테이션이 적용된 모든 클래스의 모든 메서드를 포인트컷으로 지정
@ExternalGameServerAdminHeaderValid
public class SomeController {
    public void someMethod() {
        // 이 메서드는 @within 포인트컷에 의해 AOP 적용 대상이 됨
    }

    public void anotherMethod() {
        // 이 메서드도 @within 포인트컷에 의해 AOP 적용 대상이 됨
    }
}

1.2. @annotation(com.annotation.ExternalGameServerAdminHeaderValid)

  • 의미: 메서드 레벨에 @ExternalGameServerAdminHeaderValid 어노테이션이 적용된 특정 메서드를 포인트컷으로 지정
public class AnotherController {
    
    @ExternalGameServerAdminHeaderValid
    public void specificMethod() {
        // 이 메서드는 @annotation 포인트컷에 의해 AOP 적용 대상이 됨
    }

    public void otherMethod() {
        // 이 메서드는 @annotation 포인트컷에 의해 AOP 적용 대상이 아님
    }
}
 

@Around와 @After, @AfterReturing, @AfterThrowing의 차이

@Around 어노테이션

  • @Around 어노테이션은 메서드 호출 전후에 특정 로직을 실행할 수 있음
  • ProceedingJoinPoint를 통해 메서드 호출 전후에 실행되는 로직을 정의할 수 있으며, joinPoint.proceed()를 호출함으로써 실제 메서드를 실행
    • joinPoint.proceed()를 호출하지 않으면 실제 메서드가 실행되지 않음
  • 메서드의 실행을 제어하거나, 실행 결과를 변환하거나, 예외 처리를 커스터마이징할 때 유용함
    • 메서드 실행 여부를 조건에 따라 결정 가능
@Around("@annotation(com.example.MyAnnotation)")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // 메서드 실행 전 로직
    log.info("Before method execution");

    Object result = joinPoint.proceed(); // 실제 메서드 호출

    // 메서드 실행 후 로직
    log.info("After method execution");

    return result; // 메서드의 결과 반환
}

@After 어노테이션 (= finally)

  • @After는 메서드가 정상적으로 실행되었거나 예외가 발생하더라도 항상 실행됨. 메서드의 반환 값에 접근할 수 없고, 메서드가 실행되었음을 확인하거나 리소스 정리 등의 작업을 할 때 사용.
  • @After 어노테이션은 메서드의 실행이 완료된 후에 AOP 로직을 실행함
  • joinPoint.proceed()와 같은 메서드 호출을 제어할 수 없음!!
  • 메서드의 반환 값이나 예외 처리에는 관여하지 않음. 메서드가 종료된 후 추가적인 작업(예: 리소스 정리, 로그 기록)을 수행하는 데 사용됨
    • 현재 상황에 부적합.. 무조건 실행되나 return되는 객체를 잡을 수 없다.

@AfterReturning 어노테이션

  • 메서드가 성공적으로 완료된 후에만 실행. 예외가 발생하지 않은 경우에만 호출!
  • @AfterReturning은 메서드가 성공적으로 반환된 후에만 실행됨. 반환된 값에 접근할 수 있으며, 이를 기반으로 후처리 로직을 작성할 수 있다.
  • 타겟 메서드의 반환 값에 접근할 수 있음

@AfterThrowing 어노테이션

  • @AfterThrowing: 메서드에서 예외가 발생했을 때만 실
  • 타겟 메서드가 던진 예외에 접근할 수 있음
  • 예외 발생 시, 예외를 기반으로 추가 로직이 필요한 경우
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ComprehensiveLoggingAspect {

    @AfterReturning(pointcut = "execution(* com.example.service.MyService.*(..))", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("Method returned successfully with value: " + result);
    }

    @AfterThrowing(pointcut = "execution(* com.example.service.MyService.*(..))", throwing = "ex")
    public void logAfterThrowing(Exception ex) {
        System.out.println("Method threw an exception: " + ex.getMessage());
    }
}
  • pointcut: 어떤 메서드에 대해 어드바이스를 적용할지 지정합니다. 이 예제에서는 MyService 클래스의 모든 메서드에 적용됩니다.
  • returning: 타겟 메서드의 반환 값을 받아오기 위한 매개변수 이름을 지정합니다. 이 매개변수를 통해 타겟 메서드의 반환 값에 접근할 수 있습니다.
  • throwing: 타겟 메서드가 던진 예외를 받아오기 위한 매개변수 이름을 지정합니다. 이 매개변수를 통해 발생한 예외에 접근할 수 있습니다.

참고


위의 AOP 함수는 아래의 Decoder로 변환 가능하다.

@Slf4j
@RequiredArgsConstructor
public class FeignDecoder implements Decoder {

  private final Decoder defaultDecoder;

  @Override
  public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
    String responseBody = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
    ExternalAdminResponseHeader header = JacksonUtils.jsonToObject(responseBody, ExternalAdminResponseHeader.class);
    String errorMessage = header.getHeader().getErrMsg();
    if (StringUtils.isNotEmpty(errorMessage)) {
      log.warn("invalid admin header {}", errorMessage);
      throw new AException(AapokerExceptionCode.ADMIN_SUPPORT_API_ERROR, errorMessage);
    }
    return defaultDecoder.decode(response, type);
  }
}

다만 이 경우 기존에 에러가 발생한 함수를 알 수 없다..

feign은 성공했다고 남기에 에러로그가 없어 어떤 함수에서 호출한 응답인지 확인이 어려워 결국 AOP를 적용하였다.

728x90
반응형
반응형

return은 셸 스크립트에서 주로 함수 내에서 사용되며, 함수의 종료 상태(exit status)를 반환한다. 스크립트 자체의 종료 상태를 설정하거나 반환할 때는 exit를 사용한다. 

1. return의 기본 개념

  • return은 셸 스크립트 함수 내에서 함수의 종료 상태를 반환하는 데 사용
  • return은 함수 외부에서 사용할 수 없습니다. 함수 외부에서 return을 호출하면 구문 오류가 발생
  • return은 정수 값을 반환하며, 보통 0은 성공을, 0이 아닌 값은 오류 상태
#!/bin/bash

check_file() {
    if [[ -f "$1" ]]; then
        return 0  # 파일이 존재하면 0 반환 (성공)
    else
        return 1  # 파일이 존재하지 않으면 1 반환 (오류)
    fi
}

check_file "/etc/passwd"
if [[ $? -eq 0 ]]; then
    echo "File exists."
else
    echo "File does not exist."
fi

함수 외부에서는 exit 사용

#!/bin/bash

echo "Exiting with status 2."
exit 2  # 스크립트를 종료하고 종료 상태로 2를 반환

함수에서 반환된 값 사용

return은 숫자 값만 반환할 수 있기 때문에 문자열과 같은 데이터를 반환하려면 echo나 printf 명령을 사용해야 함.

#!/bin/bash

get_username() {
    local user_id=$1
    if [[ "$user_id" -eq 0 ]]; then
        echo "root"
        return 0
    else
        echo "non-root"
        return 0
    fi
}

username=$(get_username 0)
echo "Username: $username"  # 출력: "Username: root"

종료 상태와 관례

  • 0: 성공 (정상 종료)
  • 1-255: 오류 또는 특정 상태 코드 (예: 1은 일반적인 오류, 2는 사용법 오류 등)
  • 127: 명령어가 없을 때
  • 130: Ctrl + C에 의해 종료된 경우 (SIGINT)

return과 exit의 차이점

  • return:
    • 함수 내에서만 사용 가능.
    • 함수의 종료 상태를 반환.
    • 함수 외부에서 사용 시 오류 발생.
  • exit:
    • 스크립트 전체를 종료.
    • 스크립트의 종료 상태를 반환.
    • 함수 내에서도 사용할 수 있지만, 호출 시점에서 스크립트 전체가 종료됨.

Shell script의 return 문은 다른 프로그래밍 언어의 return 문과 다음과 같은 차이점이 있습니다:

1. 사용 대상

  • 프로그래밍 언어: 함수나 메서드에서 값을 반환하기 위해 return을 사용합니다. 예를 들어, Java, Python, JavaScript 등에서는 return을 통해 함수의 실행을 종료하고 값을 호출자에게 반환합니다.
  • Shell 스크립트: return은 주로 함수 내에서 사용되며, 반환 값은 함수의 종료 상태(exit status)를 나타내는 정수 값입니다. 이는 보통 0(성공) 또는 0이 아닌 값(실패 또는 오류)으로 표현됩니다.

2. 값의 의미

  • 프로그래밍 언어: return 뒤에 오는 값이 함수의 반환 값으로, 호출한 곳에서 이 값을 사용할 수 있습니다.
  • Shell 스크립트: return 뒤에 오는 값은 함수의 종료 상태(exit status)로 사용되며, 이 값은 보통 조건문(if, &&, || 등)에서 함수 호출의 성공 여부를 판단하는 데 사용됩니다.
 
728x90
반응형

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

[bash] 기초  (0) 2023.04.25
[shell] > 와 & 에 대한 고찰  (0) 2022.01.11
반응형

자바8

  • 람다
  • 스트림 / parallelStream() 병렬처리 가능
  • 옵셔널
  • interface의 default 함수 / static method
  • java.time.local~~

자바9

  • interface의 private 함수
  • 불변 객체 생성(컬랙션 팩토리 메서트) List.of, Map.of, Set.of

자바10

  • 로컬 변수 타입 추론(Local-variable Type Inference): var 키워드 도입.

자바11

  • 옵셔널 클래스 개선: optional.isEmpty
  • 스트링 클래스에 함수 추가: String.isBlank, strip(공백제거)
  • HTTP Client API: 비동기 HTTP 요청 지원.
  • 람다에서의 지역 변수 사용(var).
  • 여러 새로운 GC 개선 사항.

자바12

  • switch 표현식 변화 preview

자바14

  • switch 표현식 정식 도입; 값 반환 가능
      1. switch 표현식 도입: 기존의 **switch 문(statement)**와 달리 값을 반환하는 **switch 표현식(expression)**을 사용할 수 있습니다.
      2. yield 키워드 도입: switch 표현식에서 값을 반환할 때 break 대신 yield 키워드를 사용하여 값을 반환할 수 있습니다.
      3. 화살표(->) 구문: case 절에 화살표 구문을 사용해 더 간결하게 작성할 수 있습니다.
      4. 중첩 case 절: 여러 케이스를 쉼표로 구분하여 하나의 case 절로 묶을 수 있습니다.
public class Main {
    public static void main(String[] args) {
        int score = 85;

        String grade = switch (score / 10) {
            case 10, 9 -> "A";
            case 8 -> "B";
            case 7 -> "C";
            case 6 -> {
                System.out.println("Just passed!");
                yield "D";
            }
            default -> "F";
        };

        System.out.println(grade); // 출력: B
    }
}
  • NullPointerException 날 때 누가 널인지 알려줌

자바16

  • record 프리뷰
  • sealed 클래스 : Sealed Classes가 정식 기능으로 도입되어, 클래스의 상속을 제어할 수 있습니다. 특정 클래스만 서브클래스로 허용할 수 있습니다.

자바17

  • instanceof 패턴 매칭
  • record 정식(from 16) 도입
  • sealed 클래스 정식 도입(from 15): 클래스 상속을 제한하는 방법. 안전성: 상속을 제한함으로써 불필요한 클래스 확장을 방지
//before
if (obj instanceof String) {
    String str = (String) obj; // 명시적으로 캐스팅
    System.out.println(str.length());
}
//after
if (obj instanceof String s) { // s에 자동으로 캐스팅
    System.out.println(s.length());
}

sealed 클래스의 주요 특징

  1. 상속 제한: sealed 클래스를 정의하면, 그 클래스를 상속할 수 있는 하위 클래스를 명시적으로 지정해야 합니다. 이를 통해 상속 계층을 엄격하게 관리할 수 있습니다.
  2. 하위 클래스 유형:
    • permits: sealed 클래스가 어떤 하위 클래스를 상속할 수 있는지 지정합니다.
    • non-sealed: 상속 제한을 받지 않는 클래스로, 다른 클래스가 이 클래스를 자유롭게 상속할 수 있습니다.
    • final: 상속을 허용하지 않는 클래스입니다. 더 이상 상속할 수 없습니다.
public sealed class Shape permits Circle, Square, Rectangle {
    // Shape 클래스의 내용
}

public final class Circle extends Shape {
    public final int radius;
    public Circle(int radius) {
        this.radius = radius;
    }
}

public final class Square extends Shape {
    public final int side;
    public Square(int side) {
        this.side = side;
    }
}

public non-sealed class Rectangle extends Shape {
    public final int length;
    public final int width;
    public Rectangle(int length, int width) {
        this.length = length;
        this.width = width;
    }
}

public class ShapeProcessor {
    public static void processShape(Shape shape) {
        switch (shape) {
            case Circle c -> System.out.println("Circle with radius: " + c.radius);
            case Square s -> System.out.println("Square with side: " + s.side);
            case Rectangle r -> System.out.println("Rectangle with length: " + r.length + " and width: " + r.width);
        }
    }
}

 

자바21

  • virtual thread
    • Virtual Threads는 경량 스레드로, 기존의 OS 스레드에 비해 훨씬 가볍고 많은 수를 생성할 수 있습니다. 이를 통해 동시성 프로그래밍을 더 간단하고 효율적으로 구현할 수 있습니다.
      • 높은 동시성을 필요로 하는 애플리케이션(예: 서버)에서 기존의 스레드 풀 관리 복잡성을 줄이고 성능을 개선합니다.
      • 스레드가 블로킹 작업을 수행하더라도 리소스 소비를 줄일 수 있습니다.
  • Sequenced Collections (새 인터페이스)
    • 설명: SequencedCollection, SequencedSet, SequencedMap이라는 새로운 인터페이스가 추가되었습니다. 이 인터페이스들은 요소의 순서와 관련된 작업을 쉽게 할 수 있도록 도와줍니다.
    • first(), last()와 같은 메서드를 통해 첫 번째 및 마지막 요소에 쉽게 접근할 수 있습니다.
    • Reversed 메서드를 통해 컬렉션을 뒤집을 수 있습니다.
  • Pattern Matching for switch (정식 기능)
    • 설명: switch 문에서 패턴 매칭을 사용할 수 있게 되었습니다. 이는 더 간결하고 읽기 쉬운 코드 작성에 도움이 됩니다. 다양한 타입의 데이터를 안전하게 분기할 수 있습니다.
    • instanceof와 switch 문이 결합되어, 타입 검사와 변환을 쉽게 수행할 수 있습니다.
    • null 값 처리도 포함되어 있어 null 안정성을 높일 수 있습니다.
Object obj = 123;
switch (obj) {
    case Integer i -> System.out.println("Integer: " + i);
    case String s  -> System.out.println("String: " + s);
    case null     -> System.out.println("It's null!");
    default       -> System.out.println("Unknown type");
}
  • Record Patterns (정식 기능)
    • 설명: Record와 Pattern Matching을 결합하여 Record 타입의 데이터를 분해할 수 있습니다. 이를 통해 복잡한 Record 타입의 데이터를 쉽게 다룰 수 있습니다.
    • Record 타입의 인스턴스를 Pattern Matching으로 구성 요소를 추출할 수 있습니다.
    • switch, if-else와 같은 조건문에서 사용할 수 있습니다.
record Point(int x, int y) {}

Point point = new Point(3, 4);

if (point instanceof Point(int x, int y)) {
    System.out.println("x: " + x + ", y: " + y);
}
728x90
반응형

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

자바와 스프링에서 thread pool  (0) 2024.11.11
[test] object mother  (2) 2024.09.26
[java8+] 함수형 프로그래밍 @FunctionalInterface  (0) 2024.09.21
Runnable vs Callable  (0) 2024.09.20
[힙 덤프] 떠있는 프로세스 분석  (0) 2024.09.19
반응형

함수형 프로그래밍?

함수형 프로그래밍은 데이터상태 변화다는 순수 함수를 조합하여 문제를 해결하는 데 중점을 둡니다. 주로 순수 함수불변성을 기반으로 하며, 부작용(Side Effect)을 최소화하고 상태 변화 없이 데이터를 처리하는 것을 목표로 합니다.

 

@FunctionalInterface는 Java 8부터 도입된 어노테이션으로, 인터페이스가 함수형 인터페이스임을 명확하게 지정하기 위해 사용됩니다. 함수형 인터페이스는 정확히 하나의 추상 메서드를 가지는 인터페이스로, 람다 표현식이나 메서드 참조를 사용할 때 주로 사용됩니다.

@FunctionalInterface의 역할

  1. 명시적인 선언: @FunctionalInterface 어노테이션을 사용하면, 해당 인터페이스가 함수형 인터페이스로 사용되기를 의도하고 있음을 명확하게 나타낼 수 있습니다. 이는 코드의 가독성을 높이고, 개발자가 의도를 쉽게 파악할 수 있도록 돕습니다.
  2. 컴파일러 검증: 어노테이션이 붙은 인터페이스가 정확히 하나의 추상 메서드를 가지는지 컴파일러가 검사합니다. 만약 두 개 이상의 추상 메서드가 있다면 컴파일 오류가 발생합니다. 이를 통해 실수로 다른 메서드를 추가하여 함수형 인터페이스의 규칙을 깨는 것을 방지할 수 있습니다.
  3. 람다 표현식 사용: 함수형 인터페이스는 람다 표현식과 함께 사용할 수 있습니다. 이는 함수형 프로그래밍 스타일을 Java에서도 사용할 수 있게 해줍니다.

함수형 인터페이스의 조건

  • 인터페이스 내에 정확히 하나의 추상 메서드가 있어야 합니다.
  • default 메서드나 static 메서드는 여러 개 있을 수 있습니다.
  • java.lang.Object 클래스에 있는 메서드(equals, hashCode, toString 등)는 추상 메서드의 수에 포함되지 않습니다.

@FunctionalInterface 사용 시 주의사항

  • @FunctionalInterface 어노테이션을 사용하지 않아도 해당 인터페이스가 하나의 추상 메서드만 가지면 함수형 인터페이스로 간주되어 람다 표현식으로 사용할 수 있습니다. 하지만, 어노테이션을 사용하면 코드의 의도를 명확히 하고, 실수를 줄일 수 있습니다.
  • 두 개 이상의 추상 메서드를 정의하려고 하면 컴파일 오류가 발생합니다.
@FunctionalInterface
public interface MyFunctionalInterface {
    // 단 하나의 추상 메서드
    void perform();

    // default 메서드 (추상 메서드가 아니므로 함수형 인터페이스 규칙을 깨지 않음)
    default void printMessage() {
        System.out.println("Hello from default method!");
    }
    
    // static 메서드 (추상 메서드가 아니므로 함수형 인터페이스 규칙을 깨지 않음)
    static void printStaticMessage() {
        System.out.println("Hello from static method!");
    }
}

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        // 람다 표현식으로 함수형 인터페이스 구현
        MyFunctionalInterface myFunc = () -> System.out.println("Performing action");
        
        myFunc.perform(); // 출력: Performing action
        myFunc.printMessage(); // 출력: Hello from default method!
        MyFunctionalInterface.printStaticMessage(); // 출력: Hello from static method!
    }
}

표준 함수형 인터페이스

Java 8에서는 java.util.function 패키지에 여러 가지 표준 함수형 인터페이스를 제공하고 있습니다. 이들 역시 @FunctionalInterface 어노테이션을 사용하고 있습니다. 대표적인 인터페이스로는 다음과 같습니다:

  1. Function<T, R>: 입력을 받아 출력으로 매핑하는 함수.
    1. a -> b 
    2. apply 메서드 사용.
  2. Consumer<T>: 입력을 받아 소비(Consumer)하고 반환값이 없는 함수.
    1. a -> () 
    2. accept 메서드 사용.
  3. Supplier<T>: 입력 없이 값을 반환하는 함수.
    1. () -> a 
    2. get 메서드 사용.
  4. Predicate<T>: 입력을 받아 논리값(boolean)을 반환하는 함수.
    1. a -> true/false 
    2. test 메서드 사용.
  5. UnaryOperator<T>: 동일한 타입의 입력을 받아 동일한 타입의 출력을 반환하는 함수.
    1. a -> a 
    2. function의 특수 타입으로 apply 사용
  6. BinaryOperator<T>: 동일한 타입의 두 개의 입력을 받아 동일한 타입의 출력을 반환하는 함수.
    1. a, a -> a 
    2. function의 특수 타입으로 apply 사용
  7. BiConsumer
    1. a, b -> ()
    2. accept
  8. BiFunction
    1. a, b -> c
    2. apply

 

compose

Java에서 Compose는 주로 함수형 프로그래밍 패러다임에서 함수 합성을 의미합니다. 이는 두 개 이상의 함수를 결합하여 하나의 함수를 만드는 과정. 여러 단계를 차례대로 실행하는 파이프라인을 구축할 수 있다.

Java의 java.util.function.Function 인터페이스는 함수 합성을 쉽게 할 수 있도록 두 가지 메서드를 제공한다:

  • compose(f): 함수 f를 먼저 실행한 후, 그 결과를 현재 함수에 전달. 즉, 뒤의 함수가 먼저 실행.
  • andThen(f): 현재 함수를 먼저 실행한 후, 그 결과를 함수 f에 전달. 즉, 앞의 함수가 먼저 실행.

function 예시

import java.util.function.Function;

public class FunctionComposeExample {
    public static void main(String[] args) {
        // 두 개의 간단한 함수 정의
        Function<Integer, Integer> multiplyByTwo = x -> x * 2;
        Function<Integer, Integer> addThree = x -> x + 3;

        // compose(): addThree를 먼저 실행하고, 그 결과에 multiplyByTwo 적용
        Function<Integer, Integer> composedFunction = multiplyByTwo.compose(addThree);
        System.out.println(composedFunction.apply(5));  // 출력: (5 + 3) * 2 = 16

        // andThen(): multiplyByTwo를 먼저 실행하고, 그 결과에 addThree 적용
        Function<Integer, Integer> andThenFunction = multiplyByTwo.andThen(addThree);
        System.out.println(andThenFunction.apply(5));  // 출력: (5 * 2) + 3 = 13
    }
}

predicate 예시

import java.util.function.Predicate;

public class PredicateComposeExample {
    public static void main(String[] args) {
        Predicate<Integer> isEven = x -> x % 2 == 0;
        Predicate<Integer> isPositive = x -> x > 0;

        // 두 Predicate를 and()로 합성
        Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
        System.out.println(isEvenAndPositive.test(4));  // 출력: true
        System.out.println(isEvenAndPositive.test(-4)); // 출력: false

        // or()로 합성
        Predicate<Integer> isEvenOrPositive = isEven.or(isPositive);
        System.out.println(isEvenOrPositive.test(-3));  // 출력: false
        System.out.println(isEvenOrPositive.test(4));   // 출력: true

        // negate()로 부정
        Predicate<Integer> isOdd = isEven.negate();
        System.out.println(isOdd.test(3));   // 출력: true
    }
}

consumer 예시

import java.util.function.Consumer;

public class ConsumerComposeExample {
    public static void main(String[] args) {
        Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
        Consumer<String> printLowerCase = s -> System.out.println(s.toLowerCase());

        // andThen()을 사용하여 두 Consumer를 합성
        Consumer<String> combinedConsumer = printUpperCase.andThen(printLowerCase);

        combinedConsumer.accept("Compose Example");  // 출력: COMPOSE EXAMPLE, compose example
    }
}

supplier 예시

import java.util.function.Supplier;
import java.util.function.Function;

public class SupplierComposeExample {
    public static void main(String[] args) {
        Supplier<String> stringSupplier = () -> "Hello";
        Function<String, Integer> stringLength = String::length;

        // Supplier의 결과를 Function에 전달
        int length = stringLength.apply(stringSupplier.get());
        System.out.println(length);  // 출력: 5
    }
}
728x90
반응형

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

[test] object mother  (2) 2024.09.26
자바 버전별 특징  (0) 2024.09.23
Runnable vs Callable  (0) 2024.09.20
[힙 덤프] 떠있는 프로세스 분석  (0) 2024.09.19
불변객체 만들기 ImmutableCollections.class  (0) 2024.09.14
반응형

멀티 스레드 환경을 지원하기 위해 사용

1. Runnable 인터페이스

특징

  • Runnable 인터페이스는 Java 1.0부터 존재하는 기본적인 인터페이스로, 단일 메서드 run()을 제공합니다.
  • 반환값이 없으며, 예외를 던질 수 없습니다.
@FunctionalInterface
public interface Runnable {
    void run();
}
public class RunnableExample {
    public static void main(String[] args) {
        // Runnable 구현체 생성
        Runnable runnableTask = () -> {
            System.out.println("Runnable Task is running...");
        };

        // 스레드에 Runnable 전달하여 실행
        Thread thread = new Thread(runnableTask);
        thread.start();
    }
}

장점

  • 간단한 구조로, 스레드에서 수행할 작업을 정의하기 쉽습니다.
  • 예외를 명시적으로 처리할 필요 없이 간단하게 작업을 정의할 수 있습니다.

단점

  • 반환값을 제공하지 않으므로, 작업 수행 결과를 받을 수 없습니다.
  • run() 메서드는 체크된 예외(Checked Exception)를 던질 수 없으므로, 예외 처리가 필요한 경우 내부적으로 처리해야 합니다.

2. Callable 인터페이스

특징

  • Callable 인터페이스는 Java 5에서 java.util.concurrent 패키지와 함께 도입된 인터페이스로, 단일 메서드 call()을 제공합니다.
  • 반환값을 가지며, 예외를 던질 수 있습니다.
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        // Callable 구현체 생성
        Callable<String> callableTask = () -> {
            return "Callable Task Completed";
        };

        // ExecutorService를 사용하여 Callable 실행
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(callableTask);

        try {
            // 작업 완료 후 결과 가져오기
            String result = future.get();
            System.out.println("Callable Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

 

장점

  • call() 메서드는 작업 수행 후 결과를 반환할 수 있습니다.
  • 체크된 예외를 던질 수 있어, 예외 처리가 더 유연합니다.
  • Future와 같은 API를 통해 비동기 작업의 결과를 받을 수 있습니다.

단점

  • Runnable에 비해 구조가 약간 복잡하며, ExecutorService와 Future 같은 추가적인 클래스와 함께 사용해야 하는 경우가 많습니다.

언제 Runnable과 Callable을 사용해야 하는가?

  • Runnable을 사용할 때:
    • 작업 수행의 결과가 필요 없고, 단순히 백그라운드 작업이나 이벤트 처리를 할 때 적합합니다.
    • 예외를 명시적으로 처리할 필요가 없을 때 사용합니다.
    • 예: 이벤트 핸들링, 타이머 작업, 단순 스레드 실행.
  • Callable을 사용할 때:
    • 작업 수행의 결과를 반환해야 할 때 적합합니다.
    • 작업 중 예외 처리가 필요하고, 호출한 쪽에서 이를 확인할 필요가 있을 때 사용합니다.
    • 예: 데이터베이스 쿼리, 복잡한 계산 작업, 외부 시스템 호출.
  • Runnable과 Callable 모두 ExecutorService를 통해 실행할 수 있습니다. Runnable을 사용하면 결과가 없는 Future<?>를 반환하고, Callable을 사용하면 작업 결과를 담은 Future<V>를 반환합니다.
ExecutorService executor = Executors.newFixedThreadPool(2);

// Runnable 예제
Runnable runnableTask = () -> System.out.println("Runnable Task Running");
Future<?> runnableFuture = executor.submit(runnableTask); // 결과가 없음

// Callable 예제
Callable<Integer> callableTask = () -> {
    return 123;
};
Future<Integer> callableFuture = executor.submit(callableTask); // 결과가 있음

executor.shutdown();

 

executor service.submit는 멀티스레드 시작!!

ExecutorService 종료 관련

ExecutorService는 작업을 스레드 풀에서 관리하고 실행하는 인터페이스로, 사용이 끝나면 반드시 종료(shutdown)해줘야 합니다. 그렇지 않으면 애플리케이션이 종료되지 않고 백그라운드에서 스레드가 계속 실행될 수 있습니다.

ExecutorService의 종료 필요성

ExecutorService는 기본적으로 백그라운드 스레드 풀을 관리합니다. 따라서 다음과 같은 이유로 사용이 끝난 후 반드시 종료해야 합니다:

  1. 리소스 해제:
    • 스레드 풀에 의해 사용되는 스레드와 기타 리소스를 해제하여 메모리 누수를 방지합니다.
  2. 정상적인 애플리케이션 종료:
    • 스레드 풀이 종료되지 않으면 JVM이 종료되지 않고 계속 대기 상태에 있을 수 있습니다.
  3. 명시적 종료 호출:
    • executor.shutdown()을 호출하여 스레드 풀을 정상적으로 종료합니다. 이 메서드는 더 이상 새로운 작업을 수락하지 않고, 기존에 제출된 작업이 완료될 때까지 기다립니다.
    • executor.shutdownNow()를 호출하면 모든 작업을 중지하고, 실행 중인 작업을 즉시 종료하려고 시도합니다.
    • ExecutorService는 AutoCloseable을 구현하지 않기 때문에 try-with-resources 구문을 직접 사용할 수 없습니다.
public class ExecutorServiceExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        try {
            executor.submit(() -> System.out.println("Task 1"));
            executor.submit(() -> System.out.println("Task 2"));
        } finally {
            // ExecutorService 종료
            executor.shutdown();
            try {
                if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
}

위 코드에서는 shutdown() 메서드를 호출하여 새로운 작업을 수락하지 않도록 하고, awaitTermination()을 사용하여 스레드 풀이 완전히 종료될 때까지 대기합니다. awaitTermination()은 주어진 시간 동안 스레드 풀이 종료될 때까지 기다리며, 그 시간이 지나도 종료되지 않으면 shutdownNow()를 호출하여 강제로 종료를 시도합니다.

728x90
반응형
반응형

기본 Xms Xmx

 java -jar -Dspring.profiles.active=real xxx.jar

로 실행하고 있는 프로세스가 있다. 띄울 때 최소/최대 힙 사이즈를 안 줘서 기본값으로 어떻게 들고 있는지 궁금했다.

java -XX:+PrintFlagsFinal -version | grep -E "InitialHeapSize|MaxHeapSize"
   size_t InitialHeapSize                          = 62914560      
   size_t MaxHeapSize                              = 994050048

위 명령어를 사용하면 현재 JVM의 기본 힙 설정을 알 수 있다.

각각은 바이트 단위이다. 따라서 좀 더 이해하기 쉽게 바꿔보면

  1. 바이트(Byte) -> 킬로바이트(Kilobyte):
    • 1KB = 1024B
    • 62914560 ÷ 1024 = 61440 KB
  2. 킬로바이트(Kilobyte) -> 메가바이트(Megabyte):
    • 1MB = 1024KB
    • 61440 ÷ 1024 = 60 MB

따라서, initialHeapSize가 62914560이라는 값은 60MB를 잡고 있다는 뜻이다.

또한 최대 값을 계산해보면

  1. 바이트(Byte) -> 킬로바이트(Kilobyte):
    • 1KB = 1024B
    • 994050048 ÷ 1024 = 970752 KB
  2. 킬로바이트(Kilobyte) -> 메가바이트(Megabyte):
    • 1MB = 1024KB
    • 970752 ÷ 1024 = 948 MB

따라서, maxHeapSize가 994050048 바이트라는 값은 약 948MB를 최대 값으로 설정했다는 것이다.

 

지금 메모리 현황 보기

free -h

              total        used        free      shared  buff/cache   available
Mem:           3.7G        933M        976M        137M        1.8G        2.3G
Swap:          2.0G         13M        2.0G

 

gc 관련 모니터링(jstat) 권한없어도 가능

jstat -gc <PID> <interval> <count>
jstat -gc 12345 1000 10  # 12345 PID의 JVM에 대해 1초 간격으로 10번 GC 정보를 출력

//
S0C    S1C    S0U    S1U      EC       EU        OC        OU      MC       MU     CCSC     CCSU       YGC    FGC
1024.0 1024.0   0.0   0.0   8192.0   1024.0   20480.0    8192.0    512.0    488.0   64.0     62.0       3      1
  • S0C/S1C: Survivor space 0/1의 용량.
  • EC: Eden 영역의 용량.
  • OC: Old 영역의 용량.
  • YGC/FGC: Young/Full GC의 발생 횟수.

 

OOM이 터질 때 자동으로 덤프를 뜨게 하는 옵션을 주려면 아래와 같은 옵션을 자바에 추가한다.

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/경로/heapdump.hprof

 

이미 뭔가 데드락이나 무한루프에 빠진 것 같다면 확인하기

각 스레드의 상태현재 실행 중인 코드를 볼 수 있음(권한 필요)

jstack <PID>
"main" #1 prio=5 os_prio=0 tid=0x00000000023f6000 nid=0x2c runnable [0x0000000002a1e000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.Thread.sleep(Native Method)
        at Example.main(Example.java:5)

 

현재 돌고 있는 프로세스의 덤프 뜨는 법

jmap이나 jcmd 명령어 사용(권한 필요)

sudo jmap -dump:format=b,file=/경로/heapdump.hprof <PID>
-- or 
jcmd <PID> GC.heap_dump <경로>


// /proc/3272/root폴더에 권한이 없을 경우
Exception in thread "main" com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file /proc/3272/root/tmp/.java_pid3272: target process 3272 doesn't respond within 10500ms or HotSpot VM not loaded
        at jdk.attach/sun.tools.attach.VirtualMachineImpl.<init>(VirtualMachineImpl.java:100)
        at jdk.attach/sun.tools.attach.AttachProviderImpl.attachVirtualMachine(AttachProviderImpl.java:58)
        at jdk.attach/com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:207)
        at jdk.jcmd/sun.tools.jmap.JMap.executeCommandForPid(JMap.java:128)
        at jdk.jcmd/sun.tools.jmap.JMap.dump(JMap.java:208)
        at jdk.jcmd/sun.tools.jmap.JMap.main(JMap.java:114)

 

MAT(Eclipse Memory Analyzer Tool)

hprof파일을 얻었으면 분석 프로그램인 eclipse MAT 프로그램이 필요하다.

다운로드하고 hprof파일을 열어준다. hprof 파일의 용량이 클 수록 오래걸린다.

 

분석 보고서 이해하기

  • Overview (개요): 메모리 상태, 누수 가능성, 가장 큰 객체 등을 요약하여 보여줌
  • Dominator Tree: 힙 메모리의 최상위 점유자를 트리 구조로 보여주며 메모리 점유 비율이 큰 객체를 쉽게 파악 가능
  • Histogram: 클래스별로 객체 수와 메모리 점유량
  • Top Consumers: 메모리 사용량이 큰 객체 그룹

힙 분석 예시

  1. 메모리 누수 확인:
    • Leak Suspects Report를 사용하면 누수 가능성이 있는 객체를 분석하여 보여줌
    • "Path to GC Root" 기능을 사용해 메모리에서 해제되지 않은 객체의 참조 경로를 추적할 수 있음
  2. Dominator Tree 분석:
    • Dominator Tree를 통해 메모리를 가장 많이 차지하는 객체 파악
    • with outgoing references를 사용하여 참조 중인 객체들을 확인
  3. Histogram 분석:
    • 클래스별로 객체 수와 메모리 점유율을 확인하여 특정 클래스가 메모리를 많이 사용하는지 파악
    • 특정 클래스에서 메모리를 많이 사용하는 객체가 있다면, 이를 "List Objects -> with incoming references"로 추적 가능

 

728x90
반응형
반응형
class ImmutableCollections {
  • Java 9 and Later: Provides factory methods (List.of, Set.of, Map.of) to create immutable collections.
    • List.of(...): Creates an immutable list.
    • Set.of(...): Creates an immutable set.
    • Map.of(...): Creates an immutable map.
  • Java 16: Enhances immutable collection creation with additional methods for more control.

 

특징

  • Thread-Safety: Immutable collections are inherently thread-safe because their state cannot change after construction.
  • No Modifications: Methods that modify the collection (like add, remove, put, etc.) throw UnsupportedOperationException.
  • Efficient: Immutable collections are often more memory-efficient and can be optimized by the JVM.

 

아래 함수가 호출되면 에러 발생! 추가 삭제 정렬 불가.

// all mutating methods throw UnsupportedOperationException
@Override public void    add(int index, E element) { throw uoe(); }
@Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); }
@Override public E       remove(int index) { throw uoe(); }
@Override public void    replaceAll(UnaryOperator<E> operator) { throw uoe(); }
@Override public E       set(int index, E element) { throw uoe(); }
@Override public void    sort(Comparator<? super E> c) { throw uoe(); }

 

 

참고로 리스트 콜랙션에도 불변객체를 바로 만들 수 있는데,, ImmutableCollections 함수와는 무관하지만 스트림에 추가된 기능이다.

toList() Method in Streams (Java 16 and Later)

  • Purpose: Collects the elements of a Stream into an immutable List.
  • Usage: Provides a convenient way to create an immutable list directly from a stream.

Collectors.toList() vs. toList()

  • Collectors.toList(): from java8, Creates a mutable list by default, which can be modified after collection. ArrayList
  • Stream.toList(): Introduced in Java 16, creates an immutable list directly from a stream, offering a more streamlined approach to get immutable results.
728x90
반응형
반응형

환경: springboot3, java17

 

소스를 배포 환경(ex. dev, stage, real...)에 따라 실행할 때 보통 profile 옵션을 줘서 환경에 맞는 프로퍼티 파일을 들고 갈 수 있게 한다.

 $JAVA11_PATH/java -jar -DappName=$APP_NAME -Dspring.profiles.active=$ACTIVE_PROFILE $DEPLOY_DIR/$JAR_NAME 1>/dev/null 2>/dev/null &

 

테스트 코드를 짤 때 필요한 설정파일은 보통 test/resources패키지 안에 application.yml로 만든다.

문득 궁금한 게 어떨 때는 test/resources/application-test.yml로 만들고 어떨 때는 test/resources/application.yml로 만들었던 것 같다.

두 개가 같은 상황으로 인식되나? 궁금해서 파헤쳐 본다.

 

테스트 코드를 위한 프로퍼티 파일

1. test/resources/application.yml로 만들면

테스트가 돌아갈 때 기본적으로 들고 가며 main/resources/application.yml을 덮어쓰는 효과가 난다.

테스트 코드에 아무 설정을 하지 않아도 적용된다.

2. test/resources/application-test.yml로 만들면

테스트 폴더 안에 있지만 어쨌건 profile이 적용된 파일이라 프로파일을 적용해야만 반영된다. 

적용하는 방법은 아래처럼 소스에다 명시한다(모든 소스에다 다 해야 하면 1번 방법을 쓰는 게 낫다.)

@ActiveProfiles("test") // test profile을 적용하라는 뜻
@Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {

혹은 외부에서 주입해야 한다.(위에 jar 실행 시처럼)

 

반대로 해당 빈을 특정 프로파일(ex. test)에서만 사용해야 한다면

1. 빈으로 프로파일을 명시한다.

@Profile("test")
@Configuration

위 설정은 profile=test 일 때만 로드된다.

2. 테스트에서만 사용하는 설정의 경우

프로파일에 상관없이 테스트에서만 사용된다면 아래 어노테이션을 사용하면 된다.

@TestConfiguration

 

사용 예제

즉, 테스트 코드에서 아래 어노테이션을 사용한다면..

@ActiveProfiles("test") 

1. 테스트 코드에서 2. 테스트 프로파일을 불러오는 것이므로

@Profile("test")
@Configuration

가 달린 설정과

@TestConfiguration

가 달린 설정 모두를 불러오게 된다.


추가적으로 궁금한 사항.....

@TestConfiguration
public class TestBatchConfig {
 @Bean
  public JobTestUtils jobTestUtils(){

해당 파일은 테스트 코드가 돌 때 자동으로 빈으로 등록하는 것인데

@ActiveProfiles("test") // test profile을 적용하라는 뜻
@ContextConfiguration(classes = {TestBatchConfig.class}) //or @Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {
		
  @Autowired private JobTestUtils jobTestUtils;

정작 테스트 코드에서 두 번째 줄을 지우면 JobTestUtils 빈을 못 찾아서 테스트가 실패한다. 아니 테스트 코드 설정이라면서 왜정작 못 불러오는 거지..

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'DailyRankingJobConfigTest'
: Unsatisfied dependency expressed through field 'jobTestUtils': No qualifying bean of type 'JobTestUtils' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations:

 

관련하여 spring공식 문서를 보니 아래와 같은 부분이 있다.

정리하면, 일반 클래스(top-level)로 configuration클래스를 만들면 스캐닝되지 않고 inner class에 static으로 만들어야지만 된다는 것...?!

https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html#testing.spring-boot-applications.detecting-configuration

 

Testing Spring Boot Applications :: Spring Boot

To test whether Spring MVC controllers are working as expected, use the @WebMvcTest annotation. @WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverte

docs.spring.io

그래서 아래처럼 inner class로 만들어보니 진짜 된다..! 이럴 수가

@ActiveProfiles("test") 
@SpringBootTest
class DailyRankingJobConfigTest {
	@Autowired private JobTestUtils jobTestUtils;
    
    ...
    
    
  @TestConfiguration
  public static class TestBatchConfig {

    @Bean
    public JobTestUtils jobTestUtils() {
      return new JobTestUtils();
    }
  }
}

 

하지만 매번 inner class로 넣을 수도 없어서 그냥 Import구문을 넣어야겠다..

728x90
반응형

+ Recent posts