쇼핑몰 웹사이트 만들어보기 - 데이터베이스 선택과 회원 테이블

Shiba·2024년 5월 2일
0

프로젝트 및 일기

목록 보기
5/29
post-thumbnail

시험기간이라 프로젝트를 제대로 잡고 있을 수 없어서 포스팅이 늦어버렸다. 기간 중에도 짬내서 이리저리 하다보니 로그인 창과 회원가입 창은 얼추 만들어내었다. 이제 회원 가입 창과 로그인 창에서 보내는 데이터를 받아서 기록하거나 이를 대조하여 로그인이 되고, 안되는 과정이 필요하기 때문에 데이터베이스를 선택하는 것으로 이 포스팅을 시작해볼까 한다.

먼저, 데이터베이스는 어떤 방식으로 고르는지에 대해 알아보았다.

1. 데이터 모델
- 데이터 종류,구조,관계 등
- 관계형 데이터는 RDBMS, 대량의 비구조화된 데이터는 NoSQL을 고려
2. 확장성
- 데이터베이스가 처리해야 하는 데이터의 크기와 트랜잭션 볼륨
- 대규모 데이터와 고성능을 필요로 하면 분산 데이터베이스 시스템을 고려
3. 성능
- 응답 시간, 처리 속도 등
- 실시간 처리가 필요한 경우 빠른 읽기/쓰기 성능을 제공하는 DB선택
4. 트랜잭션
- 데이터의 일관성과 정확성이 중요한 경우
- ACID 원칙을 지원하는 데이터베이스를 선택
5. 유연성과 확장성
- 시스템의 변경과 발전에 대응할 수 있어야 함
- 비즈니스 요구 사항이 변경될 때 유연하게 대응할 수 있는 DB 선택

내가 생각하기엔, 쇼핑몰에서 db에 저장할 데이터들은 빠르면 물론 좋겠지만 무엇보다 정확성과 일관성이 더 중요하다고 생각한다. 아무래도 금전적인 거래를 하기위한 목적으로 이 사이트를 사용하기 때문에 가격에서 정확성의 오류가 난다고 가정해본다면... 다음 날 그 제품을 파는 판매자나 구매자들의 민원이 엄청나게 쏟아질 것이다. 그러니 정확성과 일관성이 높은 관계형데이터베이스를 사용하도록 하자.
관계형 데이터베이스에도 여러 종류가 있겠지만, DB를 처음 다루는 입장에서는 역시 가장 보편적으로 사용되는 것을 먼저 다루어보는게 좋지않을까? 그러니 MySQL을 사용하도록 하자

데이터베이스를 정했으니 연결해서 사용해보고 잘 작동되는지 확인해보도록 하자!


시작부터 오류가 발생했었다..mysql을 처음 사용해보는 터라 몰랐는데 workbench의 버전이 8.0.3x이면 서버상태를 보지 못하는 오류가 있어 8.0.22버전으로 다운그레이드하니 정상적으로 실행되었다.

또한, 데이터가 전송되는 과정을 chatGPT를 통해 원리와 코드를 같이 학습하였다.
먼저, 웹에서 서버로 데이터를 전송할 때, HTTPRequest의 POST요청을 통해 보내고자하는 데이터를 서버로 전송한다. 이때, value를 application/json으로 하면 json형태로 서버에 전송된다. application/json과 같은 타입을 MIME 타입이라고 하고, MIME 타입은 데이터의 형식과 용도를 지정하는 데 사용되며, 웹에서 데이터를 전송하고 처리할 때 중요한 역할을 한다고 한다.
서버에 전송된 데이터는 Spring의 Controller에서 POST요청을 받는 코드를 작성하여 받게된다. 이때, 파라미터를 (@RequestBody User user)로 받게 되면 해당 데이터를 Spring이 알아서 User객체의 형태에 맞게 값을 만들어준다. 이렇게 만든 User엔티티(@Entity를 이용한 엔티티 매핑 필요)를 jpa를 통해 db로 전송할 수 있다.


또 다른 에러가 있었다. 의존성 설정 관련 문제였는데 MySQL Server가 8.0버전 이상이라면 해당 버전을 명시해준 후,

의존성 설정에서 버전 명시
implementation 'mysql:mysql-connector-java:8.0.33'

application.properties에 다음 코드를 추가해야 했다

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

또한 sql에서 database의 이름은 스키마라는 것을 기억하자.


오류의 연속이다... 맨땅에 헤딩하는건 역시 살짝 힘이 든다. 이번 오류는 submit을 통해 js에서 제출 시, 415 Unsupported Media Type가 발생했다.
이 오류를 해결하다가 밤을 새버렸다.. 요지는 @RequestBody는 json형식의 데이터형식으로 전송을 받아야하는데 html에서 form을 사용함으로서 view에서 json이 아닌 기본 형식으로 보내버려 @RequestBody가 원하는 형식에 맞지 않아서 오류가 발생한다는 것이다.
즉, @RequestBody말고 @ModelAttribute를 사용하고 Model에 관한 설정들을 해주거나, html에서 form형식을 사용하지 않고 json형식으로 보내주어야 한다는 것이다. 그래서 form형식으로 되어있던 태그를 div로 고쳤다.

<div class="form-group" method="post">

또한 데이터베이스의 필드 값과 User엔티티의 필드값의 이름을 같게하여 매칭시켜주었다.

@Entity
public class User {

    @Id
    private String id;

    @Column(name = "password")
    private String password;

    @Column(name = "name")
    private String name;
    
    @Column(name = "place")
    private String place;
    
    @Column(name = "phone")
    private String phone;

    @Column(name = "birth")
    private String birth;

    @Column(name = "email")
    private String email;

    @Column(name = "level")
    private Level level = Level.BASIC; //등급
    
    @Column(name = "total")
    private int total=0; // 총 구매액

이렇게하니 해당 오류가 해결되었다.(해결하고보니 간단한 것같다)
항상 html이 script를 포함하고 있는지 먼저 확인하자! 당연한걸 하지 않으니 당연히 실행이 안되는 경우가 생김

테스트용 정보 작성

적은 정보대로 db에 들어온 모습!!!

이제 db도 연결했으니 다음시간에는 아이디 중복 확인 메소드, 로그인 메소드들을 수정해보도록하자!

++20240510)아이디 중복 확인 메소드 만들기

아이디 중복확인 메소드를 만들어보자! 기존에 만들어둔 코드를 사용해도 되겠지만, 우리는 웹페이지에 사용가능한 ID인지 불가능한 ID인지 표기를 해주어 사용자가 이를 인지하고 적절한 ID를 사용할 수 있도록 해보자.

여기서도 당연히 수많은 에러와 시행착오가 있었다. 처음엔 중복확인 버튼을 따로 만들고 버튼을 누르면, 팝업을 통해 사용가능판별을 내려주려고 했으나, 등록 버튼을 누를 시, Label의 내용을 바꾸어 표기를 하는 방법을 사용하였다. 이유는 후자가 코드의 양이나 사용자의 불편도 측면에서보나 더 효율적이라고 생각했기 때문이다.

하지만, 역시나 error를 맞이할 수 있었는데 첫 에러는 배열길이가 초과해버린다는 에러이다. findById()메소드를 실행시킬 때, 해당 작업 중에 배열의 길이를 초과해버리는 경우가 존재하는 것 같다. 정확히 무엇이 문제인지 아직 정확히 판단하지 못하여 js코드에서 xhr.status === 500일 때, 중복된 ID라고 표기해주도록 바꾸었다.

xhr.onreadystatechange = function() {
    if(xhr.status === 500){
    	document.getElementById("label1").style.color = "red";
        document.getElementById("label1").innerText = "사용 불가능한 ID 입니다.";
        document.getElementById("id").value = ""; // 아이디 입력란 비우기
        document.getElementById("id").style.borderColor = "red";
        document.getElementById("id").focus();
    }
};

이렇게 하여 중복 아이디를 사용하면 사용 불가능한 ID라고 뜨게 된다

해당 오류가 떠서 id를 바꾸어 입력하면 db에 정상적으로 입력이 됨을 확인했다.
그러나 이제 회원가입 완료창으로 보내기 위해 redirect를 사용하는데 오류가 발생했다. 처음에는 controller를 통해 회원가입 완료페이지를 매핑을 해주지 않아 404에러가 떴었다. 그래서 controller를 통해 매핑을 해주니 redirect자체는 status === 200으로 정상적으로 가지만, 회원가입 페이지에서 회원가입 완료페이지로 이동하지 않는 오류가 발생했다.
몇시간 삽질을 하면서 전반적인 흐름을 파악해보았다. redirect요청이 url이라면 302요청이 들어가며 이를 GET으로 받아서 처리를 할 수 있게 되면 상태코드 200이 출력된다. 즉, controller로 redirect:만 사용하는 것이 아닌 js에서도 redirect관련 설정을 해주어야 한다는 것이다.

@PostMapping(value = "/user/new")
    public String handleRequest(@RequestBody User user, RedirectAttributes redirectAttributes) {
        if (!userService.isIdExists(user.getId())) {
            userService.join(user);
            redirectAttributes.addAttribute("id", user.getId());
            return "redirect:/user/complete";
        }
        return "redirect:/";
    }

    // 회원가입 완료 페이지 매핑
    @GetMapping(value = "/user/complete")
    public String complete(@RequestParam(value = "id", required = false) String id, Model model) {
        model.addAttribute("id", id);
        return "user/complete";
    }
xhr.onreadystatechange = function() {
	if (xhr.readyState === 4) { // 요청 완료
		if (xhr.status === 200) { // 요청 성공
 		// 여기에서 리다이렉션을 수행
			window.location.href = "/user/complete?id="+encodeURIComponent(id);
		} else {
		// 요청이 실패한 경우에 대한 처리
		console.error("Request failed with status: " + xhr.status);
		}
	}
};

위와 같이 요청이 성공하였을 때, 해당 url로 이동하도록 해주니 오류가 해결이 되었다.

++20240515) 로그인 메소드 구현하기

로그인 메소드를 구현하기 위해선 위의 상태코드500 에러(배열크기 초과 오류)를 해결해야 findById()함수가 제대로 구동이 되면서 로그인이 가능한 것으로 나타났다. 그래서 500코드 에러를 고치기위해 대체 어떤 배열이 초과되는 것일까? 에러코드에 따르면 Servlet의 배열에서 초과가 난다는 것을 알게되었고, Servlet이 User에 넣을 데이터를 받는 도중 배열이 초과된다는 것을 알게되었다. 그래서 level변수와 총 구매액 변수는 회원가입에 필요가 없으므로, 나중에 정규화를 통해 추가를 할 수 있도록 하고, 일단은 삭제를 하는 쪽으로 실험을 해보았다. 결과는 성공적이였다. level변수와 총 구매액 변수가 입력이 되지않아 생성되는 오류였다고 생각된다.

public boolean Login(LoginRequest loginRequest){
        String id = loginRequest.getId();
        String password = loginRequest.getPassword();
        User user = userRepository.findById(id);
        if(user==null) return false;
        if(user.getPassword().equals(password)) return true;
        else return false;
}
    /////////////////////////login page///////////////////////////////
    const login = document.getElementById("login");

    login.addEventListener("click", function(event) {
        const id = document.getElementById("id").value;
        const password = document.getElementById("password").value;

        if(id == '' || id.length == 0) {
            document.getElementById("label1").style.color = "red";
            document.getElementById("label1").innerText = "아이디를 입력하세요.";
            document.getElementById("label1").focus();
            return false;
        }
        if(password == '' || password.length == 0) {
            document.getElementById("label2").style.color = "red";
            document.getElementById("label2").innerText = "비밀번호를 입력하세요.";
            document.getElementById("label2").focus();
            return false;
        }
        const data = {
            id: id,
            password: password
        };

        const xhr = new XMLHttpRequest();
        xhr.open("POST", "/user/login", true);
        xhr.setRequestHeader("Content-Type", "application/json"); // 요청 헤더를 JSON으로 설정
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) { // 요청 완료
                if (xhr.status === 200) { // 요청 성공
                    //console.log(xhr.responseText);
                    if(xhr.responseText === "success"){
                        localStorage.setItem(id,password);
                        // 여기에서 리다이렉션을 수행
                        window.location.href = "/";
                    }
                    else{
                        document.getElementById("label1").style.color = "red";
                        document.getElementById("label1").innerText = xhr.responseText;
                        document.getElementById("id").value = ""; // 아이디 입력란 비우기
                        document.getElementById("password").value = ""; //비밀번호 입력란 비우기
                    }
                } else {
                    // 요청이 실패한 경우에 대한 처리
                    document.getElementById("label1").style.color = "red";
                    document.getElementById("label1").innerText = xhr.responseText;
                    document.getElementById("id").value = ""; // 아이디 입력란 비우기
                    document.getElementById("password").value = ""; //비밀번호 입력란 비우기
                }
            }
        };
        xhr.send(JSON.stringify(data));
    });

또한, @RestController라는 애노테이션을 처음으로 사용해보았다. 해당 에노테이션을 사용하면 메소드의 반환 값이 JSON 또는 XML로 변환되어 클라이언트에 전송된다고 한다. 즉, 보통 컨트롤러에서 사용하던 템플릿 파일의 이름이 아닌 순수한 텍스트도 보낼 수 있다는 뜻이다.
이를 이용해서 위에서 만든 회원가입 코드의 POST요청도 @RestController를 사용하여 변형해보았다. 이전에는 500코드 오류가 나면 멈추도록 했다면, 이제는 더욱 안정적이게 검사를 통과한다면 success를, 실패한다면 fail을 내보내도록 하고, 이를 xhr.responseText를 통해 받아내어 그에 맞는 행동을 하도록 하였다.

@org.springframework.web.bind.annotation.RestController
public class RestController {

    private final UserService userService;

    public RestController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping(value = "/user/login")
    public String handleLogin(@RequestBody LoginRequest loginRequest){
        if(userService.Login(loginRequest)) return "success";
        else return "fail";
    }

    // 회원가입 처리
    @PostMapping(value = "/user/new")
    public String handleRequest(User user, RedirectAttributes redirectAttributes) {
        if (!userService.isIdExists(user.getId())) {
            userService.join(user);
            redirectAttributes.addAttribute("id", user.getId());
            return "success";
        }
        return "fail";
    }
}
let checkPw = false; //패스워드가 조건에 맞는지 체크하는 스위치
    let reCheckPw = false; //패스워드가 다시 작성한 패스워드와 일치하는지 체크하는 스위치
    let checkId = false; // 공백 아이디 체크
    function checkName(){ //이름 체크
        var name = $("#name").val();

        if(name == '' || name.length == 0) {
            return false;
        }
        else{
            return true;
        }
    }
    function checkBirth(){ //생일 체크
        var birth = $("#birth").val();

        if(birth == '' || birth.length == 0) {
            return false;
        }
        else{ //30일만 있는 날, 윤년 처리는 아직 하지 않음
            // YYYYMMDD 형식인지 확인하는 정규 표현식
            const regex = /^\d{4}\d{2}\d{2}$/;
            if (!regex.test(birth)) {
                return false;
            }

            // 연도, 월, 일 추출
            const year = parseInt(birth.substring(0, 4), 10);
            const month = parseInt(birth.substring(4, 6), 10);
            const day = parseInt(birth.substring(6, 8), 10);

            // 월이 1~12 사이인지 확인
            if (month < 1 || month > 12) {
                return false;
            }

            // 일자가 해당 월에 존재하는지 확인
            const date = new Date(year, month - 1, day);
            return (
                date.getFullYear() === year &&
                date.getMonth() === month - 1 &&
                date.getDate() === day
            );
        }
    }
    function checkPhone(){ //전화번호 체크
        var phone2 = $("#phone2").val();
        var phone3 = $("#phone3").val();

        if(phone2 == '' || phone2.length == 0) {
            return false;
        }
        else{
            if(phone3 == '' || phone3.length == 0) {
                return false;
            }
            return true;
        }
    }

    function checkPlace(){ //주소 체크
        var place = $("#place").val();

        if(place == '' || place.length == 0) {
            return false;
        }
        else{
            return true;
        }
    }

    //회원가입 정보 받아서 서버로 보낸 후 결과에 따라 가입 성공 실패 코드
    const signup=document.getElementById("signB");
    signup.addEventListener("click", function(event) {
        if(!checkId){
            document.getElementById("label1").style.color = "red";
            document.getElementById("label1").innerText = "아이디를 작성해주세요.";
            document.getElementById("id").style.borderColor = "red";
            document.getElementById("id").focus();
            return;
        }
        else
            document.getElementById("label1").innerText ="";
        
        if(!reCheckPw) {
            document.getElementById("label3").style.color = "red";
            document.getElementById("label3").innerText = "패스워드가 일치하는지 확인하세요";
            document.getElementById("pw_check").value = ""; // 아이디 입력란 비우기
            document.getElementById("pw_check").style.borderColor = "red";
            document.getElementById("pw_check").focus();
            return;
        }
        else
            document.getElementById("label3").innerText ="패스워드가 올바릅니다";

        if(!checkName()){
            document.getElementById("label4").style.color = "red";
            document.getElementById("label4").innerText = "이름을 작성해주세요.";
            document.getElementById("name").style.borderColor = "red";
            document.getElementById("name").focus();
            return;
        }
        else
            document.getElementById("label4").innerText ="";

        if(!checkBirth()){
            document.getElementById("label5").style.color = "red";
            document.getElementById("label5").innerText = "생일을 확인해주세요.";
            document.getElementById("birth").style.borderColor = "red";
            document.getElementById("birth").focus();
            return;
        }
        else
            document.getElementById("label5").innerText ="";

        if(!checkPhone()){
            document.getElementById("label6").style.color = "red";
            document.getElementById("label6").innerText = "전화번호를 작성해주세요.";
            document.getElementById("phone1").style.borderColor = "red";
            document.getElementById("phone2").style.borderColor = "red";
            document.getElementById("phone3").style.borderColor = "red";
            document.getElementById("phone1").focus();
            return;
        }
        else
            document.getElementById("label6").innerText ="";

        if(!checkPlace()){
            document.getElementById("label8").style.color = "red";
            document.getElementById("label8").innerText = "주소를 작성해주세요.";
            document.getElementById("place").style.borderColor = "red";
            document.getElementById("place").focus();
            return;
        }
        else
            document.getElementById("label8").innerText ="";

        const id = document.getElementById("id").value;
        const password = document.getElementById("pw").value;
        const name = document.getElementById("name").value;
        const birth = document.getElementById("birth").value;
        const phone =
            document.getElementById("phone1").value +
            document.getElementById("phone2").value +
            document.getElementById("phone3").value ;
        const email = document.getElementById("email").value;
        const place = document.getElementById("place").value;

        const data = {
            id: id,
            password: password,
            name: name,
            birth: birth,
            phone: phone,
            email: email,
            place: place
        };

        const xhr = new XMLHttpRequest();
        xhr.open("POST", "/user/new", true);
        xhr.setRequestHeader("Content-Type", "application/json"); // 요청 헤더를 JSON으로 설정
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) { // 요청 완료
                if (xhr.status === 200) { // 요청 성공
                    // 여기에서 리다이렉션을 수행
                    if(xhr.responseText == "success"){
                        window.location.href = "/user/complete?id="+encodeURIComponent(id);
                    }
                    else {
                        document.getElementById("label1").style.color = "red";
                        document.getElementById("label1").innerText = "중복된 ID 입니다.";
                        document.getElementById("id").value = ""; // 아이디 입력란 비우기
                        document.getElementById("id").style.borderColor = "red";
                        document.getElementById("id").focus();
                    }
                } else {
                    // 요청이 실패한 경우에 대한 처리
                    console.error("Request failed with status: " + xhr.status);
                }
            }
        };
        xhr.send(JSON.stringify(data));
    });

위와 같이 코드를 작성하여 회원가입 및 로그인 메소드를 만들어 보았다. 다음에는 로그인이 유지되기위해 필요한 세션에 대해 알아보고 이를 적용해보는 시간을 가지도록 하자.

profile
모르는 것 정리하기

0개의 댓글