기술 면접(트랜잭션)

유요한·2024년 3월 15일
1

기술면접

목록 보기
25/27
post-thumbnail

트랜잭션이란?

데이터를 저장할 때 단순히 파일을 저장해도 되는데, 데이터베이스에 저장하는 이유가 무엇일까?

여러가지 이유가 있지만 가장 대표적인 이유는 바로 데이터베이스는 트랜잭션이라는 개념을 지원하기 때문이다.

트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다. 그런데 하나의 거래를 안전하게 처리하려면 생각보다 고려해야 할 점이 많다. 예를들어, A의 5000원을 B에게 계좌이체한다고 생각해보면 A의 잔고에서 5000원을 감소하고 B의 잔고에서 5000원을 증가해야 한다. DB의 상태를 변경시키기 위해 수행하는 작업 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미한다.

계좌이체라는 거래는 2가지 작업이 하나의 작업처럼 동작해야 한다. 둘 중 하나의 작업에서 문제가 발생하면 심각한 문제가 발생한다. 데이터베이스의 트랜잭션 기능을 사용하면 둘 다 성공해야 저장하고 하나라도 실패하면 거래 전의 상태로 돌아갈 수 있다. 모든 작업이 성공해서 데이터베이스에 정상 반영하면 커밋(commit)이라고 하고 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백(Rollback)이라고 한다.

데이터베이스의 상태를 변화한다는 것은 SELECT, INSERT, DELETE 같은 SQL문을 이용해 데이터베이스에 접근 하는 것을 뜻한다.

트랜잭션 ACID

  • 원자성(Atomicity)
    트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.

  • 일관성(Consistency)
    모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어, 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.

    무결성 제약 조건

    무결성 제약조건이란 데이터베이스의 정확성, 일관성을 보장하기 위해 저장, 삭제, 수정 등을 제약하기 위한 조건을 뜻합니다. 그래서 데이터베이스를 일관성 있게 유지하기 위해서 입니다.

  • 격리성(Isolation)
    동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어, 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.

    트랜잭션 격리 수준 - Isolation level

    • READ UNCOMMITTED(커밋되지 않은 읽기)
    • READ COMMITTED(커밋된 읽기)
    • REPEATABLE READ(반복 가능한 읽기)
    • SERIALIZABLE(직렬화 가능)
  • 지속성(Durability)
    트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

트랜잭션 매너저와 트랜잭션 동기화 매니저

스프링은 트랜잭션 동기화 매니저를 제공한다. 이것은 쓰레드 로컬을 사용해서 커넥션을 동기화해준다. 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있다. 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.

트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다.

동작 방식 설명

  1. 트랜잭션을 시작하려면 커넥션이 필요하다. 트랜잭션 매니저는 데이터 소스를 통해 커넥션을 만들고 트랜잭션을 시작한다.

  2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션트랜잭션 동기화 매니저저장한다.

  3. 레포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 따라서 파라미터로 커넥션을 전달하지 않아도 된다.

  4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션도 닫는다.

트랜잭션 문제

트랜잭션을 시작하고 비즈니스 로직을 실행하고 성공하면 커밋하고 예외가 발생하면 실패해서 롤백한다. 다른 서비스에서 트랜잭션을 시작하려면 try, catch, finally를 포함한 성공하면 커밋, 실패하면 롤백 코드가 반복될 것이다. 이러한 코드가 각각의 서비스에서 반복된다. 여기서 달라지는 부분은 비즈니스 로직뿐이다.

이럴 때 탬플릿 콜백 패턴을 활용하면 이런 반복 문제를 해결할 수 있다. 하지만 이곳은 서비스 로직인데 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다. 애플리케이션을 구성하는 로직을 핵심 기능과 부가 기능으로 구분하자면 서비스 입장에서 비즈니스 로직은 핵심 기능이고 트랜잭션 부가 기능이다.

이렇게 비즈니스 로직과 트랜잭션을 처리하는 기술 로직이 한 곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다. 결과적으로 유지보수하기 힘들다. 서비스 로직은 가급적 비즈니스 로직만 있어야 한다. 하지만 트랜잭션 기술을 사용하려면 트랜잭션 코드가 나와야 한다. 어떻게 이 문제를 해결할 수 있을까?

이럴 때 AOP를 통해 프록시를 도입하면 문제를 깔끔하게 해결한다. 프록시를 도입하면 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다.

@Transactional

@Transactional은 스프링 프레임워크에서 트랜잭션을 지원하는 데 사용되는 어노테이션입니다. 이 어노테이션을 사용하면 해당 메서드 또는 클래스의 메서드들이 트랜잭션 경계 내에서 실행됩니다. 즉, 여러 개의 연관된 데이터베이스 작업이 하나의 트랜잭션으로 묶여서 실행되거나, 전체가 성공 또는 실패할 때 롤백되는 등의 트랜잭션 처리를 할 수 있습니다. 메서드가 정상적으로 완료되면 트랜잭션은 커밋되고, 예외가 발생하면 롤백됩니다.

@Transactional 어노테이션에서 readOnly 속성을 사용하는 경우가 있는데, 이 속성을 true로 설정하면 해당 트랜잭션이 읽기 전용으로 동작하게 됩니다. 즉, 데이터를 조회하는 쿼리만 수행하고 데이터를 변경하는 쓰기 작업은 허용되지 않습니다. 이렇게 하면 트랜잭션의 격리 수준을 낮출 수 있어 일부 성능 향상을 기대할 수 있습니다.

테스트 환경에서 @Transactional를 추가하면 rollback을 시켜준다. 하지만 일반적으로 @Transactional를 구성하면 메소드를 실행할 때 오류 발생 시 전체 실행 내용을 롤백 시켜준다.

@Transactional은 어떻게 동작할까?

이를 알기 위해서는, SpringBoot의 핵심 개념인 AOP프록시 패턴에 대해서 알아야한다. 트랜잭션은 Spring AOP를 통해 구현되어있다. 더 정확하게 말하면, 어노테이션 기반 AOP를 통해 구현되어있다.

AOP

AOP관점 지향 프로그래밍으로, 애플리케이션의 공통 관심사를 애플리케이션의 핵심 로직으로부터 분리해서 모듈화하는 프로그래밍 패러다임이다. SpringBoot에서는 AOP를 이용하여 선언적 트랜잭션 관리, 즉 @Transactional을 구현한다.

SpringBoot는 AOP를 구현하기 위해 주로 프록시 패턴을 사용한다. 프록시 패턴은 특정 객체에 대한 접근을 제어하거나 추가적인 기능을 제공하기 위해, 해당 객체의 대리인 역할을 하는 객체를 두는 디자인 패턴이다. @Transactional이 붙은 클래스나 메서드를 호출할 때, SpringBoot는 실제 객체 대신 프록시 객체를 사용하여 호출을 가로챈다.

프록시

프록시 객체는 원본 객체를 대신해서 호출될 객체로, 원본 객체를 감싸서 클라이언트의 요청을 처리하는 중간 단계에 위치합니다. 프록시 객체는 원본 객체와 같은 인터페이스를 구현하고 있어서, 클라이언트는 프록시 객체를 호출하는 것으로 인해 원본 객체의 메소드를 호출하는 것과 같은 효과를 얻을 수 있습니다. 프록시 객체를 사용하는 이유는, 프록시 객체를 통해 원본 객체에 대한 접근을 제어하거나, 부가적인 기능을 제공하기 위해서입니다.

AOP를 구현하기 위해서 프록시 객체를 이용하는 이유?

원본 객체에 대한 코드 수정 없이, 런타임 시점에서 프록시 객체를 통해 부가적인 처리를 추가하기 위해서입니다.

프록시 객체는 원본 객체를 감싸고, 클라이언트가 프록시 객체를 호출할 때 원본 객체의 메소드를 호출하기 전에 추가적인 로직을 수행할 수 있도록 도와줍니다. 이렇게 하면 클라이언트는 원본 객체를 호출하는 것처럼 보이지만, 실제로는 프록시 객체가 원본 객체를 감싸고 부가적인 로직을 수행하게 됩니다.

Spring에서 프록시 객체를 생성하기 위해서는 프록시 객체를 생성할 인터페이스를 구현하는 클래스를 만들고, 이 클래스에 부가적인 처리를 수행하는 Advice를 정의한 뒤, Spring AOP가 프록시 객체를 생성하여 클라이언트에게 제공하게 됩니다. 이렇게 함으로써 원본 객체에 대한 수정 없이 부가적인 처리를 적용할 수 있으며, 코드의 재사용성과 유지보수성을 높일 수 있습니다.

@Transactional 동작 과정

  1. @Transactional이 붙은 클래스나 메서드가 있을 때, SpringBoot는 해당 클래스나 메서드를 호출 할 때 트랜잭션을 관리하는 프록시 객체를 생성한다.

  2. Spring AOP를 사용하여 실제 객체의 프록시를 생성한다.

  3. 프록시 객체는 @Transactional이 붙은 메서드가 호출될 때, 트랜잭션 매니저를 사용하여 새로운 트랜잭션을 시작하거나, 이미 진행 중인 트랜잭션에 참여한다.

  4. 메서드 실행 중에 예외가 발생하면, 프록시 객체는 트랜잭션 매니저에 ROLLBACK을 요청하고, 만약 성공적으로 완료되면 COMMIT을 요청한다.

특징

  • 클래스, 메소드에 @Transactional이 선언되면 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성

  • 프록시 객체는 @Transactional이 포함된 메서드가 호출될 경우, 트랜잭션을 시작하고 Commit or Rollback을 수행

  • CheckedException or 예외가 없을 때는 Commit

  • UncheckedException이 발생하면 Rollback

주의점

  • 우선순위

    • @Transactional은 우선순위를 가지고 있다. 클래스 메서드에 선언된 트랜잭션의 우선순위가 가장 높고, 인터페이스에 선언된 트랜잭션의 우선순위가 가장 낮다.

      따라서 공통적인 트랜잭션 규칙은 클래스에, 특별한 규칙은 메서드에 선언하는 식으로 구성할 수 있다. 또한, 인터페이스 보다는 클래스에 적용하는 것을 권고한다.
    • 인터페이스나 인터페이스의 메서드에 적용할 수 있다. 하지만, 인터페이스 기반 프록시에서만 유효한 트랜잭션 설정이 된다.
    • 자바 어노테이션은 인터페이스로부터 상속되지 않기 때문에 클래스 기반 프록시 or AspectJ 기반에서 트랜잭션 설정을 인식 할 수 없다.
  • 트랜잭션의 모드
    @Transactional은 Proxy ModeAspectJ Mode가 있는데 Proxy Mode가 Default로 설정되어있다. Proxy Mode는 다음과 같은 경우 동작하지 않는다.

    • 반드시 public 메서드에 적용되어야한다.
    • Protected, Private Method에서는 선언되어도 에러가 발생하지는 않지만, 동작하지도 않는다.
    • Non-Public 메서드에 적용하고 싶으면 AspectJ Mode를 고려해야한다.
  • @Transactional이 적용되지 않은 Public Method에서 @Transactional이 적용된 Public Method를 호출할 경우, 트랜잭션이 동작하지 않는다.


질문 예상

💡@Transactional의 동작 원리에 대해 설명해주세요.

@Transactional이 붙은 메소드를 호출할 경우 무슨일이 벌어질까?

@Transactional이 클래스 내지 메서드에 붙을 때, Spring은 해당 메서드에 대한 Proxy를 만든다. 프록시 패턴은 디자인 패턴 중 하나로 어떤 코드를 감싸면서 추가적인 연산을 수행하도록 강제하는 방법이다. 트랜잭션의 경우, 트랜잭션의 시작과 연산 종료시의 커밋 과정이 필요하므로 프록시를 생성해 해당 메서드의 앞뒤에 트랜잭션의 시작과 끝을 추가하는 것이다. 또한 스프링 컨테이너는 트랜잭션의 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.

서비스 클래스에서 @Transactional을 사용할 경우 해당 코드 내의 메서드를 호출할 때 영속성 컨텍스트가 생긴다는 뜻이다. 영속성 컨텍스트는 트랜잭션 AOP가 트랜잭션을 시작할 때 생겨나고 메서드가 종료되어 트랜잭션 AOP가 트랜잭션을 커밋할 경우 영속성 컨텍스트 flush되면서 해당 내용이 반영된다. 이후 영속성 컨텍스트 역시 종료되는 것이다.

이러한 방식으로 영속성 컨텍스트를 관리해 주기 때문에 @Transactional을 쓸 경우 트랜잭션의 원칙을 정확히 지킬 수 있다.

주의할점은 만약 같은 트랜잭션 내에서 여러 EntityManager를 쓰더라도 이는 같은 영속성 컨텍스트를 사용하고 같은 EntityManager를 쓰더라도 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.

💡@Transactional를 스프링 Bean의 메소드 A에 적용하였고, 해당 Bean의 메소드 B가 호출되었을 때, B 메소드 내부에서 A 메소드를 호출하면 어떤 요청 흐름이 발생하는지 설명해주세요.

@Transactional을 메소드 또는 클래스에 명시하면, AOP를 통해 Target이 상속하고 있는 인터페이스 또는 Target 객체를 상속한 Proxy 객체가 생성되며, Proxy 객체의 메소드를 호출하면 Target 메소드 전 후로 트랜잭션 처리를 수행합니다. @Transactional은 클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션이 되도록 보장해준다.

선언적 트랜잭션이라고도 하는데, 직접 객체를 만들 필요 없이 선언만으로도 관리를 용이하게 해주기 때문이다. 특히나 SpringBoot에서는 선언적 트랜잭션에 필요한 여러 설정이 이미 되어있는 탓에, 더 쉽게 사용할 수 있다.

프록시는 클라이언트가 타겟 객체를 호출하는 과정에만 동작하며, 타겟 객체의 메소드가 자기 자신의 다른 메소드를 호출할 때는 프록시가 동작하지 않습니다. 즉, A 메소드는 프록시로 감싸진 메소드가 아니므로 트랜잭션이 적용되지 않은 일반 코드가 수행됩니다.

💡@Transactional에 readOnly 속성을 사용하는 이유에 대해서 설명해주세요.

트랜잭션 안에서 수정/삭제 작업이 아닌 ReadOnly 목적인 경우에 주로 사용하며, 영속성 컨텍스트에서 엔티티를 관리 할 필요가 없기 때문에 readOnly를 추가하는 것으로 메모리 성능을 높일 수 있고, 데이터 변경 불가능 로직임을 코드로 표시할 수 있어 가독성이 높아진다는 장점이 있습니다.

readOnly 속성이 없는 보통의 트랜잭션은 데이터 조회 결과 엔티티가 영속성 컨텍스트에 관리되며, 이는 1차 캐싱부터 변경 감지(Dirty Checking)까지 가능하게 된다. 하지만, 조회시 스냅샷 인스턴스를 생성해 보관하기 때문에 메모리 사용량이 증가한다.

이거와 비슷한 개념인 JPA Hint가 있습니다.

JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)

    @QueryHints(value =  @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    MemberEntity findReadOnly(String userName);

Hint는 스냅샷을 만들지 않기 때문에 메모리가 절약됩니다. 즉, 읽기 전용입니다. 이거와 비슷한 것이 @Transaction(readOnly=true)입니다. @Transaction(readOnly=true)는 트랜잭션 커밋 시점에 flush를 하지 않기 때문에 이로 인한 dirty checking 비용이 들지 않습니다. 따라서 cpu가 절약됩니다.

스프링 5.1 버전 이후를 사용하시면 @Transaction(readOnly=true)로 설정하시면, @QueryHint의 readOnly까지 모두 동작합니다. 즉, @Transaction(readOnly=true)로 대체해서 사용하면 됩니다.

💡A라는 Service 객체의 메소드가 존재하고, 그 메소드 내부에서 로컬 트랜잭션 3개(다른 Service 객체의 트랜잭션 메소드를 호출했다는 의미)가 존재한다고 할 때, @Transactional을 A 메소드에 적용하면 어떤 요청 흐름이 발생하는지 설명해주세요.

트랜잭션 전파 수준에 따라 달라지는데, 만약 기본 옵션인 Required를 가져간다면 로컬 트랜잭션 3개가 모두 부모 트랜잭션인 A에 합류하여 수행됩니다. 그래서 부모 트랜잭션이나 로컬 트랜잭션 3개나 모두 같은 트랜잭션이므로 어느 하나의 로직에서 문제가 발생하면 전부 롤백이 됩니다.

💡Spring 프레임워크에서 제공하는 7가지 트랜잭션 레벨에 대해 설명해주세요

Spring 트랜잭션의 전파레벨은 다음과 같습니다.

  1. Propagation.REQUIRED
  • 디폴트 속성으로, 모든 트랜잭션 매니저가 지원합니다.
  • 해당 메서드를 호출한 곳에서 별도의 트랜잭션이 설정되어 있지 않다면 트랜잭션을 새로 시작
  • 부모 트랜잭션이 존재하면 포함되어 동작
  1. Propagation.SUPPORTS
  • 부모 트랜잭션이 존재하면 포함되어 동작
  • 부모 트랜잭션이 없다면 트랜잭션 없이 동작
  • 해당 경계 안에서 Connection 객체나 하이버네이트의 Session 등은 공유 가능
  1. Propagation.MANDATORY
  • 부모 트랜잭션이 존재하면 포함되어 동작
  • 부모 트랜잭션이 없다면 예외 발생
  • 혼자서 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용
  1. Propagation.REQUIRES_NEW
  • 부모 트랜잭션 존재 여부와 상관없이 트랜잭션을 새로 생성
  • 만약 부모 트랜잭션이 존재하면 기존 트랜잭션은 잠시 대기시키고 자신의 트랜잭션을 실행
  • 새로운 트랜잭션 안에서 예외가 발생해도 호출한 곳으로 롤백이 전파되지 않는다
  • 부모 트랜잭션이 존재하면 2개의 트랜잭션이 완전 독립적으로 동작
  1. Propagation.NOT_SUPPORTED
  • 트랜잭션을 사용하지 않는다.
  • 부모 트랜잭션이 존재하면 보류시키고 트랜잭션 사용을 정지시킨다
  1. Propagation.NEVER
  • 트랜잭션 사용을 강제로 금지시킨다.
  • 부모 트랜잭션이 존재할 경우 예외를 발생 시킨다
  1. Propagation.NESTED
  • 부모 트랜잭션이 존재하면 부모 트랜잭션 안에 다시 트랜잭션을 만든다
  • 부모 트랜잭션의 커밋과 롤백에 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에 영향을 주지 않는다
  • 예시) 메인작업을 진행하며 이와 관련된 로그를 DB에 저장
  • 메인 작업이 실패 -> 로그 작업 롤백
  • 로그를 저장하는 작업이 실패 -> 메인 작업의 트랜잭션은 롤백 X
  • 모든 트랜잭션 매니저에 적용 가능하진 않다.
profile
발전하기 위한 공부

0개의 댓글