UMC 1기 Server Session 7주차 워크북

redjen·2021년 11월 19일
0

목차

  1. 학습 목표
  2. 7주차 수업 후기
  3. 실습
  4. 핵심 키워드
  5. 논의해보면 좋은 것들
  6. 참고자료

1. 학습 목표

  1. 인증과 권한 부여(인가)에 대한 지식 습득
  2. 로그인 유지 방법에 대해서 이해 및 적용
  3. DB가 항상 정확하고 일관된 상태를 유지하는 방법

2. 7주차 수업 후기

💡 7주차 수업 듣고 느낀점 이야기, 각자 진행상황 공유

본격적으로 API를 제대로 개발하는 느낌이 났습니다. JWT를 사용한 것은 처음이라 좀 낯설었지만 사용자를 인증할 때 사용하며 이미 만들어 놨던 API들에도 적용을 해야겠다는 생각이 들었습니다. 또 이 포스트를 정리하며 구현했던 코드들을 다시 되짚어 보니 부족한 부분이 많이 보였습니다. 모르는 개념을 대충 넘겨 짚고 넘어가지 않도록 리팩토링해야 하는 API들을 다시 구현해야겠습니다.

3. 실습

20211118 API 명세서

Github 주소


CompanyController에는 숙소 정보에 대한 API들을 구현하였습니다.
좋아요 내림차 순 카테고리 별 숙소 조회와 예약 내림차 순 카테고리 별 숙소 조회는 Paging을 사용하여 구현하였습니다.

    @GetMapping("/list/by-likes")
    @ApiOperation(value="좋아요 내림차 순 카테고리 별 숙소 조회", notes="좋아요 수의 내림차순으로 숙소 목록을 조회한다.")
    public ResponseEntity<Map<String, Object>> searchCompanyByLikes(@RequestParam int listSize, @RequestParam int pageNum) {
        Map<String, Object> resultMap = new HashMap<>();
        List<Company> companyList = companyService.searchCompanyByLikes(listSize, pageNum);
        resultMap.put("data", companyList);
        return new ResponseEntity<>(resultMap, HttpStatus.OK);
    }

offset을 listSize * pageNum으로 주어서 paging이 잘 작동하는 것을 확인할 수 있었습니다.

CouponController에는 쿠폰의 정보와 관리에 대한 API를 구현하였습니다.

SELECT * FROM coupon
INNER JOIN couponavaillist c
ON coupon.couponIdx = c.couponIdx
WHERE startTime < now()
AND now() < endTime
AND c.roomIdx = #{roomIdx};

쿠폰의 등록 날짜를 통해 현재 방 인덱스에 적용 가능한 쿠폰 리스트를 조회하는 쿼리는 상기와 같이 구현했습니다.

    @ResponseBody
    @GetMapping("/list")
    @ApiOperation(value="가용한 쿠폰 리스트 조회", notes="현재 방에 사용 가능한 쿠폰 목록를 조회한다.")
    public ResponseEntity<Map<String, Object>> selectAvailableCouponList (@RequestParam int roomIdx) {
        Map<String, Object> resultMap = new HashMap<>();

        List<Coupon> availList;
        try {
            availList = couponService.selectAvailableCouponList(roomIdx);
        }
        catch (Exception e) {
            e.printStackTrace();
            resultMap.put("resultCode", 4);
            resultMap.put("resultMsg", "데이터베이스 접근 오류");
            return new ResponseEntity<>(resultMap, HttpStatus.OK);
        }

        resultMap.put("resultCode", 0);
        resultMap.put("resultMsg", "사용 가능한 쿠폰 정보 불러오기 성공");
        resultMap.put("data", availList);
        return new ResponseEntity<>(resultMap, HttpStatus.OK);
    }

위의 쿼리는 위와 같이 처리되어서 사용자에게 조회될 수 있도록 구현했습니다.

사용자 정보의 관리에 대한 API는 모두 MemberController에서 구현했습니다.
사용자 로그인, 사용자 정보 업데이트, 좋아요 요청은 모두 jwt를 통해 사용자 인증을 진행했습니다.
구현하는데 가장 재미있었던 API는 숙소의 '좋아요' 요청을 하는 API 입니다.
좋아요 요청 API 코드 Gist Embed가 안되다니!! 😥😥

    @Transactional
    @ResponseBody
    @PostMapping("/like/company/{companyIdx}")
    @ApiOperation(value="사용자 숙소 좋아요 요청", notes="사용자 별 숙소 좋아요 요청 처리, 동일한 요청 재전송 시 좋아요 취소")
    public ResponseEntity<Map<String, Object>> makeLikeToCompany(@PathVariable int companyIdx, @RequestParam int memberIdx) {
        Map<String, Object> resultMap = new HashMap<>();
        try {
            int memberIdxByJwt = jwtService.getUserIdx();
            if (memberIdxByJwt != memberIdx) {
                resultMap.put("resultCode", 4);
                resultMap.put("resultMsg", "jwt 인증 오류");
                return new ResponseEntity<>(resultMap, HttpStatus.OK);
            }
        }
        catch (BaseException exception) {
            resultMap.put("resultCode", 3);
            resultMap.put("resultMsg", "jwt 검증 오류");
            return new ResponseEntity<>(resultMap, HttpStatus.OK);
        }

        try {
            int checkAlreadyLikedCompany = memberService.checkAlreadyLikedCompany(memberIdx, companyIdx);
            if(checkAlreadyLikedCompany == 1) {  // 이미 클릭했던 경우
                int cancelRes = memberService.cancelLikeToCompany(memberIdx, companyIdx);
                if (cancelRes == 1) {
                    resultMap.put("resultCode", 0);
                    resultMap.put("resultMsg", "좋아요 취소 완료");
                }
                else {
                    resultMap.put("resultCode", 5);
                    resultMap.put("resultMsg", "좋아요 취소 실패");
                }
            }
            else {
                int makeRes = memberService.makeLikeToCompany(memberIdx, companyIdx);
                if (makeRes == 1) {
                    resultMap.put("resultCode", 0);
                    resultMap.put("resultMsg", "좋아요 등록 완료");
                }
                else {
                    resultMap.put("resultCode", 5);
                    resultMap.put("resultMsg", "좋아요 등록 실패");
                }
            }
        }
        catch (Exception e) {
            e.printStackTrace();
            resultMap.put("resultCode", 2);
            resultMap.put("resultMsg", "데이터베이스 접근 오류");
            return new ResponseEntity<>(resultMap, HttpStatus.OK);
        }
        return new ResponseEntity<>(resultMap, HttpStatus.OK);
    }

사용자 별 숙소 좋아요 목록이 저장된 likelist 테이블을 먼저 조회하여 (companyIdx, memberIdx) 쌍의 정보가 이미 존재하는 경우에는 좋아요 취소 (DELETE)를 하도록 했고, 존재하지 않는 경우에는 좋아요 (INSERT)를 하도록 했습니다. 아직 쿼리 실력이 부족하여 뭔가 한방 쿼리로 짤 수 있을 것 같아 아쉬웠습니다. @Transactional 어노테이션을 적용해서 구현하여 잡히지 않은 Exception에 대해 Rollback 처리되도록 했지만 역시 Transactional 어노테이션을 잘 사용해 본 경험이 많이 없는 관계로.. 나중에 실력이 더 늘고 이 포스트를 다시 보게 된다면 꼭 한번 리팩토링해보고 싶은 API입니다.

사용자의 후기에 대한 요청을 처리하는 API는 ReviewController에 구현하였습니다. 특별한 점이 없는 그냥 CRUD이기 때문에 그냥 넘어가겠습니다.

방 정보, 예약에 대한 요청을 처리하는 API는 RoomController에 구현하였습니다.
이전 포스트에서도 다뤘던 방 예약 API가 가장 재미있는 부분입니다. 방 예약 API 코드

    @PostMapping("/reserve/make")
    @ApiOperation(value="방 예약", notes="방 예약 정보를 기록한다.")
    public ResponseEntity<Map<String, Object>> makeReservation(@RequestBody HashMap<String, String> paramMap) {
        Map<String, Object> resultMap = new HashMap<>();

        if(paramMap.isEmpty() || !paramMap.containsKey("memberIdx") || !paramMap.containsKey("companyIdx") || !paramMap.containsKey("roomIdx")
                || !paramMap.containsKey("reserveType") || !paramMap.containsKey("reserveStart") || !paramMap.containsKey("reserveEnd")) {
            resultMap.put("resultCode", -1);
            resultMap.put("resultMsg", "유효하지 않은 매개변수입니다.");
            return new ResponseEntity<>(resultMap, HttpStatus.OK);
        }

        int memberIdx = Integer.parseInt(paramMap.get("memberIdx"));
        int couponIdx;

        if(!paramMap.containsKey("couponIdx")) {
            couponIdx = 0;
        }
        else {
            couponIdx = Integer.parseInt(paramMap.get("couponIdx"));
        }

        int companyIdx = Integer.parseInt(paramMap.get("companyIdx"));
        int roomIdx = Integer.parseInt(paramMap.get("roomIdx"));
        boolean reserveType = paramMap.get("reserveType").equals("1");
        String reserveStart = paramMap.get("reserveStart");
        String reserveEnd = paramMap.get("reserveEnd");

        int result;

        if (couponIdx == 0) {
            result = reservationService.makeReservation(memberIdx, companyIdx, roomIdx, reserveType, reserveStart, reserveEnd);
        }
        else {
            result = reservationService.makeReservationWithCoupon(memberIdx, couponIdx, companyIdx,roomIdx,reserveType,reserveStart,reserveEnd);
        }

        if(result == 1) {
            resultMap.put("resultCode", 0);
            resultMap.put("resultMsg", "정상적으로 예약되었습니다.");
        }
        else {
            resultMap.put("resultCode", -1);
            resultMap.put("resultMsg", "정상적으로 예약되었습니다.");
        }
        return new ResponseEntity<>(resultMap, HttpStatus.OK);
    }

지금와서 생각해보니 reservation에 쓰일 객체를 따로 만들어서 @RequestBody 어노테이션을 사용하는 것이 훨씬 깔끔했을 것 같습니다.. 😰
이것도 추후 jwt를 넣어 인증도 추가해야 하니, 리팩토링 대상에 포함되는 컨트롤러입니다!

📝실습 체크리스트

  • API 20개까지 구현하기
  • API Sheet 작성하기
  • JWT 활용하여 해당하는 모든 API 디벨롭하기
  • Paging 처리를 하여 조회 일부 API 디벨롭하기
  • Transaction 활용하여 API를 디벨롭하기

4. 핵심 키워드

  • Stateless(무상태성) : 사용자나 클라이언트의 컨텍스트를 서버에 저장하지 않는다는 것을 나타내는 용어입니다. REST 아키텍쳐는 HTTP 프로토콜을 더 잘 사용하기 위해 고안되었다는 것을 생각해보면 무상태성을 지향하는 HTTP 프로토콜에는 REST 아키텍쳐가 정말 잘 들어맞는다는 것을 다시 한번 느낄 수 있습니다. 사용자가 REST 아키텍쳐를 사용하며 이전의 정보나 현재 통신의 상태를 유지시키기 위해서 등장한 것이 세션과 토큰입니다.

  • Request Header를 활용한 로그인 방식 : HTTP 프로토콜을 사용할 때 사용자를 인증하기 위해서 Authorization Request Header를 사용합니다. 이를 사용하면 요청을 전송한 존재의 데이터 접근 권한과 조작 권한을 확인할 수 있으며 종류에는 Basic Auth / Bearer Token / API Key / Digest Auth / OAuth 2.0 / Hawk Authentication / AWS Signature 등이 있습니다.

  • 쿠키 HTTP 쿠키는 서버가 사용자의 웹 브라우저에 사용자 인증 정보를 저장하는 작은 데이터입니다. 우리가 흔히 볼 수 있는 쿠키에는 사용자가 검색헀던 검색어를 저장해서 자동완성에 사용하는 경우입니다. 다들 쿠키를 삭제했을 때 이런 자동완성 정보가 전부 지워졌었던 경험이 한 번 쯤은 있을 것이라 생각합니다. 쿠키는 클라이언트의 웹 브라우저가 지정하는 메모리 혹은 디스크에 저장됩니다. 보안 상 탈취되기 쉽기 때문에 누군가에게 조작되어도 큰 지장이 없는 덜 중요한 정보들만 저장합니다.

  • 쿠키를 활용한 로그인 방식 : 쿠키를 사용해서 로그인할 때 서버는 응답과 함께 Set-Cookie 헤더를 전송합니다. Set-Cookie 헤더 안에는 만료일 및 지속 시간도 명시할 수 있습니다. 브라우저는 이를 쿠키를 저장하고, 세션이 끝날 때 삭제되거나 헤더와 함께 보내진 생명 주기가 끝날 때 삭제됩니다.

  • 세션 : 세션은 클라이언트가 TCP 연결을 수립한 뒤 요청을 보낸 후 응답을 받는 과정으로 이루어집니다. 웹 브라우저 당 1개씩 생성되어 브라우저 종료 시 소멸되는 것이 특징입니다. 쿠키와는 달리 세션은 서버의 메모리에 저장됩니다.

  • 세션을 활용한 로그인 방식 : 클라이언트가 서버로 요청을 하면 서버는 클라이언트를 식별하기 위한 세션 ID를 생성합니다. 이 세션 ID로 key / value를 저장하는 HttpSession을 생성하고 클라이언트에게 Set-Cookie 값으로 세션 ID를 저장하고 있는 쿠키를 전송합니다. 클라이언트는 서버 측에 요청을 보낼 때 세션 ID를 가지고 있는 쿠키를 전송하고 서버는 쿠키에 포함되어 있는 세션 ID로 HttpSession 정보를 찾습니다. 쿠키에는 세션 ID 정보가 있지만 서버 안에서만 세션 ID에 해당하는 인증 정보가 있기 때문에 세션은 탈취되기 어렵고, 따라서 쿠키보다 더 중요한 정보들을 관리하는데 사용합니다. https://sh77113.tistory.com/243를 참고하였습니다.

  • JWT(Json Web Token) : 전자 서명된 URL-Safe한 JSON입니다. JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http Request Header에 JSON 토큰을 넣은 후 별도의 인증과정 없이 헤더에 포함되어 있는 JWT 정보를 통해 인증하는 것이 특징입니다.

  • OAuth : 서버가 믿을 수 있는 대리인을 사용하여 클라이언트를 인증하는 것입니다. 네이버 / 카카오 / 구글과 같은 OAuth Provider들에게 다음과 같이 요청하는 것이라고 생각합니다.

    "야, 나는 너희 믿는데, 이 사용자 믿을만 한거야? 그럼 난 너희 믿을테니 이 사용자들 인증을 대신해서 해줘. 끝나면 인증이 끝났다는 증거만 넘겨주라"

    현재 사용하는 OAuth2는 정식 버전이 아닙니다. 아직 최종안이 아님에도 불구하고 여러 서비스에서 OAuth2를 사용하는 이유는 OAuth1과 비교했을 때 인증 절차가 간략하다는 장점이 있기 때문이라고 합니다.

  • OAuth 원리와 과정 : 앞서 설명했던 것처럼, OAuth의 인증 과정은 다음과 같이 이루어집니다.

    https://ko.wikipedia.org/wiki/OAuth

  • Paging 처리 : 여러 데이터의 리스트를 조회 / 검색 / 정렬해야 할 때, 사용자에게 갑자기 한꺼번에 여러 개의 데이터를 보여주게 되면 눈에 잘 들어오지 않습니다. 그래서 흔히 사용하는 것이 Paging이라는 기법입니다. 10개씩 데이터를 끊어서 보여준다고 했을 때, 첫 페이지에는 0~9번째의 데이터를 보여주고, 다음 페이지에는 10~19번째의 데이터를 보여주는 식으로 보통 구성을 많이 합니다.

  • Transaction : 한번에 여러 개의 DB 접근을 해야 하는 요청 및 로직들이 있습니다. 결제가 바로 그런 예시 중 하나입니다. 결제를 한다고 하면 결제가 성공했다는 로그도 남기고, 실제로 결제가 일어났는지 확인도 해야 하고, 결제도 해야 하고.. 해야 하는 것들이 많습니다. 그런데 이 일들 중에서 하나라도 오류가 났을 때, 이미 처리했던 것들은 어떡하지?란 생각이 들어서 나온 것이 Transaction입니다. 트랜잭션은 ACID라 불리는 4가지 특징을 가집니다.

  1. 원자성 (Atomicity) : 트랜잭션이 전부 반영되거나, 하나도 반영되지 않아야 합니다.
  2. 일관성 (Consistency) : 진행되는 동안 DB의 변경이 있어도 변경된 DB로 진행되어야 합니다.
  3. 독립성 (Isolation) : 하나의 특정 트랜잭션이 완료될 때까지, 다른 트랜잭션이 그 결과를 참조하거나 영향을 받아서는 안됩니다.
  4. 지속성 (Durability) : 트랜잭션이 성공적으로 완료되었을 때 결과가 영구적으로 반영되어야 합니다.

5. 논의해보면 좋은 것들

  • REST API를 활용한 카카오 로그인 구현하는 방법
  • REST API를 활용한 구글 로그인 구현하는 방법

6. 참고자료

  • 인증과 권한 부여(인가)

NAVER D2

REST API의 이해와 설계-#3 API 보안

  • 쿠키와 세션

쿠키와 세션 개념

쿠키(Cookie)와 세션(Session) & 로그인 동작 방법

  • JWT

JWT.IO

[JWT] 토큰(Token) 기반 인증에 대한 소개

JWT란?

  • OAuth

[OAuth 2.0] OAuth란?

WEB2 - OAuth 2.0 - 생활코딩

  • Paging 처리

페이징(Paging)에 대한 이해 - (1) 페이지 번호를 생성하자

  • Transaction

트랜잭션(Transaction)이란?

  • 카카오 로그인 (OAuth)

Kakao Developers

profile
make maketh install

0개의 댓글