@Transactional
public String save() {
try {
Member member = memberRepository.save(new Member("NICKNAME"));
return member.nickname();
} catch (DataIntegrityViolationException e) {
log.info("중복키 에러 발생");
}
return "DEFAULT_NICKNAME";
}
데이터가 정상 저장되면, 저장된 닉네임을 반환하는 메서드가 있다. 하지만 중복키 에러에 의해 에러 발생시에는 기본 닉네임(DEFAULT_NICKNAME) 을 반환하는 요구사항이 있다.
기본키 생성 전략이 어떤 것인지에 따라 try - catch 에서 에러가 발생할 수 있고, 잡히지 않을 수도 있다.
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
특징: JPA 구현체에 의존하여 자동으로 전략 선택
쓰기 지연 영향: 선택된 전략에 따라 다름
Hibernate 5부터 MySQL에서의 GenerationType.AUTO는 IDENTITY가 아닌 TABLE을 기본 시퀀스 전략으로 가져간다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
특징: 데이터베이스의 Identity 컬럼을 사용하여 ID를 생성 (예: MySQL의 AUTO_ INCREMENT)
쓰기 지연 영향: 거의 없음 (일부 데이터베이스에 따라 다름)
JPA는 트랜잭션이 commit 되는 시점에 쓰기 지연 저장소에 모아놓은 SQL을 한 번에 DB로 전송하며 실행한다. 이렇게 해야 어플리케이션과 DB 사이에 네트워크를 오가는 횟수가 줄어들고 성능면에서 이득을 볼 수 있기 때문이다.
하지만 IDENTITY전략은 DB에 기본키 생성을 위임하므로, Mysql의 경우 AUTO_INCREMENT를 활용하여 생성하는데,이 때, JPA 입장에선 DB에 INSERT SQL를 실행하기 전엔 도저히 AUTO_INCREMENT되는 값을 알 수 없으므로, persist() 시점에 insert 쿼리가 실행되는 것이다. (영속성 컨텍스트로 엔티티를 관리하려면 1차 캐시에 Id값을 key 값으로 들고 있어야 하기 때문에)
@Entity
@SequenceGenerator(
name = “MEMBER_SEQ_GENERATOR",
sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 50)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
}
특징: 데이터베이스 시퀀스를 사용하여 ID를 생성 (예: 오라클 시퀀스)
쓰기 지연 영향: 일반적으로 있음
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = “MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
특징: 특정 테이블을 사용하여 ID를 생성 (모든 데이터베이스에 적용 가능하나, 성능적인 손해가 있어서 잘 쓰지 않는다)
쓰기 지연 영향: 일반적으로 있음
@Id
private Long id;
특징: 수동으로 기본키를 설정하는 경우
쓰기 지연 영향: 있음 (트랜잭션 내에서 데이터 변경이 발생하고, 해당 트랜잭션이 커밋되면 변경 사항이 데이터베이스에 즉시 반영)
✅ 유니크키 에러 발생하면 try - catch 에 잡혀야한다.
✅ 유니크키 에러가 발생한 경우, 기본값을 응답한다.
@Transactional
public String save() {
try {
Member member = memberRepository.save(new Member("NICKNAME"));
return member.nickname();
} catch (DataIntegrityViolationException e) {
log.info("중복키 에러 발생");
}
return "DEFAULT_NICKNAME";
}
try - catch 에서 에러가 잡히지 않고, DataIntegrityViolationException 이 메서드에서 발생한다. 메서드 상위에 @Transactional
이 존재하기 때문에, 쓰기지연이 동작하여 메서드가 끝난 후에 save 쿼리가 날라가고, try - catch 밖에서 에러가 발생하게 된다.
@Transactional
public String save() {
try {
Member member = memberRepository.saveAndFlush(new Member("NICKNAME"));
return member.nickname();
} catch (DataIntegrityViolationException e) {
log.info("중복키 에러 발생");
}
return "DEFAULT_NICKNAME";
}
saveAndFlush로 변경하면, 메서드가 끝나기 전에 saveAndFlush 에서 쿼리가 날라가게 된다. try - catch 에서 에러가 잡히지만, 하지만 기본값 응답이 아닌 롤백 에러가 발생한다.
그 이유는 내부 롤백 마크가 찍혀서 인데, 자세한 내용은 롤백 마크가 생긴 트랜잭션은 재사용이 불가능할까?를 참고해보자.
public String save() {
try {
Member member = memberRepository.save(new Member("NICKNAME"));
return member.nickname();
} catch (DataIntegrityViolationException e) {
log.info("중복키 에러 발생");
}
return "DEFAULT_NICKNAME";
}
내부 롤백 마크가 찍히는 것을 방지하기 위해, @Transactional
을 제거하자. 이 외에도 memberRepository.save 하는 로직을 Propagation.REQUIRES_NEW 로 따로 메서드를 만들어서 내부 롤백마크로 처리하지 않도록 구현할 수도 있다.
트랜잭션 제거시에는 memberRepository.save() 호출 시에는 쿼리가 즉시 날라가서 memberRepository.saveAndFlush() 를 사용하지 않아도 된다.
중복키 에러 발생 시에 try - catch 에서 로그를 남기고, 기본값을 응답하는 것을 알 수 있다. 요구사항이 모두 충족되었다.
@Transactional
public String save() {
try {
Member member = memberRepository.save(new Member("NICKNAME"));
return member.nickname();
} catch (DataIntegrityViolationException e) {
log.info("중복키 에러 발생");
}
return "DEFAULT_NICKNAME";
}
중복키 에러 발생 시에 try - catch 에서 로그를 남기고, 기본값을 응답하는 것을 알 수 있다. 물론, 내부 롤백마크가 찍혀서 전체 롤백이 일어났지만, memberRepository.save() 에서 발생한 에러가 try - catch 에 잡힌다!
즉, 쓰기 지연이 발생하지 않은 것이다. 트랜잭션을 커밋한 후에 실제 DB로 쿼리가 나갈 줄 알았지만, generationType.IDENTITY는 em.persist일 때 쓰기 지연을 하지 않기 때문에 memberRepository.save() 메서드를 호출하자마자 바로 실제 DB에 쿼리가 나간 것을 확인할 수 있다. IDENTITY 전략은 DB column 수에 따라 식별자 id를 저장해주기 때문이다. 따라서 쓰기 지연이 아닌 실제 DB에 바로 쿼리를 날리고, repository save 메서드를 호출지점을 감싸고 있는 try - catch 에서 예외가 잡히게 된다.
public String save() {
try {
Member member = memberRepository.save(new Member("NICKNAME"));
return member.nickname();
} catch (DataIntegrityViolationException e) {
log.info("중복키 에러 발생");
}
return "DEFAULT_NICKNAME";
}
내부 롤백 마크가 찍히는 것을 방지하기 위해, @Transactional 을 제거하자. 요구사항이 모두 충족되었다.
@Transactional
public Member save() {
try {
Member member = memberRepository.save(Member.of(1L, "NICKNAME"));
return member;
} catch (DataIntegrityViolationException e) {
return memberRepository.findByNickname("NICKNAME")
.orElseThrow();
}
}
null id in entry (don't flush the Session after an exception occurs)
에러가 발생하게 된다. hibernate exception 이 일어나면 자동으로 세션을 clear 해주지 않는다! entity는 detached 상태로 세션에 남아있고.. 다음 flush 시점에서 이것이 오류로 남는다.
@Transactional
public Member save() {
try {
Member member = memberRepository.save(Member.of(1L, "NICKNAME"));
return member;
} catch (DataIntegrityViolationException e) {
entityManager.clear();
return memberRepository.findByNickname("NICKNAME")
.orElseThrow();
}
}
이런 경우에는 entityManager.clear()를 통해서 엔티티 매니저에는 미완상태의 detached entity를 제거할 수 있다. 물론 clear() 를 해도 위의 코드에서는 내부 롤백 마크가 찍혀 전체롤백 에러가 발생하게 된다.