Project/LOATODO

[LOATODO] 모집 게시판 API 리팩토링

마볼링 2024. 9. 13. 18:55

1. 서론

로아투두 새로운 기능으로 "모집 게시판"을 개발하는 중이다.

 

전형적인 게시판 형식이기 때문에 API 개발에 그렇게 큰 시간이 들지않았다.

  1. List 조회하는 Get API
  2. 상세내용 조회하는 Get API
  3. 글작성 Post API
  4. 글수정 Put API
  5. 글삭제 Delete API

 

2. 문제

메인화면 데이터는 각 카테고리별 최신 5개의 데이터를 불러온다.

만약 이 데이터를 앞서 개발한 List 조회하는 Get API로 불러온다면
새로고침 한번에 6번의 API 요청이 필요하다!!

 

기획할 때 놓쳐서 새롭게 API를 하나 개발하였다.

 

 

3. 개발

1) SQL 작성

원하는 형태의 데이터를 불러오기 위한 SQL문을 작성해본다.

카테고리 별로 최신 5개의 데이터를 불러와야하기 때문에, UNION ALL을 사용해서 작성해보았다.

(
    SELECT *
    FROM recruiting_board
    WHERE recruiting_category = 'FRIENDS'
    ORDER BY created_date DESC
    LIMIT 5
)
UNION ALL
(
    SELECT *
    FROM recruiting_board
    WHERE recruiting_category IN ('RECRUITING_GUILD', 'LOOKING_GUILD')
    ORDER BY created_date DESC
    LIMIT 5
)
UNION ALL
(
    SELECT *
    FROM recruiting_board
    WHERE recruiting_category IN ('RECRUITING_PARTY', 'LOOKING_PARTY')
    ORDER BY created_date DESC
    LIMIT 5
)
UNION ALL
(
    SELECT *
    FROM recruiting_board
    WHERE recruiting_category = 'ETC'
    ORDER BY created_date DESC
    LIMIT 5
);

 

2) SQL -> QueryDSL 변환

이제 작성된 SQL을 QueryDSL로 작성하면 편한데

안타깝게도 QueryDSL은 UNION ALL을 지원하지 않는다... 

 

그래서 Native Query 형태로 작성해야 한다.

그냥 SQL문 그대로 작성할 수 도 있지만, 추후 확장의 가능성을 염두해두고

코드 형식으로 작성하였다.

 

@Override
public List<RecruitingBoard> searchMain() {
    String sql = buildUnionAllQuery();
    Query query = entityManager.createNativeQuery(sql, RecruitingBoard.class);
    return query.getResultList();
}

private String buildUnionAllQuery() {
    return String.join(" UNION ALL ",
            buildCategoryQuery(FRIENDS.name()),
            buildCategoryQuery(RECRUITING_GUILD.name(), LOOKING_GUILD.name()),
            buildCategoryQuery(RECRUITING_PARTY.name(), LOOKING_PARTY.name()),
            buildCategoryQuery(ETC.name())
    );
}

private String buildCategoryQuery(String... categories) {
    String categoryCondition = categories.length > 1 ?
            String.format("IN ('%s')", String.join("', '", categories)) :
            String.format("= '%s'", categories[0]);

    return String.format(
            "(SELECT * FROM recruiting_board WHERE recruiting_category %s ORDER BY created_date DESC LIMIT 5)",
            categoryCondition
    );
}

 

buildCategoryQuery 메소드를 이용해 SELECT SQL 문을 작성하고

buildUnionAllQuery 메소드를 이용해 UNION ALL로 작성된 SQL 문들을 연결한다.

 

 

3) Map과 Dto 형식으로 변환

이제 위에 searchMain 메소드로 받아온 리스트의 결과를 카테고리별로 나누어야 한다.

@Transactional(readOnly = true)
public Map<String, List<SearchRecruitingBoardResponse>> searchMain() {
    List<RecruitingBoard> recruitingBoards = recruitingBoardDao.searchMain();

    // 카테고리 그룹 정의
    Map<RecruitingCategoryEnum, String> categoryMap = Map.of(
            RecruitingCategoryEnum.FRIENDS, "FRIENDS",
            RecruitingCategoryEnum.RECRUITING_GUILD, "GUILD",
            RecruitingCategoryEnum.LOOKING_GUILD, "GUILD",
            RecruitingCategoryEnum.RECRUITING_PARTY, "PARTY",
            RecruitingCategoryEnum.LOOKING_PARTY, "PARTY"
    );

    return recruitingBoards.stream()
            .collect(Collectors.groupingBy(
                    recruitingBoard -> categoryMap.getOrDefault(recruitingBoard.getRecruitingCategory(), "ETC"),
                    Collectors.mapping(
                            SearchRecruitingBoardResponse::new,
                            Collectors.toList()
                    )
            ));
}

 

 

Enum에 따라서 카테고리별로 그룹을 정의하고 

stream 형식으로 해당 카테고리에 맞게 Map 형식으로 변경한다.

 

4) 결과물 확인

그럼 출력된 결과물은 다음과 같다.

{
    "PARTY": [
        {
            "recruitingBoardId": 34,
            "recruitingCategory": "LOOKING_PARTY",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:44.769773",
            "showCount": 0
        },
        {
            "recruitingBoardId": 33,
            "recruitingCategory": "LOOKING_PARTY",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:44.295649",
            "showCount": 0
        },
        {
            "recruitingBoardId": 32,
            "recruitingCategory": "LOOKING_PARTY",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:42.696684",
            "showCount": 0
        },
        {
            "recruitingBoardId": 31,
            "recruitingCategory": "LOOKING_PARTY",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:41.769048",
            "showCount": 0
        },
        {
            "recruitingBoardId": 30,
            "recruitingCategory": "LOOKING_PARTY",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:40.665982",
            "showCount": 0
        }
    ],
    "GUILD": [
        {
            "recruitingBoardId": 24,
            "recruitingCategory": "LOOKING_GUILD",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:29.839134",
            "showCount": 0
        },
        {
            "recruitingBoardId": 23,
            "recruitingCategory": "LOOKING_GUILD",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:29.489643",
            "showCount": 0
        },
        {
            "recruitingBoardId": 22,
            "recruitingCategory": "LOOKING_GUILD",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:29.060159",
            "showCount": 0
        },
        {
            "recruitingBoardId": 21,
            "recruitingCategory": "LOOKING_GUILD",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:28.731865",
            "showCount": 0
        },
        {
            "recruitingBoardId": 20,
            "recruitingCategory": "RECRUITING_GUILD",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:24.949237",
            "showCount": 0
        }
    ],
    "FRIENDS": [
        {
            "recruitingBoardId": 16,
            "recruitingCategory": "FRIENDS",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:15.496221",
            "showCount": 0
        },
        {
            "recruitingBoardId": 15,
            "recruitingCategory": "FRIENDS",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:12.582111",
            "showCount": 0
        },
        {
            "recruitingBoardId": 14,
            "recruitingCategory": "FRIENDS",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:11.96305",
            "showCount": 0
        },
        {
            "recruitingBoardId": 13,
            "recruitingCategory": "FRIENDS",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:11.342354",
            "showCount": 0
        },
        {
            "recruitingBoardId": 12,
            "recruitingCategory": "FRIENDS",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:10.666694",
            "showCount": 0
        }
    ],
    "ETC": [
        {
            "recruitingBoardId": 39,
            "recruitingCategory": "ETC",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:51.434333",
            "showCount": 0
        },
        {
            "recruitingBoardId": 38,
            "recruitingCategory": "ETC",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:51.01957",
            "showCount": 0
        },
        {
            "recruitingBoardId": 37,
            "recruitingCategory": "ETC",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:50.618782",
            "showCount": 0
        },
        {
            "recruitingBoardId": 36,
            "recruitingCategory": "ETC",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:50.208557",
            "showCount": 0
        },
        {
            "recruitingBoardId": 35,
            "recruitingCategory": "ETC",
            "title": "3차테스트",
            "mainCharacterName": null,
            "itemLevel": 1643.39,
            "createdDate": "2024-09-13T18:03:49.825298",
            "showCount": 0
        }
    ]
}

 

 

4. To-Be

이제 6번 호출하는 API를 1번으로 호출할 수 있게 되었다.

 

여기서 또 추가 개선을 해본다면

메인화면은 자주 호출하는 API이기 때문에 호출할 때 마다 데이터를 불러오는 것이 비효율적일 수 있다.

캐시를 이용하면 그러한 문제를 해결 할 수있다.