본문 바로가기
일기/항해99

[항해99 6기] 실전 프로젝트 5 주차 - 느낀점

by 고구밍 2022. 5. 30.

느낀점

https://moolmooldoctor.firebaseapp.com/

 

물물박사

똑똑한 교환 생활, 물물박사

moolmooldoctor.firebaseapp.com

 

" 설문조사 "

설문 자세히보기

더보기

 

아쉬운 점

 

자유 코멘트

 

 

 

 

 

4주간 만들었던 사이트를 사람들에게 처음으로 배포하는 한 주였습니다.

그동안 팀원들과 열심히 만들었던 프로젝트를 마무리하고 정비를 하면서,

제대로 된 작품을 만들어서 뿌듯하였습니다.

 

비록 백엔드파트에서 제가 구현한 코드는 20%정도로 비중이 많지는 않았지만,

저희 프로젝트에 있어서 거래에대한 플로우를 가장 잘 이해할 수 있었던

거래내역의 상태변화 (거래완료, 거래취소, 상대방에게 평가하기)을 무사히 구현을 하였다는 부분에서,

이번 4주간의 저의 업무는 성공이였다고 자신있게 이야기 할 수 있습니다.

 

또한 어플리케이션에서 DB에서 자료를 조회할 때

1. JPA문

2. JPQL @Query (SQL문법)

3. QueryDSL

 

등 거래내역을 조회하기위해서 동일한 결과를 얻기위해서

1,  2, 3 모두를 사용하고 JMETER를 이용해서 6000회에서부터 2,6000회까지 조회테스트를 통해서,

내가 짠 코드가 과연 좋은 코드인지 테스트하는 과정이 가장 재미있었던 부분이였습니다.

 

마지막으로 카페인캐시를 스스로 적용해보면서,

비록 캐시에 대해서 모든 것을 이해한 것은 아니지만 잘 돌아갈 수 있도록

캐시의 이름, 저장시간, 갯수를 정하고 각각의 메서드에서

 

사용하는 파라미터를 사용하여 key값을 설정하는 과정을 스스로 찾는 과정도 재미가 있었고,

눈으로 성능개선이 잘 보였기 때문에 많이 뿌듯했던 작업 이었습니다.

 

 

 

내가 한 일들

월요일 :  QueryDSL적용 - 거래내역보기 (Dto로 조회하기) / 찜한목록 조회 수정(status값 추가)

더보기

 

-> 1개의 거래내역에 2개이상의 Item 엔티티를 조회해야되고, 컬럼의 수가 많기 때문에

-> 쿼리의 길이를 줄이기 위해서는 Dto형식으로 필요한 정보만 담아서,

-> 어플리케이션에서 return값을 조리해줘야했습니다.

아래는 거래내역에 사용했던 queryDSL문입니다.

 

거래내역

@Override
public BarterItemListDto findByBarterItems(Long itemId) {
    return queryFactory
            .select(new QBarterItemListDto(
                    item.id,
                    item.title,
                    item.itemImg,
                    item.contents
            ))
            .from(item)
            .where(item.id.eq(itemId))
            .fetchOne();
}

 

 마이페이지

 // 성훈 - 찜하기를 한 아이템을 찾는다
    @Override
    public List<ItemUserResponseDto> findByMyScrabItems(Long userId) {

        return queryFactory
                .select(new QItemUserResponseDto(
                        item.id,
                        item.itemImg,
                        item.status
                ))
                .from(item)
                .join(scrab1).on(scrab1.itemId.eq(item.id))
                .fetchJoin()
                .distinct()
                .where(scrab1.userId.eq(userId), scrab1.scrab.eq(true))
                .orderBy(scrab1.modifiedAt.desc())
                .limit(3)
                .fetch();
    }

 

 

 

화요일 :

1. 거래완료시 다른 사람이 신청한 거래내역을 삭제 & 신청한 거래내역을 2 -> 0으로 초기화 업데이트

2. DB에서 IMG url 리스트가 String형식으로 ","으로 되어있는 것을 파싱하여 조회할 수 잇는 방법을 구현하기

https://goguming2.tistory.com/102?category=932343 

 

JPQL 콘솔로 쿼리문 작성해보기 - 1

내가 하고있는 업무 코드 성능 높이기 -> FOR문을 줄이자 -> 파싱을 조회쿼리에서 한번에 받아오자 1. 마이페이지 이미지가 여러장이 저장되는데, 문자열 "," 을 기준으로 나눠서 [0] 번째 (첫번 째)

goguming2.tistory.com

더보기

 

1. 신청받은 사람(SellerId)의 거래내역을 전체 조회

2. for문으로 각 거래를 파싱하여

3. if문으로 현재 거래완료한 셀러의 아이템Id와 동일한 다른사람의 거래내역이면 

4. 각 다른 요청자 (bueyer)의 아이템상태를 0으로 초기화하고, 거래내역을 삭제한다.

-> JPA문으로 barterRepository.deleteById(barterId)로 삭제하는 것을 구현하고자 하였지만,

잘되지않아서 barter를 넣어서 삭제하는 방식입니다.

 

List<Barter> sellerBarterList = barterRepository.findAllBySellerId(sellerId);
            for (Barter eachBarter : sellerBarterList) {

                Long eachBarterId = eachBarter.getId();
                String[] eachBarterIdList = eachBarter.getBarter().split(";");
                String[] eachBuyerItemId = eachBarterIdList[0].split(",");
                String eachSellerItemId = eachBarterIdList[1];

                // 해당 거래내역이 아닌, 동일한 셀러아이템일 경우
                if (!eachBarterId.equals(barterId) && eachSellerItemId.equals(sellerItemId)) {
                    for (String eachBuyerItem : eachBuyerItemId) {
                        Long buyerItemIds = Long.parseLong(eachBuyerItem);
                        Item eachBuyerItems = itemRepository.findById(buyerItemIds).orElseThrow(() -> new CustomException(NOT_FOUND_BUYER_ITEM));
                        // 다른 바이어 아이템들을 0으로 초기화해준다.
                        eachBuyerItems.statusUpdate(buyerItemIds, 0);
                        // 거래내역을 삭제한다.
                        barterRepository.delete(eachBarter);
                    }
                }
            }

 

 

 

수요일 : JPQL문을 이용하여 DB에서 IMG를 파싱하여 조회 / JMETER테스트 비교

-> 처리속도가 크게 개선되지 않았지만, 오류발생율이 3.85 %-> 6.12% 으로 늘어서 사용하지 않기로 함

https://goguming2.tistory.com/103?category=932188 

 

JMETER테스트하기 - 2

마이페이지 테스트 결과입니다. 요약) 1. 기존 JPA보다 40% 속도를 감축하였습니다. 2. 평균 Bytes는 200 감소하였습니다. 3. db에서 파싱해서 받아오는 JPQL은 DSL보다 10% 빨랐습니다. 4. JPQL은 평균 Bytes

goguming2.tistory.com

 

 

목요일 : 거래내역 수정하기 추가 / 캐시 카페인 공부 / 배포

더보기

https://goguming2.tistory.com/104?category=932188 

 

실전프로젝트 배포후 오류발생

진현님이랑 평가 완료했던 내역 -> 알림에서 저장되어 있는 곳이 막히지 않아서 접근 -> 거래취소 버튼 -> 거래내역은 삭제되고, 아이템의 status값이 0으로 업데이트가 되네요.예외처리로 if문이

goguming2.tistory.com

 

거래내역 수정하기 추가

 

 @Transactional
    @Caching(evict = {
            @CacheEvict(cacheNames = "barterMyInfo", key = "#userDetails.userId", allEntries = true),
            @CacheEvict(cacheNames = "userProfile", key = "#userDetails.userId", allEntries = true),
            @CacheEvict(cacheNames = "itemInfo", allEntries = true),
            @CacheEvict(cacheNames = "itemDetailInfo", key = "#userDetails.userId + '::' + #editRequestDto.itemId", allEntries = true),
            @CacheEvict(cacheNames = "itemTradeCheckInfo", key = "#userDetails.userId+ '::' + #editRequestDto.itemId", allEntries = true)})
    public void editBarter(EditRequestDto editRequestDto, UserDetailsImpl userDetails) {
        // 수정할 거래내역 찾기
        Barter barter = barterRepository.findById(editRequestDto.getBarterId()).orElseThrow(() -> new CustomException(NOT_FOUND_BARTER));
        // 유저
        User user = userRepository.findById(userDetails.getUserId()).orElseThrow(() -> new CustomException(NOT_FOUND_USER));

        String barterItems = barter.getBarter();
        String[] buyerItemList = barterItems.split(";")[0].split(",");
        String sellerItem = barterItems.split(";")[1];

        // 수정하기 전 아이템의 buyer의 아이템 상태를 0으로 초기화 해준다.
        for (String eachItem : buyerItemList) {
            Long eachItemId = Long.parseLong(eachItem);
            Item eachItems = itemRepository.findById(eachItemId).orElseThrow(() -> new CustomException(NOT_FOUND_ITEM));
            eachItems.statusUpdate(eachItemId, 0);
        }

        StringBuilder editItemIds = null;
        // 수정할 아이템의 아이템 상태를 거래중 (2)로 만들어준다.
        for (Long eachEditItemId : editRequestDto.getItemId()) {
            Item editItems = itemRepository.findById(eachEditItemId).orElseThrow(() -> new CustomException(NOT_FOUND_ITEM));
            editItems.statusUpdate(eachEditItemId, 2);
            // 아이템의 아이디를 String형태로 변환하여 edit
            if (editItemIds != null) {
                editItemIds.append(",").append(eachEditItemId);
            } else {
                editItemIds = new StringBuilder(String.valueOf(eachEditItemId));
            }
        }
        // 수정된 거래사항을 업데이트합니다.
        String editBarter = editItemIds + ";" + sellerItem;
        barter.editBarter(editBarter);
    }

 + 포스트맨으로 @RequestParam형태로 List를 보낼 때 다음과 같이 보내 주면 된다는 점을 처음 알았습니다.

 

 

캐시공부

https://goguming2.tistory.com/105?category=932343 

 

카페인 로컬 캐쉬를 알아보자 -1

https://blog.yevgnenll.me/posts/spring-boot-with-caffeine-cache Spring boot 에 caffeine 캐시를 적용해보자 - 어떻게하면 일을 안 할까? 부제: 어떻게 하면 일을 조금이라도 안할까? blog.yevgnenll.me https..

goguming2.tistory.com

https://goguming2.tistory.com/106?category=932343 

 

캐시를 알아보자 - 2

HashMap을 사용한 메소드만 캐시가 적용했다. 다른 컨트롤러에서 map을 사용하지 않았을 경우 캐쉬에 담아서 사용할 수 없는 걸까? https://gngsn.tistory.com/157 Spring Cache, 제대로 사용하기 Spring Cache 사..

goguming2.tistory.com

 

 

금요일 : 카페인 캐시 적용하기

5조 JMETER 부하테스트.xlsx
0.11MB

더보기

https://goguming2.tistory.com/107?category=932188 

 

카페인 캐시를 적용해보자.

참고한 링크 더보기 결정적으로 참고한 코드) https://velog.io/@_koiil/Caffeine [Spring] Caffeine 이름부터 귀엽다 ☕️ velog.io 결정적으로 변형한 코드) https://github.com/eugenp/tutorials/blob/master/..

goguming2.tistory.com

 

카페인 캐시를 적용하고 JMETER테스트를 통해서 동일한 게시판을 여러번 요청을 할 경우,

어플리케이션에서 복잡한 계산을 하지 않고, 캐시에 저장해 두었다고 보내 줄 수 있어서

성능향상이 크게 되었습니다.

 

 마이페이지 JMETER 테스트 

1번째 해석

dsl2차와 전송속도를 비슷하게 생각해서평균처리시간 * 2해서 비교해보면28%정도 빨라졌네요
 

전송속도로 보정해서 비교를 한 이유?

캐시를 적용한 이후 초당 더 많이 주고받는다는거니까 성능이 좋아진 것 같은데,
이게 시간대별로 송수신 상태가 달라서 믿을 수 잇는 자료인지는 잘몰라서
단순히 비슷하게 숫자를 맞춰서 평균 시간을 계산해 본거에요

 

거래내역 JMETER 테스트 

< 1만회 >

 
 
 
수신과 전송이 둘다 7배 늘어버렸네요...뭐지
평균속도는 송수신값과 동일하게 놓았을경우 송수신 차이가 7배 차이이므로
100 - (5000*7 / 58696<- DSL평균속도  * 100<- 백분율) 해서 DSL에비해서 40% 빨라 졌네요.
그리고 오류도 2.53% -> 0.88%으로 줄었습니다

 

거래내역 JMETER 테스트

< 2.6만회 >

 
 
극단적으로 측정했엇던 2만 6천회 조회했을 때 결과입니다.
위와 같은 방법으로 계산해보면 평균속도는 DSL에비해서 13% 단축하였고,
오류는 58.5% -> 38.6% 으로 34% 감소하였습니다.
 
근데, 뭔가 캐시처리해서그런지 송수신 비율로 평균속도를 보정한다는게 이상하긴하네요
JPA랑 비교했을 경우 오히려 느린걸로 되서, 그리 정확한 방법은 아닌 것 같아요
차후에 송수신 와 캐시의 관계를 공부해야 겠습니다.

 

 

 

장난삼아서 계산해보았는데,
거래내역에서 2.6만번 조회했을 때 단순 비교로 평균속도가

 

JPA 93080.5 기준으로 (93초 : 1분 33초)

 

DSL (DTO로 조회했을 경우)

61426.3 -> 32.96% (1분 2초)


캐시 (DSL + 캐시)

7686.4 -> 96.78% (7.6초)

으로 단축한거네요

 

 

토요일 :  api요청 주소 수정, 낙관적 락 적용 / 거래완료시 알람 중복생성 및 거래취소시 알람 삭제 구현

더보기

거래완료 알람을 상대 userId으로 저장하기

 1. 내 포지션이 buyer와 seller중에 하나일 경우에 상대userId로 알람이 저장이 되어있기 때문에,

2. 내가 buyer일경우 sellerId와 현재 거래내역Id로 모든알람을 가져옵니다.

3. 이미 알람이 존재한다면 stomp으로만 메시지를 보내주고, notificationRepository에 추가로 저장하지 않습니다.

4. else 만약 그렇지 않는다면 알람정보를 상대바의 userId로 내 닉네임 정보를 담아서 저장합니다.

 

List<Notification> notificationCeck;
            if (myPosition.equals("buyer")) {
                notificationCeck = notificationRepository.findAllByUserIdAndChangeId(myBarter.getSellerId(), barterId);
            } else {
                notificationCeck = notificationRepository.findAllByUserIdAndChangeId(myBarter.getBuyerId(), barterId);
            }

            // 이미 저장된 알람이면 스톰프 메시지만 보낸다.
            if (notificationCeck.size() >= 1) {
                for (Notification notification : notificationCeck) {
                    sendTradeMessage(myBarter, myPosition, notification);
                }
            } else {
                // 알림 내역 저장
                Notification notification = notificationRepository.save(Notification.createOfBarter(myBarter, user.getNickname(), myPosition, "Barter"));
                // 상대방의 sup주소로 전송
                sendTradeMessage(myBarter, myPosition, notification);
            }

            // 내게 거래완료 메시지 보내기
            sendMyMessage(barterId, myBarter, myPosition);
            return new BarterStatusDto(opponentTrade, false, myBarter.getStatus());
        }

 

거래취소시 상대방과 거래내역에 대한 알람을 일괄적으로 삭제

String myPosition;
if (myBarter.getBuyerId().equals(userId)) {
    myPosition = "buyer";
    // 유저가 셀러라면 셀러거래완료를 true로 변경한다.
} else {
    myPosition = "seller";
}

// 거래취소시 저장된 알림도 삭제된다.
List<Notification> notificationCeck;
if (myPosition.equals("buyer")) {
    notificationCeck = notificationRepository.findAllByUserIdAndChangeId(myBarter.getSellerId(), barterId);
    notificationRepository.deleteAll(notificationCeck);
} else {
    notificationCeck = notificationRepository.findAllByUserIdAndChangeId(myBarter.getBuyerId(), barterId);
    notificationRepository.deleteAll(notificationCeck);
}

 

어려웠던 점

사실 간단한 건데, 알람에 저장되어 있는

ENUM 타입이 BARTER(거래중)가 아닌 FINISH(거래완료)인데,

이 부분을 잠시 잊어버리고 있어서, WHERE절에 조건을 잘못 조회해서 3시간 째 삽질하고 있었습니다...ㅠㅠㅠ

@Override
public List<Notification> findAllByUserIdAndChangeId(Long userId, Long changeId) {
    return queryFactory
            .selectFrom(notification)
            .where(notification.userId.eq(userId),
                    notification.changeId.eq(changeId),
                    notification.type.eq(FINISH))
            .fetch();
}

 

 

https://reiphiel.tistory.com/entry/understanding-jpa-lock 

 

JPA 잠금(Lock) 이해하기

JPA(Hibernate:하이버네이트)에 의한 잠금(Lock:락) 사용중에 생각하고 있던바와 동작이 좀 다른 부분이 있어서 전반적으로 정리해 보았습니다. 잠금(Lock)의 종류 낙관적 잠금(Optimisstic Lock) 낙관적 잠

reiphiel.tistory.com

https://www.notion.so/b308c025e9954cadb9fdc19d86bd411f?v=b6b1eb52cfe24793b97163d7b4825b07 

 

API 설계

A new tool for teams & individuals that blends everyday work apps into one.

www.notion.so

 

api주소를 api/item 이였던 부분은 item/ 혹은 user/barter 이런방향으로 수정하였습니다.

 

내가 해야하는 

더보기

1. 카페인 캐시 정교화하기

2. 부족한 부분 찾아서 채우기

3. 버그수정

4. 발표준비 돕기

5. 개인 면접자료 만들기