졸업 프로젝트로 진행하고 있는 Tori는 공유 플랫폼이어서 팔로우/팔로잉(Tori에서는 친구맺기/친구끊기) 기능을 구현해야 했다.
🎈 Tori의 친구맺기/친구끊기 기능 개요
친구맺기/친구끊기 기능을 다음과 같이 요약해보았다.
👥 친구 맺기
🤝 A 사용자(cat)이 B 사용자(lion) 프로필에서 '친구맺기' 버튼을 누르면,
✔️ 버튼은 '친구끊기'으로 변한다.
✔️ B 사용자의 팔로워 수가 하나 증가한다.
✔️ A 사용자의 팔로잉 수가 하나 증가한다.
✔️ B 사용자의 팔로워 리스트에 A가 추가된다.
✔️ A 사용자의 팔로잉 리스트에 B가 추가된다.
👥 친구 끊기
친구 맺기의 반대
🎈 구현 방법
크게 두 가지 방법을 생각할 수 있다.
1️⃣ User 엔티티 안에 FollowerList와 FollowingList를 만들기
2️⃣ Follow 엔티티를 따로 만들기
엔티티 안에는 팔로우를 요청한 유저와 팔로우를 요청받은 유저가 들어가게 된다.
나는 두 번째 방법을 구현해보았다.
🎈 구현
💡여기서 잠깐!
User Entity와 Follow Entity의 관계는 어떻게 될까?
한 사용자가 여러 팔로우 관계를 맺을 수 있기 때문에 일대다 관계이다.
이것을 기억하고 JPA를 사용하여 코드를 짜보자.
🚩 Entity
👤 User Entity
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long user_id;
@Column(name = "user_name")
private String userName;
private String password;
private String email;
private String profile;
//팔로우
@OneToMany(mappedBy = "from_user", fetch = FetchType.LAZY)
private List<Follow> followings;
@OneToMany(mappedBy = "to_user", fetch = FetchType.LAZY)
private List<Follow> followers;
}
User Entity는 이와 같이 만들 수 있다.
User와 Follow가 일대다 관계이기 때문에 @OneToMany 어노테이션을 사용한다.
한 유저에게는 팔로워와 본인이 팔로잉하는 유저들이 있기 때문에, followings와 followers를 만든다.
👭 Follow Entity
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Follow {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "follow_id")
private long id;
@ManyToOne
@JoinColumn(name = "from_user")
private User fromUser;
@ManyToOne
@JoinColumn(name = "to_user")
private User toUser;
@CreationTimestamp
@Column(name = "create_date")
private Timestamp createDate;
}
여러 Follow가 한 명의 유저와 연결되기 때문에 @ManyToOne 어노테이션을 사용한다.
친구맺기를 요청한 사람은 fromUser, 친구맺기를 받은 사람은 toUser가 된다.
🚩 Controller
이제 화면과 비즈니스 로직을 이어주는 Controller를 작성해보자.
@RestController
@RequiredArgsConstructor
public class FollowController {
private final UserService userService;
private final FollowService followService;
/**
* 친구 맺기
*/
@PostMapping("/users/follow/{friendName}")
public ResponseEntity follow(Authentication authentication, @PathVariable("friendName") String friendName) {
User from_user = userService.findUser(authentication.getName());
User to_user = userService.findUser(friendName);
followService.follow(from_user, to_user);
return ResponseEntity.ok().build();
}
/**
* 팔로잉 조회
*/
@GetMapping("/users/{userName}/following")
public ResponseEntity<List<FollowDTO>> getFollowingList(@PathVariable("userName") String userName, Authentication auth) {
User from_user = userService.findUser(userName);
User requestUser=userService.findUser(auth.getName());
return ResponseEntity.ok().body(followService.followingList(from_user, requestUser));
}
/**
* 팔로워 조회
*/
@GetMapping("/users/{userName}/follower")
public ResponseEntity<List<FollowDTO>> getFollowerList(@PathVariable("userName") String userName, Authentication auth) {
User to_user = userService.findUser(userName);
User requestUser=userService.findUser(auth.getName());
return ResponseEntity.ok().body(followService.followerList(to_user, requestUser));
}
/**
* 친구 끊기
*/
@DeleteMapping("/users/follow/{friendName}")
public ResponseEntity<String> deleteFollow(Authentication authentication){
return ResponseEntity.ok().body(followService.cancelFollow(userService.findUser(authentication.getName())));
}
}
✔️친구맺기/끊기
- 친구맺기/끊기를 요청하는 유저는 Authentication에 담긴 유저 정보를 통해 얻는다.
- 친구맺기/끊기를 요청받는 유저는 url의 pathvariable로 받는다.
- Service의 follow함수를 통해 DB에 팔로우 관계가 저장/삭제된다.
✔️팔로잉/팔로워 조회
- 조회하고자 하는 유저는 url의 pathvariable로 받는다.
- 팔로잉/팔로워 리스트의 유저들과 요청한 유저와의 관계도 표시한다.
예) cat이 lion의 팔로잉 리스트를 조회함. lion이 cat(조회자)를 팔로잉하고 있는 상태이기 때문에
cat도 팔로잉 리스트에 뜸. 다만 cat은 조회자이기 때문에 우측에 친구맺기 버튼이 뜨지 않음.
반면, lion이 팔로잉하고 있지만, cat이 팔로잉하고 있지 않은 rabbit의 경우 친구맺기 버튼이 뜸.
이 버튼 클릭을 통해 cat은 rabbit을 팔로잉할 수 있음.
→다른 사람의 목록을 볼 때에도, 내가 이미 팔로우를 한 사람인지, 아예 관련이 없는 사람인지 보여주고 싶기 때문에 추가했다.
🚩 Service
Follow
//follow 요청
public String follow(User from_user, User to_user) {
// 자기 자신 follow 안됨
if (from_user == to_user)
throw new FollowException(ErrorCode.INVALID_REQUEST, "자기 자신을 follow할 수 없습니다.");
// 중복 follow x
if (followRepository.findFollow(from_user, to_user).isPresent())
throw new FollowException(ErrorCode.FOLLOW_DUPLICATED, "이미 follow했습니다.");
Follow follow = Follow.builder()
.toUser(to_user)
.fromUser(from_user)
.build();
followRepository.save(follow);
return "Success";
}
Follow 기능에서는 자기 자신을 follow하는 경우와 이미 follow한 사람을 중복 follow하는 경우를 막아놓았다. 두 경우를 시도하는 경우 예외가 발생한다.
Following/Follower 리스트
//following 리스트
public List<FollowDTO> followingList(User selectedUser, User requestUser) {
List<Follow> list = followRepository.findByFromUser(selectedUser);
List<FollowDTO> followList = new ArrayList<>();
for (Follow f : list) {
followList.add(userRepository.findByUserName(f.getToUser().getUserName())
.orElseThrow().toFollow(findStatus(f.getToUser(), requestUser)));
}
return followList;
}
//follower list
public List<FollowDTO> followerList(User selectedUser, User requestUser) {
List<Follow> list = followRepository.findByToUser(selectedUser);
List<FollowDTO> followerList = new ArrayList<>();
for (Follow f : list) {
followerList.add(userRepository.findByUserName(f.getFromUser().getUserName())
.orElseThrow().toFollow(findStatus(f.getFromUser(), requestUser)));
}
return followerList;
}
두 메서드는 거의 비슷하게 생겼다.
Following 리스트는 selectedUser가 follow한 사람들이기 때문에 FromUser에서 검색해서 찾고,
Follower 리스트는 selectedUser를 follow한 사람들이어서 ToUser에서 찾는다.
findStatus 메서드는 로그인한 사용자(requestUser)와의 관계를 알기 위한 메서드다.
//A와 B의 follow관계 찾기
protected String findStatus(User selectedUser, User requestUser) {
if (selectedUser.getUserName() == requestUser.getUserName())
return "self";
if (followRepository.findFollow(requestUser, selectedUser).isEmpty())
return "none";
return "following";
}
requestUser가 selectedUser를 following하는지 안 하는지만 판별한다.
Follow 취소
public String cancelFollow(User user) {
followRepository.deleteFollowByFromUser(user);
return "Success";
}
친구맺기를 요청한 유저가 친구끊기를 하기 때문에 from_user를 이용하여 Follow 관계를 찾고 삭제한다.
🚩 Repository
public interface FollowRepository extends JpaRepository<Follow, Long> {
List<Follow> findByFromUser(User from_user);
List<Follow> findByToUser(User to_user);
void deleteFollowByFromUser(User from_user);
@Query("select f from Follow f where f.fromUser = :from and f.toUser = :to")
Optional<Follow> findFollow(@Param("from") User from_user, @Param("to") User to_user);
}
JpaRepository를 이용하여 작성하였다.
완성!!
🧐 피드백
졸업프로젝트를 도와주시는 멘토님께 실제 회사에서는 이런 팔로우/팔로잉 관계를 어떻게 구현하는지 여쭈어 봤더니...
내가 사용한 방법이 아닌, 1번 방법(User Entity 안에 follwer/following 리스트)으로 로직을 짠다고 말씀하셨다.
여러 이유를 말씀해주셨는데, follow 중간 테이블을 두는 것의 가장 큰 단점은 연관관계가 양방향으로 매핑된다는 것이었다. 이로 인해 다음과 같은 문제가 발생할 수 있다고 하셨다....
❌ 트랜잭션, 롤백의 문제
❌ 여러 번 연산이 발생→트래픽이 많으면 복잡해지고 사용시간, 메모리가 기하급수적으로 늘어날 수 있음
❌ 다른 user의 pk값만 가지는 리스트를 쓰면 연관관계 쓰지 않아도 된다. → 시간복잡도 O(1)으로 이루어질 수 있다.
그렇지만 실제 서비스를 런칭할 것이 아니고, 방금 갓 시작한 학생 개발자로서 잘 구현했다고 칭찬해주셨다...ㅎ
이번 달까지 바쁜 일정들이 끝나면 멘토님이 알려주신 대로 리팩토링을 시도해 볼 것이다!
'캡스톤프로젝트' 카테고리의 다른 글
[Spring/JPA] Slice를 사용한 무한 페이지네이션 (0) | 2023.09.29 |
---|---|
[HTTP Method] PUT vs PATCH 결정하기 (0) | 2023.09.22 |
[Nginx, Amazon Linux] Certbot을 이용한 https 인증받기 (0) | 2023.09.13 |
졸업프로젝트, 태초마을로 돌아가다 (0) | 2023.09.02 |
[SpringBoot] 팔로워, 팔로잉 기능 구현하기 2 (0) | 2023.08.30 |