Notice
Recent Posts
Recent Comments
Link
250x250
반응형
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

백고등어 개발 블로그

데이터베이스 트랜잭션 면접 질문 TOP 10 - 핵심 정리 본문

면접

데이터베이스 트랜잭션 면접 질문 TOP 10 - 핵심 정리

백고등어 2025. 9. 16. 16:50
728x90
반응형

데이터베이스 트랜잭션은 백엔드 개발에서 가장 중요한 개념 중 하나입니다. 데이터의 일관성과 무결성을 보장하는 핵심 메커니즘이죠. 면접에서도 자주 나오는 주제이므로 핵심 개념들을 확실히 정리해보겠습니다.

1. 트랜잭션이란 무엇이고 왜 필요한가요?

**트랜잭션(Transaction)**은 데이터베이스에서 수행되는 작업의 논리적 단위입니다. 여러 개의 연산을 하나의 단위로 묶어서 모두 성공하거나 모두 실패하도록 보장하는 메커니즘이에요.

은행 계좌 이체 예시:

  1. A 계좌에서 10만원 출금
  2. B 계좌에 10만원 입금

이 두 작업은 반드시 함께 성공하거나 함께 실패해야 합니다. 만약 1번만 성공하고 2번이 실패하면 돈이 사라지는 심각한 문제가 발생하죠.

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100000 WHERE id = 'B';
COMMIT; -- 모든 작업이 성공하면 커밋

트랜잭션이 없다면 시스템 장애나 네트워크 문제로 인해 데이터가 일관성 없는 상태가 될 수 있어요.

2. ACID 속성을 설명해주세요.

트랜잭션이 보장해야 하는 4가지 속성을 ACID라고 합니다.

원자성(Atomicity): All or Nothing

  • 트랜잭션의 모든 연산이 완전히 수행되거나 전혀 수행되지 않아야 함
  • 중간에 실패하면 이미 실행된 작업들도 모두 취소(롤백)

일관성(Consistency): 데이터 무결성 유지

  • 트랜잭션 실행 전후에 데이터베이스가 일관된 상태를 유지해야 함
  • 제약 조건, 규칙 등이 위반되지 않아야 함

격리성(Isolation): 동시 실행되는 트랜잭션들이 서로 영향을 주지 않음

  • 여러 트랜잭션이 동시에 실행되어도 순차 실행한 것과 같은 결과

지속성(Durability): 영구 저장

  • 커밋된 트랜잭션의 결과는 시스템 장애가 발생해도 유지되어야 함

식당 주문으로 비유하면, 원자성은 "주문한 모든 음식이 다 나오거나 아예 안 나오거나", 일관성은 "재료가 부족하면 주문을 받지 않기", 격리성은 "다른 테이블 주문과 섞이지 않기", 지속성은 "주문서를 안전하게 보관하기"와 같습니다.

3. 트랜잭션 격리 수준(Isolation Level)을 설명해주세요.

격리 수준은 동시에 실행되는 트랜잭션들 사이의 격리 정도를 나타냅니다. 성능과 일관성 사이의 트레이드오프가 있어요.

READ UNCOMMITTED (레벨 0):

  • 커밋되지 않은 데이터도 읽을 수 있음
  • Dirty Read 발생 가능: 다른 트랜잭션이 롤백할 수 있는 데이터를 읽음

READ COMMITTED (레벨 1):

  • 커밋된 데이터만 읽을 수 있음
  • Non-Repeatable Read 발생 가능: 같은 쿼리를 두 번 실행했을 때 결과가 다름

REPEATABLE READ (레벨 2):

  • 트랜잭션 시작 시점의 데이터를 일관성 있게 읽음
  • Phantom Read 발생 가능: 범위 조건으로 조회할 때 새로운 행이 나타남

SERIALIZABLE (레벨 3):

  • 가장 엄격한 격리 수준
  • 모든 이상 현상을 방지하지만 성능이 가장 낮음
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateUser(Long userId, String name) {
    User user = userRepository.findById(userId);
    user.setName(name);
    userRepository.save(user);
}

4. 교착상태(Deadlock)란 무엇이고 어떻게 해결하나요?

교착상태는 두 개 이상의 트랜잭션이 서로 상대방이 점유한 자원을 기다리면서 무한히 대기하는 상황입니다.

교착상태 발생 예시:

-- 트랜잭션 A
BEGIN;
UPDATE users SET name='A' WHERE id=1;  -- users 테이블의 1번 행 잠금
UPDATE posts SET title='A' WHERE id=1; -- posts 테이블의 1번 행 잠금 대기

-- 트랜잭션 B (동시 실행)
BEGIN;
UPDATE posts SET title='B' WHERE id=1; -- posts 테이블의 1번 행 잠금
UPDATE users SET name='B' WHERE id=1;  -- users 테이블의 1번 행 잠금 대기

해결 방법:

  1. 예방: 모든 트랜잭션이 같은 순서로 자원에 접근
  2. 감지 및 회복: 데이터베이스가 교착상태를 감지하고 하나의 트랜잭션을 강제로 롤백
  3. 타임아웃: 일정 시간 대기 후 자동으로 롤백
@Transactional(timeout = 30) // 30초 후 타임아웃
public void updateUserAndPost(Long userId, Long postId) {
    // 항상 같은 순서로 자원에 접근
    userRepository.findById(userId);
    postRepository.findById(postId);
}

5. 트랜잭션 전파(Propagation) 속성을 설명해주세요.

트랜잭션 전파는 이미 시작된 트랜잭션이 있을 때 새로운 트랜잭션을 어떻게 처리할지 결정하는 속성입니다.

REQUIRED (기본값):

  • 기존 트랜잭션이 있으면 참여, 없으면 새로 생성
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    methodB(); // 같은 트랜잭션에서 실행
}

REQUIRES_NEW:

  • 항상 새로운 트랜잭션을 생성
  • 기존 트랜잭션은 잠시 중단
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 독립적인 새 트랜잭션에서 실행
}

SUPPORTS:

  • 기존 트랜잭션이 있으면 참여, 없어도 실행
@Transactional(propagation = Propagation.SUPPORTS)
public void methodC() {
    // 트랜잭션이 있든 없든 실행
}

NEVER:

  • 트랜잭션이 있으면 예외 발생
@Transactional(propagation = Propagation.NEVER)
public void methodD() {
    // 트랜잭션 내에서 호출하면 예외 발생
}

6. 낙관적 락과 비관적 락의 차이점은 무엇인가요?

비관적 락(Pessimistic Lock):

  • 데이터를 수정하기 전에 미리 락을 걸어서 다른 트랜잭션의 접근을 차단
  • 충돌이 자주 발생할 것으로 예상되는 상황에 적합
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findByIdWithLock(@Param("id") Long id);

낙관적 락(Optimistic Lock):

  • 데이터를 읽을 때는 락을 걸지 않고, 업데이트할 때 데이터가 변경되었는지 확인
  • 충돌이 드물게 발생하는 상황에 적합
@Entity
public class User {
    @Id
    private Long id;
    
    @Version
    private Long version; // 버전 필드로 낙관적 락 구현
    
    private String name;
}

사용 시나리오:

  • 비관적 락: 은행 계좌 잔고 업데이트, 재고 관리
  • 낙관적 락: 게시글 수정, 사용자 프로필 업데이트

7. 트랜잭션 롤백은 언제 발생하나요?

자동 롤백 조건:

  • RuntimeException이나 Error 발생 시
  • 체크드 예외는 기본적으로 롤백되지 않음
@Transactional
public void createUser(User user) {
    userRepository.save(user);
    if (user.getEmail() == null) {
        throw new IllegalArgumentException("이메일 필수"); // 롤백됨
    }
}

@Transactional(rollbackFor = Exception.class)
public void createUserWithChecked(User user) throws Exception {
    userRepository.save(user);
    if (user.getEmail() == null) {
        throw new Exception("이메일 필수"); // 설정으로 인해 롤백됨
    }
}

수동 롤백:

@Transactional
public void createUserWithManualRollback(User user) {
    try {
        userRepository.save(user);
        externalService.sendEmail(user.getEmail());
    } catch (EmailException e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        log.error("이메일 발송 실패로 사용자 생성 롤백", e);
    }
}

8. 분산 트랜잭션이란 무엇인가요?

분산 트랜잭션은 여러 데이터베이스나 시스템에 걸쳐 실행되는 트랜잭션입니다. 마이크로서비스 환경에서 자주 마주치는 문제예요.

2PC (Two-Phase Commit):

  1. 준비 단계: 모든 참여자에게 커밋 준비 요청
  2. 커밋 단계: 모든 참여자가 준비되면 커밋, 하나라도 실패하면 롤백

문제점:

  • 성능 오버헤드가 큰
  • 단일 장애점(coordinator) 존재
  • 네트워크 장애에 취약

대안 패턴:

// Saga 패턴 예시
@Service
public class OrderSagaService {
    
    public void createOrder(Order order) {
        try {
            // 1. 주문 생성
            orderService.createOrder(order);
            
            // 2. 결제 처리
            paymentService.processPayment(order.getPayment());
            
            // 3. 재고 차감
            inventoryService.decreaseStock(order.getItems());
            
        } catch (Exception e) {
            // 보상 트랜잭션 실행
            compensate(order);
        }
    }
    
    private void compensate(Order order) {
        inventoryService.increaseStock(order.getItems());
        paymentService.refund(order.getPayment());
        orderService.cancelOrder(order.getId());
    }
}

9. 트랜잭션 성능 최적화 방법은 무엇인가요?

1. 트랜잭션 범위 최소화:

// 나쁜 예: 트랜잭션이 너무 김
@Transactional
public void processOrder(Order order) {
    validateOrder(order);           // 검증 로직
    sendNotification(order);        // 외부 서비스 호출
    saveOrder(order);              // DB 저장
}

// 좋은 예: 트랜잭션 범위를 최소화
public void processOrder(Order order) {
    validateOrder(order);           // 트랜잭션 밖에서 실행
    sendNotification(order);        // 트랜잭션 밖에서 실행
    saveOrderInTransaction(order);  // 최소한의 트랜잭션
}

@Transactional
private void saveOrderInTransaction(Order order) {
    orderRepository.save(order);
}

2. 읽기 전용 트랜잭션 사용:

@Transactional(readOnly = true)
public List<User> getUsers() {
    return userRepository.findAll(); // 성능 최적화
}

3. 배치 처리:

@Transactional
public void createUsersBatch(List<User> users) {
    int batchSize = 100;
    for (int i = 0; i < users.size(); i += batchSize) {
        int end = Math.min(i + batchSize, users.size());
        List<User> batch = users.subList(i, end);
        userRepository.saveAll(batch);
        
        if (i % batchSize == 0) {
            entityManager.flush();
            entityManager.clear(); // 메모리 해제
        }
    }
}

4. 적절한 격리 수준 선택:

// 읽기 중심 작업에는 낮은 격리 수준 사용
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<Product> getProducts() {
    return productRepository.findAll();
}

10. 트랜잭션 관련 흔한 실수와 해결책은 무엇인가요?

1. @Transactional이 적용되지 않는 경우:

@Service
public class UserService {
    
    // 같은 클래스 내부 호출 시 @Transactional이 동작하지 않음
    public void createUser(User user) {
        validateUser(user);
        saveUser(user); // 트랜잭션이 적용되지 않음
    }
    
    @Transactional
    private void saveUser(User user) {
        userRepository.save(user);
    }
}

// 해결책: 별도 서비스로 분리하거나 self-injection 사용
@Service
public class UserService {
    
    @Autowired
    private UserService self;
    
    public void createUser(User user) {
        validateUser(user);
        self.saveUser(user); // 트랜잭션이 정상 동작
    }
    
    @Transactional
    public void saveUser(User user) {
        userRepository.save(user);
    }
}

2. 예외 처리 실수:

// 잘못된 예: 체크드 예외가 발생해도 롤백되지 않음
@Transactional
public void createUser(User user) throws Exception {
    userRepository.save(user);
    if (user.getEmail() == null) {
        throw new Exception("이메일 필수"); // 롤백되지 않음
    }
}

// 올바른 예
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) throws Exception {
    userRepository.save(user);
    if (user.getEmail() == null) {
        throw new Exception("이메일 필수"); // 롤백됨
    }
}

3. LazyInitializationException:

// 문제가 되는 코드
@Transactional
public User getUser(Long id) {
    return userRepository.findById(id);
}

public void processUser(Long userId) {
    User user = userService.getUser(userId);
    // 트랜잭션이 끝난 후 지연 로딩 시도
    user.getPosts().size(); // LazyInitializationException 발생
}

// 해결책 1: Fetch Join 사용
@Query("SELECT u FROM User u JOIN FETCH u.posts WHERE u.id = :id")
Optional<User> findByIdWithPosts(@Param("id") Long id);

// 해결책 2: 트랜잭션 범위 확장
@Transactional
public void processUser(Long userId) {
    User user = userService.getUser(userId);
    user.getPosts().size(); // 트랜잭션 내에서 실행
}

4. 긴 트랜잭션으로 인한 성능 문제:

// 문제가 되는 코드: 외부 API 호출이 트랜잭션에 포함됨
@Transactional
public void processOrder(Order order) {
    orderRepository.save(order);
    
    // 외부 API 호출 (시간이 오래 걸림)
    paymentGateway.processPayment(order.getPayment());
    
    // 이메일 발송 (시간이 오래 걸림)
    emailService.sendOrderConfirmation(order);
    
    order.setStatus(OrderStatus.COMPLETED);
    orderRepository.save(order);
}

// 개선된 코드: 트랜잭션을 분리
public void processOrder(Order order) {
    // 1단계: 주문 저장
    saveOrderInTransaction(order);
    
    // 2단계: 외부 서비스 호출 (트랜잭션 밖에서)
    PaymentResult result = paymentGateway.processPayment(order.getPayment());
    emailService.sendOrderConfirmation(order);
    
    // 3단계: 상태 업데이트
    updateOrderStatusInTransaction(order.getId(), OrderStatus.COMPLETED);
}

@Transactional
private void saveOrderInTransaction(Order order) {
    orderRepository.save(order);
}

@Transactional
private void updateOrderStatusInTransaction(Long orderId, OrderStatus status) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.setStatus(status);
    orderRepository.save(order);
}

5. 트랜잭션 전파 속성 오해:

@Service
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        
        // 로그는 독립적으로 저장되어야 함
        logService.saveLog("주문 생성: " + order.getId());
    }
}

@Service
public class LogService {
    
    // 기본 REQUIRED 속성으로 인해 주문 트랜잭션과 함께 롤백됨
    @Transactional
    public void saveLog(String message) {
        logRepository.save(new Log(message));
    }
}

// 해결책: REQUIRES_NEW 사용
@Service
public class LogService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(String message) {
        logRepository.save(new Log(message));
    }
}

핵심 정리

트랜잭션은 데이터의 일관성과 안정성을 보장하는 핵심 메커니즘입니다. ACID 속성을 이해하고, 적절한 격리 수준을 선택하며, 성능과 일관성 사이의 균형을 맞추는 것이 중요해요.

실무에서는 단순히 @Transactional을 붙이는 것만으로는 충분하지 않습니다. 트랜잭션의 범위를 적절히 설정하고, 예외 상황을 올바르게 처리하며, 성능을 고려한 설계를 해야 합니다.

면접에서는 이론적 지식뿐만 아니라 실제 프로젝트에서 트랜잭션 관련 문제를 어떻게 해결했는지, 성능 최적화를 위해 어떤 전략을 사용했는지에 대한 경험을 구체적으로 설명할 수 있어야 해요.

728x90
반응형