이 포스팅에서는 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다.
오늘은 친구 기능을 구현해보려고 합니다. 복잡한 것은 아니지만 구글링해도 명쾌하게 나오지 않아 제가 한번 구현해본 것을 작성해보려고 합니다.
고민이 많았습니다. 첫번째 생각 났던 방법은, User객체에 친구 User 객체의 아이디 리스트를 가지고 있게 하려고 했습니다. 하지만 그러면 성능 면에서 별로 좋지 않을것 같다는 생각이 들었습니다. (개별적으로 조회를 해야하고 수락 대기 기능을 구현하기 어려움, 순환참조 무한루프에 갇힐 가능성 등)
그래서 중간에 매핑 테이블을 만들어 보기로 하였습니다. 관계를 뜻하는 FriendShip
테이블을 만들었습니다. 보내는 쪽과 받는 쪽 모두 외래키로 DB를 구성하게 된다면, 이것 또한 순환참조가 되거나 잘 조회가 되지 않을것이라고 판단하였습니다.
그래서 저는 이 테이블을 보내는쪽으로 하나, 받는 쪽으로 하나씩 외래키를 생성하여 각각 가지고 있도록 해보았습니다.
설명 보다는 코드를 보면 더 이해가 잘 되실것 같습니다.
public class Users {
....
@OneToMany(mappedBy = "users")
private List<Friendship> friendshipList = new ArrayList<>();
...
}
Friendship에 대한 리스트를 가지고 있도록 하였고, @OneToMany
로 일대다 관계로 연결 해주었습니다.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Friendship {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private Users users;
private String userEmail;
private String friendEmail;
private FriendshipStatus status;
private boolean isFrom;
private Long counterpartId;
public void acceptFriendshipRequest() {
status = FriendshipStatus.ACCEPT;
}
public void setCounterpartId(Long id) {
counterpartId = id;
}
}
여기서 user는 정보를 저장할 유저의 정보가 되고, friend는 요청이 오거나 전송한 상대를 지칭합니다.
public enum FriendshipStatus {
ACCEPT, WAITING
}
현재 친구 요청 진행상태가 수락되었는지, 대기중인지를 나타내는 Enum클래스입니다.
user_id를 통해 외래키를 설정하고, 상대의 아이디와 이름정보를 저장합니다.
isFrom
: 어디선가 보내온 요청인가? (보낸 요청일 수도 있기 때문)FriendshipStatus
: enum 타입으로 (WAITING, ACCEPT) 두가지의 상태가 존재한다.요청을 2개 생성하므로 각각 서로의 아이디를 저장하여 상태 업데이트를 한번에 진행할 수 있도록 합니다.
설명으로 이해가 잘 안되신다면 아래 코드를 보면서 이해하면 도움 될것 같습니다.
@PostMapping("/user/friends/{email}")
@ResponseStatus(HttpStatus.OK)
public String sendFriendshipRequest(@Valid @PathVariable("email") String email) throws Exception {
if(!usersService.isExistByEmail(email)) {
throw new Exception("대상 회원이 존재하지 않습니다");
}
friendshipService.createFriendship(email);
return "친구추가 성공";
}
여기서 받은 email은 친구 요청을 보낼 상대의 이메일입니다. 보내는 사람의 정보가 포함되지 않은 이유는, 보내는 사람은 현재 로그인되어 있는 사람으로 설정하였기 때문입니다.
public void createFriendship(String toEmail) throws Exception{
// 현재 로그인 되어있는 사람 (보내는 사람)
String fromEmail = SecurityUtil.getLoginEmail();
// 유저 정보를 모두 가져옴
Users fromUser = usersRepository.findByEmail(fromEmail).orElseThrow(() -> new Exception("회원 조회 실패"));
Users toUser = usersRepository.findByEmail(toEmail).orElseThrow(() -> new Exception("회원 조회 실패"));
// 받는 사람측에 저장될 친구 요청
Friendship friendshipFrom = Friendship.builder()
.users(fromUser)
.userEmail(fromEmail)
.friendEmail(toEmail)
.status(FriendshipStatus.WAITING)
.isFrom(true) // 받는 사람은 이게 보내는 요청인지 아닌지 판단할 수 있다. (어디서 부터 받은 요청 인가?)
.build();
// 보내는 사람 쪽에 저장될 친구 요청
Friendship friendshipTo = Friendship.builder()
.users(toUser)
.userEmail(toEmail)
.friendEmail(fromEmail)
.status(FriendshipStatus.WAITING)
.isFrom(false)
.build();
// 각각의 유저 리스트에 저장
fromUser.getFriendshipList().add(friendshipTo);
toUser.getFriendshipList().add(friendshipFrom);
// 저장을 먼저 하는 이유는, 그래야 서로의 친구요청 번호가 생성되기 떄문이다.
friendshipRepository.save(friendshipTo);
friendshipRepository.save(friendshipFrom);
// 매칭되는 친구요청의 아이디를 서로 저장한다.
friendshipTo.setCounterpartId(friendshipFrom.getId());
friendshipFrom.setCounterpartId(friendshipTo.getId());
}
이렇게 친구 요청 전송까지는 완료했습니다. 다음으로는 받은 친구 요청을 조회해보겠습니다.
@GetMapping("/user/friends/received")
@ResponseStatus(HttpStatus.OK)
public ResponseEntity<?> getWaitingFriendInfo() throws Exception {
return friendshipService.getWaitingFriendList(SecurityUtil.getLoginEmail());
}
// 받은 친구 요청 중, 수락 되지 않은 요청을 조회하는 메서드
@Transactional
public ResponseEntity<?> getWaitingFriendList() throws Exception {
// 현재 로그인한 유저의 정보를 불러온다
Users users = usersRepository.findByEmail(SecurityUtil.getLoginEmail()).orElseThrow(() -> new Exception("회원 조회 실패"));
List<Friendship> friendshipList = users.getFriendshipList();
// 조회된 결과 객체를 담을 Dto 리스트
List<WaitingFriendListDto> result = new ArrayList<>();
for (Friendship x : friendshipList) {
// 보낸 요청이 아니고 && 수락 대기중인 요청만 조회
if (!x.isFrom() && x.getStatus() == FriendshipStatus.WAITING) {
Users friend = usersRepository.findByEmail(x.getFriendEmail()).orElseThrow(() -> new Exception("회원 조회 실패"));
WaitingFriendListDto dto = WaitingFriendListDto.builder()
.friendshipId(x.getId())
.friendEmail(friend.getEmail())
.friendName(friend.getName())
.status(x.getStatus())
.imgUrl(friend.getImgUrl())
.build();
result.add(dto);
}
}
// 결과 반환
return new ResponseEntity<>(result, HttpStatus.OK);
}
@Data
@Builder
static class WaitingFriendListDto {
private Long friendshipId;
private String friendEmail;
private String friendName;
private FriendshipStatus status;
private String imgUrl;
}
@PostMapping("/user/friends/approve/{friendshipId}")
@ResponseStatus(HttpStatus.OK)
public String approveFriendship (@Valid @PathVariable("friendshipId") Long friendshipId) throws Exception{
return friendshipService.approveFriendshipRequest(friendshipId);
}
친구 요청 수락 버튼을 누르면, 눌러진 친구 요청의 id를 받도록 하였습니다.
public String approveFriendshipRequest(Long friendshipId) throws Exception {
// 누를 친구 요청과 매칭되는 상대방 친구 요청 둘다 가져옴
Friendship friendship = friendshipRepository.findById(friendshipId).orElseThrow(() -> new Exception("친구 요청 조회 실패"));
Friendship counterFriendship = friendshipRepository.findById(friendship.getCounterpartId()).orElseThrow(() -> new Exception("친구 요청 조회 실패"));
// 둘다 상태를 ACCEPT로 변경함
friendship.acceptFriendshipRequest();
counterFriendship.acceptFriendshipRequest();
return "승인 성공";
}
이렇게 간단하게 친구 요청/조회/수락 로직을 구성해보았습니다. 간단하게 구성한거라 예외 처리, 중복 조회 등의 안정화에 필요한 로직은 들어가 있지 않고 개념만 구현해 놓았습니다.(향후 추가 예정)
안녕하세요. 전 게시글까지 모두 참고하여 로그인까지는 구현이 완료됐습니다.
위 내용을 참고하며 친구 기능을 구현하고 있는데 친구 요청을 전송하면
java.lang.ClassCastException class java.lang.String cannot be cast to class UserDetailsImpl
위와 같은 오류가 발생합니다. 아마 현재 로그인된 사용자의 email을 불러오는 과정 중 SecurityUtil에서 (UserDetailsImpl)로 강제 형변환을 해서 그런 것 같습니다. 혹시 해결방안 또는 도움될 점이 있을까요,,?ㅜㅜ
FriendShip 객체 생성 할 때 FriendshipStatus 여기에 빨간줄 뜨면서 '심볼 'FriendshipStatus' 를 해결할 수 없습니다' 라고 뜨네요ㅠ. 어디 임포트 하거나 클래스를 만들어야 하나요?