인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

blossom_mind님의 프로필 이미지

작성한 질문수

JPA insert 에러 transaction 질문입니다.

작성

·

52

0

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "tb_board")
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Getter
@Setter
public class Board {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "BOARD_ID",  columnDefinition = "BIGINT COMMENT '게시판ID'")
	private Long id;

	@Column(name = "BOARD_NM", nullable = false, unique = true, length = 100, columnDefinition = "VARCHAR(100) COMMENT '게시판명'")
	private String boardNm;
	
	@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Article> articleList = new ArrayList<Article>();
	
	@Builder
	private Board(Long boardId, String boardNm) {
		this.boardNm= boardNm;
		this.id = boardId;
	}
	
	public Board createBoard(Long boardId, String boardNm) {
		return Board.builder().boardId(boardId).boardNm(boardNm).build();
	}
	
	public void addArticle(Article article) {
		this.articleList.add(article);
	}
	
	
	@Column(name="REG_ID" ,nullable=false,columnDefinition="VARCHAR(100) comment '등록자'")
	private String regId;
	
	
	@Column(name = "CREATED_AT", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
	private LocalDateTime createdAt;
	
	@Column(name="UPD_ID" ,nullable=false,columnDefinition="VARCHAR(100) comment '등록자'")
	private String updId;
	
	@Column(name = "UPDATED_AT", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
	private LocalDateTime updatedAt;
	
	
}
@Entity
@Table(name = "tb_article")
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
 public class Article {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ARTICLE_ID", columnDefinition = "BIGINT COMMENT '게시글ID'")
	private Long id;

	@Column(name = "ARTICLE_TITLE", columnDefinition = "VARCHAR(100) COMMENT '게시글제목'")
	private String title;

	@Column(name = "ARTICLE_CONTENT", columnDefinition = "VARCHAR(600) COMMENT '게시글내용'")
	private String content;

	@Column(name = "REG_ID", nullable = false, columnDefinition = "VARCHAR(100) comment '등록자'")
	private String regId;

	@Column(name = "CREATED_AT", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
	private LocalDateTime createdAt;

	@Column(name = "UPD_ID", nullable = false, columnDefinition = "VARCHAR(100) comment '등록자'")
	private String updId;

	@Column(name = "UPDATED_AT", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
	private LocalDateTime updatedAt;

	@ManyToOne
	@JoinColumn(name = "BOARD_ID", nullable = false)
	private Board board;

}
 @Transactional(rollbackFor = Exception.class)  // 체크 예외도 롤백
     public void saveArticle(Article article) {
		try {
			log.info("article",article.getTitle());
 			Long boardId = 13L;
 			Board paramBoard = null;
			Optional<Board> rBoard = boardRepository.findById(boardId);
			if(rBoard.isPresent()) {
				paramBoard =rBoard.get();
			}else {
				paramBoard = new Board().createBoard(boardId, "게시판");
				paramBoard = boardRepository.save(paramBoard);
 			}
 			Article paramArticle = Article.builder()
                                               .title(article.getTitle())
                                               .content(article.getContent())
                                               .regId("등록자")
                                               .createdAt(LocalDateTime.now())
                                               .updatedAt(LocalDateTime.now())
                                               .board(paramBoard)
                                               .updId("수정자")
                                               .build();
			
			if(paramBoard.getArticleList() == null) {
				List<Article> articles =new ArrayList<Article>();
				articles.add(paramArticle);
				paramBoard.setArticleList(articles);
			}else {
				paramBoard.getArticleList().add(paramArticle);
			} 
			
			boardRepository.save(paramBoard);
			
		}catch(Exception ex) {
	        log.error("Error saving article", ex);
			throw ex;
		}
		
	}
else {
 paramBoard = new Board().createBoard(boardId, "게시판");
 paramBoard = boardRepository.save(paramBoard);
 			}
여기서 save를 돌떄 
org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.backend.com.entity.Board#13]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:325) ~[spring-orm-6.2.2.jar:6.2.2]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244) ~[spring-orm-6.2.2.jar:6.2.2]
	요런식으로 뜨는데

해본 방법

  1. boardRepository.save(paramBoard) 부분을 빼고 마지막
    boardRepository.save(paramBoard);

     

만 돌려봤지만 마찬가지 에러가 발생했었고

 

  1. 혹시 table 쪽에 lock이 걸려있나 싶어서 보니 lock도 없었음

3. 지피티 한테 물어보니깐
@Lock(LockModeType.PESSIMISTIC_WRITE)
쓰라고 해서 save쪽에 넣어봄 . 마찬가지 ..

Board쪽에
@Version @Column(name = "version") private Long version;
을 추가하라고 해서 추가해봐도 마찬가지 ..

이유는
"Row was updated or deleted by another transaction" 오류는 동시에 여러 트랜잭션에서 동일한 데이터를 수정하거나 삭제할 때 발생할 수 있습니다. 즉, 한 트랜잭션에서 데이터를 수정하는 동안 다른 트랜잭션에서 동일한 데이터를 수정하거나 삭제했기 때문에 충돌이 발생하는 것

이라는데 잘 모르겠습니다 뭐가 문제일까요 .

public interface BoardRepository extends JpaRepository<Board, Long>, QuerydslPredicateExecutor<Board> {
	Optional<Board> findById(Long boardId);
}

querydsl과 jpa같이 사용하고싶어서 셋팅한거 입니다.

답변 1

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴입니다.

blossom_mind님, 작성하신 코드에서 발생하는 Row was updated or deleted by another transaction 예외는 데이터 동시 수정 문제로 인해 발생할 수 있습니다. 이 예외는 보통 두 가지 주요 이유로 발생할 수 있습니다.

  1. 동시성 제어 문제: 동시에 여러 트랜잭션이 동일한 엔티티 데이터를 수정하려고 할 때 발생합니다. 이를 해결하기 위해서는 다음과 같은 방법을 고려해 볼 수 있습니다.

    • 낙관적 잠금(Optimistic Locking): @Version annotation을 사용하여 버전 관리를 통해 동시성 문제를 해결할 수 있습니다. 이미 적용하신 것처럼 Board 엔티티에 @Version을 사용하셨다면, 데이터가 올바르게 업데이트되고 있는지도 확인해 보십시오.
    • 비관적 잠금(Pessimistic Locking): @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션을 사용하여 특정 트랜잭션이 완료될 때까지 다른 트랜잭션이 해당 데이터를 수정하지 못하게 할 수 있습니다. 하지만, 이 방법도 시도하셨던 것처럼 예외를 해결하지 못했다면, 다른 부분에서 동시성 제어가 필요할 가능성이 있습니다.
  2. ID 충돌: IDENTITY 전략을 사용하는 경우 저장할 때마다 새로운 ID가 생성되어야 하지만, 특정한 상황에서 ID가 충돌할 수 있습니다. 특히 수동으로 ID 값을 설정하는 부분이 있으니, 생성 시에 대한 로직도 유의하셔야 합니다.

추천하는 해결 방법:
- 데이터 수동 설정 점검: createBoard 메서드를 통해 직접 ID를 설정하지 않고, JPA에 ID를 자동 생성하도록 맡기는 것이 안전합니다. 이후 persist 메소드를 사용할 때 생성된 ID를 확인하시고 사용해보세요.
- Logging 및 Debugging: 예외가 발생하는 시점에 해당 데이터에 대한 로그를 남겨 어느 부분에서 충돌이 발생하는지 구체적으로 알아보는 것도 도움이 됩니다.

위의 방법들도 확인하고, 트랜잭션 관리 방식에서도 동시성 문제를 확인할 수 있도록 주의하시면 좋습니다. 추가적인 동시 트랜잭션이 없도록 서비스 구조나 메소드 접근 제어도 점검해보세요.

문제가 해결되지 않는다면, 조금 더 구체적인 로깅이나 테스트 케이스를 통해 어떤 상황에서 예외가 발생하는지 확인해 보시는 것도 좋습니다.

문서: Spring Data JPA - Transactionality

도움이 되시길 바랍니다.