메타코딩 SNS프로젝트

33. 프로필 페이지 - 회원정보 수정 오류 - 무한참조 해결하기

정현3 2022. 6. 27. 19:22

'이미지 업로드'에 대한 오류 원인파악과 해결이 끝이 났다.

로그인을 하고 프로필페이지로 가보면 이렇게 '회원정보'가 비어있는것을 알 수 있다.

내용을 적으러 가보면 '에러'가 발생한다

-> '세션'이 없다는 에러문구이다. 우리가 저번시간에 배웠던 Open In View 에러와 같은 문제이다. UserController에 '회원수정페이지'를 응답하는 메서드를 살펴보면 

여기도 sysout이 있어서 그런듯 하다. sysout을 '주석'처리하고 다시 '회원 수정 페이지'로 돌아가보자

-> 데이터를 입력하고 '제출'버튼을 클릭하면 아무런 이벤트도 발생하지 않는다. VSCode 콘솔을 확인해보면 StackOverFlow 오류가 발생한것을 볼 수 있다. 

DB에서 User 테이블을 확인해보면 내가 입력한 데이터는 INSERT된 것을 볼 수 있다. 어떤 이유 때문에 '무한 참조'가 발생했을까? 

UserApiController로 가서 '회원정보 수정' 데이터를 응답하는 메서드로 가보자.

 

@RequiredArgsConstructor
@RestController //데이터를 응답할것이기 때문
public class UserApiController {

    private final UserService userService; //DI 주입

    @PutMapping("/api/user/{id}")
    public CMRespDTO<?> update(@PathVariable int id,
                               @Valid UserUpdateDto userUpdateDto,
                               BindingResult bindingResult,     //꼭 Vaild가 적혀있는 다음 파라미터에 적어야함 !!!!
                               @AuthenticationPrincipal PrincipalDetails principalDetails) { //세션정보 변경

        if (bindingResult.hasErrors()) {     //오류가 발생하면 getFieldErrors() 컬렉션에 모아준다
            Map<String, String> errorMap = new HashMap<>();

            for (FieldError error : bindingResult.getFieldErrors()) {

                errorMap.put(error.getField(), error.getDefaultMessage()); //20이하여야 합니다
            }
            throw new CustomValidationApiException("유효성 검사 실패", errorMap); //예외 발생시킴
        } else {

            User userEntity = userService.회원수정(id, userUpdateDto.toEntity()); //userObject를 날린다
            principalDetails.setUser(userEntity); //세션정보 변경

            return new CMRespDTO<>(1, "회원수정 완료", userEntity); //응답의 DTO. 1은 성공
            //응답시에 userEntity의 모든 getter함수가 호출되고 JSON으로 파싱하여 응답한다.
        }
    }
}

즉, 우리가 저번 시간에 배웠던 Entity 응답시 자바 객체를 JSON으로 파싱하면서 발생하는 오류인것이다.

이 역시 LAZY 로딩으로 image 오브젝트를 호출하면서 발생하는 문제이다.

Image 오브젝트에도 User 오브젝트가 들어있기 때문에 발생하는 '양방향 매핑'의 문제이다.

 

이 에러를 해결하기 위해선 User 오브젝트를 호출할 때는 Image 오브젝트도 호출되는데

User를 호출하여 같이 호출된 Image 오브젝트 내부에 있는 User 오브젝트는 호출되지 않도록 해주면 된다. 

User 클래스로 돌아가보자.


@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity // DB에 테이블을 생성
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //번호 증가 전략이 데이터베이스를 따라간다.
    private int id; //Primary 키

    @Column(length = 20, unique = true) //회원가입 아이디 중복방지
    private String username;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private String name;
    private String website; //웹 사이트
    private String bio; //자기 소개
    @Column(nullable = false)
    private String email;
    private String phone;
    private String gender;

    private String profileImageUrl; //사진

    private String role; //권한

    // 나는 연관관계의 주인이 아니다. 그러므로 테이블에 컬럼을 만들지마
    // User를 Select할때 해당 User id로 등록된 image들을 전부 가져와
    // Lazy = User를 Select할때 해당 User id로 등록된 image들을 가져오지마 - 대신 getImages() 함수의 image들이 호출될때 가져와
    // Eager = User를 Select할때 해당 User id로 등록된 image들을 전부 Join해서 가져와
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @JsonIgnoreProperties({"user"})
    private List<Image> images; // 양방향 매핑 - 프로필페이지를 응답할때, 같이 담아올 image의 정보
    private LocalDateTime createDate;


    @PrePersist // DB에 INSERT 되기 직전에 실행
    public void createDate() {
        this.createDate = LocalDateTime.now();
    }

}

-> Image 오브젝트에 @JsonIgnoreProperties 어노테이션을 걸어준다.

@JsonIgnoreProperties는 JSON으로 파싱하지 않게 해주는 어노테이션이다.

괄호, 중괄호 안에 해당 오브젝트가 가지고 있는 변수를 지정해줄 수 있는데, 문제가 된 변수는 User 오브젝트이고, Image 오브젝트 내부의  user변수명을 적어주면 되겠다.


@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image { //N:1

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String caption; //해당 Image를 설명하는 영역
    private String postImageUrl; //Image을 전송받아서 그 사진을 서버의 특정 폴더에 저장 - DB에 그 저장된 경로를 INSERT

    @JoinColumn(name = "userId")  // 1:1
    @ManyToOne(fetch = FetchType.EAGER)
    private User user; //1명의 User는 여러개의 Image를 만들어낼수 있다. - Image를 누가 올렸는지 알기위해 받은 USerObject

    //이미지 좋아요 기능 (추후에 업데이트)

    //이미지 좋아요 카운팅 (추후에 업데이트)

    //댓글 기능 (추후에 업데이트)
    private LocalDateTime createDate; //모든 DB에는 시간정보가 필요하다 - 데이터가 입력된 시간

    @PrePersist
    public void createDate() {
        this.createDate = LocalDateTime.now();
    }

    /**오브젝트를 콘솔에 출력할 때 문제가 될 수 있어서 User부분을 출력되지 않게 함
     * @Override
    public String toString() {
        return "Image{" +
                "id=" + id +
                ", caption='" + caption + '\'' +
                ", postImageUrl='" + postImageUrl + '\'' +
                ", createDate=" + createDate +
                '}';
    }
    **/
}

 

-> 다시 '회원 수정'을 시도해보면 성공적으로 수정이 완료되었다.

즉, User 호출 -> Image 호출 -> User 파싱안함 의 경로가 되는 것이다. 

그렇다면 반대로 Image 호출 -> User 호출 -> Image 파싱안함 도 해주어야한다. 이부분은 나중에 진행하도록 하겠다.