📚 참고자료

  • [10분 테코톡] 🌊 바다의 JUnit5 => 링크

 

1️⃣ JUnit이란?

  • 자바 개발자의 93%가 사용하는 단위 테스트 프레임워크
  • JUnit5는 2017년 10월 공개
  • 스프링 부트 2.2버전 이상부터 기본 제공

 

 

  • Platform : 테스트를 실행해주는 런처 제공. TestEngine API 제공
  • Jupiter : JUnit 5를 지원하는 TestEngine API 구현체
  • Vintage : Junit 4와 3을 지원하는 TestEngine 구현체

 

셋 다 JUnit5 세부 모듈이다

 

 

 

2️⃣ JUnit5 시작하기

 

  • 스프링 부트 프로젝트
    => 스프링 부트 2.2버전 이상부터는 기본적으로 JUnit5 의존성이 추가된다.

  • 스프링 부트 프로젝트가 아닐 경우
    => 다음과 같이 의존성을 추가해주면 된다.
 <dependency>
 	<groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.6.2</version>
    <scope>test</scope>
 </dependency>

 

 

 

3️⃣ 어노테이션들(Annotaions)

 

@Test

  • 테스트 메서드라는 것을 나타내는 어노테이션
  • JUnit4와 다르게 어떠한 속성도 선언하지 않는다.
// JUnit4
@Test(expected = Exception.class)
void create() throws Exception {
	...
}

// JUnit5
@Test
void create() {
	...
}

 

생명주기(LifeCycle) 어노테이션

  • @BeforeAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 딱 한번 실행되는 메서드
  • @AfterAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 딱 한번 실행되는 메서드
  • JUnit4의 @BeforeClass / @AfterClass 와 유사

  • @BeforeEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 실행되는 메서드
  • @AfterEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 실행되는 메서드
  • JUnit4의 @Before / @After와 유사
  • 매 테스트 메서드마다 새로운 클래스를 생성(new)하여 실행 (비효율적)

 

 

@Disabled

  • 테스트를 하고 싶지 않은 클래스나 메서드에 붙이는 어노테이션
  • JUnit4의 @Ignore 과 유사
class DisabledExample {
	@Test
    @Disabled("문제가 해결될 때까지 테스트 중단")
    void test() {
    	System.out.println("테스트");
    }
    
    @Test
    void test2() {
    	System.out.println("테스트2");
    }
}

 

 

@DisplayName

  • 어떤 테스트인지 쉽게 표현할 수 있도록 해주는 어노테이션
  • 공백, Emoji, 특수문자 등을 모두 지원
@DisplayName("특수 테스트\uD83D\uDE00")
class DisplayNameExample {
	@Test
    @DisplayName("굉장한 테스트입니다.")
    void test() {
    }
}

 

 

 

@RepeatedTest

  • 특정 테스트를 반복시키고 싶을 때 사용하는 어노테이션
  • 반복 횟수와 반복 테스트 이름을 설정가능
@RepeatedTest(10)
@DisplayName("반복 테스트")
void repeatedTest() {
	...
}

@RepeatedTest(value = 10, name = "{displayName} 중 {currentRepetition} of {totalRepetitions})
@DisplayName("반복 테스트")
void repeatedTest2() {
	...
}

 

 

@ParameterizedTest

  • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행할 때 사용하는 어노테이션
@ParameterizedTest
@CsvSource(value = {"ACE,ACE:12", "ACE,ACE,ACE:13", "ACE,ACE,TEN:12"}, delimiter = ':')
@DisplayName("에이스 카드가 여러 개일 때 합 구하기")
void calculateCardSumWhenAceIsTwo(final String input, final int expected) {
	final String[] inputs = input.split(",");
    for (final String number : inputs) {
    	final CardNumber cardNumber = CardNumber.valueOf(number);
        dealer.receiveOneCard(new Card(cardNumber, CardType.CLOVER));
    }
    assertThat(dealer.calculateScore()).isEqualTo(expected);
}

 

 

 

 

@Nested

  • 테스트 클래스 안에서 내부 클래스 정의해 테스트를 계층화 할 때 사용
  • 내부클래스는 부모클래스의 멤버 필드에 접근가능
  • Before / After와 같은 테스트 생명주기에 관계된 메소드들도 계층에 맞춰 동작

 

 

Assertions

  • 사전적 의미: 주장, 행사, 단정문
  • 테스트 케이스의 수행 결과를 판별하는 메서드
  • 모든 Junit Jupiter Assertions는 static 메서드

 

 

Assertions - assertAll(executables...)

  • 매개변수로 받는 모든 테스트코드를 한 번에 실행
  • 오류가 나도 끝까지 샐행한 뒤 한 번에 모아서 출력
@Test
public void create_study() {
	Study study = new Study();
    assertNotNull(study);
    assertEquals(Status.STARTED, study.getStatus(), "처음 상태값이 DRAFT");
    assertTrue(study.getLimit() > 0, () -> "최대 인원은 0보다 커야한다.");
}

@Test
public void create_study() {
	Study study = new Study();
    assertAll(
    	() -> assertNotNull(study),
		() -> assertEquals(Status.STARTED, study.getStatus(), "처음 상태값 DRAFT"),
        () -> assertTrue(study.getLimit() > 0, "최대 인원은 0보다 커야한다.")
    );
}

 

 

 

Assertions - assertThrows(expectedType, executable)

  • 예외 발생을 확인하는 테스트
  • executable의 로직이 실행하는 도중 expectedType의 에러를 발생시키는지 확인
// JUnit4
@Test(expected = Exception.class)
void create() throws Exception {
	...
}

// JUnit5
@Test
void exceptionThrow() {
	Exception e = assertThrows(Exception.class, () -> new Test(-10));
    assertDoesNotThrow(() -> System.out.println("Do Something"));
}

 

 

Assertions - assertTimeout(duration, executable)

  • 특정 시간 안에 실행이 완료되는지 확인
  • Duration : 원하는 시간
  • Executable : 테스트할 로직
@Rule
public Timeout timeout = Timeout.seconds(5);

class TimeoutExample {
	@Test
    @DisplayName("타임아웃 준수")
    void timeoutNotExceeded() {
    	assertTimeout(ofMinutes(2), () -> Thread.sleep(10));
	}

	@Test
	@DisplayName("타임아웃 초과")
	void timeoutExceeded() {
		assertTimeout(ofMillis(10), () -> Thread.sleep(100));
	}
}

 

 

Assumption

  • 전제문이 true라면 실행, false라면 종료
  • assumeTrue : false일 때 이후 테스트 전체가 실행되지 않음
  • assumingThat : 파라미터로 전달된 코드블럭만 실행되지 않음
void dev_env_only() {
	assumeTrue("DEV".equals(System.getenv("ENV")), () -> "개발 환경이 아닙니다.");
    assertEquals("A", "A");	 // 단정문이 실행되지 않음
}

void some_test() {
	assumingThat("DEV".equals(System.getenv("ENV")),
    	() -> {
        	assertEquals("A", "B");  // 단정문이 실행되지 않음
        });
	assertEquals("A", "A");  // 단정문이 실행됨
}

 


느낀 점

  • 어노테이션을 단순히 적용하는 것이 아닌 좀 더 깊게 파고 들어가 공부 할 수 있도록 진행
  • 이론만 공부하면 확실히 해당 내용을 익힐 수 없다는 느낌을 받았습니다.
    => 정리를 하고 다음에 JUnit5를 사용할 때 이 글을 참고하면서 공부
반응형

📚 참고자료

  • [10분 테코톡] 샐리의 트랜잭션 => 링크

0️⃣ 트랜잭션 개념 공부를 하게 된 계기

스프링을 사용하면서 @Transactional 이라는 어노테이션을 적용해본 적이 있습니다.

메서드 위에 해당 어노테이션을 적용하면 메서드 안에 있는 쿼리들이 하나의 단위로 묶인다는 대략적인 용도만 고 있습니다. 그래서 @Transactional 어노테이션의 정확한 개념을 모르고 사용했기 때문에 의문점이 많이 들었습니다.

  • 트랜잭션이란?
  • 트랜잭션을 사용해야 하는 상황은?
  • 스프링에서는 트랜잭션을 어떻게 지원하는지?

 

 

 

 

1️⃣ 트랜잭션이란?

  • 더이상 나눌 수 없는 가장 작은 하나의 단위를 의미
  • 데이터베이스에서는 트랜잭션을 조작함으로써 사용자가 데이터베이스에 대한 완전성을 신뢰할 수 있도록 함
  • 모든 데이터베이스는 자체적으로 트랜잭션을 지원
  • 하나의 명령을 실행했을 때 데이터베이스가 온전히 그 명령을 실행해주는 것
    => 성공하면 커밋(Commit)
    => 실패하면 롤백(Rollback)
  • 데이터베이스는 기본적으로 트랜잭션을 관리하기 위한 설정을 갖고 있습니다.
    • 명령을 끝마칠 때까지 수행 내역을 로그에 저장해둡니다.
      - 데이터베이스에 반영된 내용을 재반영하기 위한 redo log
      - 수행을 실패해 이전의 상태로 되돌리는 undo log
      * 두 개의 log를 이용해 트랜잭션을 지원합니다.

 

 

 

2️⃣ 트랜잭션을 사용하는 상황(예시)과 성질

트랜잭션에서 가장 흔히 볼 수 있는 입출금을 예시로 들어보겠습니다.

 

  1. A가 B에게 만원을 송금하려고 합니다.
  2. A의 계좌에 만원보다 많은 금액이 있는지 확인합니다.
  3. A의 계좌에 만원보다 많은 금액이 있다면 만원을 차감합니다.
  4. 차감한 만원의 금액을 B의 계좌에 더해줍니다.

 

위 업무는 순서를 나눠놨지만 절대로 분리되거나 일부만 실행되면 안 되는 하나의 작업입니다.

이렇게 절대로 깨져서는 안되는 하나의 작업을 트랜잭션이라고 합니다.

 

 

트랜잭션의 네가지 성질

  • 원자성(Atomicity)
    - A가 B에게 송금을 하는데 A의 계좌에서 만원이 차감만 되고 작업이 종료되면 문제가 발생합니다.
    - 이렇듯 일부만 실행되는 경우는 없다는 원자성을 지닙니다.
    - [롤백]1번부터 4번까지 연결된 하나의 작업에서 어느 하나의 명령이라도 실패하면 롤백을 하게 됩니다.
    - [트랜잭션 커밋]반대로 1번부터 4번까지 작업이 성공적으로 수행하면 수정된 내용 데이터베이스에 반영
    - 롤백이나 커밋이 실행되어야 트랜잭션이 종료됩니다.

  • 일관성(Consistency)
    - 데이터베이스 내의 상태 혹은 계층 관계
    - 컬럼의 속성이 항상 일관되게 유지되어야 함
    - 컬럼의 속성이 수정되었다면 trigger를 통해 일괄적으로 모든 데이터베이스에 적용해야 합니다.
  • 지속성(Durability)
    - 트랜잭션이 성공적으로 수행되어 커밋되었다면 어떠한 문제가 발생하더라도 데이터베이스에 그 내용이
    영원히 지속되어야한다는 지속성이 있습니다.
    - 이를 위해 모든 트랜잭션은 로그로 남겨져 어떠한 장애에도 대비할 수 있도록 합니다.

  • 독립성(Isolation)
    - 트랜잭션 수행 시 다른 트랜잭션이 끼어들 수 없고 각각 독립적으로 실행된다는 성질
    DB 독립성의 문제점(더보기 클릭)
    더보기
    하지만 데이터베이스에 작업이 들어왔을 때 모든 작업의 독립성을 보장해 하나씩 순차적으로 진행하게 된다면, CPU는 DBMS보다 인풋, 아웃풋이 빈번히 수행되기 때문에 트랜잭션을 순차적으로 시행하면 CPU는 점점
    응답을 기다리는 시간이 길어지게 됩니다. 그 결과 프로그램이 비효율적으로 동작하는 문제가 발생합니다.

 

이처럼 데이터베이스에 저장된 데이터의 무결성과 동시성의 성능을 지키기 위해 트랜잭션의 설정이 중요한 것입니다.

여러 명령을 하나의 트랜잭션으로 묶고 싶은 경우 개발자가 직접 트랜잭션의 경계설정을 통해 트랜잭션 명시

 

 

 

 

3️⃣ 스프링에서 지원하는 트랜잭션

 

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
    void commit(TransactionStatus status);
    void rollback(TransactionStatus status);
}

 

  • 트랜잭션 추상화 인터페이스인 PlatformTransactionManager를 제공
    • getTransaction 메서드
      - 파라미터로 전달되는 TransactionDefinition에 따라 트랜잭션을 시작
      - 트랜잭션을 문제없이 마치면 commit을, 문제가 발생하면 Rollback을 호출
    • getTransaction부터 commit이나 rollback을 하는 부분까지가 트랜잭션의 경계설정입니다.
  • 다양한 DataSource에 맞게 트랜잭션을 관리

 

 

스프링이 제공하는 다양한 트랜잭션 매니저 구현체

 

  1. DataSourceTransactionManager
    - JDBC에 사용되는 매니저
    - 하나의 데이터베이스를 사용하거나 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션에 사용

  2. JpaTransactionManager
    - JPA에 사용되는 매니저
    - 하나의 데이터베이스를 사용하거나 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션에 사용

  3. JtaTransactionManager
    - 하나 이상의 데이터베이스가 참여하는 경우 글로벌 트랜잭션에 사용되는 JtaTransactionManager 사용
    - 여러 개의 데이터베이스에 대한 작업을 하나의 트랜잭션에 묶을 수 있고, 다른 서버에 분산된 것도 묶을 수 있음

 

 

🍃 어노테이션을 기반으로 트랜잭션을 설정하는 방법

 

  @Transactional
    public void addRestaurantFoods(
            Long restaurantId,
            List<FoodRequestDto> requestDtoList
    ) {
        Optional<Restaurant> foundRestaurant = restaurantRepository.findById(restaurantId);

        checkRestaurant(foundRestaurant);
        Restaurant restaurant = foundRestaurant.get();

        for (FoodRequestDto requestDto : requestDtoList) {
            String foodName = requestDto.getName();
            int foodPrice = requestDto.getPrice();

            checkDuplicateRestaurantFood(restaurant, foodName);

            checkFoodPrice(foodPrice);

            Food food = Food.builder()
                    .name(foodName)
                    .price(foodPrice)
                    .restaurant(restaurant)
                    .build();

            foodRepository.save(food);
        }
    }

 

  • 어노테이션이 적용된 메서드는 메서드 시작부터 트랜잭션이 시작
    => 메서드를 성공적으로 끝마치면 트랜잭션 커밋
    => 도중에 문제가 발생하면 롤백
  • 어노테이션은 데이터베이스에 여러번 접근하면서 하나의 작업을 수행하는 서비스 계층 메서드에 붙인다.

 

 

 

반응형

+ Recent posts