일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- Spring JPA
- JPA
- 그래프탐색
- 백준
- DB replication
- 프로젝트
- 임베디드타입
- 산업은행청년인턴
- 스케일아웃
- CS
- springboot
- 파이널프로젝트
- BFS
- 해시
- 트리셋
- flyway
- 컴퓨터구조
- SpringBatch
- 운영체제
- 폰켓몬
- CPU스케줄링
- 외래키제약조건위반
- 산업은행it
- 트리맵
- fatch
- 코테
- 구현
- 2178
- 프로그래머스
- findById
- Today
- Total
나 JAVA 봐라
@Transactional과 DataIntegrityViolationException 본문
Service에서 reviewReaction 엔티티를 DB에 저장하는 메소드를 아래와 같이 작성했다.
reviewReaction 엔티티는 review, user 엔티티를 참조하고 있기 때문에 revieReaction 엔티티 생성 시에 참조하고 있는 값이 DB에 존재하지 않으면 java.sql.SQLIntegrityConstraintViolationException을 던진다.
- 해당 연관관계는 sql 스크립트로 FK를 맺어주고 엔티티에는 JPA 연관관계를 따로 명시하지 않고 있다.
- 따라서 위의 Exception은 데이터베이스에서 던지는 에러이며, SQL 실행 중 무결성 제약 조건이 위반되었을 때 발생한다.
@Transactional
public void createReviewReaction(ReviewReactionDto reviewReactionDto) {
try {
ReviewReaction reviewReaction = reviewReactionDto.toEntity();
reviewReactionRepository.save(reviewReaction);
} catch (DataIntegrityViolationException e) {
throw new BusinessException(ReviewReactionErrorCode.BAD_REQUEST);
}
}
여튼,,, 위와 같이 해서 Exception 처리를 해주려고 했는데, 계속해서 Exception을 잡아주지 못해서 같은 에러가 발생했다.
위에서 설명한 대로 참조하고 있는 값이 DB에 존재하지 않아서 생기는 에러다.
java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (orury.review_reaction, CONSTRAINT FK_user_TO_review_reaction_1 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE)
왜 try-catch에서 Exception을 잡아주지 않을까?
범인은 @Transactional 어노테이션 이었다.
@Transactional 이 어떻게 동작하는지 알기 위해, 먼저 트랜잭션이 뭔지 간단히 정리해봤다.
트랜잭션
- 데이터베이스에서 데이터를 처리하는 하나의 논리적인 작업 단위
- 트랜잭션에는 여러 개의 쿼리를 포함할 수 있고, 하나라도 실패할 경우 전체 트랜잭션이 롤백되어서 데이터의 일관성을 보장한다.
@Transactional의 동작 순서
- @Transactional 이 붙은 메소드가 호출되면, Spring은 트랜잭션을 시작한다. 이 때, 새로운 연결을 데이터베이스에서 가져오거나 이미 존재하는 연결을 재사용할 수 있다.
- 메소드 내에서 데이터베이스에 대한 쿼리를 수행하면, 이 쿼리는 모두 해당 트랜잭션 안에서 실행된다. 이 때, 각 쿼리의 결과는 '즉시' 데이터베이스에 반영되지 않고, 트랜잭션 내에서만 이뤄진다.
- 메소드가 종료되면, Spring은 트랜잭션을 커밋하려고 시도한다. 이 때, 트랜잭션 내에서 실행된 모든 쿼리가 성공적으로 수행되었는지를 확인하게 된다. 외래 키 제약조건 확인 등의 데이터베이스 무결성 검사도 이 때 이뤄진다.
- 모든 쿼리가 성공적으로 수행되었다면, 트랜잭션은 커밋되어 데이터베이스에 반영된다. 만약 하나의 쿼리라도 실패하거나, 데이터베이스 무결성 검사에서 문제가 발견되면 트랜잭션은 롤백되고, 트랜잭션 내에서 수행된 모든 쿼리의 결과는 데이터베이스에 반영되지 않는다.
-> 따라서 @Transactional 을 사용하면, 여러 쿼리를 하나의 논리적인 작업 단위로 묶고, 작업의 성공 여부에 따라 한번에 커밋하거나 롤백하도록 해준다. 이를 통해 데이터 일관성 보장이 가능해지고, 여러 사용자가 동시에 데이터를 변경하더라도 데이터베이스의 상태를 안정적으로 유지할 수 있다.
즉, @Transactional 로 인해 reviewReactionRepository.save(reviewReaction); 에 해당되는 쿼리가 실제로는 메소드가 종료된 이후 수행되기 때문에, try-catch에서 익셉션을 처리해줄 수 없는 것이었다.
해결 방법은 무엇이 있을까? 생각해본 방법으론 두 가지가 있다.
- @Transactional 어노테이션을 지운다.
- 해당 메소드에서 참조하는 값(review, user)이 실제로 존재하는지 검증하는 로직을 추가한다. 아래와 같은 코드로 구현될 수 있다.
- + 이 후에 생각난 방법으로는, @ControllerAdvice와 @ExceptionHandler를 사용해서 DataIntegrityViolationException을 처리해주는 방법도 있을 듯 하다.
@Transactional
public void createReviewReaction(ReviewReactionDto reviewReactionDto) {
Optional<User> user = userRepository.findById(reviewReactionDto.getUserId());
Optional<Review> review = reviewRepository.findById(reviewReactionDto.getReviewId());
if (!user.isPresent() || !review.isPresent()) {
throw new BusinessException(ReviewReactionErrorCode.BAD_REQUEST);
}
ReviewReaction reviewReaction = reviewReactionDto.toEntity();
reviewReactionRepository.save(reviewReaction);
}
방법은 위의 두 가지인데, 상황에 따라 선택할 방식이 다를 것 같아 장단점을 정리해봤다.
@Transaction 어노테이션을 지운다. | 참조 값을 검증하는 로직을 추가한다. | |
장점 | - DB에 한 번만 접근하기 때문에 성능 측면에서 유리하다. 트래픽이 많거나 성능이 중요하다면 DB 접근을 최소화하기 위해 이 방식을 택할 수 있다. | - 외래 키 제약조건 위반을 미리 방지하여 더 명확한 에러 메시지를 제공할 수 있다. 데이터 무결성을 보장하는 것이 중요하다면 이 방식을 택할 수 있다. |
단점 | - 외래키 제약조건을 검사하기 위해 데이터베이스 내부적으로는 여러 번의 조회가 발생한다. (user, review 테이블 조회) -> 그러나 이런 조회는 데이터베이스 엔진이 최적화해서 수행하기 때문에 일반적으로 애플리케이션 레벨에서 별도로 조회하는 것보다는 빠르게 처리된다. (그닥 단점 아닌듯) |
- DB에 세 번이나 접근해야한다는 단점이 있다. (user 찾기 조회, review 찾기 조회, reviewReaction 저장) -> 네트워크 지연과 같은 추가적인 오버헤드가 발생할 수 있는 문제도 있다. 데이터베이스가 애플리케이션과 분리되어 있거나, 분산 시스템 한경에서는 중요하게 생각할 요인이다. |
문제의 코드의 경우 save() 코드 한 줄만 실행하기에 쿼리가 하나만 생성된다. 따라서 @Transactional 을 사용할 필요가 없다고 판단하여, @Transactional 어노테이션을 지워서 간단하게 해결하기로 했다.
추가적으로 @Transactional 을 사용하고 싶다면 .save()가 아닌 .saveAndFlush()를 사용하면 된다. flush()를 사용하면, 메소드가 끝난 후에 일괄적으로 쿼리를 보내는 것이 아니라 '즉시' 데이터베이스에 반영할 수 있다.
이 때, 문득 궁금해진 것이 생겼다. 예를 들어, @Transactional 이 달린 메소드에서 flush()가 두 번 쓰인다. 이 때, 첫번째 flush는 예외없이 실행되었지만, 두번째 flush에서 예외가 발생했을 경우, 첫번째 flush를 통해 DB에 반영된 것은 롤백이 될까?
-> 롤백 된다 !
'Spring > Spring JPA' 카테고리의 다른 글
[Spring JPA] Spring JPA 와 SQL의 연관관계 (0) | 2024.01.20 |
---|---|
[Spring JPA] JPA의 findById와 EAGER, LAZY 전략 (0) | 2024.01.04 |