Web/SpringBootStudy

코드로 배우는 스프링부트 웹 프로젝트_ MVC/JPA/Thymeleaf (1)

꼬부기개발자 2024. 8. 13. 10:42

앞서 학습한 내용을 바탕으로 전체적인 프로젝트의 구조를 실습하기 위해 방명록(Guestbook)을 구성해보고자 한다.

이번 장에서 다음과 같은 내용을 학습한다.

  • 프로젝트의 계층별 구조와 객체들의 구성
  • Querydsl을 이용해서 동적으로 검색 조건을 처리하는 방법
  • Entity 객체와 DTO 구분
  • 화면에서의 페이징 처리

화면 구성

  1. 목록화면 - 전체 목록을 페이징 처리해서 조회, 제목/내용/작성자 항목으로 검색과 페이징 처리
  2. 등록화면 - 새로운 글을 등록할 수 있고 등록 처리 후 목록화면으로 이동
  3. 조회화면 - 목록 화면에서 특정글을 선택하면 상세조회화면으로 이동, 수정 버튼 클릭 시 수정/삭제가능 화면으로 이동
  4. 수정/삭제화면 - 수정/삭제 가능, 삭제 후 목록페이지 이동, 수정 후 수정된 내용 확인

 

기능 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

 

프로젝트 기본구조

 

출처 : https://youngkyonyou.github.io/springboot/2021/03/02/learning-springboot-web-with-code-chapter04-post1.html

  • 브라우저에서 전달되는 RequestGuestbookController 에서 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");
    }

 

생성한 엔티티코드를 바탕으로 테스트 코드를 작성하였다.