JPA_Template 파일 분석 2

변현섭·2023년 6월 13일
0

8) try-catch문

try-catch문의 try에서 MemberService 클래스의 createMember 함수의 반환 값을 리턴하고 있다. 당연히 MemberService 클래스의 createMember 함수의 반환 값은 PostMemberRes여야한다.

정상적으로 멤버가 create되면 성공 시에 사용되는 생성자를 호출하겠지만, 어떠한 문제가 생겨 예외가 발생하면, catch문으로 이동해 실패 시에 사용되는 생성자가 호출될 것이다.

catch문의 내용은 이미 설명한 바 있으니, try문에 대해 자세히 설명하면서 간간히 예외사항에 대해 언급하도록 하겠다. MemberService 클래스의 createMember 메서드는 아래와 같이 정의되어 있다.

반환 타입이 PostMemberRes임을 확인 가능하다. 또한 입력 파라미터도 컨트롤러에서 요청 본문에 입력 받은 데이터를 그대로 사용하고 있다. 만약 createMember 로직을 실행하다가 예외가 발생하면 BaseException이라는 예외클래스를 던진다.

이 예외클래스를 처리해야 하는 대상은 createMember 메서드를 호출한 쪽이다. 즉, controller의 catch문이다. 이제 어떤 상황에서 예외가 발생할 수 있는지 차근차근 살펴보도록 하자.

① POST_USERS_EXISTS_EMAIL

  • 처음 등장하는 if문에서 PostMemberReq의 이메일 값을 findByEmailCount 함수의 인자로 전달하고, 만약 이것이 1 이상이면, POST_USERS_EXISTS_EMAIL 생성자가 호출된다. findByEmailCount는 아래와 같이 정의되었다.
  • MemberRepository에는 선언 전 어노테이션이 없다. 엄밀히 말하면 @Repository 어노테이션이 생략된 것이다. JpaRepository를 상속받는 경우, @Repository어노테이션을 사용할 필요가 없어지고, 구현체 클래스도 필요 없어진다.
  • 또한 JpaRepository에 제네릭스를 적용하는 경우, 반드시 엔티티 클래스의 타입과 식별자의 타입 순으로 입력해야 한다.
  • @Query 어노테이션은 JPQL쿼리를 정의하는 데 필요한 어노테이션이다.

    ※ JPQL
    간단히 말해 JPA에서 제공하는 SQL로, SQL과 유사한 문법을 갖는다. 다만, JPQL은 엔티티 객체를 대상으로 쿼리를 날리고, SQL은 데이터베이스 테이블을 대상으로 쿼리를 날린다는 차이가 있다.

  • 쿼리의 끝에 :email이라는 부분이 있는데, 이를 약칭으로 콜론변수라 하자. @Param 어노테이션이 붙은 변수(String email의 email)를 @Param의 인자("email"의 email)와 동일한 이름의 콜론변수로 넘긴다.
  • 쿼리의 실행결과는 count라는 집계함수를 사용하고 있으므로, 쿼리의 결과를 반환하는 findByEmailCount 메서드의 리턴타입은 Integer라는 wrapper class가 되어야 할 것이다.
  • 처음부터 다시 정리하자면, 생성하려는 사용자로부터 받은 postMemberReq에서 이메일을 가져와 기존 멤버의 이메일과 겹치는지 확인하고 겹친다면(같은 이메일을 갖는 멤버의 수가 1 이상이라면) 예외상황이 발생한다. 클라이언트에게는 POST_USERS_EXISTS_EMAIL 필드에 해당하는 속성 값이 결과로 전달될 것이다.

② PASSWORD_ENCRYPTION_ERROR

  • if문을 무사히 통과했다고 하면, 비밀번호를 암호화하는 로직을 수행한다.

  • AES128 알고리즘을 이용하여 비밀번호를 암호화하므로 AES128의 생성자를 호출하기 위해 new 키워드를 사용해주었다.

  • new키워드를 사용하면 생성자가 호출되면서 객체도 생성된다. 즉, 닷 오퍼레이터를 사용해 메서드를 호출할 수 있다. AES128의 생성자는 아래와 같이 정의되었다.

  • AES128 생성자는 비밀키로 사용될 값을 입력 받는다. 비밀 키인 USER_INFO_PASSWORD_KEY는 Secret 클래스의 필드이다.

  • 이는 16진수 형식의 문자열로 표현되며 길이는 64이다.

  • AES128 생성자 코드에 대해 구체적인 설명은 어려우니, 이러한 의미를 가지고 있다는 정도로만 이해하고 넘어가자.

    • byte[] keyBytes = new byte[16];
      16바이트 크기의 keyBytes 배열을 선언한다. 이 배열은 암호화에 사용될 키를 저장하는데 사용됩니다.
    • byte[] b = key.getBytes(UTF_8);
      주어진 키를 UTF-8 문자열로 변환하여 b 배열에 저장한다. 이를 통해 입력된 키를 바이트 배열로 변환한다.
    • System.arraycopy(b, 0, keyBytes, 0, keyBytes.length);
      b 배열의 내용을 keyBytes 배열로 복사한다. keyBytes 배열은 16바이트의 길이를 가지며, b 배열의 내용 중 처음 16바이트만 복사된다. 이를 통해 keyBytes 배열은 주어진 키의 바이트 표현의 처음 16바이트를 가지게 된다.
    • SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
      keyBytes 배열을 사용하여 SecretKeySpec 객체를 생성한다. SecretKeySpec은 암호화 알고리즘에 필요한 키를 지정하는 데 사용되며, 여기서는 "AES" 알고리즘에 사용될 키를 생성한다.
    • this.ips = key.substring(0, 16);
      key 문자열에서 처음 16글자를 추출하여 ips 변수에 저장한다. 이는 추후 암호화 작업에서 사용될 초기화 벡터(Initialization Vector)로 사용될 것이다.
    • this.keySpec = keySpec;
      생성된 SecretKeySpec 객체를 keySpec 멤버 변수에 저장한다. 이 keySpec은 암호화 작업에서 실제로 사용되는 키를 나타낸다.
  • encrypt 메서드도 마찬가지로 이러한 내용이 있다는 정도로만 알고 넘어가기로 하자.

    • NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException
      암호화 및 복호화 작업에 필요한 예외처리 목록이다. 자세한 설명은 생략하기로 하겠다. 이러한 예외처리를 해주어야한다는 정도로 이해하고 넘어가자.
    • Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      "AES/CBC/PKCS5Padding" 암호화 알고리즘을 사용하기 위해 Cipher 객체를 생성한다. 이 알고리즘은 AES 암호화를 사용하고, CBC (Cipher Block Chaining) 모드와 PKCS5Padding 패딩을 적용한다.
    • cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(ips.getBytes()));
      Cipher 객체를 암호화 모드로 초기화한다. 이때 암호화에 사용될 키는 생성자에서 전달받은 keySpec 객체를 사용하며, 초기화 벡터(IV)로는 ips 변수에 저장된 값의 바이트 표현을 사용한다.
    • byte[] encrypted = cipher.doFinal(value.getBytes(UTF_8));
      주어진 값을 UTF-8 문자열로 변환한 후, Cipher 객체를 사용하여 암호화한다. 결과는 encrypted 바이트 배열에 저장된다. 암호화 작업은 doFinal 메서드를 통해 수행되며, 암호화 결과는 바이트 배열로 반환된다.
    • return new String(Base64.getEncoder().encode(encrypted));
      암호화된 바이트 배열을 Base64 인코딩하여 문자열로 변환한 후 반환한다. Base64.getEncoder().encode() 메서드는 바이트 배열을 Base64(64진법) 형식으로 인코딩하고, new String() 생성자를 사용하여 인코딩된 바이트 배열을 문자열로 변환한다.
  • 다시 본론으로 돌아와서, 생성하려는 사용자의 password를 가져와서 encrypt를 시도한 후 성공하면 pwd 변수에 값을 저장하고, 실패하면(위의 예외 중 하나가 발생하면) catch문으로 이동할 것이다.

  • catch 문에 사용된 Exception ignored는 어떤 예외가 발생했는지에 관심이 없다는 의미로 사용되었다. 어떠한 예외가 발생했던 간에 예외처리문이 동일할 때 사용할 수 있다. 어떤 예외가 발생하든지 항상 status가 PASSWORD_ENCRYPTION_ERROR인 BaseException을 throw로 발생시킨다.

  • 위에서도 한번 설명한 바 있듯 이 예외는 이 함수를 호출한 controller에서 처리한다. 컨트롤러에서는 PASSWORD_ENCRYPTION_ERROR의 속성 값을 BaseResponse에 담아 클라이언트에게 보낼 것이다.

③ DATABASE_ERROR

  • 비밀번호 암호화까지 성공했다고 하자. 이제 member 객체를 생성하여 postMemberReq에서 받은 이메일과 닉네임, 방금 암호화한 pwd를 인자로 Member 클래스의 createMember 함수를 호출한다.
  • createMember 메서드는 인자로 받은 값을 각각 객체의 email과 nickName, password로 set한다. 그리고 이 멤버를 DB에 save해주면 PostMemberRes가 반환되면서 메서드가 종료된다.
  • 만약 DB에 save하는 과정에서 어떠한 문제가 생겼을 경우 DATABASE_ERROR를 status로 갖는 BaseException을 컨트롤러에게 떠넘긴다.

위 과정이 예외 발생 없이 잘 통과된다면 새로운 멤버가 성공적으로 create되어 DB에 저장된다. create API를 실행한 결과는 아래와 같다.

데이터베이스에도 정상적으로 결과가 입력된다. (refresh를 눌러줘야 결과를 확인할 수 있다.)

만약 create에 문제가 생긴다면 아래와 같은 결과가 나올 것이다. 같은 요청을 한 번 더 입력하여 중복 이메일 예외를 발생시켜보자.

4. Member Controller "log-in" 요청


RestAPI를 설계할 때에는 스네이크 케이스나 카멜 케이스를 사용하지 않는다. 분간이 쉬운 하이픈을 사용한다. loginMember 메서드의 반환타입은 PostLoginRes를 result로 갖는 BaseResponse이다.

파라미터는 http요청의 본문(body)에서 JSON형식으로 클라이언트에게 PostMemverReq를 전달 받는다. PostMemberReq 클래스는 아래와 같이 정의되었다.

이메일이 정규표현식을 만족한다면 memberService의 login 메서드를 호출한다.

1) MEMBER_NOT_FOUND

데이터베이스에 등록된 이메일과 일치하는 멤버를 member 객체에 저장한다. 만약 사용자가 등록되지 않은 이메일로 로그인을 실행하는 경우 예외를 발생시켜주기 위한 간단한 로직을 추가해보자.

BaseResponseStatus에도 아래의 코드를 추가한다.

이를 Postman에서 테스트해보면 아래와 같은 결과가 나온다.

2) PASSWORD_DECRYPTION_ERROR

로그인을 시도하는 이메일에 해당하는 멤버의 암호화된 비밀번호를 가져와 복호화를 시도한다. decrypt메서드는 아래와 같이 정의되었다.

코드에 대한 자세한 설명은 생략하기로 한다. 복호화 과정에서 어떤 예외가 발생했는지와 무관하게 항상 PASSWORD_DECRYPTION_ERROR를 status로 갖는 BaseException 생성자를 호출하므로 exception class를 ignore하고 있다.

3) JWT

만약, 복호화된 패스워드와 클라이언트가 입력한 패스워드가 일치한다면 jwtService의 createJwt메서드를 호출한다.

JWT에 대해서 간단히 설명하자면, Json Web Token의 줄임말로 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다. 이는 토큰 기반 인증시스템의 일종이다. 토큰 기반 인증시스템은 클라이언트가 서버에 접속했을 때 서버에서 해당 클라이언트에게 인증되었다는 의미로 토큰을 부여하는 방식이다.

이 토큰은 유일하며, 클라이언트는 서버에 요청을 보낼 때 발급 받은 토큰을 요청 헤더에 담아 보내야 한다. 서버는 이 토큰을 확인하여 인증 과정을 처리한다. JWT의 경우 JWT를 HTTP 헤더에 담아 클라이언트를 식별한다.

① Date

  • Date는 현재 날짜와 시간을 나타내는 자료형이다. 일반적으로 Date보다는 LoacalDate, LocaleDateTime, LocalTime 사용이 권장된다.
  • 다만, 아래의 Jwts.builder()에서 setIssuedAt()과 setExpiration()에서 Date를 인자로 받기 때문에 만약 now를 LocalDateTime으로 선언한다면 이를 다시 Date로 바꿔주어야 한다. 따라서 여기서는 Date 자료형을 사용하였다.

② Jwts.builder()

  • builder를 호출해 객체를 생성한다.

③ .setHeaderParam("type", "jwt")

  • 헤더 파라미터를 설정한다. type은 JWT의 유형을, jwt는 JWT 헤더에 입력될 내용을 의미한다.

④ .claim("memberId", memberId)

  • 클레임(claim)을 설정한다. 클레임은 JWT에 포함될 정보를 의미한다.
  • 이 경우 "memberId"라는 이름의 클레임에 memberId 값을 설정한다.
  • 클레임은 JWT의 Payload에 들어가는 내용이다.

⑤ .setIssuedAt(now)

  • JWT의 발행 시간을 설정한다.
  • 여기서는 now 변수에 저장된 현재 시간을 사용한다.

⑥ .compact()

  • 최종적으로 JWT를 문자열로 변환한다.
  • 변환된 문자열이 createJwt 메소드의 결과로 반환된다.

즉, 이 코드는 입력된 memberId를 포함하고, 현재 시간과 만료 시간을 설정하며, 비밀 키를 사용하여 서명된 JWT를 생성하여 반환하는 함수이다.
생성한 jwt를 jwt변수에 할당하고 멤버의 id와 jwt값을 PostLoginRes에 담아 반환한다.

4) FAILED_TO_LOGIN

만약, 클라이언트가 입력한 비밀번호(PostLoginReq의 password 값)가 이메일을 통해 find한 멤버의 password 값과 다르다면 status가 FAILED_TO_LOGIN인 BaseException을 호출한다.

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

0개의 댓글