writePost 리팩토링

이리·2022년 5월 5일
0

velo9

목록 보기
2/3

🚀 Controller

	/*
	@PostMapping("/write")
	public Result write(@Valid @RequestBody PostSaveDTO postSaveDTO, HttpSession session) {
		Post post = postService.write(postSaveDTO, getMemberId(session));
		tagService.addTags(post, postSaveDTO.getTags());
		tagService.removeUselessTags();
		return new Result<>(post.getId());
    }
    */
    
    @PostMapping("/write")
    public Result<Long> write(@Valid @RequestBody PostWriteDTO postWriteDTO, HttpSession session) {
		Long postId = postService.write(postWriteDTO, getMemberId(session));
		return new Result<>(postId);
    }
기존에는 /write GET, POST 방식 모두 PostSaveDTO로 처리를 하였었지만,
가독성을 위하여 PostLoadDTO, PostWriteDTO로 구분하였습니다.

화면에서 넘어온 dto와 세션의 멤버id로 글 작성을 합니다.
처음에는 대략
-----
Long id = postService.write(dto);
tagService.addTags(id)
-----
이런 식으로 처리하였지만

tagService에서 id값을 이용하여 또 select쿼리가 나가는 불필요함을 줄이기 위하여
-----
Post post = postService.write(dto);
tagService.addTags(post)
-----
이런 식으로 수정을 했던 게 주석에 해당하는 부분입니다.
OSIV = false를 통해서 트랜잭션 범위를 벗어났으므로 영속성 컨텍스트를 닫아주긴 하였으나
컨트롤러단에 엔티티가 존재한다는 것이 마음에 걸리긴 하였습니다.
1.
위 컨트롤러단에 엔티티가 존재하는 문제와 더불어
tag를 추가/삭제하는 작업은 글작성시에만 동작하기 때문에
일괄처리 하도록 수정하였습니다.

2.
Raw타입 사용도 자제하였습니다.(Result -> Result<Long>)

🚀 Service

  1. 수정 전
	@Transactional
	public Post write(PostSaveDTO postSaveDTO, Long memberId) {
		PostThumbnail postThumbnail = getPostThumbnail(postSaveDTO.getThumbnailFileName());
		Series series = getSeries(postSaveDTO.getSeriesId());
		Member member = getMember(memberId);

		Post post = null;

		if (postSaveDTO.getPostId() == null) {
			post = postRepository.save(postSaveDTO.toPost(member, series, postThumbnail));
		}

		if (postSaveDTO.getPostId() != null) {
			post = postRepository.findById(postSaveDTO.getPostId()).orElseThrow();
			checkSameMember(post,memberId);
			postSaveDTO.setIntroduce();
			post.edit(postSaveDTO.getTitle(),
				postSaveDTO.getIntroduce(),
				postSaveDTO.getContent(),
				postSaveDTO.getAccess(),
				series,
				postThumbnail);
		}

		return post;
	}
  1. 수정 후
	@Transactional
	public Long write(PostWriteDTO postWriteDTO, Long memberId) {
		return isNewPost(postWriteDTO) ? writeNew(postWriteDTO, memberId) : edit(postWriteDTO, memberId);
	}
    
    private Long writeNew(PostWriteDTO postWriteDTO, Long memberId) {
		PostThumbnail postThumbnail = getPostThumbnail(postWriteDTO.getThumbnailFileName());
		Series series = getSeries(postWriteDTO.getSeriesId());
		Member member = getMember(memberId);

		Post post = postRepository.save(postWriteDTO.toPost(member, series, postThumbnail));
		updateTags(post, postWriteDTO.getTags());
		return post.getId();
	}

	private Long edit(PostWriteDTO postWriteDTO, Long memberId) {
		PostThumbnail postThumbnailToSave = getPostThumbnail(postWriteDTO.getThumbnailFileName());
		Series series = getSeries(postWriteDTO.getSeriesId());

		Post post = postRepository.findByIdWithJoin(postWriteDTO.getPostId()).orElseThrow();
		checkSameWriter(post, memberId);

		postWriteDTO.makeIntroduce();
		post.edit(postWriteDTO.getTitle(),
			postWriteDTO.getIntroduce(),
			postWriteDTO.getContent(),
			postWriteDTO.getAccess(),
			series,
			updateThumbnail(post.getPostThumbnail(), postThumbnailToSave));

		cleanTags(post);
		updateTags(post, postWriteDTO.getTags());

		return post.getId();
	}
    
    private PostThumbnail updateThumbnail(PostThumbnail savedPostThumbnail, PostThumbnail postThumbnailToSave) {
		return savedPostThumbnail != null && postThumbnailToSave != null ?
			savedPostThumbnail.edit(postThumbnailToSave.getUuid(),
				postThumbnailToSave.getPath(),
				postThumbnailToSave.getName()
			) : postThumbnailToSave;
	}
수정 전 코드의 흐름을 보자면,
dto의 내용과 일치하는 썸네일, 시리즈, 멤버를 다 가져와서

id가 null이면 save(insert)
null이 아니면 변경감지를 활용한 update로 작성하였었습니다.

새 글 작성의 경우에는 썸네일, 시리즈, 멤버 다 가져올 필요가 있지만
수정의경우에는 멤버를 가져올 필요가 없기 때문에 writeNew와 edit으로 분리하였습니다.
(tag관련 메서드는 아래에서 부가 설명)

수정 전에는 썸네일의 교체 시,
일대일관계 + cascade + 고아객체제거를 때문에
delete + insert로 동작하였습니다.

수정 후 updateThumbnail()에서 변경감지를 이용하여
삭제/삽입 두 쿼리를 update쿼리 하나로 줄였고
적어도 하나가 달라야만 update쿼리가 나가는 성질을 활용하였기 때문에
모든 요소가 차이가 없는 경우에는 쿼리가 나가지 않는다는 것을 알게 되었습니다.

🚀 Repository(fetch join)

	//기존의 findById대신 사용한 쿼리
	@Query("select p from Post p left join fetch p.postThumbnail left join fetch p.temporaryPost where p.id = :id")
	Optional<Post> findByIdWithJoin(@Param("id") Long postId);

	//참고예시
	private void checkSameWriter(Post post, Long memberId) {
		if (!post.getMember().getId().equals(memberId)) {
			throw new IllegalStateException("잘못된 접근입니다.");
		}
	}
fetch join을 활용하여 다음의 경우에 발생하는 쿼리 개수를 줄였습니다.
1. getter를 이용하여 참조객체를 사용할 때 추가적인 select쿼리가 나가는 점
2. null값이 들어가 delete쿼리가 나와야할 때 있는지 검사하는 select쿼리가 추가적으로 나가는 점

postThumbnail과 TemporaryPost는 null일수도 있기 때문에
정상적인 동작을 위하여 left join을 사용하였습니다.

또한, 참조속성을 사용하더라도 Id값만 사용한다면
추가적인 쿼리가 나가지 않는다는 사실을 깨달았습니다.
(checkSameWriter()에서 getMember()하지만 fetch join하지 않은 이유)

덕분에 불필요한 쿼리를 줄일 수 있었습니다.


🚀 updateTags

  1. 수정 전
private void updateTags(Post post, List<String> tags) {
	postTagRepository.deleteAllByPost(post);
       
    if (isEmpty(tags)) {
		return;
	}
    
    tags = removeDuplication(tags);

	List<String> realTags = tagRepository.getTagNames();

	tags.stream().filter(name -> !realTags.contains(name))
		.forEach(name -> tagRepository.save(
			Tag.builder().name(name).build()
	));

	tags.forEach(name -> postTagRepository.save(
		PostTag.builder()
			.tag(tagRepository.findByName(name))
			.post(post)
			.build()
	));
}
Post와 Tag는 교차테이블(post_tag) + 일대다관계2개를 이용한 다대다관계입니다.

1. 수정시에도 동작해야하므로 전체 postTag를 삭제합니다.
2. 실제 테이블에 존재하는 태그들의 이름들을 가져와서
3. dto에 없는 것들을 저장합니다.
4. 이제는 dto에 태그리스트 중 테이블에 없는 것은 없을테니 찾아서 postTag를 갱신합니다.

- 수정시에만 postTag이력을 delete하여도 충분하다고 생각했습니다.
- postTag에 insert할 때 tag와 post를 넣어줄 때, 앞에서 엔티티가 추가 되었는데
tag자리에 들어갈 엔티티를 굳이 쿼리를 사용하여 가져와야할 까 의문이 들었습니다.
  1. 수정 후
	private void updateTags(Post post, List<String> tagNames) {

		//생략..

		List<Tag> existingTags = addTagsUnknown(tagNames);

		putPostTags(post, existingTags, tagNames);
	}

	private List<Tag> addTagsUnknown(List<String> tagNames) {
		List<Tag> existingTags = tagRepository.findAll();

		existingTags.addAll(
			tagNames.stream()
				.filter(name -> existingTags.stream().map(Tag::getName).noneMatch(name::equals))
				.map(name -> tagRepository.save(Tag.builder().name(name).build()))
				.collect(Collectors.toList())
		);

		return existingTags;
	}

	private void putPostTags(Post post, List<Tag> existingTags, List<String> tagNames) {
		tagNames.stream().map(name -> existingTags.stream().filter(tag -> tag.getName().equals(name)).findFirst().orElseThrow())
			.forEach(tag -> postTagRepository.save(
				PostTag.builder()
					.tag(tag)
					.post(post)
					.build()
			));
	}
위 작업을 통하여

기존 쿼리(새글작성, 수정 동일)
- delete PostTag ByPost(x 1)
- select All Name(x 1)
- insert Tag(x 없는개수만큼)
- select Tag ByName(x n)
- insert PostTag(x n)

->

새글작성
- select All Name(x 1)
- insert Tag(x 없는개수만큼)
- insert PostTag(x n)

글수정
- delete PostTag ByPost(x 1)
- select All Name(x 1)
- insert Tag(x 없는개수만큼)
- insert PostTag(x n)

위와 같이 쿼리를 줄일 수 있었습니다.

아쉬운 점은 쿼리 줄이자고 서비스 로직이 복잡해졌는데 괜찮은가에 대한
명확한 답이 필요했다는 점입니다.

🚀 removeUselessTags

사용되지 않는 태그는 테이블에 없게 설계하였습니다.

PostController에서 글작성/수정/삭제 마다,
tagService.removeUselessTags()를 해주었습니다.

1. 처리하는데 쿼리가 너무 많이 나가는 것과
2. 회원이 탈퇴 시에도 tagService.removeUselessTags()를 해주어야 했습니다.

그래서 Quartz라이브러리로 위 작업을 수행하게 수정하였습니다.
	/*
	public void removeUselessTags() {
		tagRepository.findAll().stream().filter(tag -> postTagRepository.countByTag(tag) == 0)
			.forEach(tagRepository::delete);
	}*/
    
    /*
    @Scheduled
    public void removeUselessTags() {
		tagRepository.findAll().stream().filter(tag -> postTagRepository.findFirstByTag(tag).isEmpty())
			.forEach(tagRepository::delete);
	}*/
    
	@Scheduled
	public void removeUselessTags() {
		List<Long> usedTagIds = postTagRepository.findTagIds();
		List<Long> allTagIds = tagRepository.findIds();
		List<Long> idsToRemove = allTagIds.stream().filter(id -> !usedTagIds.contains(id)).collect(Collectors.toList());
		tagRepository.deleteByIds(idsToRemove);
	}
tag를 모두 조회해서 postTag에 없는 것만 삭제하는 로직입니다.

count()를 사용하지 않기 위해 select해서(하나만) 결과값 유무로
사용되지 않는 tag를 삭제하게 수정하였고

삭제할 태그 개수만큼 delete쿼리가 나가는 것을
delete-where-in을 활용하게 수정하였습니다.

수정전 쿼리
- select-All-Tag(x 1)
- SelectPostTag(x 전체 태그개수만큼)
- delete-Tag(x 사용되지 않는 태그개수만큼)

수정후 쿼리
- select-All-Tag(id만)(x 1)
- select-All-PostTag(id만)(x 1)
- delete-where-in(x 1)
profile
어제보다 나은 사람이 되자

0개의 댓글