🔥인트로


어느덧 마지막 주가 되었습니다. 평소 프로젝트를 개발할 때는 늘 프론트엔드 백엔드 상관없이 풀스택으로 개발을 했었느데 백엔드와 프론트엔드 둘로 나뉘어서 개발을 해보니 확실히 다른 느낌이었습니다. backend로 기능을 구현해도 화면으로는 확인이 어려워서 저절로 TDD 실력이 키워지는 느낌입니다. 그래도 @SpringBootTest 어노테이션에 의존한 TDD를 작성하여 동작이 조금 느리지만 개선의 여지가 있습니다. 토이 프로젝트를 진행하면서 또한 이 방법이 올바른 방식인가 해깔릴 때가 많았습니다. 특히 TDD를 작성할 때 고민이 많았는데 단위 테스트끼리 서로 충돌이 계속 났기 때문입니다. 결국엔 해결하였는데 MvcMock을 생성할 때 @AutoWired 어노테이션을 사용하지 않고 직접 생성자를 통해 생성하는 방식으로 해결하였습니다.

🚀MockMvc 사용방법


    @Autowired
    private MockMvc mockMvc;

처음에 MockMvc를 사용할 때는 위와같이 MockMvc에 Autowired 어노테이션을 붙여서 의존성을 부여했습니다. 해당 방식은 간단했지만 통합 테스트를 할때 각 테스트 끼리 충돌하는 에러가 계속 발생하였습니다. 에러는 "Failed to load ApplicationContext" 라는 에러가 발생하였습니다. 처음에는 에러가 왜 나는지 몰랐지만 MockMvc를 생성하는 다른 방식이 있다는걸 알고 시도해봄으로써 MockMvc를 사용할 때는 @Autowired 어노테이션을 사용하게되면 통합 테스트할 때 오류가 발생한다는 사실을 배웠습니다. 그러면 @Autowired를 사용하지 않고 MockMvc를 사용하는 코드를 공개하겠습니다.

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Autowired
    private Filter springSecurityFilterChain;
    
    @BeforeEach
    public void beforeEach() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac)
                .addFilter(springSecurityFilterChain)
                .build();
    }

위와같은 방식으로 MockMvc를 단위 테스트 이전에 한번씩 초기화 하니 통합 테스트를 하여도 에러가 발생되지 않았습니다. 안되었던 이유는 정확하진 않지만 통합 테스트를 하게되면 테스트를 빠르게 하게되어 MockMvc에 의존성 주입하는 것이 제대로 이루어지지 않았던 것 같습니다.

깔끔한 테스트 결과

테스트는 깔끔하게 되었지만 SpringBootTest 어노테이션을 사용하지 않는 방법을 배워야 할 것 같습니다. 지금 테스트 코드는 많이 느리기 때문에 테스트 코드가 많아진다면 문제가 될 수 있습니다.

조금은 복잡한 QueryDsl 사용해보기


이번에는 myPage를 구현할 차례입니다. myPage에서 보여줄 정보는 아래와 같습니다.

클라이언트에게 전달해야하는 값은 count값과 그리고 place 장소에 관한 값인 것을 확인할 수 있습니다. 하지만, 장소와 더불어 평균 별점과 댓글 수도 가져와 됩니다. 그렇게 구현하기 위해서는 단순히 장소에관한 값만 리턴하는 것이 아니라 review에 있는 값도 리턴해야 되기 때문에 QueryDsl에서 Projections 이라는 것을 사용할 수 있습니다. Projections을 통해 내가 원하는 값들을 지정할 수 있습니다. 그리고 원하는 값을 지정하기 위해서는 dto를 만들어야 됩니다.

@Data
@NoArgsConstructor
public class PlaceQResponseDto {

    private Long id;
    private String name;
    private String address;
    private String img_src;
    private String tag;
    private Long like_count;
    private String phone;
    private String category;
    private String region_1;
    private Double avg;
    private Long review_count;

}

dto를 보면 위에 @Data, @NoArgsConstructor 어노테이션이 붙어있는 것을 확인할 수 있습니다. 여기서 쿼리dsl에서 dto로 사용할려면 @Getter, @NoArgsConstructor 어노테이션이 포함되있어야 됩니다. 저같은 경우 @Data의 어노테이션에 @Getter도 포함되있기 때문에 사용하지 않았습니다.

서브쿼리 사용하기

내가 좋아요를 누른 장소를 특정하기 위해서 where 조건에서 서브쿼리문을 사용하였습니다. 서브쿼리는 성능 저하의 원인이 되기 때문에 가능하면 다음에 리팩토링 해볼 생각입니다.

                .where(place.in(
                                     select(subLove.place)
                                        .from(subLove)
                                        .where(subLove.member.id.eq(memberId))
                        )
                )

where절에서 사용된 서브 쿼리입니다. select 앞에는 JPAExpressions가 생략되어 있습니다. 서브 쿼리를 사용하기 위해서는 별도의 Qtype 클래스가 필요합니다. 서브 쿼리를 해석해보면 멤버id가 같은 장소 id값을 리턴해달라는 의미입니다.

마지막으로 전체 소스코드입니다.

    @Override
    public Page<PlaceQResponseDto> findMyFavoritePlaces(Long memberId,Pageable pageable) {

        QMember member = QMember.member;
        QLove love = QLove.love;
        QLove subLove = new QLove("subLove");
        QPlace place = QPlace.place;
        QReview review = QReview.review;

        QueryResults<PlaceQResponseDto> results = queryFactory
                .select(Projections.fields(
                        PlaceQResponseDto.class,
                        place.id,
                        place.name,
                        place.address,
                        place.image.as("img_src"),
                        place.tag,
                        place.likeCount.as("like_count"),
                        place.phone,
                        place.category,
                        place.region1.as("region_1"),
                        review.reviewRating.avg().as("avg"),
                        review.id.count().as("review_count")
                )).from(review)
                .leftJoin(place)
                .on(review.place.eq(place))
                .where(place.in(
                                     select(subLove.place)
                                        .from(subLove)
                                        .where(subLove.member.id.eq(memberId))
                        )
                )
                .groupBy(place.id, place.name, place.address, place.image, place.tag,place.likeCount)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<PlaceQResponseDto> content = results.getResults();
        long total = results.getTotal();

        return new PageImpl<>(content, pageable, total);
    }

해당 코드가 조금 복잡해보이지만 일반적인 QueryDsl에 페이징이 추가되어서 복잡해보입니다. 페이징과 관련된 부분은 offset, limit인데 시작 행과 끝 행을 지정하여 페이징 처리하는 부분입니다.
해당 쿼리문이 실제로 돌아갈 떄 hibernate가 만들어준 쿼리문입니다.

SELECT place1_.place_id            AS col_0_0_,
       place1_.placename           AS col_1_0_,
       place1_.address             AS col_2_0_,
       place1_.img_src             AS col_3_0_,
       place1_.tag                 AS col_4_0_,
       place1_.like_count          AS col_5_0_,
       place1_.phone               AS col_6_0_,
       place1_.region1             AS col_7_0_,
       Avg(review0_.review_rating) AS col_8_0_,
       Count(review0_.review_id)   AS col_9_0_
FROM   review review0_
       LEFT OUTER JOIN place place1_
                    ON ( review0_.place_id = place1_.place_id )
WHERE  place1_.place_id IN (SELECT love2_.place_id
                            FROM   love love2_
                                   CROSS JOIN place place3_
                            WHERE  love2_.place_id = place3_.place_id
                                   AND love2_.member_id = ?)
GROUP  BY place1_.place_id,
          place1_.placename,
          place1_.address,
          place1_.img_src,
          place1_.tag,
          place1_.like_count;

재밌는 점은 서브 쿼리문에서 CROSS JOIN을 사용하지 않았는데 CROSS JOIN이 적용된 것입니다. 크로스 조인을 하게되면 조인을 한 테이블 끼리의 행의 개수를 곱한 것 만큼 행의 개수가 늘어난다고 합니다.

만났던 에러


tag already exists in the remote" error after recreating the git tag

place수정이라는 release 브랜치를 머지하는중 해당 오류가 발생했습니다. 오류를 보아하니 태그 관련한 오류인 것 같습니다.

https://stackoverflow.com/questions/19298600/tag-already-exists-in-the-remote-error-after-recreating-the-git-tag

스택 오버 플로우 방식을 참조하여 문제를 해결하였습니다.
그 중 제가 선택한 방법은 sourceTree를 사용한 방법입니다.

  1. 소스트리 상단에 태그를 선택합니다.
  2. release 브랜치의 이름과 같은 태그를 선택합니다.
  3. 모든 원격 저장소에서 태그 제거를 선택합니다.

그리고 다시 release 브랜치 병합을 진행하니 문제가 해결 되었습니다.

FirebaseApp name [DEFAULT] already exists!

해당 에러는 모든 테스트 코드를 돌릴 때 발생하였습니다. FirebaseApp이 한번만 호출 되어야되는데 테스트 코드가 여러번 호출되면서 발생하게 되는 것 같습니다.

could not execute statement; SQL [n/a]; constraint ["FKSIU6M9JXQ651MLOGIR3ILVM05: PUBLIC.LOVE FOREIGN KEY(PLACE_ID) REFERENCES PUBLIC.PLACE(PLACE_ID) (1)";

해당 에러는 제약키 관련 에러입니다. 연관관계에 있는 객체를 강제로 삭제할 때 주로 나타나는거 같습니다. 그리고 아래 에러도 동시에 발생한 에러입니다.

Referential integrity constraint violation

결과적으로 원인은 외래키로 참조하고 있는 값이 존재하지 않아서 발생한 오류였습니다. place라는 외래키가 있는데 Id값은 자동 생성이지만 제가 직접 생성하였더니 Repository.save()가 잘 작동하지 않아서 발생한 오류였습니다. id값은 제가 생성한 값을 사용하면 안되고 생성되어진 것을 사용해아 되는 것 같습니다.

Jwt 토큰 인증 방식에서 유저 정보를 받는 방식

Jwt 토큰 인증을 하면서 유저 정보를 받아오는 방식은 아래와 같습니다.

     public Long delete(@PathVariable Long id , Authentication authentication) {
        Member member = (Member)authentication.getPrincipal();

하지만 처음에는 SpringSecurity로 구현하는 일반 로그인 방식처럼 해봤다가 오류가 났었습니다.

public Long delete(@PathVariable Long id, @AuthenticationPrincipal Member member)

세션방식, 토큰방식, 일반 로그인 방식에서 유저 정보를 가져오는 방식은 모두 차이가 있는 것 같습니다.

profile
개발에 재미를 느끼며 꾸준히 성장하는 개발자 김종완 입니다.

0개의 댓글