필자는 현재 자바 스프링 JPA 프로젝트를 진행하며 과도한 최상위단 Transaction 을 걷어내는 작업을 진행하고 있다. 위 과정을 겪으면서 생긴 문제를 하나하나 정리해나가고자 한다.
하지만 오늘은 위같은 경우는 다루지 않을것이다 위 같은경우는 로그에도 남을뿐더러 매우 찾기 쉬운에러에 해당한다.
바로 아래에 있는 코드를 살펴보자.
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "member")
@ToString(of = {"id", "username", "age"})
public class Member {
@Id
/*@GeneratedValue(strategy = GenerationType.IDENTITY)*/
@Column(name = "member_id")
private Long id;
@Setter
private String username;
private int age;
public void changeName(String name){
this.username = name;
}
}
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY,cascade = ALL)
@JoinColumn(name = "member_id")
@Setter
private Member member;
@OneToOne(
fetch = FetchType.EAGER,
mappedBy = "team"
)
private Coach coach;
public Team(String name) {
this.name = name;
}
}
팀과 멤버는 양방향메핑이 되어있으며 Team 이 Member 의 FK 로 받고 있어 TEAM객체가 두 연관관계의 주인이다. 위 같은 상황에서 아래코드를 동작시킬경우
@Transactional
public String test(){
Member m = Member.builder()
.id(15L)
.username("Patrick")
.age(33)
.build();
repository.save(m);
Team team = Team.builder()
.name("테스트3")
.member(m)
.build();
teamRepository.save(team);
return null;
}
위 코드를 동작시켰을경우 아래와 같은 에러가 발생한다.
org.springframework.dao.DataIntegrityViolationException: A different object with the same identifier value was already associated with the session : [me.patrick.laboratory.finalvalue.entity.Member#15]; nested exception is javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [me.patrick.laboratory.finalvalue.entity.Member#15]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:400)
에러코드를 좀 들여다 보면 이미 관계를 맺고 있는 객체가 이미 세션에 연관되어있다 라는 내용이다.
쉽게 말해 위 코드에서 15번 아이디를 갖고있는 객체가 이미 세션에 있는데 15번을 가진 다른객체가 또 들어왔다
라고 하는것이다.
아니 내가 15번이라는 멤버를 만들어서 영속성컨텍스트에 넣고 그 멤버를 그대~~로 Team 에다가 넣었다.
나는 죄가없다. 하지만 내가 만든 Member 객체의 주소와 실제 영속성컨텍스트에 들어있는 Member의 주소가 같을까?
확인해보자.
@Transactional
public String test(){
Member m = Member.builder()
.id(15L)
.username("Patrick")
.age(33)
.build();
repository.save(m);
Member m2 = entityManager.find(Member.class,15L);
System.out.println(System.identityHashCode(m));
System.out.println(System.identityHashCode(m2));
Team team = Team.builder()
.name("테스트3")
.member(m)
.build();
teamRepository.save(team);
return null;
}
확인 결과 실제 영속성컨텍스트에 들어있는 m2와 직접 생성한 객체 m은 실제로 참조하고 있는 주소값이 완전히 다르다.
하여 Team 에 Member객체를 집어 넣을때 이미 15번이 영속성 컨테이너에 위와같이 1121669391 번으로 존재하고있는데 Team 객체를 넣으려고 할때 903471365 주소값을 참조하고있는 멤버가 원래 객체를 밀어내려고 하니 서로 충돌이 나는것이다.
@Transactional
public String test(){
Member m = Member.builder()
.id(15L)
.username("Patrick")
.age(33)
.build();
m = repository.save(m);
Team team = Team.builder()
.name("테스트3")
.member(m)
.build();
teamRepository.save(team);
return null;
}