JPA_Template 파일 분석 3

변현섭·2023년 6월 14일
0

5. Member Controller "Read" 요청


뼈대에 대한 설명은 주석으로 대체하고, getMembers()와 getMembersByNickName메서드에 대해서만 알아보자.

1) findMembers

단순히 Member테이블에 있는 모든 member를 리스트 형태로 가져오는 명령이다.

2) members.stream()

① members.stream()

  • members 리스트를 스트림으로 변환한다. 스트림을 사용하면 연속적인 데이터 처리를 할 수 있다.

② .map(member -> new GetMemberRes(member.getId(), member.getNickName(), member.getEmail(), member.getPassword()))

  • 각 멤버 객체를 GetMemberRes 객체로 매핑합니다. map() 메서드는 스트림의 각 요소에 대해 지정된 함수를 적용하여 새로운 요소로 매핑하는 역할을 한다.
  • 여기서는 GetMemberRes 객체를 생성하고, 해당 멤버의 속성 값을 사용하여 초기화한다.

③ collect(Collectors.toList())

  • 매핑된 GetMemberRes 객체들을 리스트로 수집한다.
  • collect() 메소드는 스트림의 요소들을 수집하는 역할을 한다.
  • Collectors.toList()는 스트림 요소를 리스트로 수집하는 컬렉터이다.

3) getMembersByNickname



닉네임은 멤버 간의 중복을 허용할 수도 있고 불허할 수도 있다. 만약 닉네임 중복이 없는 경우라면 굳이 List로 반환할 필요는 없을 것이다.

다만, 컨트롤러에서 RequestParam으로 닉네임을 입력 받은 경우에는 해당 닉네임의 멤버를 반환하고, RequestParam을 입력 받지 않은 경우에는 모든 멤버의 리스트를 출력해야 하므로, 두 경우에 대해 반환타입을 맞춰주기 위해 List로 선언한 것이다. 당연히 닉네임 중복이 없다하더라도 List로 반환하는 것 자체가 문제가 되지는 않는다.

6. Member Controller "update" 요청


이 로직은 클라이언트로부터 email과 nickName을 입력받아 클라이언트의 이메일을 수정한다. 먼저 입력받은 이메일과 일치하는 멤버를 찾아주자. 그 후 jwtService의 getUserIdx() 메서드로 user의 인덱스를 가져오자.

1) getJwt() 메서드


① HttpServletRequest request

  • HttpServletRequest 타입의 변수를 선언하여 HTTP 요청을 저장한다.
  • 이 변수는 현재 요청 객체를 가져오기 위해 RequestContextHolder.currentRequestAttributes()를 호출하고, 그 결과를 ServletRequestAttributes로 형변환하고 있다.
  • 이렇게 함으로써 현재 스레드의 HTTP 요청 객체를 얻을 수 있다.

② request.getHeader("Authorization")

  • HTTP 요청 헤더에서 "Authorization" 헤더 값을 가져온다.
  • JWT는 일반적으로 "Authorization" 헤더에 포함시키는 경우가 많다.

따라서, 이 코드는 현재 HTTP 요청에서 "Authorization" 헤더 값을 추출하여 JWT를 반환하는 기능을 수행한다.

2) EMPTY_JWT

만약 이 jwt 값이 null이라면, jwt가 http 요청의 헤더에 입력되지 않은 것이므로 예외를 호출한다.

3) JWT Parsing

JWT를 성공적으로 추출했다면, JWT를 검증하고 parsing 해야 한다.

※ 파싱(Parsing)
주어진 데이터를 해석하고 구조화하는 과정을 말한다. 데이터를 파싱한다는 것은 원시 데이터를 의미있는 부분으로 분해하고, 그 부분들을 의미 있는 방식으로 해석하는 것을 의미한다.

① Jws<Claims> claims

  • Java 웹 토큰 (Java Web Token) 라이브러리인 Jws를 사용하여 선언된 변수이다.
  • 즉, 서명된 페이로드를 담는 변수 claims를 선언한다는 의미이다.

② try문

  • Jwts.parser()로 파싱한다.
  • setSigningKey로 JWT의 서명 키를 설정한다.
  • parseClaimsJws를 호출하여 claims 변수에 accessToken의 파싱 결과를 저장한다.
  • JWT의 서명이 유효하지 않거나 파싱에 실패하면 예외가 호출된다.

③ claims.getBody().get()

  • claims 객체를 통해 JWT의 내용에 접근하여 memberId를 추출한다.
  • JWT의 본문(Claims)에 get() 메서드를 사용하여 "memberId"를 키로 하는 값을 가져온다.

④ return

  • 이 값은 Long 타입으로 반환된다.
  • 이렇게 추출한 memberId가 사용자의 고유 식별자인 UserIdx로, getUserIdx() 메서드의 반환 값이 된다.

한마디로 JWT를 파싱하여 검증하고, JWT의 본문(Claims)에서 memberId를 추출하여 사용자의 고유 식별자를 반환하는 코드인 것이다. 이메일로 데이터베이스에서 조회한 멤버의 Id 값과 현재 요청 헤더에 있는 jwt에서 추출한 Id 값이 일치하는지 확인 후 결과에 따라 예외를 호출하거나 유저 네임 변경을 시도할 것이다.

JWT를 확인하는 로직은 매우 중요하고 자주 쓰이기 때문에 반드시 이해하고 넘어가야 한다. 이해가 잘 안된 사람들을 위해 아래의 예시를 적어놓겠다. 이해가 된 사람들은 다음으로 넘어가도 된다.

chrome이라는 사람이 있고, kin이라는 사람이 있다고 하자. 그리고 kin이 chrome의 이메일을 알고 있다고 하자. 이 때, kin이라는 사람이 chrome의 이메일을 가지고 chrome의 닉네임을 hyunseop으로 마음대로 변경하려고 한다. 이 요청이 처리될 수 있을까?

당연히 처리되지 않는다. 그 이유가 뭘까? kin이라는 사람이 patch 요청을 서버로 보내게 되면, 서버에서는 kin이 보낸 HTTP 요청 헤더의 jwt 값에서 kin의 id를 추출할 것이다. 그리고 DB에서 입력받은 이메일에 해당하는 멤버를 찾아 id를 가져와 두 값을 비교한다.

요청 헤더에서 추출된 id의 값은 kin의 것이고, DB에서 조회된 id의 값은 chrome의 것이기 때문에 요청이 처리되지 않는 것이다. 즉, 본인만이 본인의 닉네임을 수정할 수 있게 되는 것이다.

JWT를 확인하는 코드를 추가해주지 않을 경우, 위와 같이 kin이 chrome의 닉네임을 마음대로 바꾸는 것이 가능해진다.

4) modifyUserName() 메서드

① @Transactional

  • 트랜잭션과 관련한 어노테이션이다.
  • 트랜잭션은 일련의 작업을 하나의 단위로 처리한다는 의미를 담고 있어 일련의 작업 중 어느 하나라도 실패한다면, 그 작업이 아예 실행되지 않도록 만드는 것이다.
  • 주로 수정 및 삭제 작업에 사용되는 어노테이션이다.
  • PatchMemberReq에는 Id와 nickName이 있다.

② getReferenceById()

  • 특이한 점은 findById가 아닌 getReferenceById를 사용한다는 점이다.
  • getReferenceById는 JPQL로 직접 정의한 메서드가 아니라 JPA에서 제공하는 메서드이다.
  • findById는 식별자(여기서는 Id)에 해당하는 객체를 직접 로딩하여 반환하는 반면, getReferenceById는 식별자에 해당하는 프록시 객체를 반환한다.
  • 프록시 객체는 실제 데이터를 로딩하지 않고, 객체의 실제 필드에 대한 액세스가 필요한 시점까지 객체를 로딩하지 않는다. 이를 Lazy Loading이라 한다. 이는 실제 필드에 대한 액세스가 필요하지 않은 경우에 대해 불필요한 데이터베이스 쿼리를 방지할 수 있어 성능 상의 이점이 있다.
  • 위와 같이 객체를 조작하는 정도의 로직은 굳이 필드에 직접 액세스하지 않더라도 프록시 객체를 사용하여 업데이트 할 수 있다. (즉, 성능의 최적화를 위해 사용한 것일 뿐 findById를 사용해도 동작에는 무리가 없다.)

5) updateNickName()

updateNickName메서드는 Member 클래스에 정의되어 있다.

왜 MemberService에 정의하지 않았는지 궁금해할 수도 있을 것이다. 만약 MemberService에 별도로 updateNickName을 정의했다면, 그 메서드 내부에서 다시 해당 멤버를 찾거나, 멤버를 넣어주어야 하는데, 이러한 불편한 점을 개선한 것으로 볼 수 있다. 물론, 이것은 개발자의 선택이니, 필수 사항은 아니다. 다만 코드의 가독성을 높이기 위한 방법일 뿐이다.

일련의 과정이 마치고 나면 회원정보가 수정되었다는 결과와 함께 데이터베이스에서 변경된 닉네임을 확인할 수 있게 될 것이다.

7. Member Controller의 "Delete" 요청


이 API는 클라이언트에게 email과 password를 받아서 최종적으로 멤버를 delete하는 요청이다.

1) DeleteMemberReq


쿼리스트링으로 전달된 이메일과 패스워드를 DTO의 생성자 입력으로 넣어주자.

2) deleteMember


① findMemberByEmail() 메서드

  • 말 그대로 이메일에 해당하는 멤버를 리턴한다. 이메일은 중복이 불가하므로 항상 한명 또는 null이 반환될 것이다.

② List<Board> boards

  • 삭제하려고 하는 멤버가 작성한 게시글이 있는 경우, 삭제를 하지 못하도록 만들기 위해 생성한 리스트이다.
  • 현재 Board 테이블은 멤버의 Id를 외래키로 갖고 있기 때문에 게시글이 있는 멤버는 삭제하는게 불가능하다.
  • 따라서 게시글을 먼저 지워야 멤버가 삭제될 수 있도록 처리해준 것이다.
  • 물론 멤버 삭제를 요청하면 게시글도 함께 지워버리도록 구현할 수도 있을 것이다. 이것은 개발자의 취향이다.
  • 멤버와 게시글의 관계는 일대다이므로 findBoardByMemberId의 반환타입은 List여야 한다.

삭제하려는 멤버가 작성한 게시글이 없을 때에만, 멤버를 삭제하는 쿼리가 동작한다.

사실은 멤버삭제 또한 닉네임 변경 API처럼, 멤버의 JWT 토큰을 헤더의 Authorization 필드에 입력했을 때에만 동작하게 만들어야 한다. 이것은 비단 삭제 뿐 아니라, 로그인이 필요한 모든 요청에 대해 헤더의 Authorization 필드의 JWT 값을 확인해야 한다.
다만, 이번 포스팅에서는 중복된 코드를 계속 적을 필요는 없다보니 생략한 것뿐이다. 실제로는 JWT를 확인하는 로직이 매번 추가되어야 한다.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글