6/17 ~ 6/23

2024. 6. 16. 15:47

6/16

1. 매칭 신청을 위한 검색 기능 시작하려한다. Elastic Search를 쓰면 고속 검색 가능하고, 복잡한 쿼리도 가능하다.

2. 사실 데이터가 적을 때는 문제가 없지만 많을 경우 고속 검색이 필요하다. 이를 예상하고 진행하는 것이므로

구현 후에 100,000개의 데이터를 넣고 성능을 꼭 비교하고 kibana를 통해 데이터를 시각화까지 하자.

- 먼저 FULL TEXT INDEX로 시작을 하고, 이후 성능 테스트 진행해보고 개선사항이나 성능 문제 찾아서 해결하면서 도입 고려해보자.

 

현재 자신의 동네 위치 정보를 받고,

그 동네 지도가 자동으로 나오며,

동네 지도에 환자 or 간병인들이 존재하는 모든 곳들에 marker가 표시 되고,

오른쪽에는 리스트가 나와서 상세보기를 누를 수 있어야 한다.

 

현재 프로필 등록 시에 Address로 주소를 받고 있다. 이 주소를 기반으로 하는게 맞다. 환자의 실제 주소 = 간병이 필요한 주소, 간병인 실제 주소 = 환자가 필요한 간병인 실제 주소니까. 그렇다면 현재 자신의 동네 위치 정보를 받는 것이 아니라, 프로필 등록 시에 등록한 주소를 기반으로 띄워줘야 한다.

 

- 다음 주소 api를 사용하면, 우편 번호, 도로명 주소, 상세 주소로 나뉘는데, 그러면 지도에 동네별로 표시가 가능한가?

디자인 보니까 동네 단위가 아니라, 구 단위로 한다. 그럼 도로명 주소 오케이지

 

다 다르네. 어떻게 구분하지 ?

 심지어 '구'가 없는 곳도 있네

 

=> 도, 군으로 나눠서 검색하는 것이 아니라, 카카오 주소 입력 양식을 form에 그대로 적용 시키고 어차피 회원가입할 때도 저 양식 대로만 가입을 받을 거니까 주소db에서 같은 시/도, 시/군/구 인 결과 찾아서 반환하면 된다.

 

 

 

2.

package com.patientpal.backend.patient.dto;

import com.patientpal.backend.member.domain.Gender;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class ProfileSearchCondition {

    //가까운 순 정렬 -> 경도 위도 따야하나
    //별점 순
    //후기 많은 순
    //성별
    //나이 어린 순
    //경력 순
    //최신 순

    private String firstAddress;
    private String secondAddress;
    private Gender gender;
    private Integer ageGoe;
    private Integer ageLoe;
    private String name;
}

 

querydsl 검색 조건 클래스 만들었는데, @AllArgsConstructor이게 없으면 조건이 null로 들어오고, 넣어줘야만 조건이 정상적으로 실행된다.

왜??

=> 지피티 :

@AllArgsConstructor 어노테이션이 있어야 ProfileSearchCondition 객체가 정상적으로 생성되어 컨트롤러 메서드에 전달될 수 있습니다. 이 어노테이션은 모든 필드를 인자로 받는 생성자를 자동으로 생성해줍니다. 스프링 프레임워크는 이 생성자를 이용해 객체를 생성하고 값을 주입할 수 있습니다. 만약 이 어노테이션이 없다면, 스프링은 기본 생성자를 통해 객체를 생성하고, Setter 메서드나 Reflection을 통해 값을 주입하려 할 것입니다. 그러나 @NoArgsConstructor(access = AccessLevel.PRIVATE)로 기본 생성자의 접근 수준이 PRIVATE로 설정되어 있기 때문에 객체를 생성할 수 없습니다.

이 문제를 해결하려면, @AllArgsConstructor 어노테이션을 유지하거나, 생성자를 명시적으로 정의하고, 스프링이 객체를 생성할 때 사용할 수 있도록 접근 수준을 조정해야 합니다.

 

 

===============================

6/18

1. service에서 다른 service 의존 가능? = Member찾는거 Memberrepository에서 따로 다 찾았었는데 MemberService에 메서드가 정의되어 있네. 흠?

https://jangjjolkit.tistory.com/62

 

2. ProfileSearchCondition에 검색 조건 null 들어가는 문제 @AllArg~로 해결했었는데, 생성자들 다 없애고 @Setter만 있어도 값이 들어간다. 왜? = 내 생각에는 값을 넣어주는게 하나라도 있어야 돼서. @Setter든 전체 생성자든.

 

 

3. 동적 검색은 쉽게 했는데, 동적 정렬이 잘 안됐다. QueryDSL 사용하면서 같이 정렬을 해야했다. 정렬 조건이 많으니까. 그래서 https://velog.io/@seungho1216/Querydsl%EB%8F%99%EC%A0%81-sorting%EC%9D%84-%EC%9C%84%ED%95%9C-OrderSpecifier-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%ED%98%84

이 블로그 보고 하니 쉽게 되었다. 원리는 공부해봐야한다. 코드는 이해못함

 

=========================================================

6/20

1 + n 문제 fetch join과 batch size로 해결하려고 보니, 현재 발생하는 상황이 없다.

matchlist를 조회할 때는 member, patient, caregiver, match 이렇게 네번 쿼리가 나가고

사실 고양이 집사(10명) 과 각각의 집사마다 고양이 10마리씩 보유할 때, 모든 고양이 집사 리스트를 조회하는 과정에서 n + 1 문제가 일어나는데, (select 고양이 where 집사 = ?) <- 10번의 쿼리 나감. 하지만 현재 프로젝트에서 이런 과정이 없음. 간병인 찾기 리스트 조회 시에도, 모든 간병인 조회하는데, 간병인이 가지고 있는 N 필드가 없어서 쿼리가 한 번 나감. 

추가 설명 : order, orderItem의 관계 경우, order 10개를 조회할 때, order 각각이 가진 orderItem가 N 관계여서, order 수만큼 쿼리가 N번 추가로 나가는데, 현재 프로젝트에서는, 모든 간병인 조회 시에, 간병인 간단 정보(이름, 나이, 성별 등) 만 조회하므로 n + 1 문제가 발생안함. 추후 발생할 수 있는 곳 잘 찾아보기.

 

그래서 테스트를 진행한다. 매칭 테스트. 그런데 현재 controller에서 service로 username을 넘기는게 아니라 User를 넘기네? 테스트가 좀 귀찮아지는데 리팩토링 할까?? 흠 우짤까. 정답이 없나? 구현하기 나름인가?

 

==================================

6/21 

매칭 테스트코드 작성중.

공통된 when절 두 줄 전체 클래스레벨에서 @Beforeeach돌리려니 당연히 필요없는 코드라고 에러나옴.

왜냐하면 각 테스트마다 필요할 수도 있고 새로 when절 만드는곳 도있는데 공통으로 쓰면 당연히 필요없는 코드라 나오지. 그래서 내부 클래스별로 Beforeeach하려고 보니, 딱히 모든 테스트가 when절이 공통되는 클래스가 없음. 다 제각각이라 그냥 테스트메서드별로 있는게 가독성도 그렇고 좋겠다 싶어서 각각 진행함.

 

다음 진행할 기능. 이제 찾기 기능과 매칭 기능 고도화를 하면 좋을 것 같은데,

상상력을 발휘하자. 대용량 트래픽이라고 가정하고 진행하자.

 

찾기는 레디스를 써서 구 단위로 캐싱?

매칭 요청은 시간 테스트 해보자. 백만건의 동시 요청에 버틸 수 있을까?

매칭 요청시 알람 기능하자. (어떻게? 웹소켓? sse? 등) - sse로 진행하자.

왜 sse를 쓰는지 확실하게 이유 찾아놔.

일단 알림은 polling, sse, websocket 등으로 할 수 있는데 polling은 느리고, 웹소켓은 지속연결이라 부담이고 서버에서 클라이언트로 일방적인 알림전송이므로 sse로 충분하다고 생각. 

 

 

===============

6/22

알림 기능 도입 sse + aop

 

============

6/23

createForPatient에 알림 적용하려고 하니, member <-> caregiver 순환참조 발생.

"id"8,
    "patientId"1,
    "caregiverId"1,
    "createdDate""2024-06-23T10:48:30.3193131",
    "matchStatus""PENDING",
    "readStatus""UNREAD",
    "firstRequest""PATIENT_FIRST",
    "patientProfileSnapshot""Patient Snapshot - Name: 4444_user, Address: com.patientpal.backend.member.domain.Address@401f4753, PatientSignificant: lob, CareRequirements : null",
    "caregiverProfileSnapshot"null,
    "receiver": {
        "id"1,
        "username""lhssss1111CAREGIVER",
        "password""{bcrypt}$2a$10$vR1FCzNOX5oYrXdyvTzN8.Z0quF7Mw/z4GqcfqlPANveM1YMKSEqW",
        "provider""LOCAL",
        "patient"null,
        "caregiver": {
            "createdDate""2024-06-22T19:58:21.506259",
            "lastModifiedDate""2024-06-22T19:59:46.954731",
            "id"1,
            "member": {
                "id"1,
                "username""lhssss1111CAREGIVER",
                "password""{bcrypt}$2a$10$vR1FCzNOX5oYrXdyvTzN8.Z0quF7Mw/z4GqcfqlPANveM1YMKSEqW",
                "provider""LOCAL",
                "patient"null,
                "caregiver": {
                    "createdDate""2024-06-22T19:58:21.506259",
                    "lastModifiedDate""2024-06-22T19:59:46.954731",
                    "id"1,
                    "member": {
                        "id"1,
                        "username""lhssss1111CAREGIVER",
                        "password""{bcrypt}$2a$10$vR1FCzNOX5oYrXdyvTzN8.Z0quF7Mw/z4GqcfqlPANveM1YMKSEqW",
                        "provider""LOCAL",
                        "patient"null,
                        "caregiver": {
                            "createdDate""2024-06-22T19:58:21.506259",
                            "lastModifiedDate""2024-06-22T19:59:46.954731",
                            "id"1,
                            "member": {
원인은 
private static class MatchNotificationProxy extends MatchResponse implements NotificationInfo {
    private final Member receiver;
    private final Match match;

    public MatchNotificationProxy(MatchResponse matchResponse, Member receiver, Match match) {
        super(matchResponse.getId(), matchResponse.getPatientId(), matchResponse.getCaregiverId(), matchResponse.getCreatedDate(),
                matchResponse.getMatchStatus(), matchResponse.getReadStatus(), matchResponse.getFirstRequest(),
                matchResponse.getPatientProfileSnapshot(), matchResponse.getCaregiverProfileSnapshot());
        this.receiver = receiver;
        this.match = match;
    }

    @Override
    public Member getReceiver() {
        return receiver;
    }

    @Override
    public Long getGoUrlId() {
        return match.getId();
    }

    @Override
    public Notification.NotificationType getNotificationType() {
        return Notification.NotificationType.MATCH;
    }
}

이 프록시 dto에서 receiver를 반환할때 member엔티티를 반환하고, member entity에서 caregiver엔티티를 참조하고 caregiver는 다시 member참조해서 순환참조 발생함.

해결  => 엔티티에

@JsonManagedReference
private Member member;
@JsonBackReference
private Caregiver caregiver;

이렇게 해서 순환참조 막았더니 해결되긴함. 그런데 엔티티를 반환한다는 거에서 근본적인 해결이 맞나?

Member를 바로 반환안하고 dto로 반환할 수 있나?

ㅇㅇ

private static class MatchNotificationProxy extends MatchResponse implements NotificationInfo {
    private final MemberResponseNotification memberResponseNotification;
    private final Match match;

    public MatchNotificationProxy(MatchResponse matchResponse, MemberResponseNotification memberResponseNotification, Match match) {
        super(matchResponse.getId(), matchResponse.getPatientId(), matchResponse.getCaregiverId(), matchResponse.getCreatedDate(),
                matchResponse.getMatchStatus(), matchResponse.getReadStatus(), matchResponse.getFirstRequest(),
                matchResponse.getPatientProfileSnapshot(), matchResponse.getCaregiverProfileSnapshot());
        this.memberResponseNotification = memberResponseNotification;
        this.match = match;
    }

    @Override
    public String getReceiver() {
        return memberResponseNotification.getUsername();
    }

    @Override
    public Long getGoUrlId() {
        return match.getId();
    }

    @Override
    public Notification.NotificationType getNotificationType() {
        return Notification.NotificationType.MATCH;
    }
}

이렇게 하니 해결함. 근데 난 여기서 반환값이 MemberResponseNotification하고  Match 라고 예상했는데, 실제 postman 출력시 반환값은 receiver, goUrlId, NotificationType 이렇게 세 개였음. 이유 찾아보니 getter가 있는 필드에 한해서 반환값을 준다고 함. getMatch넣어봤으나 바로 순환참조 발생 ~. 응답값을 뭐로 할지 정해지면 수정하면 될듯.

 

//TODO TX안걸려 있는데 어떻게 SAVE가 되는거지? save메서드가 알아서 해줌. 일부러 tx 안걸었다. tx걸면 한 작업으로 크게 묶여서 알림 전송이 실패하면 save가 롤백됨. tx 안걸면 save됨.
//그러면 여기서 해볼 수 있는것이 알림이 저장이 되고, 이후 전송이 실패할 때, 알림은 데베에 저장되어 있지만, 전송이 안된거니
//저장은 되었지만 전송은 안된 알림들을 모아서 일괄적으로 재전송하는 기능을 만들 수 있겠다.
public void send(String username, NotificationType notificationType, String content, String url) {
    Notification notification = notificationRepository.save(createNotification(username, notificationType, content, url));

    // if (true) throw new IllegalStateException("hoho");
    String eventId = username + "_" + System.currentTimeMillis();
    Map<String, SseEmitter> emitters = emitterRepository.findAllEmitterStartWithByMemberId(username);
    emitters.forEach(
            (key, emitter) -> {
                emitterRepository.saveEventCache(key, notification);
                sendNotification(emitter, eventId, key, NotificationDto.Response.createResponse(notification));
            }
    );
}

 

--------

 

// 알림 종모양 누를 시 나오는 전체 알림 읽음 처리
    //Notification 엔티티 각각 돌면서 해당하는 알림을 isRead = true 처리해야 하니 쿼리가 N번 나가는데 최적화 못하나?
    @PostMapping("/read")
    public void setReadAllNotification(@AuthenticationPrincipal User currentMember) {
        notificationService.setReadAll(currentMember.getUsername());
    }
}
@Transactional
public void setReadAll(String username) {
    List<Notification> unseenNotifications = notificationRepository.findByReceiverUsernameAndIsReadFalse(username);

    unseenNotifications.forEach(notification -> {
        notification.setIsRead(true);
        notificationRepository.save(notification);
    });
}

gpt : 여러 개의 알림을 일괄적으로 읽음 처리하는 로직을 최적화하려면, 각 알림 엔티티를 하나씩 업데이트하는 대신, 단일 쿼리를 사용하여 한 번에 모든 알림을 업데이트하는 방식으로 변경할 수 있습니다. 이를 위해 JPQL 또는 네이티브 SQL을 사용하여 업데이트 쿼리를 직접 실행할 수 있습니다.

Step 1: NotificationRepository에 일괄 업데이트 메서드 추가

NotificationRepository 인터페이스에 일괄 업데이트를 위한 메서드를 추가합니다.

JPQL을 사용하는 방법:

import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface NotificationRepository extends JpaRepository<Notification, Long> {

    @Modifying
    @Query("UPDATE Notification n SET n.isRead = true WHERE n.receiver.username = :username AND n.isRead = false")
    int markAllAsRead(@Param("username") String username);
}

'PatientPal' 카테고리의 다른 글

AWS, Presigned URL 이용한 프로필 이미지 업로드 구현기  (0) 2024.06.29
6/24 ~ 6/30  (0) 2024.06.24
6/10 ~ 6/16  (0) 2024.06.11
6/5 ~ 6/9  (0) 2024.06.05
5/27 ~ 5/28  (0) 2024.05.27

BELATED ARTICLES

more