CEOS 20th BE study - instagram clone coding
- ์ฌ์ฉ์๊ฐ ๊ธ๊ณผ ์ฌ์ง์ ์ ๋ก๋ํ๊ณ , ๊ฒ์๋ฌผ์ ๋ํ ๋๊ธ๊ณผ ๋๋๊ธ์ ์์ฑํ๊ฑฐ๋ ์ข์์๋ฅผ ํ์ํ ์ ์๋ SNS ์๋น์ค์ ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ ์ ๊ฐ์ 1: 1 ๋ฉ์์ง ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ๊ฒ์๊ธ ์กฐํ
- ๊ฒ์๊ธ์ ์ฌ์ง๊ณผ ํจ๊ป ๊ธ ์์ฑํ๊ธฐ
- ๊ฒ์๊ธ์ ๋๊ธ ๋ฐ ๋๋๊ธ ๊ธฐ๋ฅ
- ๊ฒ์๊ธ์ ์ข์์ ๊ธฐ๋ฅ
- ๊ฒ์๊ธ, ๋๊ธ, ์ข์์ ์ญ์ ๊ธฐ๋ฅ
- ์ ์ ๊ฐ 1:1 DM ๊ธฐ๋ฅ
- JPA๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด database ๊ตฌ์กฐ์ ๋งคํ๋ JPA Entity๋ฅผ ๋จผ์ ์์ฑํ๊ฒ ๋๋ค. ๊ทธ๋ฆฌ๊ณ , ๋ชจ๋ JPA์ ๋์์ ์ด Entity๋ค์ ๊ธฐ์ค์ผ๋ก ๋์๊ฐ๊ฒ ๋๋๋ฐ, ์ด ๋ Entity๋ค์ ๊ด๋ฆฌํ๋ ์ญํ ์ ํ๋ ๊ฒ์ด ๋ฐ๋ก EntityManager์ด๋ค.
- ๋น์ํ์ฑ(stateless) ์ปดํฌ๋ํธ๋ก, ์์ฒญ๋ง๋ค ์๋ก์ด ์์์ฑ ์ปจํ ์คํธ์ ์ฐ๊ฒฐ.
- ๊ฐ ์์ฒญ์ ๋ฐ๋ผ ์๋ก์ด EntityManager๊ฐ ์์ฑ๋ฉ๋๋ค. ํ์ง๋ง, ์ด ์์ฑ๋ EntityManager๋ ํธ๋์ญ์ ์ด ์์๋ ๋ ์๋์ผ๋ก ์ฃผ์ ๋๊ณ , ํธ๋์ญ์ ์ด ๋๋๋ฉด ์๋์ผ๋ก ์ข ๋ฃ
- SimpleJpaRepository๋ ์ฑ๊ธํค์ผ๋ก ๊ด๋ฆฌ๋๋ Spring Bean์ ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ์ฌ์ฉ๋๋ EntityManager๋ ํธ๋์ญ์ ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง ์ ์๊ธฐ ๋๋ฌธ์, ๋จ์ผ EntityManager ์ธ์คํด์ค๋ฅผ ๊ณ ์ ์ ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ์๋๋ผ ํ๋ก์๋ฅผ ํตํด ์ฒ๋ฆฌ๋๋ค.
- ์ฆ, Spring์ EntityManager๋ฅผ ์ง์ ์ฃผ์ ํ์ง ์๊ณ , EntityManager ํ๋ก์ ๊ฐ์ฒด๋ฅผ ์ฃผ์ .
- ์ด ํ๋ก์๋ ํธ๋์ญ์ ์ด ์์๋ ๋๋ง๋ค ์ฌ๋ฐ๋ฅธ EntityManager๋ฅผ ์ฐ๊ฒฐ
- EntityManager ํ๋ก์๋ ์ค์ EntityManager๋ฅผ ์์ฒญํ ๋ ๋์ ๋ฐ์ธ๋ฉ์ ํตํด ํธ๋์ญ์ ์ปจํ ์คํธ์ ๋ง๋ ์ค์ EntityManager๋ฅผ ์ฐ๊ฒฐ.
- SimpleJpaRepository ๊ฐ์ ์ฑ๊ธํค ๊ฐ์ฒด์์ EntityManager๋ฅผ ์์ฑ์ ์ฃผ์ ์ผ๋ก ๋ฐ๋๋ผ๋, ํ๋ก์ ์ธ์คํด์ค๋ก ํธ๋์ญ์ ๋ณ๋ก ์ค์ ๋ค๋ฅธ EntityManager๋ฅผ ์ฌ์ฉํ์ฌ tread-safe ํ๋ค.
- @PersistContext ๋ฅผ ์ด์ฉํ์ฌ EntityManager ๋ฅผ ์ฃผ์ ๋ฐ๊ฒ ๋๋ฉด ์ปจํ ์ด๋๊ฐ EntityManger ๊ฐ 1๊ฐ์ ์ค๋ ๋์ ํ ๋น๋๋๋ก ์ ํํด์ค๋ค.
- JPA๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด database ๊ตฌ์กฐ์ ๋งคํ๋ JPA Entity๋ฅผ ๋จผ์ ์์ฑํ๊ฒ ๋๋ค. ๊ทธ๋ฆฌ๊ณ , ๋ชจ๋ JPA์ ๋์์ ์ด Entity๋ค์ ๊ธฐ์ค์ผ๋ก ๋์๊ฐ๊ฒ ๋๋๋ฐ, ์ด ๋ Entity๋ค์ ๊ด๋ฆฌํ๋ ์ญํ ์ ํ๋ ๊ฒ์ด ๋ฐ๋ก EntityManager์ด๋ค.
- ๋น์ํ์ฑ(stateless) ์ปดํฌ๋ํธ๋ก, ์์ฒญ๋ง๋ค ์๋ก์ด ์์์ฑ ์ปจํ ์คํธ์ ์ฐ๊ฒฐ.
- ๊ฐ ์์ฒญ์ ๋ฐ๋ผ ์๋ก์ด EntityManager๊ฐ ์์ฑ๋ฉ๋๋ค. ํ์ง๋ง, ์ด ์์ฑ๋ EntityManager๋ ํธ๋์ญ์ ์ด ์์๋ ๋ ์๋์ผ๋ก ์ฃผ์ ๋๊ณ , ํธ๋์ญ์ ์ด ๋๋๋ฉด ์๋์ผ๋ก ์ข ๋ฃ
- SimpleJpaRepository๋ ์ฑ๊ธํค์ผ๋ก ๊ด๋ฆฌ๋๋ Spring Bean์ ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ์ฌ์ฉ๋๋ EntityManager๋ ํธ๋์ญ์ ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง ์ ์๊ธฐ ๋๋ฌธ์, ๋จ์ผ EntityManager ์ธ์คํด์ค๋ฅผ ๊ณ ์ ์ ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ์๋๋ผ ํ๋ก์๋ฅผ ํตํด ์ฒ๋ฆฌ๋๋ค.
- ์ฆ, Spring์ EntityManager๋ฅผ ์ง์ ์ฃผ์ ํ์ง ์๊ณ , EntityManager ํ๋ก์ ๊ฐ์ฒด๋ฅผ ์ฃผ์ .
- ์ด ํ๋ก์๋ ํธ๋์ญ์ ์ด ์์๋ ๋๋ง๋ค ์ฌ๋ฐ๋ฅธ EntityManager๋ฅผ ์ฐ๊ฒฐ
- EntityManager ํ๋ก์๋ ์ค์ EntityManager๋ฅผ ์์ฒญํ ๋ ๋์ ๋ฐ์ธ๋ฉ์ ํตํด ํธ๋์ญ์ ์ปจํ ์คํธ์ ๋ง๋ ์ค์ EntityManager๋ฅผ ์ฐ๊ฒฐ.
- SimpleJpaRepository ๊ฐ์ ์ฑ๊ธํค ๊ฐ์ฒด์์ EntityManager๋ฅผ ์์ฑ์ ์ฃผ์ ์ผ๋ก ๋ฐ๋๋ผ๋, ํ๋ก์ ์ธ์คํด์ค๋ก ํธ๋์ญ์ ๋ณ๋ก ์ค์ ๋ค๋ฅธ EntityManager๋ฅผ ์ฌ์ฉํ์ฌ tread-safe ํ๋ค.
- @PersistContext ๋ฅผ ์ด์ฉํ์ฌ EntityManager ๋ฅผ ์ฃผ์ ๋ฐ๊ฒ ๋๋ฉด ์ปจํ ์ด๋๊ฐ EntityManger ๊ฐ 1๊ฐ์ ์ค๋ ๋์ ํ ๋น๋๋๋ก ์ ํํด์ค๋ค.
- ๋๋๊ธ ์ํฐํฐ๋ฅผ ๋ณ๋๋ก ๊ตฌํํ๋๋ฐ, ๊ทธ๊ฒ์ ์ญ์ ํ๊ณ Comment ์ํฐํฐ ๋ด๋ถ์ parentComment ๋ถ๋ชจ๋๊ธ ํ๋๋ฅผ ๋ง๋ค์๋ค.
@JoinColumn(name = "post_id")
private Comment parentComment; // ์๊ธฐ์ฐธ์กฐ
- N์ Post์ ์, 1์ Post๋ฅผ ์กฐํํ๋ ๋ฉ์ธ ์ฟผ๋ฆฌ
- 1๊ฐ์ Post ์กฐํ ์ฟผ๋ฆฌ์ ๊ฐ Post์ ์ฐ๊ฒฐ๋ User๋ฅผ ๊ฐ์ ธ์ค๋ 2๊ฐ์ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์
- Post์ ์๋งํผ ๋ฐ๋ณต์ ์ผ๋ก User ์กฐํ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด N+1 ๋ฌธ์ . User๋ ํ ๋ฒ๋ง ๊ฐ์ ธ์๋ ๋์ง๋ง, Post ์๋งํผ ๋ถํ์ํ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ์คํ
- ์ฆ, Post๊ฐ 2๊ฐ๋ผ์ User๋ฅผ ์กฐํํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ ๋ฒ ๋ฐ์ํ ๊ฒ, ๊ฐ Post์ ์ฐ๊ฒฐ๋ User๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด ๋ณ๋์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์
-> Post์ User๋ฅผ ํจ๊ป ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๊ฐ์ ธ์ค๋๋ก ํ์น ์ ๋ต์ ์์ ํ์ง ์์ผ๋ฉด, Post๊ฐ ๋ ๋ง์์ง์๋ก User๋ฅผ ์กฐํํ๋ ์ฟผ๋ฆฌ๋ ๋น๋กํด์ ๊ณ์ ์ฆ๊ฐํ๊ฒ ๋๋ค.
ํ๋ก์ ๊ฐ์ฒด๊ฐ ์์ฑ๋ ๊ฒฝ์ฐ์๋, getUser() ์ ์คํํ ๋ ์ค์ DB ์์ ๊ฐ์ ธ์์ ํด๋น ๊ฐ์ฒด์ ๋ํ ์ฟผ๋ฆฌ๋ฅผ ๋์ค์ ๊ฐ์ ธ์์ ์คํํ๋ฏ๋ก ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ ๋ฐ์.
Hibernate:
insert
into
user
(email, password)
values
(?, ?)
Hibernate:
insert
into
user
(email, password)
values
(?, ?)
Hibernate:
insert
into
post
(caption, created_at, image_url, user_id)
values
(?, ?, ?, ?)
Hibernate:
insert
into
post
(caption, created_at, image_url, user_id)
values
(?, ?, ?, ?)
Hibernate:
select
p1_0.post_id,
p1_0.caption,
p1_0.created_at,
p1_0.image_url,
p1_0.user_id
from
post p1_0
post = com.ceos20.instagram.Domain.Post@6da53709
->post.getUser().getClass() = class com.ceos20.instagram.Domain.User$HibernateProxy$SWmpqPqD
Hibernate:
select
u1_0.user_id,
u1_0.email,
u1_0.password,
p1_0.user_id,
p1_0.post_id,
p1_0.caption,
p1_0.created_at,
p1_0.image_url
from
user u1_0
left join
post p1_0
on u1_0.user_id=p1_0.user_id
where
u1_0.user_id=?
->post.getUser() = com.ceos20.instagram.Domain.User@3f1eb1bc
post = com.ceos20.instagram.Domain.Post@2b55ac77
->post.getUser().getClass() = class com.ceos20.instagram.Domain.User$HibernateProxy$SWmpqPqD
Hibernate:
select
u1_0.user_id,
u1_0.email,
u1_0.password,
p1_0.user_id,
p1_0.post_id,
p1_0.caption,
p1_0.created_at,
p1_0.image_url
from
user u1_0
left join
post p1_0
on u1_0.user_id=p1_0.user_id
where
u1_0.user_id=?
->post.getUser() = com.ceos20.instagram.Domain.User@4ff1b0d
@Transactional
public List<Post> findAll() {
return em.createQuery("SELECT p FROM Post p JOIN FETCH p.user", Post.class)
.getResultList(); // User๋ฅผ ํจ๊ป ์ฆ์ ๋ก๋ฉํ์ฌ ๋ฐํ
}
- post.getUser()๊ฐ ํ๋ก์ ๊ฐ์ฒด๊ฐ ์๋ ์ค์ User ์ํฐํฐ๋ก ๋ก๋
- post์ ๊ด๋ จ๋ user ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์กฐํํ์ฌ n+1 ๋ฌธ์ ํด๊ฒฐ.
- ๋ง์ฝ JPA ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, @EntityGraph ์ฌ์ฉ ๊ฐ๋ฅ.
Hibernate:
select
p1_0.post_id,
p1_0.caption,
p1_0.created_at,
p1_0.image_url,
u1_0.user_id,
u1_0.email,
u1_0.password,
u1_0.username
from
post p1_0
join
user u1_0
on u1_0.user_id=p1_0.user_id
Hibernate:
select
p1_0.user_id,
p1_0.post_id,
p1_0.caption,
p1_0.created_at,
p1_0.image_url
from
post p1_0
where
p1_0.user_id=?
Hibernate:
select
p1_0.user_id,
p1_0.post_id,
p1_0.caption,
p1_0.created_at,
p1_0.image_url
from
post p1_0
where
p1_0.user_id=?
- DTO(Data Transfer Object)๋: ๊ณ์ธต๊ฐ ๋ฐ์ดํฐ ๊ตํ์ ์ํด ์ฌ์ฉํ๋ ๊ฐ์ฒด
- Entity ์ ๊ดํ ๋น์ฆ๋์ค ๋ก์ง์ ์ธ๋ถ์ ๋ ธ์ถ์ํค์ง ์๊ณ , ์ง์ ์ ์ผ๋ก ์ฌ์ฉํ์ง ์๊ธฐ ์ํด์.
- Entity ํด๋์ค์์ ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํ์ ์ผ๋ก DTO์ ๋ด์์ ์์ฑํด ์ฌ์ฉ
@Data
public class CommentRequest {
private final String comment;
@Builder
public CommentRequest(String comment) {
this.comment = comment;
}
public Comment toEntity(User writer, Post post, Comment parentComment) {
return Comment.builder()
.comment(comment) // DTO์ ๋๊ธ ๋ด์ฉ์ ์ํฐํฐ์ ์ค์
.user(writer)
.parentComment(parentComment)
.post(post)
.build();
}
}
toEntity
๋ฉ์๋๋ DTO ๊ฐ์ฒด์ ๋ด๊ธด ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋กComment
์ํฐํฐ๋ฅผ ์์ฑ- User, Post, Comment ์ํฐํฐ๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์, ์ด ๊ฐ๋ค์ ์๋ก์ด Comment ์ํฐํฐ์ ์ค์
- DTO ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ํฐํฐ๋ฅผ ์์ฑํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ฑฐ๋ ๋น์ฆ๋์ค ๋ก์ง์์ ์ฌ์ฉ
@Transactional
public void update(Long postId, Long userId, CommentRequest dto) {
Comment comment = commentRepository.findByPostIdAndUserId(postId, userId).orElseThrow(() ->
new IllegalArgumentException("ํด๋น ๋๊ธ์ด ์กด์ฌํ์ง ์์ต๋๋ค. " + userId));
comment.update(dto.getComment());
}
dto.getComment()
๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์๋ก์ด ๋๊ธ ๋ด์ฉ์ ๊ฐ์ ธ์จ๋ค.- Comment ์ํฐํฐ์ update ๋ฉ์๋๊ฐ ๊ตฌํ๋์ด ์์ด, dto๋ก๋ถํฐ ์ ๋ฌ๋ฐ์ ๋๊ธ ๋ด์ฉ์ ํตํด ํด๋น ์ํฐํฐ๋ฅผ ๊ฐฑ์
- DTO ์์
@Builder
ํจํด์ ์ฌ์ฉํ์ฌ ์์ฑ์์์ ๋ฐ์ดํฐ๋ฅผ ๊น๋ํ๊ฒ ์ฃผ์
- Optional์ด ๋น์ด์์ ๊ฒฝ์ฐ, ์ฆ ๊ฐ์ด ์์ ๋ ์์ธ๋ฅผ ๋์ง๋๋ก ํ๋ ๋ฉ์๋
- ๊ฐ์ด ํ์์ ์ผ๋ก ์กด์ฌํด์ผ ํ๋ ๊ฒฝ์ฐ์ ์์ฃผ ์ฌ์ฉ
๋ฉ์์ง ๋ณด๋ด๊ธฐ ์๋น์ค ๋ฉ์๋.
@Transactional
public void sendMessage(Long roomId, ChatDto chatDto) {
Chatroom chatroom = chatRoomRepository.findById(roomId)
.orElseThrow(() -> new IllegalArgumentException("ํด๋น ์ฑํ
๋ฐฉ์ด ์กด์ฌํ์ง ์์ต๋๋ค. roomId=" + roomId));
ChatMessage newMessage = chatDto.toEntity(chatroom);
messageRepository.save(newMessage);
}
- ๊ฐ์ด ์กด์ฌํ๋์ง ์ฌ๋ถ๋ฅผ ํ์ธํ ํ, ์กด์ฌํ์ง ์์ผ๋ฉด ์์ธ๋ฅผ ๋์ง๋ ๋ฐฉ์
- ์กด์ฌํ ๊ฒฝ์ฐ get()์ผ๋ก ๊ฐ์ ๊บผ๋ธ๋ค. ์ฑํ ๋ฐฉ ์์ฑ ์๋น์ค ๋ฉ์๋
@Transactional
public Chatroom createChatroom(String roomName, User sender, User receiver) {
// ๋์ผํ ์ฌ์ฉ์ ๊ฐ์ ์ฑํ
๋ฐฉ์ด ์๋์ง ํ์ธ
Optional<Chatroom> existingChatroom = chatRoomRepository.findByUserIds(sender.getUserId(), receiver.getUserId());
if (existingChatroom.isPresent()) {
return existingChatroom.get(); // ์กด์ฌํ๋ ์ฑํ
๋ฐฉ์ ๋ฐํ
}
- ํ์ฌ ์ฑํ ๋ฐฉ์ด ์กด์ฌํ๋ ์ง ํ์ธํ ํ, ์ฐธ์ด๋ผ๋ฉด .get() ๋ฉ์๋๋ก ์กด์ฌํ๋ ์ฑํ ๋ฐฉ์ ๋ฐํํ๋๋ก ํ๋ค.
- ์ง์ง ๊ฐ์ฒด์ ๋น์ทํ์ง๋ง ๋ฌผ๋ฆฌ์ ์ผ๋ก ๊ฐ์ง ์๊ณ ํ๋ก๊ทธ๋๋จธ๊ฐ ์ง์ ํ๋์ ๊ด๋ฆฌํ๋ ๊ฐ์ฒด
- ํ ์คํธ ์ฝ๋์์ Mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ๋, Mock์ ํน์ ๋ฉ์๋ ํธ์ถ๊ณผ ์๋ต์ ์ ์ํ๋ ๊ฒ
- @Mock : Mock ๊ฐ์ฒด์ ์ธ์คํด์ค ๋ด๋ถ๋ ๋น์ด์๋ค. (Null)
- @InjectMock :ํด๋น ๊ฐ์ฒด์ ๋ฉค๋ฒ ๋ณ์๋ก ์กด์ฌํ๋ ์์กด๋ ๋ค๋ฅธ ๊ฐ์ฒด๋ค์ด Mockํน์ Spy๋ก ์์ฑ๋ ๊ฐ์ฒด๋ผ๋ฉด ์์กด์ฑ ์ฃผ์ ์ ํด์ฃผ๋ ๊ธฐ๋ฅ์ ์ ๊ณต
@InjectMocks
private FollowService followService;
@Mock
private UserRepository userRepository;
@Mock
private FollowRepository followRepository;
- ๋งค ํ ์คํธ๋ง๋ค ์ด๊ธฐํ๋์ด์ผ ํ๋ ํด๋์ค ๋ฑ์ ์ค์ ํ๊ธฐ ์ํด ์ฌ์ฉ
- ๋ชจ๋ ํ ์คํธ์ ํ ๋ฒ๋ง ๋ก๋ฉ๋์ด์ผ ํ๋ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด, @BeforeAll ์ ์ฌ์ฉํ๋ ๊ฒ ์ค๋ณต์ ์ค์ผ ์ ์์.
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this); // Mockito ์ด๊ธฐํ
// ํ
์คํธ์ฉ ์ฌ์ฉ์ ๋ฐ ๊ฒ์๊ธ ์์ฑ
user = User.builder()
.userId(userId)
.username(userName)
.build();
post = Post.builder()
.postId(postId)
.caption("Sample Post Content")
.build();
assertNotNull(savedComment);
assertEquals(request.getComment(), savedComment.getComment());
assertEquals(user, savedComment.getUser());
assertEquals(post, savedComment.getPost());
- assertEquals
- ๊ธฐ๋ํ๋ ๊ฐ๊ณผ ์ค์ ๊ฐ์ด ๋์ผํ ์ง ๊ฒ์ฌ
- ์ฒซ ๋ฒ์งธ ์ธ์ :expected. ๊ธฐ๋ํ๋ ๊ฐ์ ๋ฃ์ด์ค๋ค.
- ๋ ๋ฒ์งธ ์ธ์ : actual. ์ค์ ๊ฐ์ ๋ฃ์ด์ค๋ค.