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

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

blossom_mind님의 프로필 이미지

작성한 질문수

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

회원 리포지토리 개발

jpa 1:N insert시 트랜잭션 오류

작성

·

59

0

[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (아니오)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)

[질문 내용]

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]
	요런식으로 뜨는데
public interface BoardRepository extends JpaRepository<Board, Long>, QuerydslPredicateExecutor<Board> {
	Optional<Board> findById(Long boardId);
}

querydsl과 jpa같이 사용하고싶어서 요렇게 셋팅하고 공부중입니다.

 

 

해본 방법

  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" 오류는 동시에 여러 트랜잭션에서 동일한 데이터를 수정하거나 삭제할 때 발생할 수 있습니다. 즉, 한 트랜잭션에서 데이터를 수정하는 동안 다른 트랜잭션에서 동일한 데이터를 수정하거나 삭제했기 때문에 충돌이 발생하는 것

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

답변 2

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. blossom_mind님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다.

실제 동작하는 전체 프로젝트를 ZIP파일로 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx

 

주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요


추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명 (오류 화면, 오류 로그 포함)

링크: 공식 서포터즈

링크: 자주하는 질문

감사합니다.

0

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

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

blossom_mind님께서는 JPAQuerydsl을 사용하여 Board 엔티티에 대한 트랜잭션 오류를 경험하고 계신 것 같습니다. 여기에 대해서 관련된 정보를 제공하겠습니다.

문제의 원인

“Row was updated or deleted by another transaction”라는 오류는 일반적으로 같은 엔티티에 대한 여러 트랜잭션이 동시에 발생할 때 일어나는 경우입니다. 이 오류는 @Version을 사용한 낙관적 락(Optimistic Locking) 전략으로도 발생할 수 있습니다.

해결 방법

  1. 낙관적 락을 사용해 충돌 감지: 이미 @Version 필드를 추가한 것으로 보입니다. 이 필드는 물리적으로 존재하지 않는 version 열을 사용하여 JPA가 상호간의 충돌을 감지할 수 있게 합니다. 문제는 @Version을 사용하더라도 정확히 설정되지 않거나, 데이터베이스의 충돌을 해결하지 못하는 경우에 이런 오류가 발생할 수 있습니다.

  2. 비관적 락 사용: @Lock(LockModeType.PESSIMISTIC_WRITE)를 시도하셨다고 했는데, 실제로 다른 트랜잭션과의 충돌을 방지하고 있는지를 확인할 필요가 있습니다. @Lock 어노테이션을 사용하는 방법 외에도, 데이터베이스 레벨에서 트랜잭션 격리 수준을 높이는 방법도 고려해보세요.

  3. 동시성 제어: 동일한 Board ID에 대해 많은 동시성 업데이트가 발생할 가능성이 있습니다. 이를 회피하기 위해 비즈니스 케이스에 따라서는 업데이트 될 가능성이 적은 엔티티를 먼저 갱신하고, 자주 변경되는 엔티티의 순서를 재조정하거나 구조적으로 격리할 수 있습니다.

참고 링크

현재 시도하신 모든 방법이 잘 작동하고 있는지, 트랜잭션 로직을 점검하고 엔티티 관리 방식에 대해 더 검토할 필요가 있습니다. 또한, 데이터베이스의 transaction isolation level 설정도 한 번 검토해 보는 것이 좋습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.

현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

blossom_mind님의 프로필 이미지
blossom_mind
질문자