Day 45 - Transaction, Proxy

haxxru log;·2026년 5월 6일
post-thumbnail

이 글은 2026년 05월 06일 작성된 글입니다.

오늘은 스프링의 의존성 주입 방식, 트랜잭션과 프록시,
JPA Auditing, 더티체킹, 테스트 환경 분리까지 정리했다.


1. 필드 주입에서 생성자 주입으로 변경

기존에는 필드에 바로 @Autowired를 붙여서 의존성을 주입받았다.

@Autowired
private PostRepository postRepository;

하지만 생성자 주입을 사용하는 방식이 더 권장된다.

private final PostRepository postRepository;

public PostService(PostRepository postRepository) {
    this.postRepository = postRepository;
}

생성자 주입은 객체가 생성될 때 필요한 의존성이 반드시 들어오도록 강제할 수 있다.

  • 필수 의존성을 명확하게 표현할 수 있다.
  • 테스트 코드 작성이 쉬워진다.
  • 객체의 불완전한 상태를 막을 수 있다.

2. 생성자 주입과 @Autowired 생략

생성자가 하나만 있는 경우에는 @Autowired를 생략할 수 있다.

@Service
public class PostService {
    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }
}

스프링이 생성자를 보고 자동으로 필요한 빈을 주입해준다.


3. @AllArgsConstructor

Lombok의 @AllArgsConstructor를 사용하면 모든 필드를 받는 생성자를 자동으로 만들어준다.

@AllArgsConstructor
@Service
public class PostService {
    private final PostRepository postRepository;
}

다만 빈 클래스에서는 모든 필드를 생성자에 포함하기 때문에
불필요한 필드까지 생성자에 들어갈 수 있어 융통성이 떨어질 수 있다.


4. @RequiredArgsConstructor + final 조합

빈 클래스에서는 보통 @RequiredArgsConstructorfinal 조합을 많이 사용한다.

@RequiredArgsConstructor
@Service
public class PostService {
    private final PostRepository postRepository;
}

final이 붙은 필드만 생성자에 포함되기 때문에
필수 의존성만 깔끔하게 주입받을 수 있다.

  • 생성자 코드 제거
  • 필수 의존성만 주입
  • 실무에서 많이 쓰이는 방식

5. 트랜잭션

트랜잭션은 여러 DB 작업을 하나의 작업 단위로 묶는 것이다.

예를 들어 계좌이체는 출금과 입금이 모두 성공해야 한다.

A 계좌 출금
B 계좌 입금

둘 중 하나만 성공하면 데이터가 깨지기 때문에
두 작업은 하나의 트랜잭션으로 묶여야 한다.


6. 트랜잭션의 ACID

트랜잭션은 ACID 성질을 가진다.

  • 원자성: 모두 성공하거나 모두 실패해야 한다.
  • 일관성: 완료 후 데이터는 항상 올바른 상태여야 한다.
  • 독립성: 동시에 실행되는 작업끼리 서로 영향을 주면 안 된다.
  • 지속성: 성공한 결과는 영구적으로 저장되어야 한다.

7. @Transactional

스프링에서는 @Transactional을 붙여서 선언적으로 트랜잭션을 관리할 수 있다.

@Transactional
public void modify() {
    // DB 작업
}

메서드 실행 중 예외가 발생하면 자동으로 롤백된다.

  • 트랜잭션 시작/종료를 직접 작성하지 않아도 된다.
  • 비즈니스 로직과 트랜잭션 처리를 분리할 수 있다.

8. 내부 호출과 트랜잭션 실패

같은 클래스 안에서 @Transactional이 붙은 메서드를 직접 호출하면
트랜잭션이 적용되지 않을 수 있다.

this.work1();

이유는 스프링의 트랜잭션이 프록시를 통해 동작하기 때문이다.
내부 호출은 프록시를 거치지 않고 실제 객체의 메서드를 바로 호출한다.

  • this.work1() → 프록시를 거치지 않음
  • self.work1() → 프록시를 거침

9. self 주입과 @Lazy

내부 메서드 호출에서도 트랜잭션을 적용하기 위해
자기 자신을 주입받아 호출하는 방식을 사용했다.

@Autowired
@Lazy
private PostService self;

@Lazy는 순환 참조 문제를 피하기 위해 프록시 객체를 먼저 주입해주는 역할을 한다.


10. 프록시 객체

프록시는 실제 객체 대신 앞에서 요청을 받아주는 대리 객체이다.

스프링에서는 다음과 같은 기능이 프록시를 통해 동작할 수 있다.

  • @Transactional
  • @Lazy
  • @Validated
  • @Cacheable
  • @Async

프록시를 거치면 트랜잭션, 검증, 캐싱 같은 부가 기능을 적용할 수 있다.


11. 서비스 계층에서 글 생성/수정 처리

글 생성과 수정 로직은 서비스 계층에 두었다.

@Service
@RequiredArgsConstructor
public class PostService {
    private final PostRepository postRepository;

    public Post write(String title, String content) {
        Post post = new Post(title, content);
        return postRepository.save(post);
    }
}

컨트롤러나 테스트 코드에서 Repository를 직접 다루기보다
Service를 통해 비즈니스 로직을 처리하는 구조가 더 좋다.


12. 롤백 테스트

일부러 RuntimeException을 발생시켜
트랜잭션 롤백이 정상적으로 동작하는지 확인했다.

@Transactional
public void work3() {
    postService.modify(...);
    throw new RuntimeException();
}

예외가 발생하면 트랜잭션 안에서 수행된 DB 변경이 취소된다.


13. JPA Auditing

생성일과 수정일을 직접 관리하지 않고
JPA Auditing으로 자동 처리했다.

@EnableJpaAuditing
@SpringBootApplication
public class Application {
}
@EntityListeners(AuditingEntityListener.class)
public class Post {
    @CreatedDate
    private LocalDateTime createDate;

    @LastModifiedDate
    private LocalDateTime modifyDate;
}
  • @CreatedDate: 생성 시간 자동 입력
  • @LastModifiedDate: 수정 시간 자동 갱신

14. 더티체킹

더티체킹은 트랜잭션 안에서 엔티티 값이 변경되면
JPA가 이를 감지해서 자동으로 UPDATE 쿼리를 실행하는 기능이다.

@Transactional
public void modify(Post post) {
    post.setTitle("수정 제목");
}

따로 save()를 다시 호출하지 않아도
트랜잭션이 끝날 때 변경사항이 DB에 반영된다.


15. BaseEntity와 @MappedSuperclass

모든 엔티티에 공통으로 들어가는 필드를
BaseEntity에 모아두고 상속받도록 정리했다.

@MappedSuperclass
public abstract class BaseEntity {
    @CreatedDate
    private LocalDateTime createDate;

    @LastModifiedDate
    private LocalDateTime modifyDate;
}

@MappedSuperclass는 실제 테이블을 만들지 않고
상속받는 엔티티의 테이블에 컬럼만 포함시킨다.


16. @SpringBootTest

Repository 테스트를 위해 @SpringBootTest를 사용했다.

@SpringBootTest
class PostRepositoryTest {
}

이 어노테이션은 실제 애플리케이션과 비슷한 환경으로 테스트를 실행한다.

  • 스프링 컨테이너 로딩
  • 빈 자동 주입
  • DB 연동 테스트 가능

17. H2 파일 모드와 테스트 문제

H2 파일 모드는 하나의 DB 파일에 두 개 이상의 프로세스가 동시에 접근하면 오류가 발생할 수 있다.

예를 들어 스프링부트 서버가 실행 중인 상태에서 테스트를 실행하면
서버와 테스트가 같은 H2 파일에 접근하게 된다.

그래서 테스트 환경에서는 메모리 모드를 사용하는 것이 좋다.

spring:
  datasource:
    url: jdbc:h2:mem:testdb
  • 개발: H2 파일 모드
  • 테스트: H2 메모리 모드
  • 운영: MySQL 또는 MariaDB

✅ 정리

  • 스프링에서는 필드 주입보다 생성자 주입을 사용하는 것이 더 안정적이다.
  • @RequiredArgsConstructorfinal 조합을 사용하면 의존성 주입 코드가 깔끔해진다.
  • @Transactional은 프록시를 통해 동작하므로 내부 호출에서는 주의가 필요하다.
  • 더티체킹을 이해하면 JPA에서 수정 로직을 더 자연스럽게 작성할 수 있다.
  • Auditing과 BaseEntity를 활용하면 생성일과 수정일 같은 공통 필드를 편하게 관리할 수 있다.
  • 테스트 환경에서는 H2 메모리 모드처럼 독립적인 DB 설정을 사용하는 것이 좋다.

0개의 댓글