본문 바로가기
Spring

Spring Day 25 : 팔로우 기능 구현

by 우 석 2024. 2. 29.

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"
    }
]