Spring 로그인 게시판 프로젝트 - (2) 엔티티, DB테이블 생성

june·2022년 2월 4일
0
post-thumbnail
post-custom-banner

Spring 프로젝트 생성

이해를 쉽게 하기 위해 spring initializr을 사용했다. IntelliJ IDEA의 프로젝트 기능을 사용해도 무방하다.

Project

메이븐도 괜찮지만, 그래들이 더 가독성이 괜찮으므로 그래들을 사용한다.

Language

당연히 자바를 사용한다.

Spring Boot

2.6.3버전을 사용한다.

Project Metadata

그룹명과 Artifact 명은 자유롭게 사용하나, Packaging을 jar로 하며, Java의 버전 또한 11로 한다.

Dependencies

세부 내용은 아래의 IDEA에서 세부 조정할때 같이 살피도록 하자.

IDEA 세부 조정

IntelliJ 기준으로 Open을 눌러 생성된 프로젝트를 연 다음

build.gradle을 선택하여 OK > Open as Project > Trust Project 하면 프로젝트가 열린다.

이후 sync작업이 끝났을때 build.gradle을 확인해 본다.

build.gradle

plugins {
	id 'org.springframework.boot' version '2.6.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0'
	testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0'
}


test {
	useJUnitPlatform()
}

파일이 이와 같지 않다면 맞게 수정한 다음 gradle을 다시 빌드하면 된다.

그러면 dependencies에 대해서 알아보자

dependencies

JPA

java에 있는 객체들과 MySql 테이블 안의 데이터를 알맞게 연결(매핑)해주는 핵심적인 역할을 한다.

Spring Security

애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크다.

Thymeleaf

컨트롤러가 전달하는 데이터를 이용하여 동적으로 화면을 구성하게 해주는 View Template다. 여기서는 프론트의 역할을 한다.

Validation

말 그대로 타당성 검사를 뜻하며, 본 프로젝트에서는 주로 해당 객체들이 올바른 형식으로 들어와 있는지 체크하는 역할을 한다.

Spring Boot Starter Web

Spring MVC를 사용한 RESTful서비스를 개발하는데 사용한다.

thymeleaf-extras-springsecurity5

타임리프에 Spring Security를 연동하여 사용하게 해준다.

lombok

getter, setter, toString 등의 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리다.

Devtools

Spring boot 개발 편의를 위한 도구다.

mysql-connector-java

Java에서 MySQL 연결시 필요한 커넥터다.

Junit

테스트 코드 작성을 위해 필요한 툴이다.

DB 연결 및 설정

resources 폴더에 있는 application.yml파일을 생성해주고, 기존의 application.properties는 삭제한다.

application.properties를 그대로 사용해도 괜찮으나, yml이 조금 더 가독성이 좋으므로 이것을 사용한다.

application.yml 을 작성한다

src/main/resources/application.yml

spring:
  datasource:
    url: mysql 주소
    username: 아이디
    password: 비밀번호
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    show-sql: true
    generate-ddl: true
    properties:
      hibernate:
        show-sql: true
        ddl-auto: create
        format_sql: true

logging:
  level:
    org.hibernate: info
    org.hibernate.SQL: debug
    org.hibernate.type: trace

datasource 관련된 부분은 굳이 MySql을 고집할 필요 없고, 사용하고 있는 DB에 맞게 적어도 큰 문제 없이 사용 가능하다.
단 RDBMS여야 호환에 대한 문제가 없이 정상 작동이 가능할 것이다.

jpa부분에서는 원활한 엔티티의 수행과정을 보기위해 show-sql, format_sql 설정했고
처음 엔티티-객체를 통해 테이블을 생성하는 것을 원했기 때문에 generate-ddl은 true로, ddl-auto는 create로 설정했다.

다만 이는 실무에서는 사용하면 매우 위험하므로, 이는 연습단계에서만 사용하자.
이렇게 사용할 경우 생기는 참사(?)에 대해서는 아래의 유튜브 영상에 자세히 나와 있다.

Video Label

엔티티 생성

build.gradle 작성도 완료되었다면 이제 본격적으로 프로젝트를 시작할 준비가 완료되었다.

먼저 로그인-게시판 프로젝트에서 꼭 필요한 엔티티 2개를 생성해보도록 하자.

하나는 게시물이고, 또 하나는 회원이다.

따라서 entity폴더를 생성한 이후 Article.javaMember.java를 만들어주도록 하자.

src/main/java/entity/Articlel.java

package com.example.ArticleLogin.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDateTime;

@Entity @Data
@NoArgsConstructor
@AllArgsConstructor
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 10, nullable = true)
    @NotEmpty
    private String userid;

    @Column(length = 10, nullable = true)
    @NotEmpty
    private String nickname;

    @Column(length = 30, nullable = true)
    @NotEmpty
    private String title;

    @Column(nullable = true)
    @NotEmpty
    private String content;

    @Column(nullable = true, updatable = false)
    @CreatedDate
    private LocalDateTime time;

    @PrePersist
    public void time() {
        this.time = LocalDateTime.now();
    }

}

PK인 id, 실제 유저의 아이디인 userid, 닉네임인 nickname, 제목 title, 내용 content, 작성시간 time과 작성시간을 작성하는 메소드 time()으로 구성되어 있다.

어노테이션

@Entity

해당 클래스가 엔티티임을 선언해주는 어노테이션이다.
이 어노테이션을 선언했으면, 반드시 id가 존재해야한다.

@Data

@Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode와 같은 데이터 관리에 도움이 되는 어노테이션을 한꺼번에 모아놓은 어노테이션이다.

@NoArgsConstructor, @AllArgsConstructor

생성자와 관련된 어노테이션들이다

@NoArgsConstructor는 기본생성자가 없고, 객체가 지정한 생성자를 사용하게 만든다.
따라서 getter/setter의 방식을 사용하지 않고도, 완전한 상태의 객체를 만들 수 있게 된다.

@AllArgsConstructor도 마찬가지로 생성자를 자동 생성해주지만, 모든 필드값을 파라미터로 받는 생성자를 만들어 준다.

@Id

이 어노테이션 아래에 선언된 객체는 SQL에서는 PK에 해당하게 된다..

@GeneratedValue

값을 일일히 적지 않고, SQL에 위임하여 값이 자동으로 설정되고 싶으면 이 어노테이션을 사용한다.

여기선 GenerationTypeIDENTITY로 설정하여 기본키 설정을 DB에 위임하였으나

SEQUENCE, TABLE 등과 같은 전략을 사용할 수도 있다.

@Column

SQL 안의 테이블의 컬럼과 1대1로 매칭이 되게 한다.
lengthnullable로 데이터의 길이와 null여부를 정할 수 있다.

@NotEmpty

value에서 null과 empty를 체크하는 기능이다. 둘 중 하나라도 있으면 오류를 내보낸다.
참고로 String과 collection에만 적용되기 때문에 boolean에서는 다른 것을 적용시켜야 한다.

@CreatedDate

Entity가 생성되어 저장될 때 시간을 자동 저장하게 해주는 어노테이션이다.

@PrePersist

Entity 객체가 JPA안에 넣어지기 전에 (정확히 말하면 영속성 컨텍스트에 관리되기 직전에) 호출하게 해주는 어노테이션이다.
따라서 time()메서드가 JPA에 넣어져서 실행되기 직전에 작성시간을 time 객체에 넣어주는 메서드가 실행됨으로써 자동으로 시간이 기록되게 된다.


src/main/java/entity/Member.java

package com.example.ArticleLogin.entity;

import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    @NotEmpty
    private String userid;

    @Column
    @NotEmpty
    private String password;

    @Column
    @NotEmpty
    private String username;

    @Column
    @NotEmpty
    private String nickname;

    @Column
    @NotEmpty
    private String email;

    @Column
    @Enumerated(EnumType.STRING)
    private UserRole role;


}

Member.javaArticle.java 엔티티와 거의 비슷한 구조다.
PK인 id, 유저 아이디인 userid, 비밀번호 password, 유저의 이름인 username, 유저의 닉네임인 nickname, 이메일 email, 사용자/관리자를 구분하게 해주는 role

src/main/java/entity/UserRole.java

package com.example.ArticleLogin.entity;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum UserRole {
    USER("ROLE_USER"),
    ADMIN("ROLE_ADMIN");

    private final String value;
}

여기서만 추가된 어노테이션을 말하자면

@Enumerated

Enum타입을 관리하는 어노테이션이다.
@Enumerated(EnumType.STRING)을 선언했다면 Enum 필드가 테이블에 저장시 숫자형인 1,2,3이 아닌, Enum의 role이 저장되게 된다.

Repository 생성

ArticleMember객체와 DB를 연결해줄 Repository를 생성해준다.
JpaRepository를 사용한다.

repository패키지를 만든 후 안에 2가지 repository interface를 생성해준다

src/main/java/repository/ArticleRepository

package com.example.ArticleLogin.repository;

import com.example.ArticleLogin.entity.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleRepository extends JpaRepository<Article, Long> {
    Article findByTitle(String title);
}

src/main/java/repository/MemberRepository

package com.example.ArticleLogin.repository;

import com.example.ArticleLogin.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByUsername(String username);

}

@Repository 어노테이션은 Repository임을 선언해주는 어노테이션이다.

Article findByTitle(String title);
Member findByUsername(String username);

이런 식으로 선언을 해놨는데 Jpa의 기능으로 여러가지 파라미터로 SELECT 할 수 있는 메서드를 만들 수 있으며, 그 방식은 메서드 명에 findBy+필드명 의 방식으로 설정하면 된다.

테스트 코드 작성 + 테이블 생성

이제 모든 준비는 끝났으니, 잘 작성했는지 테스트하고 또 테이블을 생성하기 위해 테스트 코드를 작성해보자.

Repository 인터페이스에 우클릭 후 이동 -> 테스트 -> 새 테스트 생성을 클릭한다.

라이브러리는 JUnit5을 사용할 것인데 다른 라이브러리를 사용해도 상관 없다.

ArticleRepositoryTest.java

package com.example.ArticleLogin.repository;

import com.example.ArticleLogin.entity.Article;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
class ArticleRepositoryTest {
    @Autowired
    ArticleRepository repository;


    @Test
    @Transactional
    @Rollback(false)
    public void testArticle() {
        Article article = new Article(
                null, "aaa","hi", "realA", "Hello", null
        );

        repository.save(article);

        Article findArticle = repository.findByTitle(article.getTitle());

        assertEquals(findArticle.getId(), article.getId());
        assertEquals(findArticle.getUserid(), article.getUserid());
        assertEquals(findArticle, article);
    }


}

@ExtendWith(SpringExtension.class)을 통해 Spring TestContext Framework를 JUnit프레임워크에 포함시킨다.
@SpringBootTest은 자동으로 JUnit을 따르게 된다.

테스트할 메서드 위에 @Test메서드를 붙여준다.

@Transactional 어노테이션은 중요한데, 이 어노테이션이 선언된 범위가 곧 트랜잭션의 범위이기 때문이다.
따라서 선언된 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성되며 작성 여부에 따라 Commit또는 RollBack한다.
@Rollback어노테이션을 통해 롤백하지 않고 커밋을 실행한다.

따라서 실행을 해보면...

문제 없이 테스트코드가 실행되었고, 또한 쿼리문도 문제 없이 실행 된 것을 볼 수 있다.

또한 DB 서버또한 확인해 보면

알맞게 저장되어 있음을 알 수 있다.

Member 또한 똑같이 테스트 코드를 실행해 보자.

package com.example.ArticleLogin.repository;

import com.example.ArticleLogin.entity.Member;
import com.example.ArticleLogin.entity.UserRole;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
class MemberRepositoryTest {
    @Autowired
    MemberRepository repository;

    @Test
    @Transactional
    @Rollback(false)
    public void testMember() {
        Member member = new Member(
                null, "aaa", "0000", "김에이", "에이", "aaa@aaa.com", UserRole.USER
        );

        repository.save(member);

        Member findMember = repository.findByUsername(member.getUsername());

        assertEquals(findMember.getId(), member.getId());
        assertEquals(findMember.getNickname(), member.getNickname());
        assertEquals(findMember.getRole(), member.getRole());
        assertEquals(findMember, member);
    }
}

마찬가지로 올바르게 실행되었음을 알 수 있다.

profile
초보 개발자
post-custom-banner

0개의 댓글