Group Study (2024-2025)/Spring 입문

[Spring 입문] Toy Project - Team 하와수

choubung 2024. 12. 4. 18:07

팀원 및 저장소

 

 

선택한 기능 및 구현 계획

  1. 팔로우
    • 홈화면(게시글 목록 보기 페이지)에서 작성자 옆의 'follow' 버튼을 누르면,
    • 위의 액션을 취한 로그인 유저 팔로잉 목록에 작성자가 추가되고
    • 게시글 작성자의 팔로워 목록에 로그인 유저가 추가된다.
    • 팔로우가 성공적으로 완료되면 'follow' 버튼이 'unfollow' 버튼으로 변경된다.
    • * 자기 자신의 글에는 팔로우 버튼이 뜨지 않는다.
  2. 마이페이지
    • 내 정보
      • 이름
      • 이메일 주소
      • 프로필 사진
    • 내가 쓴 글
      • 목록으로 보여주며, 제목과 작성 날짜를 표시한다.
      • 제목을 클릭하면 글 상세 페이지로 이동한다.
    • 팔로우 및 팔로잉 목록
      • 계정의 이름과 이메일을 표시한다.
  3. 기존 코드 리팩터링
    • 회원가입 유저 기본 role을 USER로 설정
      • 기존에는 기본 role이 GUEST로, 글 작성/수정/삭제 권한이 없었다.
      • 회원가입 후 별다른 절차 없이 바로 글을 작성/수정/삭제할 수 있다.
    • 게시글과 유저 매핑
      • 기존에는 게시글에 유저가 매핑되어 있지 않아 작성자를 직접 작성했다.
      • 로그인한 유저에 맞게 작성자가 자동으로 설정되도록 변경했다.
    • 게시글을 자기 자신만 수정할 수 있도록 변경
      • 기존에는 자신이 작성한 글이 아니어도 수정 및 삭제가 가능했다.
      • 로그인한 사용자와 게시글 작성자가 같을 때만 수정 및 삭제가 가능하도록 변경했다.

 

 

작동 화면

<팔로우 기능>

좌: 팔로우 전 / 오: 팔로잉 후

 

<마이페이지>

 

 

기능을 구현하기 위해 조사하며 얻은 인사이트

  1. 팔로우
  2. 마이페이지

 

 

 

API 명세서

<팔로우 API 명세서>

  1. 팔로우 - followerID 사용자가 followeeId 사용자를 팔로우
    • HTTP Method: [POST]
    • Endpoint: /api/follow/{followerId}/follow/{followeeId}
    • Request Parameters
      • followerId (Path, Long): 팔로우를 요청하는 사용자 ID
      • followeeId (Path, Long): 팔로우 대상 사용자 ID
    • Response
      • HTTP 200 OK: 팔로우 성공
      • HTTP 400 Bad Request: 잘못된 요청 
    • 예시
      • POST /api/follow/1/follow/2
  2. 언팔로우 - followerId 사용자가 followeeId 사용자를 언팔로우
    • HTTP Method: [DELETE]
    • Endpoint: /api/follow/{followerId}/unfollow/{followeeId}
    • Request Parameters
      • followerId (Path, Long): 언팔로우를 요청하는 사용자 ID
      • followeeId (Path, Long): 언팔로우 대상 사용자 ID
    • Response
      • HTTP 200 OK: 팔로우 성공
      • HTTP 400 Bad Request: 잘못된 요청 
    • 예시
      • DELETE /api/follow/1/follow/2
  3. 팔로잉 목록 조회 - userId 사용자가 팔로우하고 있는 사용자 목록을 조회
    • HTTP Method: [GET]
    • Endpoint: /api/follow/{userId}/following
    • Request Parameters
      • userId (Path, Long): 팔로잉 목록을 조회할 사용자 ID
    • Response
      • HTTP 200 OK: 팔로우 성공
      • HTTP 400 Bad Request: 잘못된 요청 
    • Response Body
      • [ { "id": 1, "followerId": 1, "followeeId": 2 }, { "id": 2, "followerId": 1, "followeeId": 3 } ]
    • 예시
      • DELETE /api/follow/1/follow/2
  4. 팔로워 목록 조회 - userId 사용자를 팔로우하고 있는 사용자 목록을 조회
    • HTTP Method: [GET]
    • Endpoint: /api/follow/{userId}/followers
    • Request Parameters
      • userId (Path, Long): 팔로워 목록을 조회할 사용자 ID
    • Response
      • HTTP 200 OK: 팔로우 성공
      • HTTP 400 Bad Request: 잘못된 요청 
    • Response Body
      • [ { "id": 3, "followerId": 2, "followeeId": 1 }, { "id": 4, "followerId": 3, "followeeId": 1 } ]
    • 예시
      • DELETE /api/follow/1/follow/2

 

<마이페이지 API 명세서>

  • HTTP Method: [GET]
  • Endpoint: /mypage
  • Response
    • HTTP 200 OK
    • HTML 페이지 렌더링 (my-page 뷰)
  • Response Body

 

 

핵심 코드

<팔로우 기능>

package com.jojoldu.book.springboot.domain.follow;

import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import com.jojoldu.book.springboot.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Follow extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "followee_id")
    private User followee; // 내가 팔로잉 하는 사람

    @ManyToOne
    @JoinColumn(name = "follower_id")
    private User follower; // 나의 팔로워

    public Long getId() {
        return id;
    }

    public User getFollowee() {
        return followee;
    }

    public User getFollower() {
        return follower;
    }
}
  • ↑ follow 엔티티
package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.service.FollowService;
import com.jojoldu.book.springboot.web.dto.FollowDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/follow")
public class FollowController {
    private final FollowService followService;

    @PostMapping("/{followerId}/follow/{followeeId}")
    public ResponseEntity<String> follow(@PathVariable Long followerId, @PathVariable Long followeeId) {
        try {
            followService.follow(followerId, followeeId);
            return ResponseEntity.ok("Followed successfully");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error following user");
        }
    }

    @DeleteMapping("/{followerId}/unfollow/{followeeId}")
    public ResponseEntity<String> unfollow(@PathVariable Long followerId, @PathVariable Long followeeId) {
        try {
            followService.unfollow(followerId, followeeId);
            return ResponseEntity.ok("Unfollowed successfully");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error unfollowing user");
        }
    }

    @GetMapping("/{userId}/following")
    public ResponseEntity<List<FollowDto>> getFollowing(@PathVariable Long userId) {
        List<FollowDto> following = followService.getFollowing(userId);
        return ResponseEntity.ok(following);
    }

    @GetMapping("/{userId}/followers")
    public ResponseEntity<List<FollowDto>> getFollowers(@PathVariable Long userId) {
        List<FollowDto> followers = followService.getFollowers(userId);
        return ResponseEntity.ok(followers);
    }
}
  •   FollowController
package com.jojoldu.book.springboot.service;

import com.jojoldu.book.springboot.domain.follow.Follow;
import com.jojoldu.book.springboot.domain.follow.FollowRepository;
import com.jojoldu.book.springboot.domain.user.User;
import com.jojoldu.book.springboot.domain.user.UserRepository;
import com.jojoldu.book.springboot.web.dto.FollowDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class FollowService {
    private final FollowRepository followRepository;
    private final UserRepository userRepository;

    public boolean isFollowing(Long followerId, Long followeeId) {
        return followRepository.existsByFollowerIdAndFolloweeId(followerId, followeeId);
    }

    @Transactional
    public void follow(Long followerId, Long followeeId) {
        User follower = userRepository.findById(followerId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid follower ID: " + followerId));
        User followee = userRepository.findById(followeeId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid followee ID: " + followeeId));

        Follow follow = Follow.builder()
                .follower(follower)
                .followee(followee)
                .build();
        followRepository.save(follow);
    }

    @Transactional
    public void unfollow(Long followerId, Long followeeId) {
        Follow follow = followRepository.findByFollowerIdAndFolloweeId(followerId, followeeId)
                .orElseThrow(() -> new IllegalArgumentException("Follow relationship not found"));
        followRepository.delete(follow);
    }

    @Transactional(readOnly = true)
    public List<FollowDto> getFollowing(Long userId) {
        return followRepository.findByFollowerId(userId).stream()
                .map(FollowDto::new)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<FollowDto> getFollowers(Long userId) {
        return followRepository.findByFolloweeId(userId).stream()
                .map(FollowDto::new)
                .collect(Collectors.toList());
    }
}
  •   FollowService

 

<마이페이지 기능>

package com.jojoldu.book.springboot.web;


import com.jojoldu.book.springboot.config.auth.LoginUser;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.service.FollowService;
import com.jojoldu.book.springboot.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;



@RequiredArgsConstructor
@Controller
public class MyPageController {

    private final PostsService postsService;
    private final FollowService followService;

    @GetMapping("/mypage")
    public String myPage(Model model, @LoginUser SessionUser user) {
        model.addAttribute("userName", user.getName());
        model.addAttribute("userEmail", user.getEmail());
        model.addAttribute("userPicture", user.getPicture());

        model.addAttribute("posts", postsService.findPostsByUser(user.getEmail()));

        model.addAttribute("followings", followService.getFollowing(user.getId()));
        model.addAttribute("followers", followService.getFollowers(user.getId()));
        return "my-page";
    }

}
  • ↑ MyPageController

 

 

트러블 슈팅

<인텔리제이 Run 불가 오류 >

  • 문제 상황
    • 인텔리제이가 spring 패키지를 인식하지 못해 application을 실행할 수 없음
  • 문제 원인과 해결 시도
    • JDK 버전 충돌: 원격 저장소의 원본 코드는 JDK 20, 저장소 코드를 pull한 로컬 환경은 JDK 17 사용
      → 로컬 JDK 버전을 20으로 올림
    • 루트 폴더가 (ToyProject/하은수/)freelec-springboot2-webservice가 아닌 ToyProject로 설정되어 gradle 등 필요한 파일 인식 불가
      → 루트 폴더를 변경하고 새로 빌드
    • 실행>실행/디버그 구성에 Spring Boot 구성이 존재하지 않음
      → Spring Boot로 Application 추가

 

<마이페이지 버튼 구현 후 팔로우/언팔로우 기능 먹통 버그>

  • 문제상황
    • 홈에 마이페이지 버튼을 구현한 뒤, 팔로우/언팔로우 기능이 오작동함
  • 문제 원인과 해결 시도
    • 머스테치에 숨겨서 보냈던 userID 정보가 삭제되어 동적 팔로우/언팔로우 구현 관련 머스테치에서 오류가 발생
      → 누락된 코드를 복원해 해결

 

 

회고

하은수

  • 교재를 보며 열심히 공부하고 실습하여 배포까지 성공했지만 와닿지 않는 내용이 많았습니다.
    토이프로젝트를 위해 기존 코드를 파악할 때 비로소 전체적인 구조와 세부적인 코드를 잘 이해할 수 있었습니다.
    특히 유저와 글 작성자를 매핑하면서 연관관계 매핑을 추가적으로 공부하게 되어 유익했습니다.
    다만 시간이 부족해 테스트 코드를 작성하는 연습을 해보지 못한 것이 아쉬움으로 남습니다.
    이번 스터디를 통해 스프링에 대한 흥미가 커졌고, 방학동안 더 깊게 공부해보려고 합니다.

이수연

  • 교재를 따라하며 스프링부트 게시판 프로젝트를 완성하긴 했지만, 코드를 완전히 이해하진 못했어서 토이프로젝트를 처음 시작할 때 원래 코드를 이해하느라 시간을 좀 소모했습니다. 토이 프로젝트를 통해 모호하게 이해했던 개념들(왜 컨트롤러와 서비스를 분리하는지 등)을 조금이나마 체화할 수 있었고, 특히 팔로우 구현 과정에서 mapped by를 사용하는 법을 새로이 알게 되었습니다. 구조를 이해하니 확실히 코드를 보는 것과 쓰는 게 수월해서 종강하면 조금 더 본격적으로 스프링 공부를 진행하고 싶다는 생각이 들었습니다.