게시글 리스트 ui를 마무리하도록 하겠습니다. 우선 더미데이터를 활용해서 게시글을 불러왔을 때 게시판이 어떻게 되는지 확인해보도록 하겠습니다.
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';
import { withRouter } from 'react-router';
import PostHeader from './PostHeader';
import PostCard from './PostCard';
const PostListTemplateBlock = styled.div`
background: ${ palette.gray[2] };
padding-top: 30px;
display: flex;
flex-direction: center;
flex-wrap: wrap;
justify-content: center;
align-items: center;
`;
const PostListTemplate = ({ history }) => {
const dummyData = [
{ "images": ["https://picsum.photos/id/0/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/1/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/2/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/3/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/4/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/5/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/6/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/7/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
{ "images": ["https://picsum.photos/id/8/1000/1000.jpg"], "title": "test-01", "nickname": "test-01", createdAt: "2020-01-01" },
];
return(
<>
<PostHeader />
<PostListTemplateBlock>
{
dummyData.map((item, i) => {
return <PostCard item={ item }
i={ i }
/>
})
}
</PostListTemplateBlock>
</>
);
};
export default withRouter(PostListTemplate);
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palettes';
const PostCardBlock = styled.div`
background-color: white;
width: 400px;
height: 300px;
margin: 20px;
box-shadow: 0px 0px 2px 1px rgba(0, 0, 0, 0.1);
border-radius: 10px;
`;
const CardTitle = styled.div`
float: left;
width: 400px;
height: 50px;
overflow: hidden;
text-align: left;
padding-top: 10px;
padding-left: 10px;
`;
const CardImage = styled.img`
float: left;
width: 400px;
height: 200px;
`;
const CardNickname = styled.div`
float: left;
width: 300px;
text-align: left;
padding-left: 10px;
`;
const CardDate = styled.div`
float: left;
width: 90px;
color: ${ palette.gray[6] }
`;
const PostCard = ({ item, i }) => {
return(
<Link to={ `/posts/post/${item.postId}` }>
<PostCardBlock>
<CardImage
src={ item.images[0] }
/>
<CardTitle>
{ item.title }
</CardTitle>
<CardNickname>
{ item.nickname }
</CardNickname>
<CardDate>
{ item.createdAt }
</CardDate>
</PostCardBlock>
</Link>
);
};
export default PostCard;
화면의 크기에 따라 가변적으로 열의 갯수가 잘 변하고, 포스트 카드 또한 잘 나오는 모습을 볼 수 있습니다. 그러면 리스트 상단에 분류 탭을 만들어 보도록 하겠습니다.
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import Select from 'react-select';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';
const HeaderBlock = styled.div`
display: flex;
padding: 40px;
align-items: center;
justify-content: center;
`;
const ButtonBlock = styled.div`
disflay: flex;
width: 40%;
justify-content: flex-start;
`;
const BorderButton = styled.button`
width: 100px;
height: 50px;
border-radius: 30px;
border: 2px solid ${palette.blue[1]};
color: ${palette.blue[1]};
background-color: white;
font-size: 20px;
font-weight: bold;
`;
const ClassificationBlock = styled.div`
display: flex;
width: 40%;
justify-content: flex-end;
align-items: center;
`;
const StyledClassification = styled(Select)`
width: 200px;
`;
const PostHeader = () => {
const [option, setOption] = useState('');
const options = [
{ value: 'ALL', label: '전체' },
{ value: 'REQUEST_RENTAL', label: '요청글' },
{ value: 'READY_RENTAL', label: '대여 가능' }
];
const onSelect = (value) => {
setOption(value);
};
return(
<HeaderBlock>
<ButtonBlock>
<BorderButton>
<Link to="/posts/write">
글쓰기
</Link>
</BorderButton>
</ButtonBlock>
<ClassificationBlock>
<StyledClassification onChange={ onSelect }
options={ options }
value={ option }
placeholder="분류"
/>
</ClassificationBlock>
</HeaderBlock>
);
};
export default PostHeader;
PostHeader컴포넌트를 위와 같이 수정했습니다. 분류탭의 경우 해당 컴포넌트에서만 사용하면되므로 리덕스 모듈을 만들어 state에 저장하는 방법은 사용하지 않도록 하겠습니다.
post-service와 게시글을 불러오는 부분은 우선 글쓰기에 대한 post-service연결을 진행한 후에 구현하도록 하겠습니다.
이전에 post-service를 작성하면서 image와 관련된 코드는 주석처리를 했습니다. 다음과 같이 주석처리를 풀어주고, status 케이스에 대한 코드도 추가하도록 하겠습니다.
...
@RestController
@RequestMapping("/")
@Slf4j
public class PostController {
...
@GetMapping("/health_check")
public String status() {
return String.format(
"It's working in Post Service"
+ ", port(local.server.port) =" + env.getProperty("local.server.port")
+ ", port(server.port) =" + env.getProperty("server.port")
);
}
// Using by RequestCreate class, Write Post
@PostMapping("/write")
public ResponseEntity<?> write(@ModelAttribute RequestWrite postVo) throws Exception {
log.info("Post Service's Controller Layer :: Call write Method!");
PostDto postDto = null;
if(postVo.getPostType().equals("빌려줄게요")) {
postDto = PostDto.builder()
.postType(postVo.getPostType())
.category(postVo.getCategory())
.title(postVo.getTitle())
.content(postVo.getContent())
.startDate(postVo.getDate().get(0))
.endDate(postVo.getDate().get(1))
.rentalPrice(postVo.getRentalPrice())
.writer(postVo.getWriter())
.status("READY_RENTAL")
.userId(postVo.getUserId())
.multipartFiles(postVo.getImages())
.build();
} else {
postDto = PostDto.builder()
.postType(postVo.getPostType())
.title(postVo.getTitle())
.content(postVo.getContent())
.startDate(null)
.endDate(null)
.rentalPrice(null)
.writer(postVo.getWriter())
.status("REQUEST_RENTAL")
.userId(postVo.getUserId())
.multipartFiles(postVo.getImages())
.build();
}
PostDto post = postService.write(postDto);
ResponsePost result = ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.images(post.getImages())
.comments(post.getComments())
.status(post.getStatus())
.build();
return ResponseEntity.status(HttpStatus.CREATED).body(result);
}
// Get All Posts
@GetMapping("/")
public ResponseEntity<?> getAllPosts() {
log.info("Post Service's Controller Layer :: Call getAllPosts Method!");
Iterable<PostDto> postList = postService.getAllPosts();
List<ResponsePost> result = new ArrayList<>();
postList.forEach(post -> {
result.add(
ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.status(post.getStatus())
.images(post.getImages())
.comments(post.getComments())
.build());
});
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@GetMapping("/post/{id}")
public ResponseEntity<?> readPostById(@PathVariable("id") Long id) {
log.info("Post Service's Controller Layer :: Call readPostById Method!");
PostDto post = postService.readPostById(id);
return ResponseEntity.status(HttpStatus.OK).body(ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.images(post.getImages())
.comments(post.getComments())
.status(post.getStatus())
.build());
}
@GetMapping("/posts/{status}")
public ResponseEntity<?> getAllPostsByStatus(@PathVariable("status") String status) {
log.info("Post Service's Controller Layer :: Call getAllPostsByStatus Method!");
Iterable<PostDto> postList = postService.getAllPostsByStatus(status);
List<ResponsePost> result = new ArrayList<>();
postList.forEach(post -> {
result.add(ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.status(post.getStatus())
.images(post.getImages())
.comments(post.getComments())
.build());
});
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@GetMapping("/{userId}/posts")
public ResponseEntity<?> getPostsByUserId(@PathVariable("userId") String userId) {
log.info("Post Service's Controller Layer :: Call getPostsByUserId Method!");
log.info("Before receive post data");
Iterable<PostDto> postList = postService.getPostsByUserId(userId);
List<ResponsePost> result = new ArrayList<>();
postList.forEach(post -> {
result.add(ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.status(post.getStatus())
.images(post.getImages())
.comments(post.getComments())
.build());
}
);
log.info("After received post data");
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@GetMapping("/posts/keyword/{keyword}")
public ResponseEntity<?> getPostsByKeyword(@PathVariable("keyword") String keyword) {
log.info("Post Service's Controller Layer :: Call getPostsByKeyword Method!");
Iterable<PostDto> postList = postService.getPostsByKeyword(keyword);
List<ResponsePost> result = new ArrayList<>();
postList.forEach(post -> {
result.add(ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.status(post.getStatus())
.images(post.getImages())
.comments(post.getComments())
.build());
}
);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@GetMapping("/posts/category/{category}")
public ResponseEntity<?> getPostsByCategory(@PathVariable("category") String category) {
log.info("Post Service's Controller Layer :: Call getPostsByCategory Method!");
Iterable<PostDto> postList = postService.getPostsByCategory(category);
List<ResponsePost> result = new ArrayList<>();
postList.forEach(post -> {
result.add(ResponsePost.builder()
.id(post.getId())
.postType(post.getPostType())
.category(post.getCategory())
.title(post.getTitle())
.content(post.getContent())
.rentalPrice(post.getRentalPrice())
.startDate(post.getStartDate())
.endDate(post.getEndDate())
.createdAt(post.getCreatedAt())
.writer(post.getWriter())
.userId(post.getUserId())
.status(post.getStatus())
.images(post.getImages())
.comments(post.getComments())
.build());
}
);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
@PostMapping("/rental")
public ResponseEntity<?> rental(@RequestBody RequestRental postVo) {
log.info("Post Service's Controller Layer :: Call rental Method!");
kafkaProducer.send("rental-topic", postVo);
return ResponseEntity.status(HttpStatus.OK).body(postVo);
}
@PostMapping("/{id}/delete")
public ResponseEntity<?> deletePost(@PathVariable("id") Long id) {
log.info("Post Service's Controller Layer :: Call deletePost Method!");
return ResponseEntity.status(HttpStatus.OK).body(postService.deletePost(id));
}
...
}
수정한 메서드 위주로 살펴 보겠습니다.
1) write : formData에 데이터를 담아 요청할 경우 RequestBody타입으로 데이터를 받을 수 없으니 ModelAttribute로 변경했습니다. 그리고 게시글 타입에 따라 status의 값을 달리 했습니다. 빌려줄게요의 경우 대여를 위한 글이므로 READY_RENTAL을, 빌려주세요의 경우 대여 요청을 위한 글이므로 REQUEST_RENTAL이란 상태값을 부여했습니다.
...
@Service
@Slf4j
public class PostServiceImpl implements PostService {
...
@Transactional
@Override
public PostDto write(PostDto postDto) throws Exception {
log.info("Post Service's Service Layer :: Call write Method!");
PostEntity postEntity = PostEntity.builder()
.postType(postDto.getPostType())
.category(postDto.getCategory())
.rentalPrice(postDto.getRentalPrice())
.title(postDto.getTitle())
.content(postDto.getContent())
.startDate(postDto.getStartDate())
.endDate(postDto.getEndDate())
.writer(postDto.getWriter())
.userId(postDto.getUserId())
.createdAt(DateUtil.dateNow())
.status(postDto.getStatus())
.build();
List<ImageEntity> images = FileUploader.parseFileInfo(
postDto.getMultipartFiles(),
postEntity
);
postRepository.save(postEntity);
if(!images.isEmpty()) {
for(ImageEntity image: images) {
postEntity.addImage(imageRepository.save(image));
}
}
return PostDto.builder()
.id(postEntity.getId())
.userId(postEntity.getUserId())
.postType(postEntity.getPostType())
.category(postEntity.getCategory())
.rentalPrice(postEntity.getRentalPrice())
.title(postEntity.getTitle())
.content(postEntity.getContent())
.startDate(postEntity.getStartDate())
.endDate(postEntity.getEndDate())
.createdAt(postEntity.getCreatedAt())
.writer(postEntity.getWriter())
.images(images)
.build();
}
@Transactional
@Override
public PostDto readPostById(Long id) {
log.info("Post Service's Service Layer :: Call readPostById Method!");
PostEntity postEntity = postRepository.findPostById(id);
List<ImageEntity> images = new ArrayList<>();
List<CommentEntity> comments = new ArrayList<>();
postEntity.getImages().forEach(i -> {
images.add(i);
});
postEntity.getComments().forEach(i -> {
comments.add(CommentEntity.builder()
.id(i.getId())
.comment(i.getComment())
.writer(i.getWriter())
.createdAt(i.getCreatedAt())
.build());
});
return PostDto.builder()
.userId(postEntity.getUserId())
.postType(postEntity.getPostType())
.category(postEntity.getCategory())
.rentalPrice(postEntity.getRentalPrice())
.title(postEntity.getTitle())
.content(postEntity.getContent())
.startDate(postEntity.getStartDate())
.endDate(postEntity.getEndDate())
.createdAt(postEntity.getCreatedAt())
.writer(postEntity.getWriter())
.images(images)
.comments(comments)
.status(postEntity.getStatus())
.build();
}
@Transactional
@Override
public List<PostDto> getAllPosts() {
log.info("Post Service's Service Layer :: Call getAllPosts Method!");
List<String> exceptList = new ArrayList<>();
exceptList.add("COMPLETE_RENTAL");
exceptList.add("DELETE_POST");
Iterable<PostEntity> posts = postRepository.findAllByStatusNotIn(exceptList);
List<PostDto> postList = new ArrayList<>();
posts.forEach(v -> {
List<CommentEntity> comments = new ArrayList<>();
v.getComments().forEach(i -> {
comments.add(CommentEntity.builder()
.id(i.getId())
.comment(i.getComment())
.writer(i.getWriter())
.createdAt(i.getCreatedAt())
.build());
});
postList.add(PostDto.builder()
.userId(v.getUserId())
.postType(v.getPostType())
.category(v.getCategory())
.rentalPrice(v.getRentalPrice())
.title(v.getTitle())
.content(v.getContent())
.startDate(v.getStartDate())
.endDate(v.getEndDate())
.createdAt(v.getCreatedAt())
.writer(v.getWriter())
.images(v.getImages())
.comments(comments)
.status(v.getStatus())
.build());
});
return postList;
}
@Transactional
@Override
public Iterable<PostDto> getAllPostsByStatus(String status) {
log.info("Post Service's Service Layer :: Call getAllPostsByStatus Method!");
Iterable<PostEntity> posts = postRepository.findAllByStatus(status);
List<PostDto> postList = new ArrayList<>();
posts.forEach(v -> {
List<CommentEntity> comments = new ArrayList<>();
v.getComments().forEach(i -> {
comments.add(CommentEntity.builder()
.id(i.getId())
.comment(i.getComment())
.writer(i.getWriter())
.createdAt(i.getCreatedAt())
.build());
});
postList.add(PostDto.builder()
.userId(v.getUserId())
.postType(v.getPostType())
.category(v.getCategory())
.rentalPrice(v.getRentalPrice())
.title(v.getTitle())
.content(v.getContent())
.startDate(v.getStartDate())
.endDate(v.getEndDate())
.createdAt(v.getCreatedAt())
.writer(v.getWriter())
.images(v.getImages())
.comments(comments)
.status(v.getStatus())
.build());
});
return postList;
}
@Transactional
@Override
public List<PostDto> getPostsByUserId(String userId) {
log.info("Post Service's Service Layer :: Call getPostsByUserId Method!");
Iterable<PostEntity> posts = postRepository.findAllByUserId(userId);
List<PostDto> postList = new ArrayList<>();
posts.forEach(v -> {
List<CommentEntity> comments = new ArrayList<>();
v.getComments().forEach(i -> {
comments.add(CommentEntity.builder()
.id(i.getId())
.comment(i.getComment())
.writer(i.getWriter())
.createdAt(i.getCreatedAt())
.build());
});
postList.add(PostDto.builder()
.userId(v.getUserId())
.postType(v.getPostType())
.category(v.getCategory())
.rentalPrice(v.getRentalPrice())
.title(v.getTitle())
.content(v.getContent())
.startDate(v.getStartDate())
.endDate(v.getEndDate())
.createdAt(v.getCreatedAt())
.writer(v.getWriter())
.images(v.getImages())
.comments(comments)
.status(v.getStatus())
.build());
});
return postList;
}
@Transactional
@Override
public PostDto deletePost(Long id) {
log.info("Post Service's Service Layer :: Call deletePost Method!");
PostEntity postEntity = postRepository.findPostById(id);
postEntity.setStatus("DELETE_POST");
postRepository.save(postEntity);
return PostDto.builder()
.id(postEntity.getId())
.status(postEntity.getStatus())
.build();
}
@Transactional
@Override
public Iterable<PostDto> getPostsByKeyword(String keyword) {
log.info("Post Service's Service Layer :: Call getPostsByKeyword Method!");
Iterable<PostEntity> posts = postRepository.findByKeywordLike(keyword);
List<PostDto> postList = new ArrayList<>();
posts.forEach(v -> {
List<CommentEntity> comments = new ArrayList<>();
v.getComments().forEach(i -> {
comments.add(CommentEntity.builder()
.id(i.getId())
.comment(i.getComment())
.writer(i.getWriter())
.createdAt(i.getCreatedAt())
.build());
});
postList.add(PostDto.builder()
.userId(v.getUserId())
.postType(v.getPostType())
.category(v.getCategory())
.rentalPrice(v.getRentalPrice())
.title(v.getTitle())
.content(v.getContent())
.startDate(v.getStartDate())
.endDate(v.getEndDate())
.createdAt(v.getCreatedAt())
.writer(v.getWriter())
.images(v.getImages())
.comments(comments)
.status(v.getStatus())
.build());
});
return postList;
}
@Transactional
@Override
public Iterable<PostDto> getPostsByCategory(String category) {
log.info("Post Service's Service Layer :: Call getPostsByCategory Method!");
Iterable<PostEntity> posts = postRepository.findAllByCategory(category);
List<PostDto> postList = new ArrayList<>();
posts.forEach(v -> {
List<CommentEntity> comments = new ArrayList<>();
v.getComments().forEach(i -> {
comments.add(CommentEntity.builder()
.id(i.getId())
.comment(i.getComment())
.writer(i.getWriter())
.createdAt(i.getCreatedAt())
.build());
});
postList.add(PostDto.builder()
.userId(v.getUserId())
.postType(v.getPostType())
.category(v.getCategory())
.rentalPrice(v.getRentalPrice())
.title(v.getTitle())
.content(v.getContent())
.startDate(v.getStartDate())
.endDate(v.getEndDate())
.createdAt(v.getCreatedAt())
.writer(v.getWriter())
.images(v.getImages())
.comments(comments)
.status(v.getStatus())
.build());
});
return postList;
}
}
1) getAllPosts : exceptList라는 ArrayList에 제외시킬 상태값을 담아 이 값들을 제외한 게시글들을 findAllByStatusNotIn으로 불러오게 수정하였습니다.
2) getPostsByKeyword : keyword를 매개로 해당 키워드가 들어가는 글들을 모두 불러오게끔 findByKeywordLike메서드를 사용하였습니다.
3) getPostsByCategory : getAllPosts와 마찬가지로 카테고리를 클릭하면 카테고리에 해당하는 글이면서 예외 리스트에 해당하는 상태값들은 제외한 글들을 불러오도록 수정하였습니다.
package com.microservices.postservice.repository;
import com.microservices.postservice.entity.PostEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.ArrayList;
import java.util.List;
public interface PostRepository extends JpaRepository<PostEntity, Long> {
Iterable<PostEntity> findAllByStatus(String status);
Iterable<PostEntity> findAllByUserId(String userId);
PostEntity findPostById(Long id);
@Query(
value="SELECT * " +
"FROM posts " +
"LIKE '%' + :keyword + '%' ",
nativeQuery = true
)
Iterable<PostEntity> findByKeywordLike(String s);
@Query(
value="SELECT * " +
"FROM posts p " +
"WHERE p.category = :category " +
"NOT IN(:exceptList.get(0), :exceptList().get(1))",
nativeQuery = true
)
Iterable<PostEntity> findAllByCategory(
String category,
ArrayList<String> exceptList
);
Iterable<PostEntity> findAllByStatusNotIn(List<String> exceptList);
}
1) findAllByCategory : 직접 쿼리를 만들어 조건에 맞는 데이터를 불러올 수 있게 하였습니다.
어느 정도 post-service가 마무리 된 것 같으니 post-service데이터베이스를 create옵션으로 초기화시키고, 서버를 구동시켜 연동이 잘 되는지 글쓰기를 진행하도록 하겠습니다.
테스트 하기전에 post-service의 vo객체인 RequestWrite에 @Setter 어노테이션을 추가해주도록 하겠습니다. 이유는 @Setter어노테이션이 없으면 ModelAttribute에 데이터가 매핑이 되지않는 오류가 생기기 때문입니다.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)
게시글 등록 중 다음과 같은 에러가 나타났습니다. Infinite recursion 예전에도 접했던 양방향 매핑에 대한 문제입니다. 당시 오류는 응답의 경우 였는데 한쪽에서 데이터 참조를 끊어서 양방향 매핑을 해결했었습니다. 하지만 이번에는 요청을 하는 중 데이터가 양방향으로 매핑이 되는 오류인 것 같습니다.
다음의 방법으로 양방향 매핑 문제를 해결했습니다.
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "post",
cascade = { CascadeType.PERSIST, CascadeType.REMOVE },
orphanRemoval = true
)
@JsonManagedReference
private List<ImageEntity> images = new ArrayList<>();
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "post",
cascade = { CascadeType.PERSIST, CascadeType.REMOVE },
orphanRemoval = true
)
@JsonManagedReference
private List<CommentEntity> comments = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name="post_id")
@JsonBackReference
private PostEntity post;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name="post_id")
@JsonBackReference
private PostEntity post;
@JsonManagedReference : 양방향 관계에서 참조할 변수에 달아주면 변수가 직렬화에 포함됩니다.
@JsonBackReference : 양방햔 관계에서 직렬화에서 제외됩니다.
직렬화란 객체에 저장된 데이터를 I/O 스트림에 쓰기 위해 연속적인 데이터로 변환하는 것입니다. 즉, JsonManagedReference를 이용해 comment, image를 I/O 스트림에 쓸 수 있는 것이고, JsonBackReference를 이용해 직렬화에서 제외되므로 I/O 스트림에 포함되지 않기 때문에 양방향 매핑이 해결되는 것입니다.
그러면 이를 바탕으로 테스트를 마저 진행하겠습니다.
Hibernate:
/* insert com.microservices.postservice.entity.PostEntity
*/ insert
into
posts
(category, content, created_at, end_date, post_type, rental_price, start_date, status, title, user_id, writer)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.microservices.postservice.entity.ImageEntity
*/ insert
into
images
(file_name, file_path, file_size, org_filename, post_id)
values
(?, ?, ?, ?, ?)
데이터가 잘 저장되는 모습을 볼 수 있습니다. 글쓰기를 이용해 데이터를 9개 정도 추가시키고 다음 포스트에서는 이 게시글 리스트를 불러오도록 하겠습니다.