중복 ID는 어떻게 검사할까? (24.05.13)

YJ·2024년 5월 13일
post-thumbnail

블로그 작성법
목표 > 공부한 내용 > 얻었고, 앞으로 이걸 해봐야지 적기

✋ 수업 목표

  • Jackson Library를 통해 FrontEnd와 BackEnd의 Naming을 맞춰보자.
  • @JsonNaming, @JsonProperty 어노테이션을 사용해보자.
  • ID 중복 검사를 해보자.

🤔 공부한 내용

Jackson LIbrary

개발을 진행하면서,
FrontEnd와 BackEnd 사이에 Naming Rule을 맞춰야하는 경우가 발생할 것이다.
다음과 같은 상황이 있다고 가정해보자.

Java 세상에서는 camelCase로 변수명을 작성하는데, 프론트에서 snake_case로 RequestBody를 요청하면 어떻게 처리해줘야 할까?

위 문제를 해결하기 위해
Jackson Library의 두 어노테이션인
@JsonProperty와 @JsonNaming을 사용해보았다.

@JsonProperty

  • 카멜 케이스(Camel Case)
    첫 글자는 소문자로, 중간 글자들은 대문자로 시작하는 표기법
    ex) memberId, memberName, memberPhoneNumber
  • 스네이크 케이스(Snake Case)
    첫 글자는 소문자로, 중간 글자들은 언더바(_)가 포함되는 표기법
    ex) member_id, member_name, member_phone_number
    userId 로 요청한 데이터는 user_id 파라미터로 저장이 되지 않는 것을 확인할 수 있다.
  • 이유는 요청한 key 이름(user_id)와 응답 객체(userId)의 파라미터 명이 다르기 때문에 발생한 문제이다.

@JsonProperty 사용 예시

package com.example.shoppingmall.Member;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
//@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Member {
    @JsonProperty("user_id")
    private String userId;
    private String pw;
    private String name;
    private String email;
    private String contact;

    @Override
    public String toString() {
        return "Member{" +
                "userId='" + userId + '\'' +
                ", pw='" + pw + '\'' +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", contact='" + contact + '\'' +
                '}';
    }
}

위 사진에서와 같이 이번에는 userId 까지 정상적으로 파싱 되어 전달된 것을 확인할 수 있었다.
그러나, 1~2개 정도인 경우에는 해당 어노테이션을 사용하여 해결할 수 있겠지만 10~20개 이상이 된다면 필드 하나하나 추가하는 것은 코드도 길어지고 유지보수 면에서도 번거로운 작업이 된다.
이럴 때에 사용하는 것이 @JsonNaming 어노테이션이다.

@JsonNaming

위 같은 상황을 한번 테스트해보았다.

비즈니스 로직은 다음과 같다.
1. 프론트에서 회원가입에 대한 정보를 camelCase 필드명으로 RequestBody에 JSON을 담아 요청한다.

  1. Spring에서 해당 JSON 데이터를 받는 타입은 다음과 같다.

  2. localhost:8080/join 요청으로 위 RequestBody 데이터를 스프링에서 요청받는 Controller를 작성한다.

  3. Controller에서 @RequestBody를 Member 타입의 변수로 담고, 이를 저장하는 요청을 Service로 보낸다. 그리고, Service에서는 다음과 같은 코드로 Repository에 요청한다.

  4. 현재 프로젝트에서는 DB를 사용하지 않아, Repository 클래스에서 FrontEnd로 부터 전달받은 RequestBody를 Map에 저장한 후, Service로 반환한다. Service는 다시 Controller로 응답을 반환한다.
    현재 코드에서는 회원가입을 Map에 put으로 저장하고, get을 통해 저장된 Member 객체를 조회하여 반환하도록 구성하였다.

위 코드로 @JsonNaming 어노테이션을 적용하기 전에 테스트를 진행해본 결과는 다음과 같다.

  1. FrontEnd - camelCase, BackEnd - camelCase

두 부분에서 동일하게 camelCase로 네이밍할 경우에는 요청에 대해 올바른 처리가 된 것을 확인할 수 있다.

  1. FrontEnd - snake_case, BackEnd - camelCase

FrontEnd에서 Request Body로 snake_case 형식으로 요청할 경우 (user_id) 그에 대한 처리가 되지 않은 것을 확인할 수 있다.
("userId": null) 반환

이러한 문제를 해결하기 위해 등장한 것이
Jaskson Library이다.
위 라이브러리는 JSON <-> Java Object 간에 Mapping을 편하게 시켜주는 라이브러리이다.

이제 Jackson Library의 @JsonNaming 어노테이션을 추가한 후,
위에 에러 케이스를 테스트 해보겠다.
추가한 부분은 DTO 클래스인 Member 클래스에 해당 어노테이션을 추가하였다.

위 어노테이션을 추가 후 테스트를 진행해보니
다음과 같이
회원가입 로직이 이루어진 이후에, DTO의 userId 필드명이 user_id로 변경 되어 응답이 이루어 진 것을 확인할 수 있었다.

@JsonProperty vs @JsonNaming

위 어노테이션을 테스트 해본 결과 두 어노테이션 다 원하는 기능을 수행할 수는 있다.
그러면, 차이점은 뭘까?
@JsonProperty는 필드에 key를 입력하여 적용할 수 있다.
@JsonNaming은 클래스에 적용할 수 있다.
만약에 필드가 100개 이런식이면 어떻게 해야할까?
일일히 @JsonProperty를 작성하는 것보다 @JsonNaming을 적용하는 것이 편할 것 이다.

참고자료
https://cbw1030.tistory.com/315
https://stir.tistory.com/387

ID 중복 검사

생각해볼 것
1) FrontEnd에서 중복 검사를 어떻게 진행하는 거지? DB에서 ID를 긁어서 중복 검사하는 것이 아닌가?

2) 회원가입 시킬 때 백엔드에서는 중복 체크 안해도 될까? 그러면 DB 무결성은 누가 책임지지?

  • 비밀번호 암호화 (로그인) 부분은 FE vs BE 어디서 담당해야 할까?

FrontEnd 딴에서
ajax + jquery를 사용하여 중복 검사를 진행한다.
화면의 리렌더링 없이 서버에 요청을 진행한다.

ajax 사용 방법
https://www.tcpschool.com/ajax/ajax_jquery_ajax

https://velog.io/@skarb4788/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%B9%84%EB%8F%99%EA%B8%B0AJAX

위를 통해 1번 질문은 다음과 같이 해결된다.
1) FrontEnd에서 중복 검사를 어떻게 진행하는 거지? DB에서 ID를 긁어서 중복 검사하는 것이 아닌가?
=> ajax + jquery를 통해 중복 검사를 진행한다.

그러면, 2번 질문이 궁금하다.
프론트에서 ajax + jquery로 중복검사를 진행하는데, 그러면 백에서 굳이 또 회원가입 요청이 오면 중복검사를 진행할 필요가 있을까?

내 생각
프론트에서 중복 체크를 진행하였을 때는
중복 ID가 아니였을 수 있으나,
나머지 정보를 입력하고 회원가입을 진행할 때 다른 유저가 동일한 ID로 먼저 회원가입을 진행할 가능성이 있기 때문이다.

웹의 안정성

웹은 24시간 열려있으며, 보안상 위험이 도사리고 있다.

웹 사이트 해킹 방법
1. SQL 인젝션
악의적인 명령어 주입 공격 방법
2. ASS
악성 스크립트를 삽입하여 실행하면 정보를 가져가는 방법
3. CSRF
해커가 의도한 행위를 특정 웹사이트에 요청하는 공격 방법

참고 자료
https://songacoding.tistory.com/126

그러면, 위 웹의 취약점 악성 행위를 예방하기 위해서는
위 3가지 문제들을 예방하는 것이 필요하다.

ID 중복 체크 로직 구현

  1. 중복 체크 해줘 API
  2. 회원가입 요청 API가 오면 중복체크 로직 추가 구현
  • 중복에 걸리면.. if 분기!
  • 사용자 정의 예외 클래스
  • throws
  • throw: 임의로 터트리고 싶을때
  • 사용자 정의 throw

준비물 1: throws 사용 방법

ID 중복 체크 로직을 구현하기 위해
throws에 대한 개념을 살펴보았다.

간단한 MBTI 테스트 코드로 throws를 구현해보았다.

package exercise.exception;

import java.util.InputMismatchException;
import java.util.Scanner;

public class MbtiThrowsEx {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        try {
            System.out.println("=== MBTI 검사를 시작합니다.===");
            // logic
            checkEorI(sc);
            System.out.println("=== MBTI 검사가 종료되었습니다.===");
            if(sc != null) {
                sc.close();
            }
        } catch(InputMismatchException e) {
            System.out.println("키보드 입력이 잘못 되었습니다.");
        }
    }

    private static void checkEorI(Scanner sc) throws InputMismatchException {
        // 1. 나는 밖에서 에너지를 얻는다.
        // 2. 나는 혼자 있을 때 에너지를 얻는다.
        System.out.println("1. 나는 밖에서 에너지를 얻는다.");
        System.out.println("2. 나는 혼자 있을 때 에너지를 얻는다.");
        System.out.print("당신의 선택은? ");
        // 1. E
        // 2. I
        // 만약 1a 처럼 잘못 입력하면, 예외!

        int answer = sc.nextInt();
        if(answer == 1) {
            System.out.println("E입니다.");
        } else if(answer == 2) {
            System.out.println("I입니다.");
        } else {
            System.out.println("1이나 2를 입력하여야합니다.");
        }
    }
}

여기서 배운거는 다음과 같다.
1. sc.nextInt()의 에러 발생
Scanner 클래스의 nextInt() 클래스는
입력값이 정수일 경우에는 nextInt()로 올바른 정수 값을 반환하나
문자열이 속할 경우에는 InputMisMatchException 에러를 던진다.

  1. 메서드에서 발생한 에러를 main 메서드로 던지기
    아래 코드에서 보이는 것 처럼
private static void checkEorI(scanner sc) throws InputMismatchException

메서드 parameter 뒤에 메서드 내에서
InputMismatchException가 발생하면 에러를 던진다.
라는 것을 명시함으로써
main 메서드에서 try - catch 문을 통해 해당 에러를 잡을 수 있다.

위 코드의 문제점
InputMismatchException이 발생하면 sc.close가 실행되지 않는다.

해결 방법

try {

} catch() {

} finally {
	if(sc != null) {
    	sc.close();
    }
}

finally는 try 부분에서 error가 발생하여도, catch문 실행 후에 finally 안에 있는 코드는 실행되는 것을 보증한다.

참고자료
자바 throws
Scanner nextInt()

준비물 2: 사용자 정의 예외 클래스

0~100 사이의 값이 아니면, 예외를 처리하고자 한다.

package exercise.exception;

import java.util.Scanner;

public class AgeThrowCheckerEx {
    public static void main(String[] args) {

        // 숫자 입력
        // 당신의 나이는 몇 살 이시네요. 반갑습니다.
        while(true) {
            try {
                Scanner sc = new Scanner(System.in);
                System.out.print("나이를 입력하세요. 범위는 (0~100): ");
                int age = sc.nextInt();
                if (age == -1) {
                    break;
                }
                // 우리 로직의 예외
                if (age < -1 || age > 100) {
                    throw new Exception("0~100 사이로 입력해주세요.");
//                System.out.println("0~100사이로 입력해주세요.");
//                    break;
                }
                System.out.println(String.format("당신의 나이는 %d 살이네요. 반갑습니다.", age));
//            sc.close();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

throw new Exception("")
위 코드를 조건문에 넣은 후, 해당 조건에 맞을 경우에
사용자 예외 클래스로 throw하여 catch 문에서 처리하도록 하였다.

출력 메서드

System.out.println()
e.getMessage() -> 사용자에게도 알려주려고 합니다.
log.info()

위 메서드를 통해 두 가지 출력을 할 수 있다.
1. Exception 처리 후, System.out.println()
2. Exception 처리 후, log.info()

어느 상황에서 위 방법들을 사용할까?

힌트: 다 콘솔창에 찍히나? 예외가 터져도 사용자는 몰라도 되나?

Spring의 MVC에 대해 생각해보자.
Controller - Service - Repository
예외는 주로 사용자가 발생시킨다.
그러면, 사용자가 예외가 발생한 것을 알아야한다.

생각해보자.
log를 찍으면 사용자는 예외가 발생한 것을 모른다.
그러므로, 예외는 사용자가에게 알려주기 위해 System.out.println()
으로 즉, FE에 알려줘야 한다.

이 말은 예외 메세지를 화면(사용자)에게 알려줘야 한다.
예외는 우리만 알면 안된다.

log만 찍는 경우는 언제인가?
1. DB가 못 받는다고 팅겨낼 경우
2. 시스템 내부

답은 다음과 같다.
1) 사용자의 입력에 따른 예외 -> 사용자에게 알려줘야
"컨트롤러'에서 처리해야 한다.

2) 시스템 내부 예외
내부적으로 log
이 경우에 사용자에게는 안 알려줄 것 인가?
=> 사용자에게 알려줄 것 이다. 서버 딴에서 에러가 났다. 이 정도로 알려 준다.

사용자 정의 예외 클래스

중복으로 다시 돌아와 보자.
만약에 사용자로 부터 중복된 ID를 받았을때,
Service까지 중복된 ID를 전달할 필요가 있을까?

사용자 정의 예외 클래스를 사용하여
Controller에서 예외를 발생시켜보자.

Controller에서 예외를 터뜨릴 때 2가지 방법이 있을 것 이다.
1. 사용자 예외 클래스에서 직접 응답하기
2. 예외만 발생시키고 method에서 응답하기

ID 중복 체크 로직
1. POST /join 회원가입 메서드에서 Request Body의 id 값으로 Service로 중복 검사 요청을 보낸다.
2. Service에서 전달 받은 id를 통해 Repository로 id가 있는지 조회한다.
3. id가 Repository의 map에 존재하면 true를 반환, 없으면 false를 반환한다.
4. true를 전달받으면, 응답코드 409를 반환한다.
5. false를 전달받으면 회원가입을 시도한다.
6. 회원가입을 시도한 후, 결과값이 null이면 응답 코드 500을 반환, 아니면 응답 코드 201을 반환하여 성공을 알린다.

ID 중복 체크
중복이면, 사용자 예외 클래스를 소환한다.
1) 예외 처리 주체가 예외 클래스
= 예외 클래스한테 니가 return 해
2) 예외 처리 주체

  • checked Exception: extends Exception
  • unChecked Exception: extends RuntimeException

Q. 예외 클래스를 광범위하게 예외를 잡아주는게 좋을까요?
아니면 구체적으로 예외를 잡아주는게 좋을까요?
=> 구체적으로 예외를 잡는 것이 좋다.

Q. 컴포넌트의 범위를 잡는 기준이 무엇일까요?

  • ResponseEntity => 표준화

1) 공통 설계 기반 필드 고려: 클래스
2) 회원 가입: 201, 409 반환 Res 표준클래스 활용
cf. 프로그래머스 주문 API 과제 테스트

Response 형식 만들기

Spring Boot에 Utils에서 Response의 형식을 만들어 놓고 사용한다는 것을 배웠다.

나는 다음 자료를 참고하여 Response 형식을 만들었다.

성공

{
    "success": true,
    "code": 0,
    "message": "Ok",
    "data": [
        1,
        2,
        3
    ]
}

실패

{
    "success": false,
    "code": 10001,
    "message": "Validation error - Reason why it isn't valid"
}

참고 자료
https://velog.io/@leeeeeyeon/Spring-boot-Response-%ED%98%95%EC%8B%9D-%EB%A7%8C%EB%93%A4%EA%B8%B0

😉 앞으로 이걸 해봐야지

오늘은 @JsonProperty, @JsonNaming으로 FrontEnd와 BackEnd의 다른 네이밍 규칙에 대한 대처를 어떻게 해야할지 배웠다.
그리고, 중복 ID를 어디서, 어떻게 검사해야 할지 배웠다.
앞으로 오늘 배운 내용들을 프로젝트 진행 시 중복 ID 검사를 진행할 때 사용할 것이며, 프론트엔드에서 snake_case로 데이터를 요청하여도 camelCase로 처리해보는 것을 진행해 보고 싶다.

profile
dev

0개의 댓글