IT/개발로그

[삽질로그] JPA 연관관계 외래키 매핑시 주의점

김솔샤르 2021. 1. 17. 00:21

귀중한 준칙 두 가지를 배운 삽질로그에 대해 기록하고자 한다.

 

여러 컬럼의 조합을 기본키로 갖는 테이블들의 JPA 연관 관계를 설정해야하는 상황이었다. 대략적으로 테이블 관계를 그리면 이렇게 된다. 

 

 

각 스토어는 매일 새로운 프로모션을 생성할 수 있고, 생성할 수 있는 프로모션의 종류는 promotion_group_no로 정의하여 관리된다. 일단 스토어가 백오피스에서 프로모션을 생성하면 백그라운드에서 프로모션 생성 API가 호출되고, promotion no가 응답값으로 넘어와 스토어 프로모션 생성현황 테이블에 기록되게 된다.

 

또한 매일 promotion no를 key로 프로모션 API를 호출하여 해당 프로모션이 종료됐는지를 확인한뒤, 결과를 스토어 프로모션 참여현황에 집계하고 집계상태코드(aggregation_status_code) 값을 완료로 업데이트 해준다. 내가 개발해야 되는 것이 바로 이 배치 작업이었다. 참고로 공유를 위해 테이블 설계는 조금 변형을 했다.

 

우선 엔티티를 만든 뒤 연관관계를 정의했다. 외래키를 가진 스토어 프로모션 참여현황을 연관관계의 주인으로 하는 ManyToOne 관계로 정의하였다. 코드는 다음과 같다.

 

/* 프로모션 참여 현황 */
public class PromotionParticipationStatus {

    @Id
    @Column(name = "store_no")
    private long storeNo;

    @Id
    @Column(name = "promo_group_no")
    private int promotionGroupNo;

    @Id
    @Column(name = "promo_date")
    private String promotionDate;

    @Id
    @Column(name = "cust_no")
    private long customerNo;

    //연관관계 정의
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns(value = {@JoinColumn(name = "store_no")
                          @JoinColumn(name = "promo_group_no")
                          @JoinColumn(name = "promo_date")},
                 foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
    private PromotionCreationStatus promotionCreationStatus;
    
    @Column(name = "aggregation_status_code")
    private String aggregationStatusCode;

 

이렇게 세팅을 해두고 테스트를 해보았다. 먼저 집계가 끝난 뒤 집계상태코드가 완료로 잘 변경되는지 확인했다.

 

@BeforeEach
void setUp() {
        PromotionParticipationStatus partiStatus = PromotionParticipationStatus.builder()
            .storeNo(1L)
            .promotionGroupNo(1L)
            .promotionDate("20200101")
            .customerNo(1L)
            .build();

        PromotionCreationStatus creatStatus = PromotionCreationStatus.builder()
            .storeNo(1L)
            .promotionNo(1L)
            .promotionDate("20200101")
            .build();
            
        //save..
}

 

테스트 코드를 대충 만들고, 프로모션 API는 Mocking하여 promotionNo가 1인 요청에 대해 무조건 성공 응답을 돌려주도록 세팅하였다. 이런식으로 단위 테스트는 문제없이 쭉쭉 통과했는데, h2에 더미 데이터를 여러건 세팅하고 로컬 환경에서 실제로 배치를 돌려보니 이상한 점이 발견되었다. storeNo = 1, promotionGroupNo = 1인 경우를 제외하고 다른 모든 케이스는 Join이 제대로 되지 않았던 것이다.

 

여기서 한참을 헤매다가 하이버네이트 select 쿼리의 로그를 다시 자세히 들여다보니..

 

where promocreat0_.promo_group_no=1234567 and promocreat0_.store_no=1 and promocreat0_.promo_date='20200101'

 

1자리 숫자여야할 promotion_group_no에 7자리 숫자가, 7자리 숫자여야할 store_no에 1자리 숫자가 들어가고 있었다. 외래키 매핑이 서로 반대로 되어있었던 것이다. 이전에 쿼리 로그를 확인하기는 했지만 where문에 파라미터가 제대로 들어가고 있는지만 확인을 했기 때문에 원인을 찾는데 오랜 시간을 허비해버렸다. 이제 원인을 알았으니 수정만하면 된다.

 

/* 프로모션 참여 현황 */
public class PromotionParticipationStatus {

    @Id
    @Column(name = "store_no")
    private long storeNo;

    @Id
    @Column(name = "promo_group_no")
    private int promotionGroupNo;

    @Id
    @Column(name = "promo_date")
    private String promotionDate;

    @Id
    @Column(name = "cust_no")
    private long customerNo;

    //연관관계 정의
    /* 외래키 매핑시 참조 컬럼명을 세팅해줌 */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns(value = {@JoinColumn(name = "store_no", referencedColumnName = "store_no")
                          @JoinColumn(name = "promo_group_no", referencedColumnName = "promo_group_no"),
                          @JoinColumn(name = "promo_date", referencedColumnName = "promo_date")},
                 foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
    private PromotionCreationStatus promotionCreationStatus;
    
    @Column(name = "aggregation_status_code")
    private String aggregationStatusCode;

 

이렇듯 외래키가 참조하는 컬럼명을 지정해주니 말끔하게 해결이 됐다. JPA를 쓸때는 쿼리를 JPA가 대신 생성하기 때문에 항상 의도한 쿼리가 정확히 나가는지를 확인해야되는 것 같다. 또한 테스트 데이터를 가급적 라이브 데이터에 가깝게 세팅했더라면 단위 테스트시에도 발견할 수도 있었던 문제다. 따라서 기본적이고도 중요한 두 가지를 준칙을 기억하기로 했다.

 

  • 쿼리부터 확인하자
  • 테스트 데이터는 가급적 라이브 데이터에 가깝게 세팅하자

 

 

반응형