[GDG] 홍대 맛집 아카이빙 프로젝트 #19 - OAuth 되살리기 및 정규화 보강하기

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

ㅎㅇ

오늘은 어제 하던 것에 이어서 하기로 했다.

 

우선 어제는 OAuth 로그인 기능을 '로그인'과 '기존 회원과의 DB연결을 위한 로그인'으로 나눴고, 그 과정에서 'OAuth를 이용한 소셜로그인'이라는 기능은 같았기 때문에 로그인 메서드는 공통된 메서드 하나만을 이용했다.

 

따라서 '로그인'과 '기존 회원과의 DB연결을 위한 로그인'을 구분하기 위해 클래스 정적변수를 하나 만들어서 명확한 구분이 필요할 때마다 이를 조회하고 분기를 타는 방식으로 사용했다.

 

다만 이런 정적변수 방식은 서로 다른 사용자가 정적변수에 접근하면 값이 변경되어서 서로 영향을 끼칠 수 있다고하고, 또 이를 막기 위해 ThreadLocal 방식을 사용해도 로그인과 로그인 성공 후 이동되는 클래스의 엔드포인트가 달라서 정적변수가 제대로 전달되지 않고 끊긴다.

 

솔직히 이 부분에 대해서는 쿠키로 설정할까하다가 OAuth 요청 url에 쿼리 파라미터를 추가할 수 있는 줄 알았으나 OAuth config에서 더욱 복잡한 설정을 해야하기 때문에 그냥 쿠키로 결정했다.

 

    public ResponseEntity<?> googlelogin(HttpServletResponse response) {
            String url = "/oauth2/authorization/google";
            setStateInUrl(response);

        return ResponseEntity
                .status(HttpStatus.FOUND)
                .location(URI.create(url))
                .build();
    }

    public ResponseEntity<?> naverlogin(HttpServletResponse response) {

            String url = "/oauth2/authorization/naver";
            setStateInUrl(response);

        return ResponseEntity
                .status(HttpStatus.FOUND)
                .location(URI.create(url))
                .build();
    }

    public ResponseEntity<?> kakaologin(HttpServletResponse response) {
            String url = "/oauth2/authorization/kakao";
            setStateInUrl(response);

            return ResponseEntity
                    .status(HttpStatus.FOUND)
                    .location(URI.create(url))
                    .build();
    }

우선 실제로 로그인하는 기능들은 여기서 진행되고

 

public ResponseEntity<?> loginMethodSorting(int oauthprovider,
                                                String secret,
                                                HttpServletRequest request,
                                                HttpServletResponse response) {
        setSessionJWT(secret, request, response);

        switch (oauthprovider) {
            case 1:
                return googlelogin(response);
            case 2:
                return naverlogin(response);
            case 3:
                return kakaologin(response);
            default:
                ResponseDTO responseDTO = ResponseDTO.builder()
                        .status(400)
                        .message("잘못된 authprovider가 부여되었습니다.")
                        .build();

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

위 메서드들을 호출하는 메서드. 여기가 거의 본체에 해당한다. 컨트롤러의 호출을 받아서 메서드를 호출하고, 쿠키도 이것저것 설정하고

 

'로그인' 기능과 '회원연결' 기능 모두 두 메서드는 공통으로 공유한다.

 

    public void setStateInUrl(HttpServletResponse response) {
        Cookie cookie = new Cookie("mode", "login");
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setMaxAge(3600);
        response.addCookie(cookie);

        System.out.println("로그인");
    }

'로그인' 기능에서의 쿠키설정

'회원연결' 기능에서는 쿠키의 value값만 "register"로 변경한채로 이 메서드를 오버라이드 했다.

    @Override
    public void setSessionJWT(String secret,
                               HttpServletRequest request,
                               HttpServletResponse response) {

        String token = jwtFilter.getTokenFromHeader(secret, request).getAccessToken();

        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        response.addCookie(cookie);

        System.out.println("쿠키저장완료!");
    }

'회원연결' 기능에서는 그 회원이 누군지 잠깐동안 알고있어야하기 때문에 헤더에서 온 JWT토큰을 그대로 쿠키에 저장한다. '로그인' 기능에서는 그냥 추상메서드로 선언되어 있는 코드다.


쿠키

잠깐 쿠키에 대해서 한 번 알아보면 좋을 것 같아서 바로 위의 코드를 예시로 들어보면

        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        response.addCookie(cookie);

new Cookie 를 통해 새 쿠키객체를 불러오고, key-value 형식으로 저장할 값의 이름-값을 저장한다.

.setPath 메서드는 인자로 준 URL의 하위경로에 쿠키를 공유한다. /를 포함해서 "/login", "/singup" 이렇게

 

setHttpOnly 메서드와 .setSecure 메서드는 쿠키와 관련된 보안에 관련된것인데, 이 블로그를 참고하면 좋을 것 같다.

https://nsinc.tistory.com/121

 

[Web] HTTP Only와 Secure Cookie 이해하기

Cookie에 대한 이해 쿠키는 ASP.NET, PHP와 같은 특정 기술영역에 국한된 것도 아니고, 특정 Client나 Server에만 국한된 기술도 아닙니다. 쿠키는 수십 년 전부터 사용되어 왔으며 최근에는 HTTP에 있어서

nsinc.tistory.com

 

.setHttpOnly 메서드는 true로 설정할 시 HttpOnly 쿠키로 저장된다. 

false로 설정되면 JS에서 쿠키에 접근이 가능한데, JS로 쿠키를 가로채는 XSS 공격에 취약해지는 것을 방지하기 위해서 브라우저에서 쿠키에 접근하는 것을 막는다.

 

.setSecure 메서드는 true로 설정하면, HTTPS 프로토콜을 사용하는 웹사이트에서만 쿠키를 저장하도록 설정한다. HTTP 프로토콜을 사용하면 네트워크 감청 등의 문제가 생길 수 있어 HTTP 프로토콜에서 보안이 더 강화된 프로토콜이 HTTPS 프로토콜이다. 다만 로컬호스트는 HTTP 프로토콜을 사용하므로 false로 설정해야한다.

 

이후에 addCookie를 하면 되는데, response는 HttpServletResponse의 인스턴스다.

이외에도 .setMaxAge 도 있는데, 쿠키의 만료시간을 설정한다. 단위는 초 / 0은 즉시삭제, -1은 브라우저 종료 시 삭제


 

Cookie[] list = request.getCookies();
                            

for (Cookie cookie : list) {
	if ("mode".equals(cookie.getName())) {
    		mode = cookie.getValue();
        	found = true;
        	break;
     }
}
                            
if (!found) {
	throw new UnauthorizedException();
}

쿠키를 가져올 때는 HttpServletRequest 객체를 이용해서 가져와야한다. 가져온 쿠키는 배열의 형태로 저장되며, 쿠키를 저장할 때 설정한 이름을 가지고 쿠키들을 순회하면서 값이 맞는지 아닌지 정해야한다.

 

추가로 cokie.getName().equals("mode")에서 if-else문을 사용하면 로 하면 배열에서 첫 번째 요소만 검사하고 바로 else 루트를 타버리므로 쿠키에 저장한 값이 여러개라면 if문만 사용해야한다. 이건 너무 간단한 상식인데 왜 몰랐지


테스트

암튼 테스트를 돌려보도록 한다. 이 부분이 되게 오래 걸렸네.. 우선 두 기능이 로그인 기능이 필요하므로 상속으로 이것저것하다가 코드도 엄청 꼬이고

그런데.. 알고보니 loginMethodSorting 클래스에서 setSessionJWT 토큰을 호출하는데, 이 메서드는 동적 바인딩으로 인해 무조건 자식메서드로 호출된다고한다... 아 헤드퍼스트자바에서 읽은 것 같았는데 한 번 더 읽어야겠다.

 

암튼 이 동적바인딩 때문에 부모클래스에서 부모메서드를 호출하는게 어렵다고..

 

오늘의 교훈 -> is-a 관계라고 무조건 상속시키지 말자. 동적 바인딩 때문에 부모클래스가 자식클래스에세 끌려다닌다. 코드 짜기 전에 준비 코드 같은 기획을 좀 해보고 들어가자. 

 

public ResponseEntity<?> loginMethodSorting(int oauthprovider,
                                                HttpServletResponse response,
                                                String mode) {
        setCookie(mode, response);
        switch (oauthprovider) {
            case 1:
                return googlelogin(response);
            case 2:
                return naverlogin(response);
            case 3:
                return kakaologin(response);
            default:
                ResponseDTO responseDTO = ResponseDTO.builder()
                        .status(400)
                        .message("잘못된 authprovider가 부여되었습니다.")
                        .build();

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

    public ResponseEntity<?> googlelogin(HttpServletResponse response) {
            String url = "/oauth2/authorization/google";

        return ResponseEntity
                .status(HttpStatus.FOUND)
                .location(URI.create(url))
                .build();
    }

    public ResponseEntity<?> naverlogin(HttpServletResponse response) {

            String url = "/oauth2/authorization/naver";
        return ResponseEntity
                .status(HttpStatus.FOUND)
                .location(URI.create(url))
                .build();
    }

    public ResponseEntity<?> kakaologin(HttpServletResponse response) {
            String url = "/oauth2/authorization/kakao";

            return ResponseEntity
                    .status(HttpStatus.FOUND)
                    .location(URI.create(url))
                    .build();
    }

    public void setCookie(String mode, HttpServletResponse response) {
        Cookie cookie = new Cookie("mode", mode);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setMaxAge(3600);
        response.addCookie(cookie);

        System.out.println("로그인");
    }

그래서 코드수정을 또 해줬다.

쿠키는 어차피 공통으로 필요하니 mode 값만 매개변수로 받아서 세팅하는 것으로 해뒀고,

public ResponseEntity<?> registerOAuthService(int oauthprovider,
                                                  String secret,
                                                  HttpServletRequest request,
                                                  HttpServletResponse response) {
        String mode = "register";
        setSessionJWT(secret, request, response);
        return loginService.loginMethodSorting(oauthprovider, response, mode);
    }

    public void setSessionJWT(String secret,
                               HttpServletRequest request,
                               HttpServletResponse response) {

        String token = jwtFilter.getTokenFromHeader(secret, request).getAccessToken();

        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        response.addCookie(cookie);

        System.out.println("쿠키저장완료!");
    }

회원연결 기능도 상속관계가 아니라 인스턴스로 호출하는 관계로 재설정해주었다.

 

나한테 왜이래

 

/api/login/1 이런식으로 접근했을 때 위의 사진처럼 api/login/success로 이동하면 성공한 것이다.

로그인하면 로그인정보를 토대로 DB를 뒤져야하는데 DB에 값이 없으므로 Null 에러가 터져서 저렇게 오류가 난거고

 

/signup/1로 접근하면 이렇게 /signup/socialAccounts로 이동하는게 맞다.

회원정보를 찾아야하는데 JWT토큰을 넣어주지 않았기 때문에 저런 화면이 나오는 것이다. JWT토큰이 헤더에 없을 때 오류처리는 이미 했는데 Null에러는 전역Exception 처리를 안했기 때문에 차이가 있는 것

 

드디어 문제를 해결했구만... 어렵지만 그래도 은근히 배운 것도 써먹고 괜찮은 경험이었다.

 

자꾸 이러길래 Security Config에서 formlogin을 disable했다. oauth2Login이 알아서 하겠지

 

그리고 테스트하려는데 이게 헤더에 접근해서 토큰을 넣어주고 해야하는거라 진짜 테스트는 못한다. 다만 토큰을 꺼내서 쿠키에 저장하는 것까지는 가능하다. 여기까지는 POSTMAN으로 처리할 수 있으니..

 

쿠키에 mode값과 token값까지는 저장할 수 있다. 이건 API 연결 후에 테스트해보기로한다.


투표 DB 정규화해결하기

홍대 맛집 아카이빙 프로젝트 #17에서 STORE_FOR_VOTE 테이블에 정규화를 진행했는데..

 

문제는 현재 투표결과를 보려고하면

이렇게 가게정보가 나오긴하는데 가게 아이디가 나와버린다.

목록을 출력해야할 때는 STORE 테이블에서 가게정보를 가져와야하고, 테이블에는 정규화를 위해서 아이디와 득표수만 지정해야한다. (위의 STORE_FOR_VOTE 테이블을 형태를 유지해야함)

 

이를 당연히 지금처럼 STORE_FOR_VOTE의 요소들을 순회하면서 하나씩 출력하는 것은 안되고, 출력용 DTO 하나를 만든 다음에 저 STORE_FOR_VOTE 요소들의 ID를 가져와서 STORE 테이블을 조회해 정보를 빼내고 DTO에 담아서 최종적으로는 DTO를 반환하면 된다.

 

어제 알바가서 이 문제를 깨닫고 이 흐름을 좀 짰다. (4번에서 Member 가 아니라 Store다)

STORE_FOR_VOTE 들이 있는 리스트를 스트림에 넣고 DTO로 바꾼다. 스트림 Map 메서드를 써볼 예정이다. 때마침 스트림도 배워서 연습해볼 예정이다. (헤드퍼스트자바 #8 - 12장. 람다와 스트림 차고)

 

STORE_FOR_VOTE 를 출력용 DTO로 바꾸는 기능을 요구하는 클래스가 한 개가 아니라 여러 개이기 때문에 메서드를 구현하는게 아니라 컨버터 클래스를 하나 만들고 인스턴스를 여러 번 호출하기로 한다.

 

컨버터에 필요한 것은 STORE_FOR_VOTE와 PAST_STORE_FOR_VOTE 를 DTO로 바꾸는 컨버터 메서드를 구현해야한다.

 

DTO를 구성하고...

 

    public ResponseEntity<StoreResponseEntityDTO> convert(List<StoreForVote> storeForVotes) {

        Iterable<StoreResponseDTO> storesList = storeForVotes.stream()
                .map(storeForVote -> {
                        Store store = storeRepository.findById(storeForVote.getStoreId());

                        if (store == null) {
                            throw new UnauthorizedException();
                        }

                        return StoreResponseDTO.builder()
                            .storeId(store.getId())
                            .storeName(store.getStoreName())
                            .storeLocation(store.getStoreLocation())
                            .storeInfoOneline(store.getStoreInfoOneline())
                            .storeImg(store.getStoreImg())
                            .votedCount(storeForVote.getVotedCount())
                            .build();
                })
                .sorted((s1, s2) -> s2.getVotedCount() - s1.getVotedCount())
                .collect(Collectors.toList());

        StoreResponseEntityDTO storeResponseEntityDTO = StoreResponseEntityDTO.builder()
                .status(200)
                .message("성공")
                .stores(storesList)
                .build();
        
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(storeResponseEntityDTO);
    }
}

컨버터 클래스

헤드퍼스트자바에서 배운 스트림을 이용해봤다. 원래 storesList는 List 객체였는데 Iterable 객체로 바꿨다. 레포지토리에서 값을 긁어오면 Iterable 객체를 반환하기 때문에 Iterable 타입으로 변환했다.

 

    public ResponseEntity<StoreResponseEntityDTO> voteResult () {
        List<StoreForVote> voteList = getStoresForVote();

        return storeConverterService.convert(voteList);
    }

    public List<StoreForVote> getStoresForVote() {
        return storeForVoteRepository.findAll();
    }

기존 코드 수정

 

성공

 

                if (!member.isVoteAvailable()) { //이미 투표함. 투표 불가능
                    List<StoreForVote> voteList = getStoresForVote();

                    StoreForVoteResponseDTO storeForVoteResponseDTO = StoreForVoteResponseDTO.builder()
                            .status(200)
                            .message("성공(이미 투표한 사용자)")
                            .storeForVote(storeConverterService.convert(voteList).getBody())
                            .voteAvailable(false)
                            .build();

                    return ResponseEntity.ok(storeForVoteResponseDTO);
                } else {
                    List<StoreForVote> voteList = getStoresForVote();

                    StoreForVoteResponseDTO storeForVoteResponseDTO = StoreForVoteResponseDTO.builder()
                            .status(200)
                            .message("성공(투표가능한 사용자")
                            .storeForVote(storeConverterService.convert(voteList).getBody())
                            .voteAvailable(true)
                            .build();

                    return ResponseEntity.ok((storeForVoteResponseDTO));

이렇게 투표 기능에 GET 요청을 보내면 후보리스트를 반환하는 기능도 추가

참고로 storeConverterService.convert(voteList) 메서드는 ResponseEntity를 반환하기 때문에 getBody를 통해서 DTO만 가져오도록 했다.

 

성공

storeForVote에 status와 message도 같이 들어가서 그것도 수정해줬다.

getBody를 했는데 Iterable이 와버려서;; List에서 Iterable로 수정했다.

 

이정도로 바꾸고 나서 기존 기능들을 테스트 했는데, 딱히 이상하게 진행되지는 않았다.


투표 시 아이디 확인하기

위에서나 다른 GDG 포스팅 등에서 나왔지만 /votes URL로 body에 투표할 가게들의 아이디를 votedIdList를 담아 전해주는데, 이 아이디가 'STORE_FOR_VOTE' 테이블에 등록된 가게의 아이디인지, STORE 테이블에 등록된 아이디인지를 명확히 정하지 않고 넘어갔다.

 

그래서! STORE 테이블에 등록된 아이디라고 정했다. 문제는 과거의 나는 그 반대로 개발했다는거

storeId 값은 STORE 테이블에 저장된 아이디값이다. 그렇기 때문에 STORE 테이블의 아이디를 기준으로 투표에 등록해야한다.

 

했고, 성공했다. 딱히 사진은 안 올릴거임 왜냐하면 귀찮아


내일 할 거

계속 개발만 해서 약간 쉬어갈 예정 (GDG 프로젝트는 오늘 여기서 끝!)

- customException을 UnauthorizedException 하나로만 처리 중인데 (개발 편의상) 이제 customException을 여러 개로 만들어주기

- 테스트 계속 하기

- 로또 사기

- 암튼 생각나면 또 적을예정

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

[GDG] 홍대 맛집 아카이빙 프로젝트 #21 - 카카오지도 API 짧게  (2) 2025.08.18
[GDG] 홍대 맛집 아카이빙 프로젝트 #20 - mySQL과 각종 코드 보강하기  (6) 2025.08.18
[GDG] 홍대 맛집 아카이빙 프로젝트 #18 - OAuth와 DB 연동하기  (2) 2025.08.15
[GDG]홍대 맛집 아카이빙 프로젝트 백엔드 개발 #6.2. - Redis  (1) 2025.08.11
[GDG]홍대 맛집 아카이빙 프로젝트 #17 - 테스트 디버깅  (3) 2025.08.10
'팀 프로젝트/[2025][GDG]홍대 맛집 아카이빙 프로젝트' 카테고리의 다른 글
  • [GDG] 홍대 맛집 아카이빙 프로젝트 #21 - 카카오지도 API 짧게
  • [GDG] 홍대 맛집 아카이빙 프로젝트 #20 - mySQL과 각종 코드 보강하기
  • [GDG] 홍대 맛집 아카이빙 프로젝트 #18 - OAuth와 DB 연동하기
  • [GDG]홍대 맛집 아카이빙 프로젝트 백엔드 개발 #6.2. - Redis
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (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)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

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

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[GDG] 홍대 맛집 아카이빙 프로젝트 #19 - OAuth 되살리기 및 정규화 보강하기
상단으로

티스토리툴바