Java / Spring

java.lang.TypeNotPresentException 과 지연로딩(LAZY Loading) 본문

Error

java.lang.TypeNotPresentException 과 지연로딩(LAZY Loading)

밍구밍구밍 2025. 1. 10. 17:36

에러코드 :  java.lang.TypeNotPresentException: Type cohttp://m.hyeobjin.domain.entity.FileBox not present

 

프로젝트의 join 테이블의 데이터를 조회하는 API 테스트 도중 위와 같은 에러가 발생하였다.  

 

DB에서 Item 테이블의 file 데이터를 List로 가지고 오는 쿼리에서 문제가 발생하였고, 처음에는 뭔가 타입에러 라고 생각해서 엔티티와 DB 타입 매핑쪽을 확인해보았지만 아니였다.

 

에러코드를 좀 더 확인해본 결과,

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class

 

뭔가 직렬화와 관련된 문제라고 하는데 @JoinColum 으로 설정한 엔티티를 다시 확인하였다.

 

* 테이블 구조

메인 테이블인 Item 에서 제조사 manufacturer 정보 외래키로 가지고 있고 Item 객체가 FileBox 엔티티에 외래키로 저장되어 있는 구조이다.

 

ItemNum 를 조건으로 해당 제품의 파일과 제조사 데이터를 조회

@Entity
@Table(name = "item")
@Getter
@NoArgsConstructor
public class Item {

    @Id @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(name = "item_num")
    private String itemNum;

    @Column(name = "item_name")
    private String itemName;

    @Column(name = "item_use")
    private String itemUse;

    @Column(name = "item_spec")
    private String itemSpec;

    @Column(name = "item_type")
    private String itemType;

    @Column(name = "item_description")
    private String itemDescription;

    @Column(name = "item_yn")
    private Boolean itemYN;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "manu_id")
    private Manufacturer manufacturer;
@Entity
@Table(name = "manufacturer")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Manufacturer {

    @Id @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(name = "manu_name")
    private String manuName;

    @Column(name = "manu_yn")
    private String manuYN;
@Entity
@Table(name = "filebox")
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@EnableJpaAuditing
public class FileBox {

    @Id @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(name = "file_name")
    private String fileName;

    @Column(name = "file_orgname")
    private String fileOrgName;

    @Column(name = "file_size")
    private Long fileSize;

    @Column(name = "file_type")
    private String fileType;

    @Column(name = "file_path")
    private String filePath;

    @CreatedDate
    @Column(name = "file_regdate")
    private LocalDateTime fileRegDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")  // 외래키 컬럼을 지정
    private Item item;
 

Swagger UI 를 통해 K100 을 조회하였을 때는 문제가 없다. K100 의 아이템 객체는 FileBox 의 데이터를 가지고 있지 않기 때문에 문제가 없었다.

 

그런데 FileBox 데이터를 가지고 있는 K102 품번을 조회 했을 때 아래와 같은 에러 메세지가 발생하였다. TEST 코드에서는 데이터 조회했을 때 문제가 없었지만 Swagger API 로 데이터를 조회하니 뭔가 데이터를 조회하는 과정에서 500 error 발생

 

에러코드에서 하이버네이트의 프록시 객체와 관련되어 있는 것 같아서 처음에는 무작정 트랜잭션 쪽에서 예외처리로직 추가하고 디버깅을 시도하려 했지만 실패 ..

 

일단 스택오버플로우에 에러 코드를 검색해보았다. 비슷한 내용과 해결 방법이 있었지만 당시에는 이해하고 해결하지 못하는 내용들이라서 코드로 다시 돌아와서 생각하던 중 예전에 인프런의 강의를 보면서 JPA 공부할 때, 조인된 테이블의 외래키 조건을 지연로딩(Lazy Loading) 으로 설정한 경우 Hibernate 는 해당 연관 데이터를 프록시 객체로 생성한다고 했던 기억이 생각나서 이전에 기록해두었던 지연로딩 글을 다시 확인해본결과

 

(프록시 상태 즉, 준영속 상태의 객체는 직렬화 할 수 없음)

 

@JsonIgnore (X)

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "manu_id")
private Manufacturer manufacturer;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")  // 외래키 컬럼을 지정
private Item item;

 

현재 외래키 관계인 앤티티 객체에 @JsonIgnore 어노테이션을 적용하면 해당 엔티티의 직렬화를 무시하고 서버에서 데이터를 조회할 수 있지만 직렬화가 되지 않은 FileBox 객체는 클라이언트로 데이터가 넘어가지 않는다. (서버에서만 데이터 조회 가능)

 

일단 swagger 로 위의 결과를 조회해 보았다.

 

 

Item 이 FileBox 를 조회하여 파일에 대한 정보를 가지고 오는데 자세히 보니 FileBox 가 또 다시 Item 객체를 조회하고 있다. 그리고 현재 데이터가 조회되는 것은 swagger API 는 문서화 도구로 @JsonIgnore 를 고려하지 않고 API 문서화 모델결과를 반환한다. 실제 api 통신을 할 때는 JSON 직렬화가 이루어지며 응답에 포함되지 않는다.

 

*** 문제 원인 1

fetchJoin() 의 부재

- Item 과 FileBox, Item 과 Manufacturer 객체를 조인할 때 fetchJoin() 없이 leftJoin() 하였다.. 문제는 연관된 엔티티가 지연로딩으로 설정 되어 있어 실제로 필요할 때마다 데이터베이스에서 추가적인 쿼리가 실행된다. 그리고 이 연관된 엔티티 객체는 LAZY Loading 으로 설정되어 있기 때문에 프록시 객체가 되고 직렬화가 되지 않았다. 

@Override
public FindByItemDTO findByItem(String itemNum) {

    QItem item = QItem.item;
    QFileBox fileBox = QFileBox.fileBox;

    List<FindFileBoxDTO> fileBoxes = jpaQueryFactory
            .selectFrom(fileBox)
            .leftJoin(fileBox.item, item)
            .leftJoin(item.manufacturer, QManufacturer.manufacturer)
            .where(item.itemNum.eq(itemNum))
            .fetch()
            .stream().map(FindFileBoxDTO::new)
            .collect(Collectors.toList());

    Item selectItem = jpaQueryFactory
            .selectFrom(item)
            .leftJoin(item.manufacturer, QManufacturer.manufacturer)
            .where(item.itemNum.eq(itemNum))
            .fetchOne();

    return new FindByItemDTO(
            selectItem.getId(),
            selectItem.getItemName(),
            fileBoxes,
            selectItem.getItemNum());
}

 

fetchJoin() 적용 : 연관된 데이터를 즉시 로딩(EAGER loading) 하게 된다. 단일 쿼리로 연관된 데이터를 모두 가져오게 된다. 이 경우, 직렬화 시 item 객체안에 포함된 fileBox 와 manufactrer 데이터가 모두 직렬화된다. 

.leftJoin(fileBox.item, item).fetchJoin()
.leftJoin(item.manufacturer, QManufacturer.manufacturer).fetchJoin()

*** 문제 원인 2

- FileBoxDTO 에서 Item pk 를 다시 넘겨주고 있었다.. 그래서 FileBoxDTO 가 한번더 Item 객체를 조회하고 있었다

@Data
@NoArgsConstructor
@AllArgsConstructor
public class FindFileBoxDTO {

    private Long fileBoxId;
    private String fileName;
    private String fileOrgName;
    private Long fileSize;
    private String fileType;
    private String filePath;

    private Item item;


    public FindFileBoxDTO(FileBox fileBox) {
        this.fileBoxId = fileBox.getId();
        this.fileName = fileBox.getFileName();
        this.fileOrgName = fileBox.getFileOrgName();
        this.fileSize = fileBox.getFileSize();
        this.fileType = fileBox.getFileType();
        this.filePath = fileBox.getFilePath();
        this.item = fileBox.getItem(); // 문제의 원인
    }
}

 

아래의 쿼리에서 fileBox 를 DTO 로 변환하는 과정에서 해당 FileBox 객체의 연관관계인 Item 을 당연히 넘겨줘야 된다고 생각해서 FileBox 객체를 변환할 때 또 한번 Item 을 참조한 것이 문제였다 위의 코드를 지우고 다시 아래 쿼리를 실행하니

    @Override
    public FindByItemDTO findByItem(String itemNum) {

        QItem item = QItem.item;
        QFileBox fileBox = QFileBox.fileBox;

        List<FindFileBoxDTO> fileBoxes = jpaQueryFactory
                .selectFrom(fileBox)
                .leftJoin(fileBox.item, item).fetchJoin()
                .leftJoin(item.manufacturer, QManufacturer.manufacturer).fetchJoin()
                .where(item.itemNum.eq(itemNum))
                .fetch()
                .stream().map(FindFileBoxDTO::new)
                .collect(Collectors.toList());

        Item selectItem = jpaQueryFactory
                .selectFrom(item)
                .leftJoin(item.manufacturer, QManufacturer.manufacturer)
                .where(item.itemNum.eq(itemNum))
                .fetchOne();

        return new FindByItemDTO(
                selectItem.getId(),
                selectItem.getItemName(),
                fileBoxes,
                selectItem.getItemNum());
    }
}

 

 

하나의 제품에 두개의 File 정보를 한번의 쿼리로 가지고 올 수 있었다.

 

** 결론

1) 여러 테이블의 지연 로딩이 걸려있을 때 직렬화 문제 발생 시 fetchJoin() 을 사용

2) DTO 구현 시 좀 더 꼼꼼하게 할 것...