이번 편을 시작하기 전에 현재 프로젝트를 깃허브에 연동해보자
이 글을 보고 오면 좋을 것 같다
이번 편에서는 이렇게 다룰 예정이다
1. 게시글 추가하기
2. TimeZone 설정하기
그럼 바로 시작하자
1. HTML
1-1 헤더와 푸터
templates/common/header.html
<h1>헤더 입니다.</h1>
<hr>
templates/common/footer.html
<hr>
<h1>푸터입니다.</h1>
모든 페이지에 존재하는 헤더와 푸터
1-2 게시글 리스트 페이지
templates/board/list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- HEADER -->
<div th:insert="common/header.html" id="header"></div>
<a th:href="@{/post}">글쓰기</a>
<!-- FOOTER -->
<div th:insert="common/footer.html" id="footer"></div>
</body>
</html>
Thymeleaf 문법을 볼 수 있다.
xmls:th="http://www.w2.org/1999/xhtml"
- XHTML 문서를 위한 XML 네임스페이스를 명시하는 것으로, 생략해도 정상작동한다
- 인텔리제이에서 Thymeleaf문법 사용 시, 문법 에러가 발생하여 추가했다
th:insert
- 헤더와 푸터처럼 다른 페이지를 현재 페이지에 삽입하는 역할을 한다
- JSP의 include와 같다
th:href
- Thymeleaf에서 html 속성은 대부분 이처럼 th:로 바꿔서 사용할 수 있다
- @{}의 의미는 애플리케이션이 설치된 경로를 기준으로 하는 상대 경로이다
- 예제에서 @{/post}는 URL이 http://localhost:8080/post가 된다
1-3 글쓰기 입력 폼 페이지
templates/board/write.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/post" method="post">
제목 : <input type="text" name="title"> <br>
작성자 : <input type="text" name="writer"> <br>
<textarea name="content"></textarea><br>
<input type="submit" value="등록">
</form>
</body>
</html>
2. Controller
다음으로 URL을 매핑하고, 비즈니스 로직 함수를 호출하여 view에 뿌려주는 역할을 하는 컨트롤러를 구현해보자
이 글에서는 게시글을 DB에 INSERT 하는 write() 메서드만 구현하고, 나머지 메서드는 다음 글에서 작성할 예정이다
src/main/java/com/project/springbootproject/controller/BoardController.java
import com.victolee.board.dto.BoardDto;
import com.victolee.board.service.BoardService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@AllArgsConstructor
public class BoardController {
private BoardService boardService;
@GetMapping("/")
public String list() {
return "board/list.html";
}
@GetMapping("/post")
public String write() {
return "board/write.html";
}
@PostMapping("/post")
public String write(BoardDto boardDto) {
boardService.savePost(boardDto);
return "redirect:/";
}
}
@Controller
- 컨트롤러임을 명시하는 어노테이션
- MVC에서 컨트롤러로 명시된 클래스의 메서드들은 반환 값으로 템플릿 경로를 작성하거나, redirect를 해줘야 한다
- 템플릿 경로는 templates패키지를 기준으로 한 상대 경로이다
- @RestController는 @Controller와 @ReponseBody를 합쳐놓은 어노테이션이다
- view 페이지가 필요 없는 API 응답에 어울리는 어노테이션
@AllArgsConstructor
- Bean 주입 방식과 관련이 있으며, 생성자로 Bean 객체를 받는 방식을 해결해주는 어노테이션
그래서 BoardService 객체를 주입받을 때 @Autowired 같은 특별한 어노테이션을 부여하지 않는다 *참고
- 그 밖에, @NoArgsConstructor @RequiredArgsConstructor 어노테이션이 있다
@GetMapping / @PostMapping
- URL을 매핑해주는 어노테이션이며, HTTP Method에 맞는 어노테이션을 작성하면 된다
list() 메서드는 지금 구현하지 않고, 다음 글에서 구현할 것이다
dto는 Controller와 Service사이에서 데이터를 주고받는 객체를 의미한다
3. Service
다음으로 비즈니스 로직을 수행하는 Service를 구현해보자
src/main/java/com/project/springbootproject/service/BoardService.java
import com.victolee.board.dto.BoardDto;
import com.victolee.board.domain.repository.BoardRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@AllArgsConstructor
@Service
public class BoardService {
private BoardRepository boardRepository;
@Transactional
public Long savePost(BoardDto boardDto) {
return boardRepository.save(boardDto.toEntity()).getId();
}
}
@AllArgsConstructor
- Controller에서 봤던 어노테이션
- 마찬가지로 Repository를 주입하기 위해 사용
@Service
- 서비스 계층임을 명시해주는 어노테이션
@Transactional
- 선언적 트랜잭션이라 부르며, 트랜잭션을 적용하는 어노테이션
save()
- JpaRepository에 정의된 메서드로, DB에 INSERT, UPDATE를 담당한다
- 매개변수로는 Entity를 전달
4.Repository
다음으로 데이터 조작을 담당하는 Repository를 구현해보자
src/main/java/com/project/springbootproject/domain/repository/BoardRepository.java
import com.victolee.board.domain.entity.BoardEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}
Repository는 인터페이스로 정의하고, JpaRepository 인터페이스를 상속받으면 된다
- JpaRepository의 제네릭 타입에는 Entity 클래스와 PK의 타입을 명시해주면 된다
- JpaRepository에는 일반적으로 많이 사용하는 데이터 조작을 다루는 함수가 정의되어 있기 때문에
CRUD 작업이 편해진다
5. Entity
다음으로 DB 테이블과 매핑되는 객체를 정의하는 Entity를 구현해보자
Entity는 JPA와 관련이 깊다
5-1 BoardEntity 구현
src/main/java/project/springbootproject/domain/entity/BoardEntity.java
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "board")
public class BoardEntity extends TimeEntity {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
@Column(length = 10, nullable = false)
private String writer;
@Column(length = 100, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@Builder
public BoardEntity(Long id, String title, String content, String writer) {
this.id = id;
this.writer = writer;
this.title = title;
this.content = content;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
- 파라미터가 없는 기본 생성자를 추가하는 어노테이션 (JPA 사용을 위해 기본 생성자 생성은 필수)
- access는 생성자의 접근 권한을 설정하는 속성이며, 최종적으로 protected BoardEntity() {}와 동일
- protected인 이유는 Entity생성을 외부에서 할 필요가 없기 때문이다
@Getter
- 모든 필드에 getter를 자동생성해주는 어노테이션
- @Setter 어노테이션은 setter를 자동생성해주지만, 무분별한 setter 사용은 안정성을 보장받기 어려우므로
Builder 패턴을 사용한다
- 참고로 @Getter와 @Setter를 모두 해결해주는 @Data 어노테이션도 있다
@Entity
- 객체를 테이블과 매핑할 엔티티라고 JPA에게 알려주는 역할을 하는 어노테이션 (엔티티 매핑)
- @Entity가 붙은 클래스는 JPA가 관리하며, 이를 엔티티 클래스라 한다
@Table(name="board")
- 엔티티 클래스와 매핑되는 테이블 정보를 명시하는 어노테이션
- name 속성으로 테이블명을 작성할 수 있으며, 생략 시 엔티티 이름이 테이블명으로 자동 매핑
@Id
- 테이블의 기본 키임을 명시하는 어노테이션
@GeneratedValue(strategy=GenerationType.IDENTITY)
- 기본키로 대체키를 사용할 때, 기본키 값 생성 전략을 명시
@Column
- 컬럼을 매핑하는 어노테이션
@Builder
- 빌더패턴 클래스를 생성해주는 어노테이션
- @Setter 사용 대신 빌더패턴을 사용해야 안정성을 보장할 수 있다
Entity클래스는 테이블과 관련이 있는 것을 알 수 있다
비즈니스 로직은 Entity를 기준으로 돌아가기 때문에 Entity를 Request, Response 용도로 사용하는 것은 적절하지 못하다
그래서 데이터 전달 목적을 갖는 dto 클래스를 정의하여 사용한다
5-2 TimeEntity 구현
다음으로 BoardEntity는 TimeEntity를 상속하고 있다
TimeEntity는 데이터 조작 시 자동으로 날짜를 수정해주는 JPA의 Auditing 기능을 사용한다
src/main/java/com/project/springbootproject/domain/entity/TimeEntity.java
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
@MappedSuperclass
- 테이블로 매핑하지 않고, 자식 클래스(엔티티)에게 매핑 정보를 상속하기 위한 어노테이션
@EntityListeners(AuditingEntityListener.class)
- JPA에게 해당 Entity는 Auditing기능을 사용한다는 것을 알리는 어노테이션
@CreatedDate
- Entity가 처음 저장될 때 생성일을 주입하는 어노테이션
- 이때 생성일은 update 할 필요가 없으므로, updatable=false 속성을 추가
- 속성을 추가하지 않으면 수정 시, 해당 값은 null이 돼버린다
@LastModifiedDate
- Entity가 수정될 때 수정 일자를 주입하는 어노테이션
5-3 @EnableJpaAuditing
마지막으로 JPA Auditing활성화를 위해 Application에서 @EnableJapAuditing 어노테이션을 추가해주자
src/main/java/com/project/springbootproject/BoardApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
6. DTO
마지막으로 데이터 전달 객체인 dto를 구현해보자
src/main/java/com/project/springbootproject/dto/BoardDto.java
import com.victolee.board.domain.entity.BoardEntity;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class BoardDto {
private Long id;
private String writer;
private String title;
private String content;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
public BoardEntity toEntity(){
BoardEntity boardEntity = BoardEntity.builder()
.id(id)
.writer(writer)
.title(title)
.content(content)
.build();
return boardEntity;
}
@Builder
public BoardDto(Long id, String title, String content, String writer, LocalDateTime createdDate, LocalDateTime modifiedDate) {
this.id = id;
this.writer = writer;
this.title = title;
this.content = content;
this.createdDate = createdDate;
this.modifiedDate = modifiedDate;
}
}
toEntity()
- dto에서 필요한 부분을 빌더 패턴을 통해 entity로 만든다
- 필요한 Entity는 이런 식으로 추가하면 된다
dto는 Controller <> Service <> Repository 간에 필요한 데이터를 캡슐화한 데이터 전달 객체이다
- 그런데 예제에서 Service에서 Repository 메서드를 호출할 때, Entity를 전달한 이유는 JpaRepository에 정의된
함수들은 미리 정의되어 있기 때문이다. 그래서 Entity를 전달할 수밖에 없었는데,
요점은 각 계층에서 필요한 객체 전달은 Entity 객체가 아닌 dto 객체를 통해 주고받는 것이 좋다는 것이다
7. 테스트
코드를 모두 작성했다면 localhost:8080에 접속해서 확인해보자
정상적으로 출력되는 것을 확인할 수 있다
게시글 작성 후에 DB에 접속해보면
DB에 제대로 저장되는 걸 확인할 수 있다
근데 여기서 궁금한 점이 생겼다
내가 게시글을 작성한 시간이랑 DB에 저장된 시간이 다르게 표기되는 걸 확인할 수 있다
이유가 궁금해서 구글링을 해보니
8. TimeZone 설정하기
1편에서 yml파일에서 datasource url을 적을 때 UTC기준으로 적은걸 확인할 수 있다
그래서 위에 UTC부분을 Asia/Seoul으로 바꿔주면 한국시간으로 표시된다고 한다
사진처럼 바꿔주고
다시 게시글을 작성해보자
작성하고 등록 버튼을 누르고 DB에 가서 확인해보면??
한국시간으로 정상적으로 출력이 됐다
이 방법 말고 데이터베이스 서버에서 타임존을 변경하는 방법도 있다
좀 더 자세히 알고 싶으면 여기를 참고해보자
이번 편에서는 게시글 추가를 해보았습니다
다음 편에서는 게시글 조회, 수정, 삭제를 다뤄보도록 하겠습니다
'Spring Boot Project' 카테고리의 다른 글
Spring Boot 게시판 만들기 [3] 게시글 조회, 수정, 삭제 (0) | 2020.08.22 |
---|---|
Spring Boot 게시판 만들기 [1] 개발환경 구축하기 (1) | 2020.08.20 |