DataIntegrityViolationException 에 대해 알아보자

Patrick YOO·2022년 1월 24일
3
post-thumbnail

서론

필자는 현재 자바 스프링 JPA 프로젝트를 진행하며 과도한 최상위단 Transaction 을 걷어내는 작업을 진행하고 있다. 위 과정을 겪으면서 생긴 문제를 하나하나 정리해나가고자 한다.

DataIntegrityViolationException

  • RuntimeException 으로 뭔가 잘못된 데이터가 바인딩 되었을때 발생하는 에러이다.
  • SQL 문이 잘못되었거나 Data 가 잘못되었을경우

하지만 오늘은 위같은 경우는 다루지 않을것이다 위 같은경우는 로그에도 남을뿐더러 매우 찾기 쉬운에러에 해당한다.

  • 오늘 다룰 에러로 영속성컨텍스트에 이미 등록된 객체에 동일한 아이디 이지만 다른 참조값을 갖고있는 객체가 접근할 경우 이다.

바로 아래에 있는 코드를 살펴보자.

@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 주소값을 참조하고있는 멤버가 원래 객체를 밀어내려고 하니 서로 충돌이 나는것이다.

해결방법

  1. Team 객체의 영속성 전이 범위를 Merge로 바꾼다.
  • 위 경우에 영속성컨테이너에 해당 아이디가 존재하고 다른주소값을 참조하는 객체가 들어와도 병합과정을 거치기 때문에 충돌이 발생하지 않는다.
  1. save 한 객체를 변수로 받는다.
@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;
    }
  • save 한 결과값을 m 으로 받는다면 영속성 컨테이너에 등록된 객체와 동일한 주소값을 갖고있는 Order객체를 반환받을 수 있다.
profile
자유인을 꿈꾸는 개발자

0개의 댓글