본문 바로가기
스터디정리방

[기록] 211229

by gogumi 2022. 1. 2.

1. 트랜잭션

더 이상 나눌 수 없는 일련의 단위 작업을 뜻한다.

대표적인 예로 계좌이체를 많이 말한다.

계좌이체를 예를 보면 다음과 같은 순서로 작업이 일어난다.

  1. 계좌이체하고자 하는 금액 입력
  2. 계좌에 뽑고자하는 금액보다 많은 금액이 들어있는지 확인
  3. 들어있다면 출금
  4. 송금한 계좌에 입금된 만큼 +

2. ACID

ACID는 트랜젝션의 특징들의 앞글자를 딴 단어이다.

Atomicity(원자성)

트랜잭션을 구성하는 1~4번이 모두 수행되거나 모두 수행되지 않아야 하는 특성을 말한다. 이는 DB에 대해서 알아야 하는데 DB에서 자동으로 이전에 commit 된 상태를 임시 영역인 rollback segment 에 저장을 해놓고 장애가 발생할 떄 rollback segment 에 저장해놓았던 상태로 rollback을 하게 되는 것이다.

Consistency(일관성)

트랜잭션이 성공적으로 완료가 되었으면 일관적인 DB 상태를 유지해야 한다는 것이다. 트랜잭션 수행 전, 후에 데이터 모델의 모든 제약 조건을(기본키, 외래키, 도메인 등)을 만족하는 것을 통해 보장되어야 한다.

데이터베이스 내의 계층관계, 컬럼의 데이터 속성 등이 항상 일관되게 유지되어야 한다.

예를들어, 만약 데이터베이스의 속성이 수정되었다면 trigger를 통해 일괄적으로 모든 데이터데이스에 적용되어야 한다.

Durability(지속성)

한번 반영(커밋)된 트랜젝션의 내용은 영원히 적용되는 특성을 의미한다. 시스템에 장애가 발생하더라도 트랜잭션이 성공적으로 완료되어 커밋까지 되었으면 작업결과는 데이터베이스에 그대로 반영이 되어있어야 합니다.

Isolation(독립성)

트랜잭션 수행 시 다른 트랜잭션의 작업이 끼어들지 못하도록 보장하는 것이다.

즉, 동시성 제어가 필요하다는 것이다.

예를들면, 통장에 잔고가 5만원인 A에게 B와 C가 모두 1만원을 송금한다고 가정해보자.

B와 C가 둘 다 1만원씩 보냈으니 A의 잔액은 7만원이 되어야 하지만 트랜잭션의 독립성이 보장되지 않는다면 동시에 접근했을 때 A의 잔액을 B와 C가 모두 5만원으로 조회를 해 둘 다 6만원으로 업데이트를 치는 경우가 발생할 수 있다. (날라가버린 만원)

트랜잭션은 격리 수준 설정을 통해서 독립성을 보장하고 있다.

🦼 무결성이란 데이터베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 의미한다

3. 스프링에서의 트랜잭션 추상화

스프링은 트랜잭션 기술의 공통점을 담는 트랜잭션 추상화 기술들을 제공하고 있다.

이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API 를 이용하지 않고도, 일관된 방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.

기존의 트랜잭션을 서비스에서 이용하면 서비스에서 의존도가 발생하게 되어 OCP 를 제대로 지키지 못하게 된다. 

PlatformTransactionManager

  • getTransaction, commit, rollback 세 가지의 메소드를 가짐

getTransaction()의 파라미터인 TransactionDefinition

  • 트랜잭션의 프로퍼티를 관리한다
  • ex) 전파(propagation) , 타임아웃(timeout), 격리수준(isolation level), 읽기 속성 (read-only)

TransactionStatus 

  • 트랜잭션의 실행 제어를 관리한 다른 트랜잭션 결과를 설정하거나 트랜잭션의 완성 여부나 새 트랜잭션인지의 여부를 확인하는 데 사용
  • PlatformTransactionManager.getTransaction(TransactionDefinition a)을 통해 반환되는 값
public interface TransactionStatus extends SavepointManager {

  boolean isNewTransaction();
  boolean hasSavepoint();
  void setRollbackOnly();
  boolean isRollbackOnly();
  void flush();
  boolean isCompleted();
}

4. Transaction 속성

getTransaciont()의 파라미터인 TransactionDefinition —> 트랜잭션의 속성을 담고있는 정보

 

1. 트랜잭션 전파

  • 트랜잭션 전파란 트랜잭션 경계에서 이미 진행 중인 트랜잭션이 있거나 없을 때 동작할 것인가를 결정하는 방식을 말한다. 트랜잭션 전파 속성으로는 다음과 같이 정의할 수 있다.
    • PROPAGATION_REQUIRED
      • 가장 많이 사용되는 전파 속성으로 진행중인 트랜잭션이 없으면 새로 시작하고 이미 시작된 트랜잭션이 있으면 이에 참여한다.
    • PROPAGATION_REQUIRES_NEW
      • 항상 새로운 트랜잭션을 시작한다.
      • 앞에서 시작된 트랜잭션이 있든 없은 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작하게 한다.
      • 트랜잭션 1이 실행되고 있는 도중에 해당 속성을 가진 트랜잭션 2가 실행되면 트랜잭션 1을 보류한 상태로 트랜잭션 2를 실행한다.
    • PROPAGATION_SUPPORTS
      • 진행중인 트랜잭션이 있을 경우 REQUIRED 처럼 참여하고, 트랜잭션이 없을 경우 트랜잭션 없이 메소드 실행한다.
    • PROPAGATION_NOT_SUPPORTED
      • 트랜잭션 없이 동작할 수 있도록 한다.
      • 진행중인 트랜잭션이 있어도 무시한다.

이 외에도 NEVER, NESTED 등 전파 속성이 있다.

 

2. 격리수준(isolation)

동시에 여러 트랜잭션이 실행될 경우 다른 트랜잭션에게 트랜잭션의 작업내역을 보여줄 지 말지를 결정하는 설정으로 가능하면 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않도록 하기 위해 격리수준 설정이 필요하다.

  • 기본적으로 DB에 설정이 되어있지만 재설정 할 수 있음
    • READ_UNCOMMITED (Level 0) : 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있음
    • READ_COMMITED (Level 1) : 커밋되지 않은 정보는 읽을 수 없음
    • REPEATABLE_READ(Level 2) : 하나의 트랜잭션이 읽은 row를 다른 트랜잭션이 수정할 수 없음
    • SERIALIZABLE(Level 3) : 이 속성을 가진 트랜잭션이 존재할 시 동시에 같은 테이블의 정보에 접근할 수 없음
    • DEFAULT : 사용하는 DB 기본 설정을 따른다.

3. 제한 시간

  • 트랜잭션을 수행하는 제한시간을 설정할 수 있다.

4. 읽기전용

  • 읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다.

5. 서비스 추상화

추상화란 하위 시스템의 공통점을 뽑아내서 분리시키는 것을 말한다. 그렇게 하면 하위 시스템이 어떤 것인지
알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수가 있다.

6. Enum

이늄이란 Eumeration 으로 열거형이라고 불리며, 서로 연관된 상수들의 집합을 의미한다.

values() 메소드는 enum안에 존재하는 모든 값들을 반환한다.

enum Color {
  RED, GREEN, BLUE
}

public class Test{
    public static void main(String[] args) {

        Color arr[] = Color.values();

        for (Color color : arr) {
            System.out.println(color + " at index " + color.ordinal());
        }

        System.out.println(Color.valueOf("RED"));
    }
}

/*** 출력 
RED at index 0
GREEN at index 1
BLUE at index 2
RED
**/

-enum Method 종류
toString
name (toString + final 의 역할)
ordinal - 인덱스를 나타내며 사용하지 말것
valueOf
equals
hashCode
clone
compareTo
getDeclaringClass
finalize

7. 트랜잭션

트랜잭션이란 데이터베이스 연산들의 논리적 단위이며 트랜잭션 내 모든 연산들이 정상적으로 완료되지 않으면 아무 것도 수행되지 않은 원래 상태로 복원되어야 한다.

 

- 트랜잭션 격리 수준

1. 트랜잭션 격리 수준(Isolation Level)
 - 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준 

2. 필요성
 - 데이터베이스는 ACID(트랜잭션 특징) 같이 원자적이면서도 독립적인 수행을 하도록 한다.
 - 그래서 Locking 이라는 개념이 등장한다.
 - 하지만 무조건적인 Locking으로 동시에 수행되는 많은 트랜잭션들을 순서대로 처리하는 방식은 성능이 떨어진다.
 - 그래서 최대한 효율적인 Locking 방법이 필요하다.

3. Isolation Level 종류
 1) Read Uncommitted (레벨0) - 커밋되지 않는 읽기
  - 트랜잭션 A가 특정 컬럼 데이터를 변경하고 있을 때(커밋하지 않은 상태) 트랜잭션 B가 read하면 트랜잭션 A가 변경한 데이터를 읽어온다.
  - 커밋되지 않는 읽기는 dirty read 문제가 있다. (트랜잭션 A가 특정 컬럼 데이터를 변경하고 롤백 했을 때 발생)
  - 데이터의 일관성을 유지할 수 없다.

 2) Read Committed (레벨1) - 커밋된 읽기
  - 트랜잭션 A가 특정 컬럼 데이터를 변경하고 있을 때(커밋하지 않은 상태) 트랜잭션 B가 read하면 트랜잭션 A가 변경하기 전 데이터를 읽어온다. 
  만약 트랜잭션 A가 데이터 변경 후 커밋하게 되면 트랜잭션 B는 변경된 데이터를 읽어온다.

 3) Repeatable Read (레벨2) - 반복 가능한 읽기
  - 항상 일관성 있는 데이터 읽기를 보장하는 레벨
  - 다른 트랜잭션에서 데이터를 조작하여도 영향을 받지 않는다.

 4) Serializable (레벨3) - 직렬화 가능
  - 가장 높은 격리 수준
  - 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 수정 및 입력이 불가능하다.

* DB 별 default isolation

 - MYSQL : REPEATABLE READ
 : 트랜잭션이 시작되기 전(쿼리가 시작되기 전이 아닌) commit 된 결과를 참조합니다.
 
 - ORACLE : READ COMMITTED 
 : 쿼리가 시작되기 전 다른 트랜잭션에서 commit 된 결과를 참조할 수 있으며 동일 트랜잭션 상에서 동일한 쿼리문에 대해 서로 다른 결과를 조회(Phantom-Read)할 수 있습니다.

 - MSSQL : READ COMMITTED 
 : (WITH(NOLOCK) 으로 select 할 경우의 격리수준(Isolation Level)은 Read Uncommitted)
 

 

-트랜잭션 전파 옵션

1. 전파 옵션
 1) REQUIRED
   - 기본 속성. 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성한다.

 2) SUPPORTS
   - 이미 시작된 트랜잭션이 있으면 참여하고 이외에는 트랜잭션 없이 진행하게 만든다.

 3) REQUIRES_NEW
   - 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성된다.

 4) MANDATORY
  - REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다.
  - 트랜잭션이 없을 경우에는 예외를 발생한다.
  - 혼자서는 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용한다.

 5) NOT_SUPPORTED
  - 트랜잭션을 사용하지 않게 한다.
  - 이미 진행 중인 트랜잭션이 있으면 보류 시킨다.

 6) NAVER
  - 트랜잭션을 사용하지 않도록 강제한다.
  - 이미 진행 중인 트랜잭션도 존재하면 안되며 있을 경우 예외를 발생시킨다.

 7) NESTED
  - 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다.
  - 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에 영향을 받지만, 자신의 커밋과 롤백은 부모 트랜잭션에 영향을 미치지 않는다.

8. Test Double 

테스트계의 스턴트맨이다. 

영화에서 배우대신 위험한 액션을 대역해 주는 사람을 한국에서는 스턴트맨이라고 한다. 영어권에서 Stunt Double이라고 부른다. 테스트를 목적으로 대역을 쓰는 것이 Test Double이라고 한다. 

대역을 쓰는 것에 착안하여 이름을 지었다고 한다. 

9. Mock 

스마트폰을 파는 곳에 가면 전시용 폰이 존재한다. Mock up. 즉, 가짜를 말한다. 실제와 동일한 기능을 하지는 않지만 대략적인 생김새와 크기, 대충 이런 기능이 이렇게 동작할 것이라고 알려주는 용도다.  

테스트에서는 호출 시 동작이 잘 되었는지 확인하는데 쓰인다. 

실제 객체를 만들어 사용하기에 Cost가 높거나 혹은 객체 서로 간의 의존성이 강해 구현하기 힘들 경우 가짜 객체를 만들어 사용하는 방법이다.

10. Stub 

Stub이라는 단어가 내포하는 바는 전체 중 일부라는 뜻이다. 

테스트에서 모든 기능 대신 테스트를 하고자 하는 일부 기능에 집중하여 임의로 구현하는 것을 Stub이라고 한다. 

어떤 행위가 호출됐을 때 특정 값으로 리턴 해주는 형태가 Stub이라고도 한다.

11. Spy 

스파이는 몰래 잠입하여 훔쳐보고 기록하며 때로는 비밀 미션을 수행하기도 한다. 또 어떨 때는 그 소속인양 흉내내기도 한다. 테스트에도 이런 것이 있다. 그것이 Test Spy다. 

12. 언제 Stub을 쓰고, 언제 Mock을 써야 하나? 

Mock 테스트 (행위 검증, behavior verification) 

테스트의 출력/결과에 집중하는가? 

  • 정상적으로 호출되었는지가 더 중요한지? 

Stub 테스트 (상태 검증, state verification) 

테스트의 입력에 집중하는가? 

  • 그 입력 값에 따라 리턴 결과값을 비교하는지? 
  • 그 입력 값에 따라 exception이 발생하는지? 

Spy 테스트 

일종의 Stub이면서 Mock에서 할 수 있는 행위 기반 테스트도 지원 가능하다.

13. Mock에 대한 분류와 개념

1. 테스트 더블(Test Double)

테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체를 말한다. 포괄적인 의미로 사용된다. 

 

2. 더미 객체(Dummy Object) 

단순히 인스턴스화 될 수 있는 수준으로만 구현한 객체를 말한다.

해당 객체의 기능까지는 필요하지 않은 경우에 사용한다. 

 

3. 테스트 스텁(Test Stub) 

특정 값을 리턴 해주거나 특정 메시지를 출력해 주는 작업을 한다. 

더미 객체보다 좀 더 구현된 객체로 마치 더미 객체가 실제로 동작하는 것처럼 보이게 만들어 놓은 객체이다. 

특정 상태를 가정해서 하드코딩 된 형태이기 때문에 로직에 따른 값의 변화는 테스트할 수 없다. 

 

4. 페이크 객체(Fake Object) 

여러 상태를 대표할 수 있도록 구현된 객체로 실제 로직이 구현된 것처럼 보이게 한다. 

실제로 DB에 접속해서 비교할 때와 동일한 모양이 보이도록 객체 내부에 구현할 수 있다. 

테스트 케이스 작성을 위해서 다른 객체들과 의존성을 제거하기 위해 사용한다. 

페이크 객체를 만들 때 복잡도로 인해서 노력이 많이 들어갈 경우 적절한 수준에서 구현하거나 Mock 프레임 워크를 사용한다. 

페이크 객체를 생성하기 위한 노력이 많이 필요할 경우 실제 객체를 가져와 테스트한다. 

 

5. 테스트 스파이(Test Spy) 

테스트에 사용되는 객체, 메소드의 사용 여부 및 정상 호출 여부를 기록하고 요청 시 알려준다. 

테스트 더블로 구현된 객체에 자기 자신이 호출되었을 때 확인이 필요한 부분을 기록하도록 구현한다. 

 

6. 모의 객체(Mock Object) 

행위를 검증하기 위해 사용되는 객체를 지칭하며 수동으로 만들 수도 있고, 프레임워크를 통해 만들 수도 있다. 

행위 기반 테스트는 복잡도나 정확성 등 작성하기 어려운 부분이 많기 때문에 상태 기반 테스트가 가능하다면 만들지 않는다. 

모의 객체는 테스트 더블 하위 객체로서 좁은 의미와 테스트 더블을 포함한 넓은 의미 2가지로 사용될 수 있다. 

 

*Stub 

  • 호출이 되면 미리 준비된 답변으로 응답하는 것 
  • 테스트 시에 프로그램 된 것 외에는 응답하지 않는다. 
  • 일반적으로 Mock으로 잘못 알고 있다. 

*Mock 

  • 다른 테스트 더블과는 다르게 행위 검증 사용을 추구한다.

14. 테스트 스텁과 목 오브젝트

테스트 대역(Test Double)
테스트 하려는 객체가 다른 애들이랑 여러 관계가 엮여있어 사용하기 어려울 때, 대체할 수 있는 객체를 말한다. Dummy, Stub, Spy, Mock, Fake로 나눌 수 있다.

Stub

인스턴스화하여 구현한 가짜 객체를 이용해 실제로 동작하는 것처럼 "보이게" 만드는 객체.
그래서 기능을 구현하지 않고 해당 인터페이스나 클래스를 최소한으로 구현한다고 한다.
테스트에서 호출된 요청에 대해 미리 준비해둔 결과를 전달한다.

 

Mock

호출에 대한 기대를 명세하고, 내용에 따라 동작하도록 프로그래밍 된 객체.
테스트 작성을 위한 환경 구축이 어려울 때, 테스트하고자 하는 코드와 엮인 객체들을 대신하기 위해 만들어진 객체이다.

 

Stub과 Mock의 차이점
목과 스텁은 쉽게 혼동되지만, 목 오브젝트는 행위 검증(behavior verification)을 사용하고, stub을 포함한 다른 대역들은 상태 검증(state verification)을 사용한다.

*상태 검증이란 메소드가 수행된 후, 객체의 상태를 확인하여 올바르게 동작했는지를 확인하는 검증법이다.

SomeClass someClass = new SomeClass();
someClass.someMethod();

assertThat(someMethod.someStatus()).isEqualTo(true);

somemethod의 someStatus가 기대하는 값 ture인지 assertThat을 통해 올바른 상태 값이 나오는지 체크.


*행위 검증이란 메소드의 리턴 값으로 판단할 수 없는 경우 특정 동작을 수행하는지 확인하는 검증법이다.

SomeClass someClass = new SomeClass();

verify(someClass).someMethod();

값을 구체적으로 비교하지 않지만 목 오브젝트의 지원을 받아 별도의 코드 없이 메소드 호출 여부 등을 간단하게 검사.
특정 동작이 수행되었는지 여부를 통해 로직 수행에 대한 판단을 내린다.
verify는 Mockito에서 지원하는 함수로 특정 메소드가 호출되었는지 검증하는 함수이다.

15. 테스트 대역의 예시

더미

@ExtendWith(MockitoExtension.class)
public class UserCreateTest { // 사용자 회원가입 테스트 클래스

    // User 관련 SQL Mapper의 모의(Mock) 객체
    // 모의(Mock) 객체는 아래 이어서 설명할테니 우선 넘어가기 바란다.
    @Mock
    private UserMapper userMapper; 

    // User 관련 서비스 구현체에 User 관련 SQL Mapper를 주입
    @InjectMocks
    private UserServiceImpl userServiceImpl;

    // UserDTO 클래스의 모든 필드가 들어간 객체
    private UserDTO UserDTOAllField;

    // 매 테스트 메서드 실행 직전에 init() 메서드가 실행됨
    @BeforeEach
    void init() {
        // 객체에 모든 필드를 추가하자
        UserDTOAllField = UserDTO.builder().
                loginId("loginid123").
                name("황사이다").
                birthDate(LocalDate.of(2000,11,11)).
                sex(UserDTO.Sex.MALE).
                password("비1밀2번3호").
                nickname("닉네임123이다").
                phoneNumber("01012345678").
                build();
    }
    
    @Test
    @DisplayName("모든 UserDTO 데이터가 입력된 경우 유저 등록 성공")
    void SuccessUserCreateIfAllFieldInserted() {
        // UserDTOAllField Dummy 객체가 addUser 메서드 파라미터에 사용됨
        assertDoesNotThrow(() -> userServiceImpl.addUser(UserDTOAllField));
    }
}

가짜

public interface UserRepository {
    void save(UserDTO user);
    User findById(long id);
}

public class FakeUserRepository implements UserRepository {
    
    // 메모리를 데이터베이스 역할로 활용한다.
    private ArrayList<UserDTO> users = new ArrayList<>();
    
    @Override
    public void save(UserDTO user) {
        if (findById(user.getId()) == null) {
            user.add(user);
        }
    }
    
    @Override
    public User findById(long id) {
        for (UserDTO user : users) {
            if (user.getId() == id) {
                return user;
            }
        }
        return null;
    }
}

테스트 스텁

public class StubUserRepository implements UserRepository {

    // Fake 에서 설명한 UserRepository 인터페이스의 findById 메서드이다.
    @Override
    public User findById(long id) {
        return new UserDTO(id, "beststar");
    }
}

테스트 스파이 

public class MailingService {
    private int sendMailCount = 0;
    private Collection<Mail> mails = new ArrayList<>();

    public void sendMail(Mail mail) {
        sendMailCount++;
        mails.add(mail);
    }

    public long getSendMailCount() {
        return sendMailCount;
    }
}

모의

@ExtendWith(MockitoExtension.class)
public class UserCreateTest {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserServiceImpl userServiceImpl;

    private UserDTO UserDTOAllField;

    @BeforeEach
    void init() {
        UserDTOAllField = UserDTO.builder().
                loginId("loginid123").
                name("황사이다").
                birthDate(LocalDate.of(2000,11,11)).
                sex(UserDTO.Sex.MALE).
                password("비1밀2번3호").
                nickname("닉네임123이다").
                phoneNumber("01012345678").
                build();
    }


    @Test
    @DisplayName("아이디 중복으로 인한 유저 등록 실패")
    public void FailToUserCreateIfDuplicateLoginId() throws Exception {
    
        // findUserByLoginId() 메서드가 UserDTOAllField를 반환할지 결정하는 코드이다.
        given(userServiceImpl.findUserByLoginId("loginid123")).willReturn(UserDTOAllField);

        assertThrows(DuplicateKeyException.class,
            () -> {
                userServiceImpl.addUser(UserDTOAllField);
            }
        );
    }
}

 

 

References

서비스 추상화 정리
우아한형제들 Java Enum 활용기
이펙티브자바 item34 - int 상수 대신 열거 타입을 사용하라
Enum예제
java11 oracle document
트랜잭션 격리 수준
트랜잭션 전파 옵션
MSSQL with(nolock)
Read committed VS Repeatable read

상태검증과 행위검증

https://beststar-1.tistory.com/29

Crocus 

'스터디정리방' 카테고리의 다른 글

[기록] 220112  (0) 2022.01.16
[기록] 220105  (0) 2022.01.09
[기록] 211222  (0) 2021.12.26
[기록] 211215  (0) 2021.12.18
[기록] 211208  (0) 2021.12.02

댓글