본문 바로가기
What I Learned

[NBCAMP | Spring 6기] 58일차 TIL + Test Code, Transaction Isolation

by ㅇ달빛천사ㅇ 2024. 9. 13.
728x90

👈  이전글

[NBCAMP | Spring 6기] 57일차 TIL + AOP, SQL Injection, Test Code


KDT 실무형 스프링 백엔드 엔지니어 양성과정 6기

 


🗝 오늘의 학습 키워드 : Test Code Transaction Isolation



📖 공부한 내용 본인의 언어로 정리하기

[ 테스트 코드 특강 ]

Junit

자바 언어에서 사용되는 단위 테스트 프레임워크


Junit5 = Junit Platform + Junit Jupiter + Junit Vintage

 

  • @BeforeAll : 클래스 레벨 설정
  • @BeforeEach : 메서드 레벨 설정
  • @Test : 테스트 실행
  • @AfterEach : 메서드 레벨 설정
  • @AfterAll : 클래스 레벨 설정
    (@~Each 는 테스트 개수만큼 반복)

Dependency

 Java에서도 Junit 사용 가능

dependencies {
    testImplementation(platform("org.junit:junit-bom:5.9.1"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}

Springboot

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Mockito

  • Mock = 가짜
  • Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크
  • Mock 기반 테스트 코드는 인터넷이 안되는 비행기 안에서도 동작 및 검증 가능
  • 통합테스트인 @SpringBootTest와 다르게 별도 환경, 구성에 대한 제약 사항이 없기 때문에 매우 빠르게 동작


UnitTest

  • 단위(Unit)
    • 애플리케이션에서 가장 작은 테스트 가능 요소를 뜻함
    • 함수(Method)


SUT(System Under Test)

실제 객체를 사용하지 않고 Mocking함으로써 의존성이 적고 빠르게 테스트를 할 수 있음

 



FIRST 원칙

  • Fast : 유닛테스트는 빨라야 함.
  • Isolated : 테스트는 각 테스트 간에 독립적으로 실행해야 함.
  • Repeatable : 테스트는 환경에 상관없이 실행할 때마다 같은 결과를 만들어야 함.
  • Self-validating : 테스트는 명확히 성공/실패로 구분하여 테스트 자체가 결과를 검증할 수 있어야 함.
  • Timely : 테스트는 개발 간에 즉시 작성해야 함. 대표적으로 TDD 방법론이 있음(쉽지 않음)

Mock Unit Test

@ExtendWith(MockitoExtension.class)  // Mockito 환경을 사용하기 위한 설정

@Mock

  • 테스트에서 사용될 가짜 객체
  • 해당 객체의 메서드를 모방하기 위한 용도

@InjectMocks

  • Mock 객체를 주입하여 실제 객체의 의존성을 자동으로 설정함
  • Mock이 아닌 실제 객체
  • Mocking은 실제 객체 대신 가짜 객체를 만들어 테스트 중에 해당 객체의 동작을 시뮬레이션하는 과정

 

@Spy
마치 첩보 요원처럼, 실제 객체의 행동을 하면서 우리가 조작한 행동(stubbing)은 우리가 설정한대로 동작
실제 객체를 사용하면서 일부 기능에 대한 Mock처리가 필요할 때 사용


BDD (Behavior? Driven Development) : 행동 기반 개발 방법론 

(어떤 행동을 정의하겠다...)
given()

 

Class/Method Naming

  • 한글 명으로 지어주어도 됨
  • 대신 띄어쓰기 대신 언더바(_)를 사용해야 함.

 

클래스명

  • {TargetClass}Test형태로 작성하는 편
  • @Nested 설정을 해서 테스트를 그룹지음

함수명 

  • @DisplayName을 사용 & 함수명을 영문 테스트명 작성
  • @DisplayName을 사용하지 않음 & 한글로 테스트명을 작성(+ 언더바(_) 처리가 필요)
    (둘 다 자주 사용하난 방식, 정답 없음, 개발 간에는 취향따라 실무에서는 회사 또는 팀 규정따라)

given - when - then 패턴

given

필요한 데이터를 선언 및 할당
// given 예시
given(movieRepository.findById(anyLong())).willReturn(Optional.of(movie));
  • anyLong() : 어떤 Long 값을 넣어도
  • willReturn(*) : 이 값을 반환하겠어!

 

  • any..()
    • given에서 자원을 줄 때, mock에 어떤 값이든(any) 할당하는 값 메서드
    • willReturn으로 결과 값을 매핑하기 때문에 자원에는 어떠한 값이 매핑되어도 상관없기 때문에 사용함.
    • 앞에 어떤 값을 할당해도 willReturn() 때문에 proxy 객체라는 것이 만들어지는데
      이것 때문에 앞의 코드는 상관 없이 willReturn값만 매핑됨.
    • any()
    • anyLong()
    • anyString()
    • anyList()
    • anySet()
    • anyMap()
      등이 있음

when

어떤 메서드를 실행할 지 결정


@injectMocks 대상 객체는 mock객체가 아님
테스트하려는 실제 객체를 생성하고 그 객체의 의존성들만 mocking된 객체로 대체하는 것
>> any()를 사용할 수 없음.(실제 자원을 넣어 주어야 함.)



then

  • assert..()
    • 테스트의 결과를 검증하는 데 사용하며, 테스트 예상한 결과와 실제 결과를 비교하여 테스트 성공/실패를 결정
      • assertEquals()
      • assertIsNull() / assertNotNull()
      • assertTrue() / assertFalse()
      • assertThrows()

  • verify()
    • 특정 메서드가 호출되었는지, 호출 횟수는 몇번인지, 호출 순서는 어떤지 등을 검증하는 데 사용
// verify() 메서드 사용 예시
verify(movieRepository, times(0)).delete(any(Movie.class));


movieRepository에서 delete메서드가 몇번 호출되었는지 테스트
(실패했기 때문에 0회 호출을 검증)


void Method 테스트 방법

doNothing().when(movieRepository).delete(any(Movie.class));


Mockito.doNothing() 메서드로 아무것도 처리하지 않겠다는 의미
verify() 와 함께 핵심 지원만 호출 되었는지 정도 확인 해주면 좋음

 


WebMvcTest

전체 테스트가 아닌 Web Layer만을 테스트할 때 사용

 

 

WebMvcTest 대상

  • @Controller : WebMvcTest를 컨트롤러 테스트 용으로 사용
  • @ControllerAdvice
  • @JsonComponent
  • Converter/GeneriConverter
  • Filter
  • WebMvcConfigurer
  • HandlerMethodArgumentResolver
  • beans

(@Component, @Service, @Repository는 아니다!)


@MockBean
컨트롤러에서 서비스의 실행을 위해 SpringContext에서 관리되는 Bean를 Mock 객체로 교체

특정 컨트롤러나 웹 계층에 대한 단위 테스트 진행
컨트롤러가 예상대로 작동하는지, 웹 요청과 응답이 제대로 이루어지는지 검증


SpringBootTest

  • 통합 테스트
  • 애플리케이션이 실제로 실행됨.
  • Mock 객체가 아닌 실체 객체를 사용
    (CRUD로 postMan 돌리는 것과 동일하다고 생각하면 됨)

인증 토큰 넣는 방법

테스트 코드 왼쪽 실행 버튼 클릭하면 Modifiy Run Configuration 클릭
Modify Options 클릭
Environment variables에 넣어주면 됨.

(실제 환경처럼 Setting도 해주어야 함.)


TestCoverage

  • 애플리케이션의 테스트 케이스가 얼마나 충족되었는지를 나타내는 지표
  • 커버리지가 항상 100%가 될 필요는 없음.(사실상 불가능)
  • 커버리지 자체가 목적이 되기보다는 중요한 로직과 엣지 케이스에 대한 충분한 테스트가 필요
  • 커버리지가 높다고 해서 항상 코드가 완벽하다는 보장은 없으며, 테스트의 품질이 더 중요

커버리지 툴

 JACOCO(Java Code Coverage)

좀 더 정교하게 나오고 구조적으로 얼마나 커버되었는지, 분기(if문 같은 것들)가 얼마나 커버되었는지를 알 수 있음

 

커버리지 종류

  • 구조(Code, Line)
  • 브랜치(상태, Condition, branch)


sonarQube

조금 더 정교화된 커버리지 툴
CI/CD를 통한 배포를 하면 잰킨스를 사용할 수 있음. 

이 때, Quality Gate가 있어서 이게 낮으면 배포를 못함.


[ 하루 10분 CS 스터디 ]

트랜잭션 격리 수준(TIL; Transaction Isolation Level)
트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타내는 수준

 

ACID : Isolation(고립성)

  • 트랜잭션 수행 시, 다른 트랜잭션의 연산이 끼어들지 못하도록 보장한다.
  • 하나의 트랜잭션이 다른 트랜잭션에게 영향을 주지 않도록 격리되어 수행되어야 한다.

여러 트랜잭션이 한 자원에 동시에 접근해서 작업을 수행( 비직렬 스케줄 )하는 경우

  • 읽기, 쓰기, 삭제의 작업이 동시에 수행
  • 데이터의 정합성이 떨어짐
  • 여러 문제가 발생하게 됨

 

모든 트랜잭션이 순차적으로 데이터에 접근하여 작업을 수행( 직렬 스케줄 )하는 경우

  • 순차적 트랜잭션 자원 할당
  • 트랜잭션이 쌓이는 만큼 다른 트랜잭션의 대기 시간 길어짐
    >> 정확성은 올라가지만 전체적인 속도는 떨어짐(문제 발생)

속도 증가 > 정확도 낮아짐
속도 낮아짐 > 정확도 증가

 

즉, 완전한 격리가 아닌 완화된 수준의 격리가 필요!


트랜잭션 격리 수준이란?

속도와 데이터 정확성에 대한 트레이드 오프를 고려한 트랜잭션 격리 수준 나눈 것

트랜잭션 격리 수준

0. Uncommited Read(커밋되지 않은 읽기)

트랜잭션1의 작업이 commit되기 전에 트랜잭션2가 해당 데이터에 접근하여 읽기 연산을 하는 경우
만약 트랜잭션1이 commit을 하지 않고 Rollback을 수행한다면

트랜잭션2는 데이터 갱신과 Rollback 수행 사이에 읽기 작업을 하였으므로

Rollback되기 전의 값을 읽는 DirtyRead가 발생

 

>> RDBMS 표준에서는 이를 격리 단계로 인정x

1. Committed Read(커밋된 읽기)

commit된 데이터만 읽을 수 있음.

만약 트랜잭션1이 데이터 갱신 작업을 수행 중일 때,

트랜잭션2가 해당 데이터에 대하여 읽기 작업을 수행한다면

DB에 Undo area라는 것이 있어서 트랜잭션1이 작업을 수행하기 전의 값(즉, 이전에 commit된 값)을 읽음.

Undo area에서는 현재 작업 중인 데이터의 commit되기 전 값과 PK를 저장


Non Repeatable Read(반복해서 읽을 수 없다.)

트랜잭션1이 데이터 갱신 후, commit을 하기 전과 후에 두번의 Read에서 다른 값이 읽어짐

 

2. Repeatable Read(반복 가능한 읽기)

NonRepeatable Read에 대한 해결 방안

트랜잭션 마다 아이디를 부여하여 자신의 트랜잭션 아이디보다 작은 트랜잭션이 갱신한 값만 읽음

 

MySQL은 기본 Repeatable Read 적용

그 외에는 대부분 Commited Read가 기본


Phantom Read

트랜잭션1은 조회된 행의 개수를 세는 작업(Count(*)을 수행하고

트랜잭션2는 데이터 추가 작업(INSERT)을 수행하고 있다고 하자.(ID : 트랜잭션1 < 트랜잭션2)

트랜잭션2가 commit하기 전, 후로 트랜잭션1이 작업을 수행했을 때, 서로 다른 값이 읽혀 오는 것

앞에서 발생한 문제(NonRepeatable Read)는 데이터가 변한 것이었다면 이 문제는 행의 수가 변한 것

UPDATE, DELETE는 막아놓았지만  INSERT Lock이 안되어 있음.

 

Serializable

앞의 Repeatable Read에서 INSERT LOCK을 추가

한 트랜잭션에서 작업 중이면 다른 트랜잭션으 해당 데이터에 접근 불가
매우 단순하면서도 매우 엄격한 격리, 많이 사용하지는 않음
데이터 정합성은 높아지지만 동시 처리성이 굉장히 낮아 대부분 사용하지 않음

 

 

대부분은 Commited Read와 Repeatable Read를 많이 사용


 


오늘의 회고

내일 발표할 하루 10분 CS 스터디의 발표자료를 조사하고 PPT를 만드느라 오후 4시까지 시간을 다 써버렸다.ㅜㅜ
뒤늦게 테스트 코드 특강 녹화본도 보고
Service 클래스의 테스트 코드부터 작성하였다.
생각보다 많이 어렵지는 않고 뭔가 단순 반복 작업같기도 했지만
처음 해보는 거라 설레고 재미있었다.
그래도 처음이라 그런지 내가 모든 코드를 잘 검증하고 있는지 확신이 잘 안들었다.
예외를 던지는 경우와 if문 조건, 반환값 검증, 메서드 수행 여부를 verify()로 검증하는 것 
위주로 코드를 작성하고 있었는데
왠지 생각보다 복잡하지 않아서 뭔가 더 검증해야할 것만 같은 느낌
너무 허무하게 coverage 검사에서 코드에 초록색 줄이 표시되는 것이 아직 내가 모르는 뭔가가 있는 느낌이 들어서
조금 불안했다.
그래도 일단은 원래 제공된 프로젝트 코드의 테스트 샘플 코드와 특강에서 튜터님이 작성하신 코드를 바탕으로 코드를 작성하였다.
저녁에 피곤해서 일찍 잠이 들었다가 정규 시간 끝날 때 즈음 일어나서 밤을 새어버렸다.
중간에 한번 졸려서 앉아서 20분 정도 눈을 붙이긴 했는데
이렇게 밤을 세어버리다니!
그것도 3시 정도부터는 발표 연습하느라 코드는 하나도 작성하지 못했다.
결국 새벽에 발표 전에 샤워하고 간단히 만들어 둔 볶음밥 먹고 후식으로 토마토도 먹고
발표를 무사히 마쳤다.
영상을 찍느라 시간이 많이 소모되었는데 결국 그냥 퀄리티는 포기하고 대충 찍었다.
그래도 발표 연습을 하다보면 모르던 내용도 조금씩 이해가 되는 것이 느껴지고
말도 점점 술술 잘 나오는 것 같아서 좋았다^^
그런데 나중에 과제를 제출할 시간이 없을 것 같아서 과제 제출하고
오늘 잡혀있던 국민취업지원제도 상담을 하기 위해 출발했는데 도무지 약속했던 10시까지 못갈 것 같아서
지하철을 타고 가다가 도중에 죄송하다고 카톡드리고 상담 일정을 취소했다.
상담일정은 10월 11일 금요일로 미루고 대신 도서관에서 반납하지 못했던 책을 반납하러
등빛 도서관에 왔다. 
(어제 못 다 쓴 TIL을 쓰고 있었는데 밤을 세워서 그런지 아침 일정까지 이어지게 작성하고있다;;)
책을 반납하고 점심 시간까지 코드를 작성하다 가려고 도서관 노트북 열람실에 자리를 잡았다.
국민취업지원제도 서류 제출하고
Notion에 오늘 발표한 자료 업로드하고
유튜브 영상 올리고 하다보니 코드는 하나도 작성하지 못했는데 시간이 다 가버렸다.
특히 어제 들은 특강
(시제가 처음엔 13일을 내일로 썼는데 이제는 그냥 오늘로 이야기하고 있네요;;)
내용을 정리하느라 시간이 많이 갔다.
하지만 그만큼 많이 배운 것 같아서 기분이 좋았다.
내용을 정리하면서도 못다한 Controller 테스트 꼭 해보고 싶은데... 하는 생각이 맴돌고 있다. ㅜㅜ
얼른 TIL 마무리 짓고 제출 기한이 넘었지만 혼자서 테스트 코드를 작성해 보아야겠다.
도서관에서 공부하다보니 마우스 소음이 큰 것 같다.
다음에 이 마우스 고장나면 무소음 버티컬 마우스로 사야지!

 

🗨️ 무엇을 새롭게 알았는지

  • 테스트 코드 작성하는 법
  • 트랜잭션 격리 수준



💭 내일 학습할 것은 무엇인지

  • 테스트 코드 작성 완료
  • 개인 과제 해설 영상 보기





👉  다음글







 

728x90