Web/SpringBootStudy
코드로 배우는 스프링부트 웹 프로젝트_ MVC/JPA/Thymeleaf (1)
꼬부기개발자
2024. 8. 13. 10:42
앞서 학습한 내용을 바탕으로 전체적인 프로젝트의 구조를 실습하기 위해 방명록(Guestbook)을 구성해보고자 한다.
이번 장에서 다음과 같은 내용을 학습한다.
- 프로젝트의 계층별 구조와 객체들의 구성
- Querydsl을 이용해서 동적으로 검색 조건을 처리하는 방법
- Entity 객체와 DTO 구분
- 화면에서의 페이징 처리
화면 구성
- 목록화면 - 전체 목록을 페이징 처리해서 조회, 제목/내용/작성자 항목으로 검색과 페이징 처리
- 등록화면 - 새로운 글을 등록할 수 있고 등록 처리 후 목록화면으로 이동
- 조회화면 - 목록 화면에서 특정글을 선택하면 상세조회화면으로 이동, 수정 버튼 클릭 시 수정/삭제가능 화면으로 이동
- 수정/삭제화면 - 수정/삭제 가능, 삭제 후 목록페이지 이동, 수정 후 수정된 내용 확인
기능 | URL | HTTP Method | 기능 | Redirect URL |
목록 | /guestbook/list | GET | 목록/페이징/검색 | |
등록 | /guestbook/register | GET | 입력 화면 | |
/guestbook/register | POST | 등록 처리 | /guestbook/list | |
조회 | /guestbook/read | GET | 조회 화면(상세) | |
수정 | /guestbook/modify | GET | 수정/삭제 가능 화면 | |
/guestbook/modify | POST | 수정 처리 | /guestbook/read | |
삭제 | /guestbook/remove | POST | 삭제 처리 | /guestbook/list |
프로젝트 기본구조
- 브라우저에서 전달되는 Request는 GuestbookController 에서 DTO의 형태로 처리된다.
- GuestbookController는 GuestbookService 타입을 주입받는 구조이며 이를 이용해서 처리한다.
- GuestbookRepository는 JPA를 이용하며 GuestbookServiceImpl에 주입해서 사용한다. 엔티티 타입을 이용하므로 Service 계층에서 DTO와 엔티티변환 처리를 한다.
- 마지막 결과는 Thymeleaf를 이용해서 레이아웃 템플릿을 활용해서 처리한다.
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
@Getter
abstract class BaseEntity {
@CreatedDate
@Column(name = "regdate", updatable = false)
private LocalDateTime regDate;
@LastModifiedDate
@Column(name = "moddate")
private LocalDateTime modDate;
}
데이터의 등록/수정 시간의 경우 다른 테이블에서 사용될 수 있는 공통 컬럼들이기에 추상클래스를 사용하여 작성을 하고자동으로 처리하기 위해 어노테이션을 활용하고자 한다.
추상클래스
- 재사용성 : 다른 엔티티 클래스에 공통적으로 필요한 필드인 경우 상속을 통해서 재사용이 가능하다.
- 직접 인스턴스화 방지 : 추상클래스는 인스턴스화 될 수 없기 때문에 독립적으로 사용되지 않고 Entity 클래스 간의 상속을 통해서 사용하게 하며 설계상 실수를 방지한다.
어노테이션
- @MappedSuperclass : 이 어노테이션이 붙은 클래스가 다른 클래스의 부모 클래스 역할을 하며, 테이블로 생성되지 않고 상속받은 클래스가 테이블에 맵핑된다. 공통 필드나 메서드를 정의 할 때 사용된다.
- @EntityListeners : 라이프사이클 이벤트를 처리하는 리스너를 지정할때 사용한다. 특정 엔티티가 생성/수정/삭제 등 이벤트가 발생할 때 자동으로 로직을 처리한다. @CreatedDate, @LastModifiedDate 와 함께 사용되어 생성/수정 시점을 관리한다.
엔티티 클래스와 Querydsl 설정
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Guestbook extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gno;
@Column(length = 100, nullable = false)
private String title;
@Column(length = 1500, nullable = false)
private String content;
@Column(length = 50, nullable = false)
private String writer;
public void changeTitle(String title) {
this.title = title;
}
public void changeContent(String content) {
this.content = content;
}
}
BaseEntity 클래스를 상속받아 테이블과 맵핑되는 Guestbook 클래스를 작성한다.
Querydsl을 사용하기 위해 추가적인 설정이 필요한데, 사용버전에 굉장히 민감하다. 시행착오를 겪은 끝에 완료하였다.
설정 코드는 아래와 같다.
//build.gradle
dependencies {
//추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
//추가
}
//추가
tasks.named('test') {
useJUnitPlatform()
}
def generatedDir = "src/main/generated"
clean {
delete file(generatedDir)
}
//추가
@Test
public void save() {
//given
LongStream.rangeClosed(1, 300).forEach(i ->{
Guestbook guestbook = Guestbook.builder()
.gno(i)
.title("Title..." + i)
.content("Content..." + i)
.writer("user..." + (i % 10))
.build();
guestbookRepository.save(guestbook);
});
//when
Optional<Guestbook> result = guestbookRepository.findById(300L);
//then
if (result.isPresent()) {
Guestbook guestbook = result.get();
assertThat(guestbook.getGno()).isEqualTo(300L);
assertThat(guestbook.getContent()).isEqualTo("Content...300");
}
}
@Test
public void update() {
//given
Optional<Guestbook> result = guestbookRepository.findById(300L);
//when
if (result.isPresent()) {
Guestbook guestbook = result.get();
guestbook.changeTitle("Changed Title");
guestbook.changeContent("Changed Content");
guestbookRepository.save(guestbook);
}
Optional<Guestbook> updatedResult = guestbookRepository.findById(300L);
//then
assertThat(updatedResult.get().getTitle()).isEqualTo("Changed Title");
}
생성한 엔티티코드를 바탕으로 테스트 코드를 작성하였다.