[Spring/JPA] @JoinColumn 의 name, referencedColumnName 에 대해서(부제. org.hibernate.MappingException Column duplicate error)
resilient
·2023. 3. 8. 16:27
프로젝트를 진행하는 도중 아래와 같은 상황이 있었습니다.
하나의 엔티티에서 두 개의 같은 엔티티를 매핑해야 한다.
0. 이전 @JoinCloumn 사용 예시
예를 들어보겠습니다.
@Entity
@Table(name = "friend")
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Friend extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "friend_id")
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private Users me;
@ManyToOne
@JoinColumn(name = "user_id")
private Users mate;
}
Friend 라는 Entity에 Users라는 Entity를 가지는 column이 두 개가 존재하는 상황입니다.
위와 같이 Friend Entity를 만들고 실행을 시키면 아래와 같은 에러가 발생합니다.
이런 에러가 왜 발생했는가를 생각해 보면 @JoinCloumn을 사용할 경우, name 옵션을 무지성으로 사용했다는 이유 밖에 없었습니다.
왜 쓰고 어떤 경우에 쓰는지 알아보고 이번 프로젝트의 Friend Entity에 적절하게 사용 해보려고 합니다.
1. name 옵션과 referencedColumnName
김영한님의 JPA 프로그래밍 책을 보면 name과 referencedColumnName에 대한 설명은 다음과 같습니다.
name 옵션의 기능은 매핑할 외래 키 이름을 지정해 준다.
Default 기본값은 필드명 + _ + 참조하는 테이블의 기본 키(PK) 컬럼명
referencedColumnName 옵션의 기능은 외래 키가 참조하는 대상 테이블의 컬럼명을 지정해 준다.
Default 기본값은 참조하는 테이블의 기본키(PK) 컬럼명
그렇다면 일반적으로 referencedColumnName을 생략을 하고 사용을 하는데요. 이유는 무엇일까요?
referencedColumnName을 사용하지 않는 이유는 바로 referencedColumnName을 생략하면 대상 테이블의 PK로 자동 지정되기 때문입니다.
2. 변경한 @JoinColumn 예시
위의 내용을 참고해서 아래와 같이 수정을 했습니다.
@Entity
@Table(name = "friend")
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Friend extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "friend_id")
private Long id;
@ManyToOne
@JoinColumn(referencedColumnName = "user_id")
private Users me;
@ManyToOne
@JoinColumn(referencedColumnName = "user_id")
private Users mate;
}
이렇게 생각한 이유는 다음과 같습니다.
- ManyToOne으로 Users me와 Friend의 연관관계를 맺을 때 Users의 기본값(PK)은 user_id일 것이다.
- name을 통해서 PK에 접근을 동시에 하면 duplicate 에러가 발생하니깐 referencedColumnName을 사용해서 user_id를 입력해 주면 외래키(user_id)가 참조하는 대상 테이블의 컬럼명(user_id)를 임의로 지정해 줄 수 있고, 이렇게 되면 에러가 발생하지 않는다.
위 방법은 좋지 않은 방법입니다.
조인을 하려면 FK가 조인할 대상 테이블의 컬럼이 있어야 하는데 이게 바로 referencedColumnName입니다.
즉, FK가 조인할 대상 테이블의 컬럼을 PK값인 user_id로 지정해 주는 꼴이 되어 버립니다.
또한 김영한 님의 말을 빌리면 'referencedColumnName을 PK가 아닌 다른 컬럼에 직접 지정할 수도 있지만 정규화 관점에서 권장하지는 않는다'라고 합니다.
그렇다면 위의 예시인 Friend Entity의 경우에는 어떻게 옵션을 적용해서 써야 할까요?
3. 최종 @JoinColumn 예시
방법은 간단합니다.
referencedColumnName을 사용해서 2번처럼 구현했을 경우, default 값인 테이블 컬럼명으로 me_user_id, mate_user_id가 각각 생성 됐습니다.
(여기서 눈치를 챘었어야 했는데) me_user_id와 mate_user_id로 생성된 각각의 테이블 컬럼명으로 name옵션을 적용해 주면 됩니다.
@Entity
@Table(name = "friend")
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Friend extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "friend_id")
private Long id;
@ManyToOne
@JoinColumn(name = "me_user_id")
private Users me;
@ManyToOne
@JoinColumn(name = "mate_user_id")
private Users mate;
}
위 과정들을 거치면서 수정을 거듭했고 결론적으로 이게 올바른 방식이라고 생각이 들었습니다.
4. 정리
JPA를 사용해 보면서 느끼는 점들은 '반복되는 작업을 통해 익힌 무지성으로 사용하는 여러 옵션들이 존재한다'였습니다.
물론 반복해서 사용하는 옵션들은 이유가 있겠지만 그래도 사용할 경우에는 옵션들에 대한 학습을 바탕으로 사용해야만 올바른 사용이라고 생각이 들었습니다.
앞으로는 여러 옵션들을 사용할 경우 기본적인 이해를 바탕으로 사용해야겠다고 생각을 했습니다.
감사합니다.
'Back-end > Spring' 카테고리의 다른 글
[Spring/Java] @Builder 패턴 사용시 @AllArgsConstructor를 사용하는 이유 (2) | 2023.03.22 |
---|---|
[Spring/JPA] CascadeType.ALL 사용시 주의 해야 할 점 (1) | 2023.03.15 |
[Spring/JPA] @Transactional을 사용하는 이유에 대하여 (부제. JPA Dirty Checking) (2) | 2023.03.02 |
[Spring / TIL] SpringBoot 버전 3.X.X에 Swagger적용하기 (2) | 2023.02.13 |
[Spring / TIL] Spring profiles를 통해 application.yaml 하나로 개발환경 관리하기(부제. @Value 환경변수 사용법) (0) | 2023.01.17 |