try-catch문의 try에서 MemberService 클래스의 createMember 함수의 반환 값을 리턴하고 있다. 당연히 MemberService 클래스의 createMember 함수의 반환 값은 PostMemberRes여야한다.
정상적으로 멤버가 create되면 성공 시에 사용되는 생성자를 호출하겠지만, 어떠한 문제가 생겨 예외가 발생하면, catch문으로 이동해 실패 시에 사용되는 생성자가 호출될 것이다.
catch문의 내용은 이미 설명한 바 있으니, try문에 대해 자세히 설명하면서 간간히 예외사항에 대해 언급하도록 하겠다. MemberService 클래스의 createMember 메서드는 아래와 같이 정의되어 있다.
반환 타입이 PostMemberRes임을 확인 가능하다. 또한 입력 파라미터도 컨트롤러에서 요청 본문에 입력 받은 데이터를 그대로 사용하고 있다. 만약 createMember 로직을 실행하다가 예외가 발생하면 BaseException이라는 예외클래스를 던진다.
이 예외클래스를 처리해야 하는 대상은 createMember 메서드를 호출한 쪽이다. 즉, controller의 catch문이다. 이제 어떤 상황에서 예외가 발생할 수 있는지 차근차근 살펴보도록 하자.
① POST_USERS_EXISTS_EMAIL
※ JPQL
간단히 말해 JPA에서 제공하는 SQL로, SQL과 유사한 문법을 갖는다. 다만, JPQL은 엔티티 객체를 대상으로 쿼리를 날리고, SQL은 데이터베이스 테이블을 대상으로 쿼리를 날린다는 차이가 있다.
② 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
위 과정이 예외 발생 없이 잘 통과된다면 새로운 멤버가 성공적으로 create되어 DB에 저장된다. create API를 실행한 결과는 아래와 같다.
데이터베이스에도 정상적으로 결과가 입력된다. (refresh를 눌러줘야 결과를 확인할 수 있다.)
만약 create에 문제가 생긴다면 아래와 같은 결과가 나올 것이다. 같은 요청을 한 번 더 입력하여 중복 이메일 예외를 발생시켜보자.
RestAPI를 설계할 때에는 스네이크 케이스나 카멜 케이스를 사용하지 않는다. 분간이 쉬운 하이픈을 사용한다. loginMember 메서드의 반환타입은 PostLoginRes를 result로 갖는 BaseResponse이다.
파라미터는 http요청의 본문(body)에서 JSON형식으로 클라이언트에게 PostMemverReq를 전달 받는다. PostMemberReq 클래스는 아래와 같이 정의되었다.
이메일이 정규표현식을 만족한다면 memberService의 login 메서드를 호출한다.
데이터베이스에 등록된 이메일과 일치하는 멤버를 member 객체에 저장한다. 만약 사용자가 등록되지 않은 이메일로 로그인을 실행하는 경우 예외를 발생시켜주기 위한 간단한 로직을 추가해보자.
BaseResponseStatus에도 아래의 코드를 추가한다.
이를 Postman에서 테스트해보면 아래와 같은 결과가 나온다.
로그인을 시도하는 이메일에 해당하는 멤버의 암호화된 비밀번호를 가져와 복호화를 시도한다. decrypt메서드는 아래와 같이 정의되었다.
코드에 대한 자세한 설명은 생략하기로 한다. 복호화 과정에서 어떤 예외가 발생했는지와 무관하게 항상 PASSWORD_DECRYPTION_ERROR를 status로 갖는 BaseException 생성자를 호출하므로 exception class를 ignore하고 있다.
만약, 복호화된 패스워드와 클라이언트가 입력한 패스워드가 일치한다면 jwtService의 createJwt메서드를 호출한다.
JWT에 대해서 간단히 설명하자면, Json Web Token의 줄임말로 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다. 이는 토큰 기반 인증시스템의 일종이다. 토큰 기반 인증시스템은 클라이언트가 서버에 접속했을 때 서버에서 해당 클라이언트에게 인증되었다는 의미로 토큰을 부여하는 방식이다.
이 토큰은 유일하며, 클라이언트는 서버에 요청을 보낼 때 발급 받은 토큰을 요청 헤더에 담아 보내야 한다. 서버는 이 토큰을 확인하여 인증 과정을 처리한다. JWT의 경우 JWT를 HTTP 헤더에 담아 클라이언트를 식별한다.
① Date
② Jwts.builder()
③ .setHeaderParam("type", "jwt")
④ .claim("memberId", memberId)
⑤ .setIssuedAt(now)
⑥ .compact()
즉, 이 코드는 입력된 memberId를 포함하고, 현재 시간과 만료 시간을 설정하며, 비밀 키를 사용하여 서명된 JWT를 생성하여 반환하는 함수이다.
생성한 jwt를 jwt변수에 할당하고 멤버의 id와 jwt값을 PostLoginRes에 담아 반환한다.
만약, 클라이언트가 입력한 비밀번호(PostLoginReq의 password 값)가 이메일을 통해 find한 멤버의 password 값과 다르다면 status가 FAILED_TO_LOGIN인 BaseException을 호출한다.