sns 서버를 만드는 프로젝트에서 팔로우 기능 구현을 시도했다.
먼저 user와 follow의 연관관계를 설정하지 않아, 의존성을 제거해 종속적인 문제가 발생하지 않도록 구현했다.
Follow
Follow entity는 고유 id와, fromUserId(팔로우 시도할 userId), toUserId(팔로우받게 될 userId)를 필드로 가진다.
그리고 유저의 id 를 요청으로 받아 Follow 객체를 생성한다.
@Getter
@NoArgsConstructor
@Table(name = "follows")
@Entity
public class Follow extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "from_user_id")
private Long fromUserId;
@Column(name = "to_user_id")
private Long toUserId;
@Builder
public Follow(Long fromUserId, Long toUserId) {
this.fromUserId = fromUserId;
this.toUserId = toUserId;
}
}
FollowController
follow CRUD 요청을 받는 FollowController class.
로그인된 user 정보(인증/인가)와 @PathVariable로 userId를 받아 처리한다.
@RequiredArgsConstructor
@RestController
@Tag(name = "Follow", description = "팔로우 컨트롤러")
public class FollowController {
private final FollowService followService;
private final NotificationService notificationService;
@Operation(summary = "팔로우 하기", description = "팔로우 할 수 있는 API")
@PostMapping("/follows/{toUserId}")
public ResponseEntity<Void> createFollow(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long toUserId) {
followService.createFollow(userDetails.getUser(), toUserId);
notificationService.notifyFollow(toUserId);
return ResponseEntity.status(HttpStatus.OK.value()).build();
}
@Operation(summary = "팔로우 취소", description = "팔로우 취소할 수 있는 API")
@DeleteMapping("/follows/{toUserId}")
public ResponseEntity<Void> deleteFollow(
@AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long toUserId) {
followService.deleteFollow(userDetails.getUser(), toUserId);
return ResponseEntity.status(HttpStatus.OK.value()).build();
}
@Operation(summary = "팔로잉 목록 조회", description = "유저의 팔로잉 목록을 조회할 수 있는 API")
@GetMapping("/users/{userId}/follows/following")
public List<FollowingResponseDto> getFollowingList(
@PathVariable Long userId) {
List<FollowingResponseDto> followingResponseDtos =
followService.getFollowingList(userId);
return ResponseEntity.status(HttpStatus.OK.value()).body(followingResponseDtos).getBody();
}
@Operation(summary = "팔로워 목록 조회", description = "유저의 팔로워 목록을 조회할 수 있는 API")
@GetMapping("/users/{userId}/follows/follower")
public List<FollowerResponseDto> getFollowerList(
@PathVariable Long userId) {
List<FollowerResponseDto> followerResponseDtos =
followService.getFollowerList(userId);
return ResponseEntity.status(HttpStatus.OK.value()).body(followerResponseDtos).getBody();
}
@Operation(summary = "팔로잉 게시글 목록 조회", description = "팔로잉한 유저의 전체 게시글 목록을 조회할 수 있는 API")
@GetMapping("/follows/posts)
public ResponseEntity<List<PostResponseDto>> getAllFollowingPost(
@AuthenticationPrincipal UserDetailsImpl userDetails) {
List<PostResponseDto> postResponseDtos =
followService.getAllFollowingPost(userDetails.getUser());
return ResponseEntity.status(HttpStatus.OK.value()).body(postResponseDtos);
}
}
FollowService
followRepository.findAllByFromUserId(fromUserId) 메서드로 DB에 있는 follow 정보를 찾는다.
그리고 authService를 주입받아 userId 정보를 찾는데 활용하여, follow CRUD가 이루어진다.
@Service
@RequiredArgsConstructor
public class FollowService {
private final FollowRepository followRepository;
private final AuthService authService;
private final PostService postService;
@Transactional
public void createFollow(User fromUser, Long toUserId) {
if (fromUser.getId().equals(toUserId)) {
throw new InvalidInputException("자신을 팔로우할 수 없습니다.");
}
authService.findUser(toUserId);
Follow follow = Follow.builder()
.fromUserId(fromUser.getId())
.toUserId(toUserId)
.build();
followRepository.save(follow);
}
@Transactional
public void deleteFollow(User fromUser, Long toUserId) {
authService.findUser(toUserId);
Follow follow = followRepository.findByFromUserIdAndToUserId(fromUser.getId(), toUserId)
.orElseThrow(
() -> new InvalidInputException("해당 팔로우를 찾을 수 없습니다.")
);
followRepository.delete(follow);
}
public List<FollowingResponseDto> getFollowingList(Long fromUserId) {
List<Follow> follows = followRepository.findAllByFromUserId(fromUserId);
String username = authService.findUser(fromUserId).getUsername();
List<FollowingResponseDto> list = follows.stream()
.map(follow -> new FollowingResponseDto(follow, username)).toList();
return list;
}
public List<FollowerResponseDto> getFollowerList(Long toUserId) {
List<Follow> follows = followRepository.findAllByToUserId(toUserId);
String username = authService.findUser(toUserId).getUsername();
List<FollowerResponseDto> list = follows.stream()
.map(follow -> new FollowerResponseDto(follow, username)).toList();
return list;
}
public List<PostResponseDto> getAllFollowingPost(User fromUser) {
List<Follow> follows = followRepository.findAllByFromUserId(fromUser.getId());
List<Post> posts = new ArrayList<>();
for (Follow follow : follows) {
Long toUserId = follow.getToUserId();
posts.addAll(postService.findByUserId(toUserId));
}
return posts.stream()
.map(PostResponseDto::new)
.toList();
}
public Follow findLatestUser(Long toUserId) {
return followRepository.findFirstByToUserIdOrderByCreatedAtDesc(toUserId)
.orElseThrow(() -> new IllegalArgumentException("팔로우를 찾을 수 없습니다."));
}
}
findLatestUser(Long toUserId) 메서드는 실시간 알림 기능(SSE) 기능에 필요한 메서드로 가장 최신에 생성된 follow 객체를 찾는 동작을 한다.
사용자가 지정한 user의 팔로워 목록을 조회하기 위해
@GetMapping("/users/{userId}/follows/follower") 요청을 보낸 결과값에 user의 이름을 함께 반환하도록 설계하였다.
[
{
"toUserId": 1,
"username": "sandy"
},
{
"toUserId": 2
"username": "darby"
}
]'Spring' 카테고리의 다른 글
| Spring Day 27 : Repository 기능 제한 (0) | 2024.03.05 |
|---|---|
| Spring Day 26 : 코드 리팩토링 (팔로우 기능) (0) | 2024.03.04 |
| Spring Day 24 : SSE (Server-Sent Event) (0) | 2024.02.27 |
| Spring Day 23 : Soft Delete (0) | 2024.02.26 |
| Spring Day 21 : @ExceptionHandler, @ControllerAdvice, Spring 예외처리 (0) | 2024.02.22 |