본문 바로가기

Heute lerne ich/Java

[스프링부트 회원 프로젝트] 회원가입

 

https://youtu.be/RhM1bQ76Tv0?si=7JszM083H0aPnzxu

이 글은, 코딩레시피 님의 <스프링 부트 쉽게 해보기> 영상을 참조하였으며, 해당 영상을 공부하며 겪었던 오류들과 관련 지식들을 정리했습니다.

01. 개발환경 소개

  1. Open JDK 11 → 17
  2. Intellij IDEA Community → Intellij IDEA
  3. MYSQL Server 8.0
  4. Spring Boot 2.6.12 → 3.2.3

필자는 JDK와 Spring Boot 모두 최신 버전을 사용한다. (생각보다 버전 때문에 생기는 오류들이 많지 않았다)

build.gradle에서 필요한 dependencies를 추가한다.

  1. Lombok
  2. Spring Web
  3. Thymeleaf
  4. JDBC API
  5. Spring Data JPA
  6. MySQL Driver

02. 프로젝트 구조 살펴보기


03. application.yml 설정

스프링부트는 Tomcat을 통해 서버를 설정한다. 기본적으로 스프링부트 내 Tomcat이 내장되어 있다.

main > resources > application.yml은 기본 설정 파일이다. 이를 Safe delete후 application.yml 파일(얌)을 생성한다.

application.yml

server:
	port: 8081

 

여기까지가 영상의 내용인데, 실행하니 url 오류가 발생했다. database의 url을 적어주지 않은게 문제일까 싶어 추가했다.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 1234

 

이번에는 MySQL JDBC 드라이버 클래스인 **com.mysql.cj.jdbc.Driver**를 로드하는 데 실패했다는 오류가 발생했다.

build.gradle에서 dependencies, Edit Starters을 클릭한다.

 

"MySql Driver"을 검색하여 설치하였더니 문제없이 접속이 완료되었다.


04. 기본 주소 요청하기

controller 패키지 추가 - HomeController 자바 클래스 추가

package com.example.member.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    // 기본페이지 요청 메서드
    @GetMapping("/")
    public String index(){
        return "index"; // => templates 폴더의 index.html을 찾아감
    }
}

 

@Controller 어노테이션은 이 클래스가 컨트롤러임을 나타내는 스프링의 어노테이션이다. 스프링은 이 어노테이션이 지정된 클래스를 검색하여 웹 요청에 대한 처리를 담당한다.

@GetMapping("/") 어노테이션은 HTTP GET 메서드로 들어온 "/" 경로의 요청을 처리하는 메서드를 정의한다. 이 메서드는 **index()**라는 이름을 가지고 있고, 반환 타입은 String이다.

메서드 내부에서는 **"index"**를 반환하고 있다. 이는 스프링에게 해당 요청을 처리한 후 보여줄 View의 이름이 "index"임을 알려주는 것이다. 스프링은 "index"라는 이름의 View를 찾아서 렌더링한다. 여기서 "index"는 일반적으로 템플릿 엔진을 통해 렌더링될 HTML 파일의 이름을 가리킨다.

기본적으로 스프링 부트는 templates 폴더 아래에서 View 파일을 찾는다.

 

resources 폴더의 templates 폴더 생성 후 index.html 파일을 생성한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h2>Hello Spring Boot!!</h2>
</body>
</html>

 

localhost:8081로 접속시 index.html 파일이 나타난다

 

+) html파일 내에서 사용하려는 태그 입력 후 tab키 누르면 자동 완성이 된다.

+) html파일 내에서 주석은 <!— —>로 쓸 수 있는데, Command + /로 간편하게 쓸 수 있다.


05 - 1. 회원가입 페이지 요청하기

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h2>Hello Spring Boot!!</h2>
    <a href="/member/save">회원가입</a>
    <a href="/member/login">로그인</a>
</body>
</html>

 

index.html에 회원가입, 로그인 바로가기를 추가한다.

 

MemberController 클래스를 생성한다.

 

package com.example.member.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MemberController {
    // 회원가입 페이지 출력 요청
    @GetMapping("member/save")
    public String saverForm() {
        return "save";
    }
}

 

member/save url 요청을 받으면 save.html 파일을 return하는 코드를 작성한다.

templates/save.html 파일을 생성한다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>save</title>
</head>
<body>
<!-- action 속성 : from에 작성한 데이터를 어디로 보낼지 지정-->
<form action="/member/save" method="post">
    <!-- name 속성 : 서버로 전송할 때 변수 이름의 역할  -->
    이메일: <input type="text" name="memberEmail"> <br>
    비밀번호: <input type="password" name="memberPassword"> <br>
    이름: <input type="text" name="memberName"> <br>
    <input type="submit" value="회원가입">
</form>
</body>
</html>

 

회원가입이므로 사용자의 form을 보호해줄 필요가 있다. 따라서 url에 정보가 노출되지 않는 method, post를 사용한다.

입력을 받고자 할 때 사용하는 input은 필수적으로 name 속성을 요구한다. name 속성은 서버로 전송할 때 변수 이름의 역할을 한다.

input type은 text, password, submit등이 있으며 password type일 시 노출되지 않은 채로 입력된다. submit type일 시 버튼을 눌러 제출할 수 있다.

 

 

회원가입 버튼을 클릭하면 405 에러가 발생한다. 주소는 존재하는데 방식(method)가 다르기때문에 발생하는 오류이다.

 

 

로그인 버튼을 클릭하면 404 에러가 발생한다. /member/login이 존재하지 않기 때문이다.

 

 

1. 404 Not Found (찾을 수 없음):

  • 404 오류는 클라이언트가 요청한 리소스(웹 페이지, 이미지, 동영상 등)를 서버에서 찾을 수 없을 때 발생한다.
  • 클라이언트가 잘못된 URL을 요청하거나, 요청한 리소스가 서버에 존재하지 않을 때 발생할 수 있다.
  • 예를 들어, 사용자가 존재하지 않는 페이지에 대한 URL을 입력하거나, 삭제된 파일에 접근하려고 할 때 404 오류가 발생한다.

2. 405 Method Not Allowed (허용되지 않는 메소드):

  • 405 오류는 클라이언트가 요청한 HTTP 메소드(예: GET, POST, PUT, DELETE 등)를 서버에서 지원하지 않을 때 발생한다.
  • 예를 들어, 서버가 GET 요청만 허용하고 POST 요청을 허용하지 않는 경우, 클라이언트가 POST 요청을 보낼 때 405 오류가 발생한다.
  • 이 오류는 서버가 클라이언트가 보낸 요청에 대해 특정 HTTP 메소드를 사용할 수 없다는 것을 나타낸다.

05 - 2. 입력한 정보 컨트롤러로 전달하기

MemberController에 PostMapping을 추가한다. (회원가입 버튼 클릭시)

@PostMapping("member/save")
    public String save(){
        System.out.println("MemberController.save");
        return null;
    }

 

post가 잘 되는지 확인하기 위해 System.out.println을 사용한다.

node.js의 console.log와 같이 java에서는 System.out.println을 사용한다.

이는 단축키로 (intellij) soutm + enter로 불러올 수 있다.

 

회원가입 버튼 클릭시 나타나는 오류

다만 이전의 405 오류가 아닌, 500 오류가 발생한 것을 볼 수 있다.

500 오류는 서버 측에서 발생하는 내부 오류를 나타내는 HTTP 상태 코드이다.

일반적으로,

  1. 서버 내부에서 예외가 발생한 경우
  2. 서버가 요청을 처리하는 동안 장애가 발생한 경우
  3. 서버 구성 문제로 인해 요청을 처리할 수 없는 경우
  4. 서버의 리소스 부족으로 인해 요청을 처리할 수 없는 경우

등의 이유로 발생한다.

여기서는 null을 return했기 때문에 발생하는 오류이며, post는 정상적으로 작동됨을 알 수 있다. (요청은 잘 작동한다)

RequestParam

@PostMapping("member/save")
    public String save(@RequestParam("memberEmail") String memberEmail){
        System.out.println("MemberController.save");
        return null;
    }

 

save안에 RequestParam(”memberEmail”) String memberEmail 을 추가한다.

이는 ReqeustParam인 memberEmail에 담겨온 값을, String memberEmail에 할당한다는 뜻이다.

 

이메일: <input type="text" name="memberEmail"> <br>

 

즉 save.html에서 보냈던 name값(사용자가 입력한 이메일)을 memberEmail에 저장한다.

 

@PostMapping("member/save")
    public String save(@RequestParam("memberEmail") String memberEmail){
        System.out.println("MemberController.save");
        System.out.println("memberEmail = "+ memberEmail);
        return "index";
    }

 

error 방지용으로 return "index"로 변경해준 후, memberEmail을 print한다.

 

 

Console에 입력한 email이 찍히는 것을 확인할 수 있다.

 

마찬가지로 나머지 request params를 받아올 수 있다.

 

@PostMapping("member/save")
    public String save(@RequestParam("memberEmail") String memberEmail,
                       @RequestParam("memberPassword") String memberPassword,
                       @RequestParam("memberName") String memberName){
        System.out.println("MemberController.save");
        System.out.println("member = "+ memberEmail + memberName + memberPassword);
        return "index";

 

+) 클래스를 미리 선언하여 효율적으로, 그리고 안전하게 받아올 수 있다.

MemberDto memberDto로 받아와 memberDto.getEmail()로 정보를 꺼내올 수 있다.


05 - 3. DB 연동하기

 

먼저 프로젝트 구조를 위해 dbo, entity, repository, service 폴더를 추가한다.

각각의 폴더의 역할은 다음과 같다.

 

1. dbo(Databse Object): 데이터베이스와 관련된 객체를 저장

  • 데이터베이스 테이블, 뷰, 저장 프로시저 등과 같은 데이터베이스 관련 객체를 정의하고 저장한다.
  • 주로 데이터베이스 관리자나 데이터베이스 개발자가 유지보수한다.

2. entity: 애플리케이션에서 사용되는 엔티티 클래스를 저장

  • 엔티티 클래스는 데이터베이스 테이블의 레코드와 일치하며, 애플리케이션의 도메인 모델을 표현한다.
  • 주로 JPA(Entity Manager)나 ORM(Object-Relational Mapping) 기술을 사용하는 경우에 사용된다.

3. repository: 데이터베이스와 상호작용하는 데 사용되는 데이터 액세스 객체를 저장

  • 데이터베이스에서 데이터를 읽고 쓰는 작업을 수행하는 메서드를 포함한다.
  • 주로 Spring Data JPA, Hibernate 등과 같은 ORM 프레임워크를 사용할 때 사용된다.

4. service: 비즈니스 로직을 처리하는 서비스 객체를 저장

  • 엔티티 객체를 사용하여 비즈니스 로직을 구현하고, 이를 트랜잭션 단위로 관리한다.
  • 서비스 클래스는 컨트롤러나 다른 서비스 클래스 등에서 호출되어 사용된다.

5. controller: HTTP 요청을 처리하고 적절한 응답을 생성하는 컨트롤러 클래스를 저장

  • 클라이언트로부터의 HTTP 요청을 받아들이고, 요청을 처리하기 위해 비즈니스 로직을 호출하거나 데이터를 조회하는 등의 작업을 수행한다.
  • 주로 Spring MVC 또는 Spring WebFlux와 같은 웹 프레임워크에서 사용된다.

1. service > MemberSerivce

package com.example.member.service;

import org.springframework.stereotype.Service;

@Service
public class MemberService {

}

 

@Service annotation 추가

 

2. dbo > MemberDTO

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class MemberDTO {
    private Long id;
    private String memberEmail;
    private String memberPassword;
    private String memberName;
}

 

@Getter와 @Setter는 자동으로 private 변수의 getter와 setter을 만든다.

@NoArgsConstructor는 기본 생성자를 자동으로 만든다.

@AllArgsConstructor는 필드를 모두다 매개변수로 하는 생성자를 만든다.

@ToString는 ToString 메서드를 자동으로 만들어준다.

  • 파라미터를 서비스 → 레포지토리 → 데이터베이스로 넘기는 작업을 해야 한다.

3. MemberController 수정

@PostMapping("member/save")
    public String save(@ModelAttribute MemberDTO memberDTO){
        System.out.println("MemberController.save");
        System.out.println("memberEmail = "+ memberDTO);
        return "index";
    }

 

dbo의 MemberDTO를 type으로 하여 매개변수로 정해준다.

@ModelAttribute는 생략 가능하나 명시해주는 것이 좋다.

전달하는 파라미터의 양이 많아질 수록 유리하다.

 

run 시킨 결과는 다음과 같다.

 

스프링에서는 기존 자바처럼 객체 생성 후 메서드를 호출하는 방식이 아닌, @Serivce, @Controller처럼 객체를 주입하여 사용한다. 즉, 스프링이 관리하는 객체를 편하게 사용할 수 있다.

 

  • 생성자 주입
@RequiredArgsConstructor
public class MemberController {
    // 생성자 주입
    private final MemberService memberService;
    
    @PostMapping("member/save")
    public String save(@ModelAttribute MemberDTO memberDTO){
        memberService.save();
        return "index";
    }
  1. MemberController에 @RequiredArgsConstructor 어노테이션을 추가한다.
  2. private final Memberservice memberService로 생성자를 주입한다.
  3. 서비스 클래스의 메서드, 자원등을 사용한다.

클래스에 대한 객체를 스프링빈으로 등록할 때, 자동적으로 서비스 클래스에 대한 객체를 주입을 받는다.

즉, 컨트롤러 클래스가 서비스 클래스의 자원, 메서드, 필드등을 사용할 수 있게 하는 권한이 생긴다.

 

+) Service에서도 마찬가지로 MemberRepository를 사용할 수 있게끔 똑같은 과정을 취해준다.

+) 라이브러리 수정시 라이브러리 업데이트를 꼭 해야한다.

 

4. DB 연결

application.yml

spring:
	# db 연결
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimeZone=Asia/Seoul&characterEncoding=UTF-8
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  # spring data jpa 설정
	jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    open-in-view: false
    show-sql: true
    hibernate:
      ddl-auto: create
   

jpa 설정은 다음과 같다.

  • database-platform: MySQL 데이터베이스의 SQL 방언을 설정한다.
  • open-in-view: 영속성 컨텍스트를 요청의 끝까지 열어두는 오픈인뷰 기능을 활성화 여부를 결정한다.
  • show-sql: Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력할지 여부를 결정한다.
  • hibernate.ddl-auto: 애플리케이션 시작 시 Hibernate가 엔티티 클래스를 기반으로 데이터베이스 스키마를 자동으로 생성하는 전략을 설정한다.

참고로, 현재 버전에서는 database-platform을 org.hibernate.dialect.MysQL5Inno~ 로 하면 오류가 발생한다. 영상 댓글을 보고 광명을 찾았다.

database-platform: org.hibernate.dialect.MySQLDialect

로 변경시 원활하게 실행된다.

 

5. Entity 생성

package com.example.member.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "member_table")
public class MemberEntity {
    @Id // pk 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
    private Long id;

    @Column(unique = true) // unique 제약조건 추가
    private String memberEmail;

    @Column
    private String memberPassword;

    @Column
    private String memberName;
}

 

autoIncrement의 pk인 id, unique한 memberEmail등을 갖는 member_table을 정의한다.

 

6. Repository에서 사용

public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
}

 

7. Service에서 사용

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    public void save(MemberDTO memberDTO){
    }
}

 

실행 후 mysql에서 확인해보면 test 데이터베이스에 member_table이라는 스키마가 생성된 것을 확인할 수 있다.

워크벤치말고 cmd가 익숙한 관계로 명령어를 사용하여 확인했다.

 

# mysql 접속
mysql -u root -p

# 비밀번호 입력
1234

# 모든 db 보기
show databases;

# application.yml에서 설정했던 db 선택하기 (필자의 경우 test)
use test;

# 현재 db의 스키마 보기
show tables;

 

 

member_table이 생성된 것을 확인할 수 있다.


05 - 4. 회원가입 완료

위의 코드를 실행했을 때, 콘솔을 보면 다음과 같이 나온다.

Hibernate: drop table if exists member_table
Hibernate: create table member_table (id bigint not null auto_increment, member_email varchar(255), member_name varchar(255), member_password varchar(255), primary key (id)) engine=InnoDB
Hibernate: alter table member_table add constraint UK_tdbojd0ydkcxi394wslvmcvn3 unique (member_email)

 

(1) member_table이 존재하면 드랍한다.

(2) id등 entity에서 정의한 그대로 table을 생성한다.

(3) member_email에 unique 제약조건을 추가한다. (마찬가지로 entity에서 정의함)

  • camelCase로 entity 필드를 정의하면, db column으로는 snake_case로 들어간다. memberEmail → member_email

현재 DB에 저장되는 흐름은 다음과 같다.

 

💡 Entity : 스키마 정의

→ Repository : JpaRepository를 상속받아 Entity 접근

→ Service : Repository의 save 메서드 호출

→ Controller : Service의 save 메서드 호출, 필요한 곳에 사용

 

MemberEntity

package com.example.member.entity;

import com.example.member.dbo.MemberDTO;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "member_table")
public class MemberEntity {
    @Id // pk 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
    private Long id;

    @Column(unique = true) // unique 제약조건 추가
    private String memberEmail;

    @Column
    private String memberPassword;

    @Column
    private String memberName;

    public static MemberEntity toMemberEntity(MemberDTO memberDTO){
        MemberEntity memberEntity = new MemberEntity();
        memberEntity.setMemberEmail(memberDTO.getMemberEmail());
        memberEntity.setMemberPassword(memberDTO.getMemberPassword());
        memberEntity.setMemberName(memberDTO.getMemberName());
        return memberEntity;
    }
}

 

MemberRepository

package com.example.member.repository;

import com.example.member.entity.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
}

 

JpaRespository를 상속받는다. (간편한 CRUD 및 다양한 기능 제공)

첫번째 인자는, 다룰 entity class 이름, 즉 MemberEntity이다.

두번째 인자는 pk(@Id) column의 type이다.

 

MemberService

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    public void save(MemberDTO memberDTO){
        // 1. dto -> entity 변환
        // 2. repository의 save 메서드 호출
        MemberEntity memberEntity = MemberEntity.toMemberEntity(memberDTO);
        memberRepository.save(memberEntity);
        // repository의 save 메서드 호출(조건 : entity 객체를 넣어줘야 함)
    }

 

save 메서드에 entity 객체를 넣어주어야 한다. 따라서 MemberEntity 파일에서 dto를 entity로 변환하는 메서드를 작성하고, MemberService에서 이를 사용한다.

 

MemberController

package com.example.member.controller;

import com.example.member.dbo.MemberDTO;
import com.example.member.entity.MemberEntity;
import com.example.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequiredArgsConstructor
public class MemberController {
    // 생성자 주입
    private final MemberService memberService;
    // 회원가입 페이지 출력 요청
    @GetMapping("member/save")
    public String saverForm() {
        return "save";
    }

    @PostMapping("member/save")
    public String save(@ModelAttribute MemberDTO memberDTO){
        memberService.save(memberDTO);
        return "index";
    }
}

 

memberService.save(memberDTO)를 통해 db에 저장한다.

“member/save”로의 post 요청시 controller → service → repository → entity (db 저장)

 

실행시킨 후 회원가입에 정보를 입력한다.

select * from member_table; 명령어를 통해 잘 저장된 것을 확인할 수 있다.

 

+) 어떤 쿼리가 실행되는지 console에 나타난다.

Hibernate: insert into member_table (member_email,member_name,member_password) values (?,?,?)

 

 

 

이상으로 간단한 회원가입이 끝났다.

 

다음 게시글은 이어서 스프링부트 회원 프로젝트의 로그인/수정/삭제등을 공부할 예정이다.