[GDG]홍대 맛집 아카이빙 프로젝트 #17 - 테스트 디버깅

2025. 8. 10. 01:54·팀 프로젝트/[2025][GDG]홍대 맛집 아카이빙 프로젝트

어제 테스트를 돌렸고 오늘은 디버깅을 하면 된다.

우선 한 한 2시간 정도 시간이 있으니 그 때까지 급한 거 몇 개만 개발하기로 한다.

JWT토큰 처리하는 것과 유효성 검사 정도만하면 될 듯하다.

 

public ResponseDTO getTokenFromHeader(String secret, HttpServletRequest request) {
        String token = request.getHeader("AccessToken");

        if (token != null) {
            return checkValidityAndReturnUserRoles(secret, token);
        } else {
            return ResponseDTO.builder()
                    .status(400)
                    .message("헤더에 AccessToken이 필요합니다.")
                    .validity(false)
                    .build();
        }
    }

사실 JWT필터에서 validity 변수가 있기 때문에 true / false만 구분하면 될 듯하다.

false일 경우에는 에러를 터지게하기로 한다. Unauthorized 에러를 터지게하면 되는데 이는 Custom 에러를 만들게하면 된다.

 

package com.hongchelin.exceptions;

public class UnauthorizedException extends RuntimeException {
    public UnauthorizedException() {

    }
}

이렇게 Custom 에러를 만들고

전역Exception으로 message를 담아서 ResponseEntity를 반환하게하면 된다.

public ResponseEntity<StoreForVoteResponseDTO> voteMainService(
            String secret,
            voteRequstDTO voteRequstDTO,
            HttpServletRequest request) throws UnauthorizedException {

        boolean validity = jwtFilter.getTokenFromHeader(secret, request).getValidity();

        if (!validity) {
            throw new UnauthorizedException();
        }

이런식으로 JWT필터에서 validity를 false로 반환한 경우에는 UnauthorizedException을 터지게하면 된다.

 

토큰에 대한 정보가 없는 경우

 

아예 토큰이 전달되지 않은 경우에는 IllegalArgumentException 에러가 터지기 때문에 다른 에러를 터지게해야한다.

토큰이 전달되지 않은경우


여기까지 했는데, 코어타임 일정이 없어졌다. 4명이 모여서 해야하는데 한 분이 아프시다고하셔서 다음주로 연기

다음주는 일본여행 가느라 3일 빠진거 초과근무로 메꿔야하는데 월수를 나갈까 생각 중이다. 그러면 금토를 연속으로 쉴 수 있음 꺄르륵

 

암튼 이왕 이렇게된거 어제 테스트 디버깅만 좀하면 될 듯하다.


1. 아이디 중복확인에서 쿼리 파라미터로 온 Id가 없을 때 오류 터지게하기

 

어제의 문제상황

 

if (id == null || id.isEmpty()) {
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(200)
                    .message("아이디를 정확하게 입력해주세요.")
                    .validity(false)
                    .build();

            return ResponseEntity.ok(responseDTO);
        }

다음의 조건문을 추가

성공 및 정상작동


닉네임중복확인에서 닉네임이 기존 회원가 겹칠 경우 에러메시지를 "동일한 닉네임이 이미 있습니다."로 수정하기

아주쉽구만!

수정함

추가로 validity 속성을 닉네임 중복확인에는 적용시키지 않았길래 적용


3. 회원가입 기능에 유효성검사 추가

테스트하다가 알아낸 건데 NotNull 어노테이션 뿐 아니라 NotBlank 어노테이션도 사용해야하더라

    String nickname;

    @NotNull(message = "아이디를 입력해주세요.")
    @NotBlank(message = "아이디를 입력해주세요")
    String userId;

    @NotNull(message = "비밀번호를 입력해주세요")
    @NotBlank(message = "비밀번호를 입력해주세요")
    @Size(min = 5, message = "비밀번호는 5글자 이상이어야합니다.")
    @Pattern(regexp = ".*[!@#$%^&*(),.?]", message = "특수문자가 반드시 포함되어야 합니다.")
    String password;

    @Email(message = "이메일오류")
    String email;

DTO

        if (nickname == null || nickname.isEmpty()) { //userId 와 password 는 validity 검사 대상임
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(400)
                    .message("닉네임을 입력해주세요")
                    .build();

            return new ResponseEntity<>(responseDTO, HttpStatus.BAD_REQUEST);
        } else if (email == null || email.isEmpty()) {
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(400)
                    .message("이메일 주소를 입력해주세요")
                    .build();

            return new ResponseEntity<>(responseDTO, HttpStatus.BAD_REQUEST);
        }

NotBlank 어노테이션을 통해 공백인 경우에도 유효성검사를 통과할 수 없게 설정하고, 저 DTO를 회원가입 때와 로그인 때 모두 사용하기 때문에 이메일과 닉네임에는 유효성검사를 적용하지 못하고 회원가입 때 따로 로직을 구현해야한다.

원래는 닉네임이나 이메일이 null일 때만 조건에 걸리게하고 에러메시지를 "닉네임 또는 이메일을 입력해주세요" 뭐 이랬지만 이제는 경우를 둘로 나눠서 처리한다.

 

성공


4. 상점 등록에 유효성검사 추가

어제 문제가 된 부분

storeName이 null인데 정상적으로 처리되었고 DB에도 저장된 것을 수정하면 된다.

 

성공적으로 반영된 듯하다.

추가로 중복점포 여부도 검사함. 주소를 기준으로 중복점포를 검사한다.


5. 투표용 가게 등록 DB 정규화와 유효성 검사 추가

DB 정규화를 하려면 좀 손을 많이 봐야하므로 유효성검사부터 하자면

어노테이션만 몇 개 추가해주면 되는지라 간단하게 넘어간다.

 

DB정규화를 해보자면..

 Store store = storeRepository.findByStoreName(storeName).get(0);
                StoreForVote storeForVote = StoreForVote.builder()
                        .storeName(storeName)
                        .storeLocation(location)
                        .storeImg(store.getStoreImg())
                        .storeInfoOneline(store.getStoreInfoOneline())
                        .votedCount(0)
                        .build();

                storeForVoteRepository.save(storeForVote);

                ResponseDTO responseDTO = ResponseDTO.builder()
                        .status(200)
                        .message("성공")
                        .build();

                return ResponseEntity.ok(responseDTO);

이게 별다른 오류없이 정상적인 경우 입력된 상호명과 주소를 받아 이를 DB에서 찾고 정보를 뽑아와서 저장하는 로직이다.

솔직히 메서드로 자동화를 시켜놔서 크게 번거롭지는 않지만 정보를 수정해야할 때 삭제메서드를 수행시키고 다시 정보를 넣어야하기 때문에.. (게다가 투표용DB에 하나, 상점 저장용 DB에 하나씩 수정해야함)

 

그냥 저장하는 인자를 id와 votedCount로만 선정하기만 한다.

그리고 STORE_FOR_VOTE DB테이블에서 PAST_STORE_FOR_VOTE 테이블로 옮기는 과정에서 STORE_FOR_VOTE 테이블에 있는 상호명과 주소를 가지고 STORE DB테이블에 접근했지만 이제는 그냥 ID만 뽑아내서 보내면 된다.

 

그래서 레포지토리랑 몇 개 수정하고 다시 테스트를 돌린다.

이거 문서나 도메인 모델 같은게 있었으면 좋겠다. 아마 곧 만들지 않을까

이렇게 정규화를 했고

어제 만든 Scheduled 어노테이션 로직도 잘 작동한다.


관리자기능 관리자만 받기

투표기능에 대해서는 JWT토큰이 있는지 여부만 검사하기 때문에 회원가입한 사람만이 요청을 보낼 수 있다.

물론 이후 기능에서 JWT토큰에서 유저정보를 받고 DB 조회하는 과정도 있고

 

다만 관리자기능에 대해서는 아직 JWT토큰 분석기능을 넣지 않아 관리자 기능에 접근이 가능하다.

위는 이의 예시인 홍슐랭 투표에 가게 등록하기 요청을 보낸 것

 

우선 관리자 기능 중 하나인 홍슐랭 투표에 가게 등록하기 기능을 좀 수정했다.

테스트를 돌리며 코드 리팩토링도 좀 했다.

String location = storeRegisterInVoteRequestDTO.getStoreLocation();
        String storeName = storeRegisterInVoteRequestDTO.getStoreName();

        Integer countInDB = storeRepository.countByStoreNameAndStoreLocation(storeName, location);
        Integer count = storeForVoteRepository.countByStoreNameAndStoreLocation(storeName, location);

        if (count == 0) {
            if (countInDB == 1) {
                Store store = storeRepository.findByStoreName(storeName).get(0);
                StoreForVote storeForVote = StoreForVote.builder()
                        .storeName(storeName)
                        .storeLocation(location)
                        .storeImg(store.getStoreImg())
                        .storeInfoOneline(store.getStoreInfoOneline())
                        .build();
    
                storeForVoteRepository.save(storeForVote);
    
                ResponseDTO responseDTO = ResponseDTO.builder()
                        .status(200)
                        .message("성공")
                        .build();
    
                return ResponseEntity.ok(responseDTO);
            } else if (countInDB == 0) {
                ResponseDTO responseDTO = ResponseDTO.builder()
                        .status(400)
                        .message("가게 정보가 없습니다.")
                        .build();
    
                return ResponseEntity
                        .status(HttpStatus.BAD_REQUEST)
                        .body(responseDTO);
            } else {
                ResponseDTO responseDTO = ResponseDTO.builder()
                        .status(500)
                        .message("가게 정보가 여러 개입니다. 하나만 남기고 삭제해주세요.")
                        .build();

                return ResponseEntity
                        .status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body(responseDTO);
            }
        } else {
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(500)
                    .message("이미 등록된 가게입니다.")
                    .build();

            return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(responseDTO);
        }
    }
String location = storeRegisterInVoteRequestDTO.getStoreLocation();
        String storeName = storeRegisterInVoteRequestDTO.getStoreName();

        boolean isInDb = storeRepository.existsByStoreNameAndStoreLocation(storeName, location);

        if (!isInDb) {
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(400)
                    .message("가게 정보가 없습니다.")
                    .build();

            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body(responseDTO);
        }

        long storeId = storeRepository.findByStoreNameAndStoreLocation(storeName, location).getId();
        Integer count = storeForVoteRepository.countByStoreId(storeId);

        if (count == 0) {
            Store store = storeRepository.findByStoreNameAndStoreLocation(storeName, location);
            StoreForVote storeForVote = StoreForVote.builder()
                    .storeId(store.getId())
                    .votedCount(0)
                    .build();

            storeForVoteRepository.save(storeForVote);

            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(200)
                    .message("성공")
                    .build();

            return ResponseEntity.ok(responseDTO);

        } else {
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .status(500)
                    .message("이미 등록된 가게입니다.")
                    .build();

            return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(responseDTO);
        }
    }

위에가 리팩토링 이전, 밑에가 이후

로직은 간단하게 입력받은 상호명과 가게주소를 통해서 storeDB에서 ID를 가져오고, 그 ID를 토대로 STORE_FOR_VOTE 테이블에 저장하는 과정이다.

 

사실 STORE 테이블과 STORE_FOR_VOTE 테이블에 있는지를 조사해 이중조건문을 연결하는 것보다 그냥 boolean 변수 하나만 추가해서 true / false로 나누는게 훨씬 나아보인다. 실제로 코드도 더 간결하고? DB 정규화를 통해서 좀 더 간결해지고

 

암튼 관리자 방식은 어려운 것은 아닌데, 방법은 두 가지이다. MEMBER 테이블에 ROLE 컬럼을 추가하던가, MEMBERS_ROLE 테이블을 하나 만들던가인데, 전자가 훨씬 쉬워보인다. 새 테이블을 만들고 레포지토리를 만들고 또 MEMBER 관련 테이블이니까 회원가입이나 로그인 때 다 처리해야하는데 귀찮;

 

    @Builder.Default
    private String role = "User";

이렇게 Role 컬럼을 추가하고 다른 코드를 건드리지 않게 기본값은 User로 설정했다.

 

기존 회원제에서는 JWT 토큰의 여부로 회원 / 비회원 여부를 나누기 때문에 코드 수정은 없을듯하다.

Member adminMember1 = Member.builder()
				.userId("admin")
				.password("admin1234**")
				.nickname("관리자")
				.role("Admin")
				.voteAvailable(false)
				.email("hongchelin422@gmail.com")
				.build();

		memberRepositoryInterface.save(adminMember1);

PostConstruct 메서드에 어드민 계정을 하나 넣어주었다.

 

관리자 전용기능이 투표 가게 등록과 가게 등록 등이 있으므로 그 둘에만 관리자만 접근하도록 기능을 추가하면 됨

        String userId = jwtFilter.getTokenFromHeader(secret, request).getMemberInfo().getIdentifier();
        String role = memberRepositoryInterface.findByUserId(userId).get(0).getRole();

        if (!role.equals("Admin")){
            throw new UnauthorizedException();
        }

이렇게 추가

권한이 없을 때는 아까 CustomException으로 만든 UnauthorizedException 으로 처리한다. 아우편해

 

토큰이 전달되지 않은 경우

String userId = jwtFilter.getTokenFromHeader(secret, request).getMemberInfo().getIdentifier();

이 코드 때문에 JWTFilter에서의 전역Exception 처리가 통하지 않아서 try-catch문으로 처리했다. 왜냐하면 .getMemberInfo() 메서드에서 Null 에러가 터지기 때문

 

일반계정으로 접근

 

관계자 계정으로 접근

 

성공한듯

 

 

store URL 요청에 대해서 완료.

위에가 토큰이 없는 경우, 아래가 일반 유저의 토큰이 있는 경우

 

우선 여기까지

내일 알바가야함ㅜ


내일할거

- 홍대맛집아카이빙프로젝트 16번 게시글 참고

- JWT 토큰 만료 테스트해보기

- 

'팀 프로젝트 > [2025][GDG]홍대 맛집 아카이빙 프로젝트' 카테고리의 다른 글

[GDG] 홍대 맛집 아카이빙 프로젝트 #18 - OAuth와 DB 연동하기  (2) 2025.08.15
[GDG]홍대 맛집 아카이빙 프로젝트 백엔드 개발 #6.2. - Redis  (1) 2025.08.11
[GDG]홍대 맛집 아카이빙 프로젝트 #16 - 테스트 돌리기  (2) 2025.08.09
[GDG]홍대 맛집 아카이빙 프로젝트 #15 - 투표기능 마무리도전하기  (4) 2025.08.09
[GDG]홍대 맛집 아카이빙 프로젝트 #14 - 기타 투표기능 개발  (4) 2025.08.08
'팀 프로젝트/[2025][GDG]홍대 맛집 아카이빙 프로젝트' 카테고리의 다른 글
  • [GDG] 홍대 맛집 아카이빙 프로젝트 #18 - OAuth와 DB 연동하기
  • [GDG]홍대 맛집 아카이빙 프로젝트 백엔드 개발 #6.2. - Redis
  • [GDG]홍대 맛집 아카이빙 프로젝트 #16 - 테스트 돌리기
  • [GDG]홍대 맛집 아카이빙 프로젝트 #15 - 투표기능 마무리도전하기
_cortisol_
_cortisol_
개발을 합니다.
  • _cortisol_
    Cortisol
    _cortisol_
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
_cortisol_
[GDG]홍대 맛집 아카이빙 프로젝트 #17 - 테스트 디버깅
상단으로

티스토리툴바