이해를 쉽게 하기 위해 spring initializr을 사용했다. IntelliJ IDEA의 프로젝트 기능을 사용해도 무방하다.
메이븐도 괜찮지만, 그래들이 더 가독성이 괜찮으므로 그래들을 사용한다.
당연히 자바를 사용한다.
2.6.3버전을 사용한다.
그룹명과 Artifact 명은 자유롭게 사용하나, Packaging을 jar로 하며, Java의 버전 또한 11로 한다.
세부 내용은 아래의 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에 대해서 알아보자
java에 있는 객체들과 MySql 테이블 안의 데이터를 알맞게 연결(매핑)해주는 핵심적인 역할을 한다.
애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크다.
컨트롤러가 전달하는 데이터를 이용하여 동적으로 화면을 구성하게 해주는 View Template다. 여기서는 프론트의 역할을 한다.
말 그대로 타당성 검사를 뜻하며, 본 프로젝트에서는 주로 해당 객체들이 올바른 형식으로 들어와 있는지 체크하는 역할을 한다.
Spring MVC를 사용한 RESTful서비스를 개발하는데 사용한다.
타임리프에 Spring Security를 연동하여 사용하게 해준다.
getter, setter, toString 등의 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리다.
Spring boot 개발 편의를 위한 도구다.
Java에서 MySQL 연결시 필요한 커넥터다.
테스트 코드 작성을 위해 필요한 툴이다.
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로 설정했다.
다만 이는 실무에서는 사용하면 매우 위험하므로, 이는 연습단계에서만 사용하자.
이렇게 사용할 경우 생기는 참사(?)에 대해서는 아래의 유튜브 영상에 자세히 나와 있다.
build.gradle
작성도 완료되었다면 이제 본격적으로 프로젝트를 시작할 준비가 완료되었다.
먼저 로그인-게시판 프로젝트에서 꼭 필요한 엔티티 2개를 생성해보도록 하자.
하나는 게시물
이고, 또 하나는 회원
이다.
따라서 entity폴더를 생성한 이후 Article.java
와 Member.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()
으로 구성되어 있다.
해당 클래스가 엔티티임을 선언해주는 어노테이션이다.
이 어노테이션을 선언했으면, 반드시 id가 존재해야한다.
@Getter
, @Setter
, @RequiredArgsConstructor
, @ToString
, @EqualsAndHashCode
와 같은 데이터 관리에 도움이 되는 어노테이션을 한꺼번에 모아놓은 어노테이션이다.
생성자와 관련된 어노테이션들이다
@NoArgsConstructor
는 기본생성자가 없고, 객체가 지정한 생성자를 사용하게 만든다.
따라서 getter/setter의 방식을 사용하지 않고도, 완전한 상태의 객체를 만들 수 있게 된다.
@AllArgsConstructor
도 마찬가지로 생성자를 자동 생성해주지만, 모든 필드값을 파라미터로 받는 생성자를 만들어 준다.
이 어노테이션 아래에 선언된 객체는 SQL에서는 PK에 해당하게 된다..
값을 일일히 적지 않고, SQL에 위임하여 값이 자동으로 설정되고 싶으면 이 어노테이션을 사용한다.
여기선 GenerationType
을 IDENTITY
로 설정하여 기본키 설정을 DB에 위임하였으나
SEQUENCE
, TABLE
등과 같은 전략을 사용할 수도 있다.
SQL 안의 테이블의 컬럼과 1대1로 매칭이 되게 한다.
length
와 nullable
로 데이터의 길이와 null여부를 정할 수 있다.
value에서 null과 empty를 체크하는 기능이다. 둘 중 하나라도 있으면 오류를 내보낸다.
참고로 String과 collection에만 적용되기 때문에 boolean에서는 다른 것을 적용시켜야 한다.
Entity가 생성되어 저장될 때 시간을 자동 저장하게 해주는 어노테이션이다.
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.java
도 Article.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;
}
여기서만 추가된 어노테이션을 말하자면
Enum타입을 관리하는 어노테이션이다.
@Enumerated(EnumType.STRING)
을 선언했다면 Enum 필드가 테이블에 저장시 숫자형인 1,2,3이 아닌, Enum의 role이 저장되게 된다.
Article
과 Member
객체와 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);
}
}
마찬가지로 올바르게 실행되었음을 알 수 있다.