Technology[Back]

Spring JPA 사용 시 나타날 수 있는 문제점 해결법(referencedColumnName)

Thunderland 2022. 2. 6. 23:57

Spring에서 DB와 연동 시 직접 SQL문을 작성하지 않아도 된다는 장점으로 JPA를 자주 사용합니다. 그런데 SQL문을 직접 작성하지 않아도 된다는 것이 단점으로도 작동할 수 있는데 Hibernate가 entity를 참고하여 임의로 SQL문을 작성하므로 프로그래머가 이를 직접 제어하기가 쉽지 않습니다.(물론 SQL문을 직접 작성할 수 있지만 이럴 경우 JPA의 장점이 사라지므로 간단한 SQL문의 경우 주로 Hibernate에게 SQL문 작성을 위임합니다.) 이 때 Hibernate가 어떤 SQL문을 작성하였는지 Insert문에서 어떤 데이터를 넣으려고 시도했는지를 console창에 출력할 수 있습니다.

(1) application.properties

spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.type.descriptor.sql=trace

spring.jpa.properties.hibernate.format_sql = true 는 Hibernate가 작성한 SQL문을 보기 쉽게 정리해서 출력해주는 옵션이고, 해당 옵션 사용시 Insert문의 경우 직접 넣는 데이터를 ?로 부여하고 직접 보여주지 않는데 이를 해결할 수 있는 방법이 logging.level.org.hibernate.type.descriptor.sql = trace라는 옵션으로 직접 넣는 값을 보여주는 옵션입니다.

 

위의 방법으로 Hibernate가 작성한 SQL문을 볼 수 있는데 이 때 의도하지 않는 값을 넣는 경우 사용자가 직접 SQL을 작성하지 않아도 어노테이션을 이용해 어느정도 지정해줄 수 있습니다. 이번 게시글에서 해결할 문제는 Entity의 변수로 객체를 선언하고 이를 DB에 저장할 때 Primary key가 아닌 다른 Unique key를 외래키로 지정하여 저장하고자 할 때 발생하는 문제입니다.

DB는 객체를 저장할 수 없는 데 반해 Java는 기본적으로 객체지향을 지원하고 있으며 Java를 사용하는 Spring이나 JPA 역시 객체의 CRUD를 모두 지원하고 있습니다. 그래서 Spring에서의 Entity의 경우 Entity class 내의 변수로 객체를 저장/사용할 수 있는데 이에 반해 DB에서는 단일의 값만 저장할 수 있으므로 이 때 JPA는 이 둘을 연결시키기 위해 객체의 특정 변수를 DB에 저장하고 추후 DB에서 객체를 불러와야할 경우 해당 특정 변수로 다른 테이블과 조인해서 해당 객체를 찾아서 가지고 오는 방식으로 해결하고 있습니다. 이 때 JPA가 주로 사용하는 특정 변수가 Primary key 입니다. 물론 대부분의 외래키의 경우 해당 테이블의 기본키로 설정하고 있지만 간혹 기본키가 아닌 Unique key로 외래키를 설정하는 경우가 있는데 이 때에는 JPA가 이를 인식하지 못하므로 기본키를 Insert하려다가 Oracle이나 Mysql과 같은 DB에서 Parent key is not exist라는 에러를 발생시키곤 합니다. 당연히 특정 Unique key를 외래키로 설정해놨는데 JPA가 기본키를 여기에 Insert하려고 하니 당연히 존재하지 않는 이상한 키값으로 Insert하려는 것이므로 DB가 에러를 발생시킵니다. 이를 해결할 수 있는 방법이 @JoinColumn이라는 어노테이션입니다.

(2) Spring Entity - REPLY

@Entity
@SequenceGenerator(
        name="SEQ_CNUMBER",
        sequenceName="reply_1",
        initialValue=1,
        allocationSize=1
        )
public class REPLY {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="SEQ_CNUMBER")
	int CNUMBER;
	String CCONTENT;
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "USERID", referencedColumnName = "USERID")
	Idmanage idmanage;
	int CRECOMMAND;
	String CDATE;
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "BNUMBER", referencedColumnName = "BNUMBER")
	BOARD board;

위의 코드는 실제 프로젝트에서 일부 가져온 것으로 사용하시기 위해서는 일부 수정이 필요할 수 있습니다. 위에서 REPLY라는 class를 @Entity라는 어노테이션으로 JPA가 관리하는 객체임을 선언하고 있고 클래스 내의 변수로 여러가지를 선언하고 있는데 이는 모두 DB와 직접적으로 Insert되는 칼럼들입니다. 이 때 중간의 Idmanage idmanage 변수를 보시면 Idmanage라는 클래스의 객체를 변수로 가지고 있습니다. 이는 Java의 객체지향을 실현한 것으로 class내의 변수로 타클래스의 객체를 가지고 있을 수 있습니다. 그러나 DB의 경우 객체를 저장할 수 없으므로 Idmanage가 가지고 있는 여러 변수 중 한 가지를 선택해서 DB에 저장하고 이를 이용해 추후 DB에서 꺼낼 때 Idmanage 테이블과 JOIN을 통해 해당 객체를 가지고 옵니다. 이 때 Idmanage 클래스는 다음과 같습니다.

(3) Spring Entity - Idmanage 

@Table(name = "IDMANAGE")
@Entity


public class Idmanage implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SequenceGeneratorName")
    @SequenceGenerator(sequenceName = "IDMANAGE_1", name = "SequenceGeneratorName", allocationSize = 1)
    @Column(name = "ID")
    private Long ID;


    @Column(name = "USERID")
    private String USERID;

    @Column(name = "USERPASSWORD")
    private String USERPASSWORD;
    @Column(name = "EMAIL")
    private String EMAIL;
    @Column(name = "CELLPHONE")
    private String CELLPHONE;
    @Column(name = "USERCLASS")
    private String USERCLASS;
    @Column(name = "JOINDATE")
    private Date JOINDATE;
    @Column(name = "SPEC")
    private Integer SPEC;
    @Column(name = "USERFILE")
    private Blob USERFILE;

Idmanage 클래스를 보시면 역시 @Entity 어노테이션을 이용해 JPA가 관리하는 객체임을 알 수 있고 @Id 어노테이션으로 ID라는 변수가 해당 객체의 Primary key임을 알 수 있습니다. 이를 이용해 Hibernate는 별도의 지시가 없는 경우 기본키로써 조인하므로 Idmanage 객체의 ID라는 값을 Insert하고자 하는데 하필 REPLY라는 객체의 idmanage 변수를 보시면 @JoinColumn의 어노테이션을 보면 name 속성에 USERID라는 변수를 외래키로 사용하고 있음을 볼 수 있습니다. 분명 USERID는 Unique key이겠지만 Primary key가 아니므로 Hibernate가 SQL을 작성할 때에는 당연히 기본키로서 조인하고자 하므로 ID값으로 Insert를 진행하는데 해당 ID값은 USERID에 없는 값이므로 DB에서는 Parent key is not exist 라고 없는 값을 외래키로 사용하고자 함을 알려주고 있습니다. 이를 해결하기 위해 @JoinColumn의 어노테이션 속성 중에 referencedColumnName이라는 속성을 통해 USERID라는 변수를 조인할 때의 키값으로 사용하겠다고 명시적으로 선언해주면 Hibernate는 이를 보고 Insert시에 기본키인 ID 대신에 지정한 USERID라는 값을 Insert 시도하게 되고 이는 정상적으로 문제가 해결됩니다.

 

&&

cannot be cast to java.io.Serializable라는 오류가 발생할 수 있는데 이는 Entity를 조인하는 과정 중 PK를 조인한게 아닌 Unique Key를 사용해서 조인하고자 할 때 발생할 수 있는 오류로 참조 된 Entity 클래스에 implement Serializable을 사용하면 해결됩니다.

그러나 되도록이면 PK를 이용해 조인하는 게 좋습니다!