
이 글은 2026년 05월 06일 작성된 글입니다.
오늘은 스프링의 의존성 주입 방식, 트랜잭션과 프록시,
JPA Auditing, 더티체킹, 테스트 환경 분리까지 정리했다.
기존에는 필드에 바로 @Autowired를 붙여서 의존성을 주입받았다.
@Autowired
private PostRepository postRepository;
하지만 생성자 주입을 사용하는 방식이 더 권장된다.
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
생성자 주입은 객체가 생성될 때 필요한 의존성이 반드시 들어오도록 강제할 수 있다.
생성자가 하나만 있는 경우에는 @Autowired를 생략할 수 있다.
@Service
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
}
스프링이 생성자를 보고 자동으로 필요한 빈을 주입해준다.
Lombok의 @AllArgsConstructor를 사용하면 모든 필드를 받는 생성자를 자동으로 만들어준다.
@AllArgsConstructor
@Service
public class PostService {
private final PostRepository postRepository;
}
다만 빈 클래스에서는 모든 필드를 생성자에 포함하기 때문에
불필요한 필드까지 생성자에 들어갈 수 있어 융통성이 떨어질 수 있다.
빈 클래스에서는 보통 @RequiredArgsConstructor와 final 조합을 많이 사용한다.
@RequiredArgsConstructor
@Service
public class PostService {
private final PostRepository postRepository;
}
final이 붙은 필드만 생성자에 포함되기 때문에
필수 의존성만 깔끔하게 주입받을 수 있다.
트랜잭션은 여러 DB 작업을 하나의 작업 단위로 묶는 것이다.
예를 들어 계좌이체는 출금과 입금이 모두 성공해야 한다.
A 계좌 출금
B 계좌 입금
둘 중 하나만 성공하면 데이터가 깨지기 때문에
두 작업은 하나의 트랜잭션으로 묶여야 한다.
트랜잭션은 ACID 성질을 가진다.
스프링에서는 @Transactional을 붙여서 선언적으로 트랜잭션을 관리할 수 있다.
@Transactional
public void modify() {
// DB 작업
}
메서드 실행 중 예외가 발생하면 자동으로 롤백된다.
같은 클래스 안에서 @Transactional이 붙은 메서드를 직접 호출하면
트랜잭션이 적용되지 않을 수 있다.
this.work1();
이유는 스프링의 트랜잭션이 프록시를 통해 동작하기 때문이다.
내부 호출은 프록시를 거치지 않고 실제 객체의 메서드를 바로 호출한다.
this.work1() → 프록시를 거치지 않음self.work1() → 프록시를 거침내부 메서드 호출에서도 트랜잭션을 적용하기 위해
자기 자신을 주입받아 호출하는 방식을 사용했다.
@Autowired
@Lazy
private PostService self;
@Lazy는 순환 참조 문제를 피하기 위해 프록시 객체를 먼저 주입해주는 역할을 한다.
프록시는 실제 객체 대신 앞에서 요청을 받아주는 대리 객체이다.
스프링에서는 다음과 같은 기능이 프록시를 통해 동작할 수 있다.
@Transactional@Lazy@Validated@Cacheable@Async프록시를 거치면 트랜잭션, 검증, 캐싱 같은 부가 기능을 적용할 수 있다.
글 생성과 수정 로직은 서비스 계층에 두었다.
@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를 통해 비즈니스 로직을 처리하는 구조가 더 좋다.
일부러 RuntimeException을 발생시켜
트랜잭션 롤백이 정상적으로 동작하는지 확인했다.
@Transactional
public void work3() {
postService.modify(...);
throw new RuntimeException();
}
예외가 발생하면 트랜잭션 안에서 수행된 DB 변경이 취소된다.
생성일과 수정일을 직접 관리하지 않고
JPA Auditing으로 자동 처리했다.
@EnableJpaAuditing
@SpringBootApplication
public class Application {
}
@EntityListeners(AuditingEntityListener.class)
public class Post {
@CreatedDate
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime modifyDate;
}
@CreatedDate: 생성 시간 자동 입력@LastModifiedDate: 수정 시간 자동 갱신더티체킹은 트랜잭션 안에서 엔티티 값이 변경되면
JPA가 이를 감지해서 자동으로 UPDATE 쿼리를 실행하는 기능이다.
@Transactional
public void modify(Post post) {
post.setTitle("수정 제목");
}
따로 save()를 다시 호출하지 않아도
트랜잭션이 끝날 때 변경사항이 DB에 반영된다.
모든 엔티티에 공통으로 들어가는 필드를
BaseEntity에 모아두고 상속받도록 정리했다.
@MappedSuperclass
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime modifyDate;
}
@MappedSuperclass는 실제 테이블을 만들지 않고
상속받는 엔티티의 테이블에 컬럼만 포함시킨다.
Repository 테스트를 위해 @SpringBootTest를 사용했다.
@SpringBootTest
class PostRepositoryTest {
}
이 어노테이션은 실제 애플리케이션과 비슷한 환경으로 테스트를 실행한다.
H2 파일 모드는 하나의 DB 파일에 두 개 이상의 프로세스가 동시에 접근하면 오류가 발생할 수 있다.
예를 들어 스프링부트 서버가 실행 중인 상태에서 테스트를 실행하면
서버와 테스트가 같은 H2 파일에 접근하게 된다.
그래서 테스트 환경에서는 메모리 모드를 사용하는 것이 좋다.
spring:
datasource:
url: jdbc:h2:mem:testdb
@RequiredArgsConstructor와 final 조합을 사용하면 의존성 주입 코드가 깔끔해진다.@Transactional은 프록시를 통해 동작하므로 내부 호출에서는 주의가 필요하다.