카테고리 없음

프로필 페이지 6

have a good time 2021. 12. 17. 14:34

 

<1> 변수 호출 무한반복

(예를 들어 User.java 파일에 image 객체 변수가 있고,

Image.java 파일에 user 객체 변수가 있어서, 서로가 서로를 getter로 무한반복하면서 호출)

 

 

 

ImageService.java 파일을 아래와 같이 변경한다.

(이 코드는 사이트에서 이미지 업로드 할 때 실행되는 코드이다)

@RequiredArgsConstructor
@Service
public class ImageService {
	
	private final ImageRepository imageRepository;
	
	@Value("${file.path}")
	private String uploadFolder;
	
	@Transactional
	public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails) {
		UUID uuid = UUID.randomUUID(); 
		String imageFileName = uuid+"_"+imageUploadDto.getFile().getOriginalFilename(); 
		System.out.println("이미지 파일이름 : "+imageFileName);
		
		Path imageFilePath = Paths.get(uploadFolder+imageFileName);
		
		try {
			Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		Image image = imageUploadDto.toEntity(imageFileName, principalDetails.getUser()); 
		Image imageEntity = imageRepository.save(image);
		
		System.out.println(imageEntity);
	}
}

즉, 기존 파일에서 아래 코드 추가됨.

System.out.println(imageEntity) 하기 위해, imageRepository 에 저장된 결과를 imageEntity 라는 변수로 받음

		Image imageEntity = imageRepository.save(image);
		System.out.println(imageEntity);

 

원래 기존 파일은 

 

 imageRepository.save(image);

이렇게 데이터베이스에 저장만 하고 끝냈음.

그리고, 사진업로드 할 때 동작을 잘했다.

 

 

그런데, 변경된 ImageService.java 파일을 가지고 사진 업로드를 하게 되면 

 

그러면 사이트 화면에 아래와 같이 500 에러가 나오게 된다.

 

즉, 이는 추가된 아래 코드 때문이다.

		Image imageEntity = imageRepository.save(image);
		System.out.println(imageEntity);

 

System.out.println(imageEntity) 하게 되면, Image 테이블의 모든 변수들에 getter 가 호출된다.

 

<Image.java> 파일

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	private String caption; 
	private String postImageUrl; 
	
	@JoinColumn(name = "userId")
	@ManyToOne(fetch = FetchType.EAGER) 
	private User user; 
	
	private LocalDateTime createDate;
	
	@PrePersist
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}

}

즉 위 변수들의,

id, caption, ... user.. 이런 변수들의 getter가 호출된다.

그리고 System.out.println으로 id, caption 값들을 출력하는 것이다.

 

이때 user 변수에 대한 getter 도 호출되므로,

 

<User.java> 파일을 보면

 

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) 
	private int id;
	
	@Column(length = 100,  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 role; 
	private String profileImageUrl;
	
	@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
	private List<Image> images; 
	
	private LocalDateTime createDate;
	
	@PrePersist 
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}

}

여기서도 images라고 해서 image에 대한 getter가 호출된다.

이미 위에서 Image.java 파일 변수들에 getter가 호출되었었는데, 또 호출되는 것이다.

계속 무한반복..

 

 

 

toString()

그런데, imageEntity 라는 오브젝트에 대해 코드가 실행되면 

System.out.println(imageEntity);

아래의 코드가 실행되는 것과 같다. 즉, toString() 이라는 함수가 자동 호출됨 

	System.out.println(imageEntity.toString());

저 toString() 이 뭔지 알아보기 위해,

 

imageEntity 가 Image 객체 변수이므로,

Image.java 파일에 toString() 함수를 만들어보겠다.

그런데 사실 @Data 라는 애노테이션을 사용하면,

toString() 이라는 메서드가 자동으로 만들어진다. (다만 파일상에서는 안 보일뿐)

그래서 Image.java 파일에서 오른쪽 클릭 - source - Generate toString() 하면 

이미 만들어져 있는데 또 만드냐고 하는데, yes 클릭 후 만든다.

 

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	private String caption; 
	private String postImageUrl; 
	
	@JoinColumn(name = "userId")
	@ManyToOne(fetch = FetchType.EAGER) 
	private User user; 
	
	private LocalDateTime createDate;
	
	@PrePersist
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}

	@Override
	public String toString() {
		return "Image [id=" + id + ", caption=" + caption + ", postImageUrl=" + postImageUrl + ", user=" + user
				+ ", createDate=" + createDate + "]";
	}

}

toString() 메서드가 만들어졌다.

 

그런데, 위에서 무한 반복이 일어나는게, user 를 출력할 떄 문제가 되는 것이므로, toString() 메서드에서 user를 삭제한다. (즉, imageEntity 오브젝트를 출력할 때 user를 호출하지 않음.)

 

	@Override
	public String toString() {
		return "Image [id=" + id + ", caption=" + caption + ", postImageUrl=" + postImageUrl 
				+ ", createDate=" + createDate + "]";
	}

그런 다음 사이트에서 이미지 업로드를 하면 문제 없이 잘 업로드 된다.

즉 아래의 코드를 실행하며 생기는 무한반복 문제가 해결되었다. 

		Image imageEntity = imageRepository.save(image);
		System.out.println(imageEntity);

 

 

그래서 sts(이클립스) 콘솔창에 보면,

System.out.println(imageEntity) 가 실행되는 결과를 보면

 

이처럼 Image.java 의 다른 변수들은 출력되나, user 객체 변수는 출력되지 않는 것을 볼 수 있다.

 

때문에 JPA 를 할 때는 위처럼 오브젝트를 출력하는 System.out.println 을 조심해야 한다.

테스트할 때만 사용하고, 그 다음에는 삭제.

 

Controller 파일에서도 마찬가지이다.

UserApiController.java 파일에서 

	return new CMRespDto<>(1,  "회원수정완료", userEntity);

이처럼 userEntity를 return 하는데,

이렇게 응답시에 userEntity 의 모든 변수에 getter 함수가 호출되고

JSON으로 파싱되어 응답하면서 문제가 생길 수 있다.

(여기서는 toString()이 문제가 아니고, 리턴될 때 모든 변수의 getter가 호출되어 JSON으로 바꾸는 과정에서 무한반복)

 

userEntity는 User.java 객체의 변수로,

User 클래스는 image를 변수로 갖고있고,

Image클래스는 user를 변수로 갖고 있기 떄문에

여기서도 무한반복이 일어날 수 있음.

 

-> 다음 글에서 설명

 

 

 

그래서, 이제 다시 원래 상태로 파일을 복구 시키겠다.

ImageService.java 파일에서 System.out.println() 하는 부분제거

 

@RequiredArgsConstructor
@Service
public class ImageService {
	
	private final ImageRepository imageRepository;
	
	@Value("${file.path}")
	private String uploadFolder;
	
	@Transactional
	public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails) {
		UUID uuid = UUID.randomUUID(); 
		String imageFileName = uuid+"_"+imageUploadDto.getFile().getOriginalFilename(); 
		System.out.println("이미지 파일이름 : "+imageFileName);
		
		Path imageFilePath = Paths.get(uploadFolder+imageFileName);
		
		try {
			Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		Image image = imageUploadDto.toEntity(imageFileName, principalDetails.getUser()); 
		imageRepository.save(image);
	
	}
}

 

Image.java 파일에서도 toString() 메서드 삭제하기

 

------------------------------------------

 

<2> open - in - view

클라이언트가 요청을 보내면 톰켓 내부에서 상호작용한다.

 

디스패처 : 클라이언트에서 주소 요청을 할 때마다 어떤 컨트롤러를 선택할지 결정

 

스프링 컨테이너 <대략적 내부구조>  

 

디스패처를 통해 선택된 컨트롤러가 실행.

컨트롤러는 서비스를 호출 (c -> s)

서비스는 레파지토리 호출 (s -> r)

레파지토리는 영속성 컨텍스트를 가지고 있음

영속성컨텍스트는 데이터베이스 호출

 

스프링 컨테이너 <자세히 살펴보기>

 

디스패처 : 클라이언트가 요청한 주소에 맞는 컨트롤러 선택.

이때 데이터베이스에 접근할 수 있는 세션이 만들어짐

따라서 컨트롤러에서 데이터베이스에 접근해서 select 할 수 있게 됨

 

 

순서 1) 영속성 컨텍스트에 요청 데이터가 있을 때

 

요청 : c ->s -> r -> 영속성 컨텍스트

결과 응답 : 영속성 컨텍스트 -> r -> s-> c

(컨트롤러는(c) 그 결과를 데이터나 html 파일로 응답함)

 

 

순서 2) 영속성 컨텍스트에 요청 데이터가 없을 때

          (영속성 컨텍스트는 데이터베이스로부터 요청 데이터를 받음) 

 

요청 : c ->s -> r -> 영속성 컨텍스트 -> 데이터베이스

결과 응답 : 데이터베이스 -> 영속성 컨텍스트 -> r -> s-> c

(컨트롤러는(c) 그 결과를 데이터나 html 파일로 응답함)

 

 

가. open-in-view: false (application.yml 파일에서 설정)

 

 

위에서 세션이 컨트롤러가 선택되었을 때 만들어진다고 했는데,

응답되는 과정 중 s->c 에서 세션이 종료됨

 

그러면 c에서는 LAZY 로딩을 할 수 없음

 

LAZY 로딩을 하게 되면, 만약에 User.java 파일에 Image.java 객체 변수인 images가 있을 때,

 

<User.java> 파일

	@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
	private List<Image> images;

1) 만약에 fetchType 이 LAZY라면,

초반에 User객체를 select 할 때, 영속성 컨텍스트에는 images 는 안 들고 오고,

user 만 들고옴

이 user 데이터를 들고 결과 응답하면서  r -> s -> c 이렇게 가다가,

s -> c 과정 중 세션이 종료되는데,

만약 controller에서 LAZY 로딩으로 images 데이터를 select 해서 사용하려고 하면 사용할 수가 없게됨.

(세션이 종료되서 데이터베이스로부터 images 데이터를 받을 수 없음)

 

 

더보기

-> 이럴 때 LAZY 로딩이 가능하게 하려면, open-in-view 를 true 로 바꿔주면 됨.

아래에서 설명

 

 

 

그래서 이 상황, 

<1> open-in-view : false

 

<application.yml에 아래와 같이 설정> - 이미지에는 잘렸지만, spring 키워드에서 설정해야 하므로

더보기 클릭해서 보기 

더보기

spring:

  jpa:

    open-in-view: false

 

 

설정

 

<2> LAZY 전략일 때,

 

아래의 프로필 화면(profile.jsp) 으로 이동해보겠다.

 

<프로필 화면> - 이 화면은 open in view = true 일 때 작동한거임.

 

 

위 화면은 profile.jsp 파일에서 구현해서 출력된 결과물이다.

이 때, controller 는 user 객체 정보를 Model 객체에 담아서

profile.jsp 파일에 넘겨주고

profile.jsp 파일은 user 데이터를 이용해서 아래와 같이 사용한다.

즉, user 객체로 부터 images 데이터를 호출해서 사용한다.

그런데 이때 LAZY 전략을 사용했으므로, 문제가 생길 것이다. 

 

${user.images}

 

<결과>

 

이처럼 에러 메세지가 뜨면서 프로필 화면으로 정상적으로 이동할 수 없게 된다.

LAZY 전략을 했으므로, user 데이터만 들고 왔는데, 나중에 images 데이터도 select 해서 사용하려고 하니,

세션이 s->c 과정에서 닫혔기 때문에 컨트롤러에서는 

데이터베이스로부터 images 데이터를 받을 수 없고,

컨트롤러에서 데이터를 넘겨받는 view 단(profile.jsp)에서도 역시 사용할 수 없음

 

 

 

 

 

2) fetchType 이 EAGER 이면,

User 객체를 select 할 때, 영속성 컨텍스트에 user와, images 모두 들어가 있게 됨.

따라서 profile.jsp 파일이 문제없이 나오게 된다.

그래서 아래와 같이 프로필 화면이 잘 나온다.

 

 

나. open-in-view: true

 

여기서는 LAZY 전략으로 사용한다.

 

역시 프로필 화면으로 이동할 때 잘 작동한다.

 

open-in-view : true 가 되면

view 까지 세션을 오픈한다는 뜻이다.

그림에서 보이듯이 세션 종료가, controller 앞에서 이뤄진다.

(강의 17분 55초)

그래서 controller에서 LAZY 로딩이 가능하고, images 데이터를 잘 사용할 수 있게 되는 것이다.

 

 

<3> - 번외 : service 파일 메서드에 @Transactional 사용

 

service.java 파일의 모든 메서드에는 @Transactional 사용하자.

그런데, UserService.java 파일의 회원프로필 메서드는 데이터베이스로부터 데이터를 select 한다.

단, readOnly = true 를 붙여주자.

@Transactional(readOnly = true)

select 할 때도 @Transactional 을 사용한다. 자세히 살펴보자.

user 1번 정보를 update 하고 싶다면, 데이터베이스에서 일단 user 1번 정보를 select 해서 영속성 컨텍스트로 들고온다.

그런 다음 repository 에 그 1번 user 정보가 넘어간다. 그럼 스프링 컨테이너가 user 1번 정보를 들고 있는 상태가 된다.

(1번 유저 정보를 service, controller 등에 넘겨줄 수 있음)

이 상태에서, service, repository , 영속성 컨텍스트, 데이터베이스에 있는 1번 유저 정보는 모두 똑같음

 

이때 service에서 repository 에게 요청하길 

user 1의 username 을 hello 에서, hi 로 변경함 (즉 service 파일에서 username 변경시킴)

그러면 영속성 컨텍스트에 있는 user 1의 username도 hi 로 변경됨

 

그러면,

요청 : c -> s -> r -> 영속성컨텍스트 -> 데이터베이스

응답 : 데이터베이스 -> 영속성컨텍스트 -> r -> s

이 이뤄지고,

그 뒤의 s -> c가 이뤄지는 부분에서(서비스가 끝나고 난 뒤)

영속성 컨텍스트는 변경된 오브젝트(username이 hi로 변경됨) 를 감지.

바뀌었다면, 데이터베이스에 자동 flush. 

(즉, 데이터베이스에도 user 1의 username이 hi가 됨)

바뀌지 않았다면 데이터베이스에 flush 안 함

 

그런데,

@Transactional(readOnly = true)

이렇게 설정하면, 변경되었는지 확인 안함.

읽기 전용 데이터라고 생각함.

그러면 일을 조금 덜 하게 할 수 있음.

 

 

톰켓, 스프링 컨테이너 이미지 첨부,

readOnly true 부분 이미지 첨부

 

참고자료 : 이지업 강의 사이트 "스프링부트 SNS프로젝트 - 포토그램 만들기"