본문 바로가기
Spring Boot Project/Plming

[Plming] 게시판 기능 코드 리뷰 1편 (Entity와 DTO 객체 살펴보기)

by slchoi 2022. 4. 13.
728x90
SMALL

원래 계획은 기능을 하나하나 만들어가는 모든 과정을 기록하면서 진행하려고 했으나, 프론트엔드에게 API를 전달해주기로 한 날짜가 얼마 남지 않았어서 기능 개발을 먼저 완료하고 API를 전달한 뒤에 코드 리뷰를 해보려고 한다.

먼저 게시글 관련 코드의 폴더 구조이다.

폴더 구조

controller

controller 패키지 안에는 Controller 클래스가 들어가 있다. Controller는 HTTP 요청을 처리하고, 브라우저에 보여줄 HTML을 뷰에 요청하거나, REST 형태의 응답 몸체에 직접 데이터를 추가하는 역할을 수행한다.

BoardApiController 게시글 CRUD, 신청 기능과 관련된 Controller 클래스 
SearchApiController 게시글 검색 관련 Controller 클래스

초기 계획은 게시글 관련 URI는 "/posts", 검색 관련 URI는 "/search"로 사용하기 위해 Controller 클래스를 분리해서 관리하려고 했으나, 프론트엔드 분들의 요청이 있어 검색 관련 URI도 "/posts"로 진행하기로 했다. 이 클래스는 추후에 코드 리팩토링을 진행하면서 BoardApiController와 통합해야겠다.

 

dto

Request DTO 클래스와 Response DTO 클래스를 가지고 있는 패키지이다. Request Body를 통해 데이터가 들어오면 Request DTO에 담아 Service 로직을 수행하고 결괏값을 Response DTO에 담아 Response Body 값으로 반환한다.

ApplicationStatusRequestDto 게시글 신청 상태를 업데이트할 경우 request body의 값을 ApplicationStatusRequestDto에 저장한 후 Service 로직 수행
BoardListResponseDto 게시글 리스트를 조회할 경우 Service 로직 수행 결과를 BoardListResponseDto 객체로 변환한 뒤 response body에 넣어서 반환해줌
BoardRequestDto 게시글을 생성하거나 수정할 경우 request body로 들어온 정보를 BoardRequestDto 객체에 저장한 뒤 Service 로직 수행
BoardResponseDto 게시글 하나를 조회할 경우 (게시글을 상세 조회할 경우) 
SearchRequestDto 게시글 검색을 할 경우 쿼리 파라미터로 들어오는 검색 조건을 SearchRequestDto 객체에 저장한 뒤 Service 로직을 수행
UserBoardListResponseDto 사용자 id로 게시글을 조회할 경우 response body에 사용자가 작성한 게시글 리스트와 사용자가 댓글 단 게시글 리스트를 함께 변환해주도록 구현한 DTO 객체

 

entity

JPA 개체로 선언하는 entity 클래스를 가지고 있는 패키지

Application MySQL의 application 테이블과 매핑되는 JPA 개체
ApplicationRepository 게시글 신청과 관련된 기능을 수행하는 JPA Repository
Board MySQL의 post 테이블과 매핑되는 JPA 개체
BoardRepository 게시글과 관련된 기능을 수행하는 JPA Repository
BoardTag MySQL의 board_tag 테이블과 매핑되는 JPA 개체
BoardTagRepository 게시글 태그와 관련된 기능을 수행하는 JPA Repository

 

repository

QueryDSL을 사용하기 위해 임의로 생성한 Repository 클래스를 가지고 있는 패키지

  • entity 패키지의 "~Repository" 클래스에서 상속받아 사용한다.
ApplicationCustomRepository 게시글 신청과 관련된 기능을 수행하는 Repository의 인터페이스
ApplicationCustomRepositoryImpl ApplicationCustomRepository을 구현한 Repository 클래스
BoardCustomRepository 게시글과 관련된 기능을 수행하는 Repository의 인터페이스
BoardCustomRepositoryImpl BoardCustomRepository을 구현한 Repository 클래스
🙄 궁금한 점 (Java)
인터페이스와 이 인터페이스를 구현한 클래스로 나누어 구현한 이유는?

여기서 인터페이스와 인터페이스를 구현한 클래스를 나누어 구현한 이유는 QueryDSL을 사용하기 위해 Repository를 생성할 때 이 방식을 많이 사용한다고 했기 때문이다.

일반적으로 개발을 진행하면서 Service도 그렇고 Repository도 그렇고 인터페이스와 각각의 인터페이스를 구현한 클래스를 생성해서 개발을 진행한다. 인터페이스에 정의한 메서드들을 인터페이스를 구현하는 클래스에서 모두 Override 해서 구현해야 하는데, 이럴 경우 인터페이스가 왜 필요한지 의문이 든다. 인터페이스를 없애고 인터페이스를 구현한 클래스를 바로 사용하면 안 될까?
이 궁금증을 해결하기 위해 Java의 인터페이스 부분과 구현, 상속 부분에 대해 더 공부해봐야겠다.

 

service

ApplicationService 게시글 신청과 관련된 로직을 수행하는 Service 클래스
BoardService 게시글과 관련된 로직을 수행하는 Service 클래스
BoardTagService 게시글 태그와 관련된 로직을 수행하는 Service 클래스
SearchService 게시글 검색과 관련된 로직을 수행하는 Service 클래스

 

우선 이번 글에서는 dto 패키지에 있는 DTO 객체와 entity 패키지에 있는 Entity 객체를 살펴볼 것이다. 

1. Entity 객체

Entity 객체의 종류와 그 객체가 어떤 정보를 담고 있는지는 위에서 살펴봤으니 바로 코드를 살펴보자.

1.1. Board & BoardRepository

Board

더보기
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "post")
public class Board {

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

    /* 추가 */
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @Column(columnDefinition = "enum")
    private String category;    // 카테고리

    @Column(columnDefinition = "enum")
    private String status;  // 모집 상태

    @Column(columnDefinition = "integer")
    private Integer period;  // 진행 기간

    @Column(columnDefinition = "varchar")
    private String title;   // 제목

    @Column(columnDefinition = "varchar")
    private String content; // 내용

    @Column(columnDefinition = "Integer")
    private Integer participantMax = 0;

    @Column(columnDefinition = "bigint")
    private Long viewCnt = 0L;

    @Column(columnDefinition = "datetime")
    private LocalDateTime createDate = LocalDateTime.now();

    @Column(columnDefinition = "datetime")
    private LocalDateTime updateDate;

    @Column(columnDefinition = "enum")
    private char deleteYn = '0';

    // 역방향
    @OneToMany(mappedBy = "board")
    private List<BoardTag> boardTags;

    @Builder
    public Board(User user, Integer participantMax, String category, String status, Integer period, String title, String content, List<BoardTag> boardTags) {
        this.user = user;
        this.participantMax = participantMax;
        this.category = category;
        this.status = status;
        this.period = period;
        this.title = title;
        this.content = content;
        this.boardTags = boardTags;
    }

    /**
     * 게시글 수정
     */
    public void update(Integer participantMax, String title, String content, String category, String status, Integer period) {
        this.participantMax = participantMax;
        this.title = title;
        this.content = content;
        this.category = category;
        this.status = status;
        this.period = period;
        this.updateDate = LocalDateTime.now();
    }

    /**
     * 게시글 모집 상태 업데이트
     */
    public void updateStatus(String status) {
        this.status = status;
    }

    /**
     * 게시글 조회 수 증가
     */
    public void increaseCount() {
        this.viewCnt++;
    }

    /**
     * 게시글 삭제
     */
    public void delete() {
        this.deleteYn = '1';
    }

}

 

@Getter 필드 레벨과 클래스 레벨에서 사용 가능하다.

필드 레벨에서 사용할 경우, @Getter가 붙은 필드에 대해 자동으로 접근자(getXxx( ))를 생성해준다.
클래스 레벨에서 사용할 경우, 클래스 내부의 모든 필드에 대해 접근자를 생성해준다.
@NoArgsConstructor 파라미터가 없는 기본 생성자를 생성해준다. access를 지정해 줄 수 있어 접근 제어가 가능하다.
@Entity @Entity가 붙은 클래스를 DB의 테이블과 매핑해주는 애노테이션이다.

속성으로 name을 지정할 수 있다. name의 default 값은 클래스 이름이다.

@Entity가 붙은 클래스는 JPA가 관리하며, JPA가 Entity 객체를 생성할 때 기본 생성자를 사용하기 때문에 기본 생성자가 존재해야 사용이 가능하다. 
@Table Entity와 매핑할 테이블을 지정해주는 애노테이션이다. 생략 시 매핑한 Entity이름을 테이블 이름으로 사용한다. 

여기서는 Board Entity를 사용하지만, DB에서 Board Entity와 매핑되는 테이블 이름은 post이므로 name 속성 값으로 "post"를 지정한다.
@Id 기본 키를 매핑할 때 직접 할당을 하는 경우 @Id만 사용하고, 자동 생성으로 할당할 경우 @Id와 @GeneratedValue를 같이 사용한다.
@GeneratedValue 영속성 컨텍스트는 Entity를 식별자 값으로 구분하므로 Entity를 영속 상태로 만들기 위해서는 식별자 값이 반드시 필요하다. 이 때 @GeneratedValue 애노테이션을 사용해 기본 키 생성 전략을 지정할 수 있다.

여기서는 DB에서 id 값을 AUTO_INCREMENT로 지정했으므로, strategy에 IDENTITY를 지정해 기본 키 생성을 데이터 베이스에 위임한다.
@Column 객체 필드를 테이블 컬럼에 매핑해주는 애노테이션이다.

속성으로 columnDefinition을 지정해 데이터베이스 컬럼 정보를 직접 주었다.
@ManyToOne
@JoinColumn
게시글과 사용자는 다대일 관계이므로 @ManyToOne 애노테이션을 사용해 연관 관계를 매핑해주고, @JoinColumn을 사용해 외래키를 매핑해준다.
@Builder 객체를 생성하기 위해서는 생성자 패턴, 정적 메소드 패턴, 수정자 패턴, 빌더 패턴 등이 있다. 이 중에서 빌더 패턴을 사용해 객체를 생성하기 위해 @Builder 애노테이션을 사용한다.
Board id : 게시글 id
user : 게시글을 작성한 사용자 정보
category : 게시글 카테고리 ("스터디", "프로젝트", "공모전", "기타" 중 택 1)
status : 게시글 모집 상태 ("모집 중", "모집 완료" 중 택 1)
period : 진행 기간 (주 단위로 선택)
title : 제목 / content : 내용 / viewCnt : 조회수
participantMax : 최대 참여 가능한 인원 수
createDate : 게시글 생성 시간 / updateDate: 게시글 수정 시간
deleteYn : 게시글 삭제 여부 ('0' : 삭제 X, '1' : 삭제 O)
update update 메서드가 호출되면, 수정할 정보를 받아와 게시글 정보를 update 해준다.
updateStatus 매개변수로 변경할 게시글 모집 상태 정보가 들어오면, 해당 모집 상태로 게시글 모집 상태를 업데이트한다.
increaseCount 조회수를 1 증가시켜주는 메서드이다. GET 요청으로 게시글 상세 정보 보기 요청이 들어오면 호출된다.
delete deleteYn의 컬럼 값을 '1'로 변경해주는 메서드이다.

게시글을 작성한 사용자가 게시글을 삭제할 경우 실제 DB에서는 삭제되지 않고, deleteYn의 컬럼 값이 변경된다.
공부해야 할 부분 (DB)
DB 관련 기본 개념

프로젝트를 시작할 때 DB 관련 공부를 하나도 하지 않은 상태에서 시작했기 때문에 Entity 객체에서 연관 관계를 매핑해주는 과정에서 헷갈리는 부분과 어려운 부분이 있었다. (ex. 애노테이션을 연관 관계의 주인에게 지정해주어야 한다는데 연관 관계의 주인이 누구인지) 프로젝트를 마치고 나면 DB 공부를 먼저 시작해야겠다는 생각이 들었다.
🙄 궁금한 점 (JAVA)
객체를 생성할 때 빌더 패턴을 사용하는 것이 편해서 빌더 패턴을 사용했지만, 언제, 왜 빌더 패턴을 사용해야 하는지, 다른 패턴들에 비해 빌더 패턴의 장점, 단점은?

처음 프로젝트를 시작하면서 게시글 CRUD를 구축할 때 참고했던 블로그에서 객체를 생성할 때는 빌더 패턴을 사용하는 것이 좋다고 해서 개발을 진행하는 동안 Entity나 Dto 객체를 생성할 때 항상 빌더 패턴을 사용해왔다. 그러다 문득 변수의 개수가 적은 경우 혹은 변수의 변경 가능성이 없는 경우에도 빌더 패턴으로 구현하는 게 좋을까 하는 생각이 들었다. 객체를 생성할 때 많이 사용되는 패턴들과 각 패턴들의 장단점에 대해 공부해봐야겠다.

(참고자료)
🏭 Refactoring
Board Entity 내부에 있는 update, updateStatus, increaseCount, delete 메서드는 QueryDSL을 적용하기 전에 개발했던 부분인데, 이 부분도 QueryDSL을 사용해서 게시글 정보를 수정할 수 있도록 리팩터링 해야겠다.

 

BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long>, BoardCustomRepository {

}
  • JPA 레포지토리와 Board와 관련된 기능을 QueryDSL을 사용해 구현한 BoardCustomRepository를 상속받는 인터페이스 레포지토리이다.

 

1.2. BoardTag & BoardTagRepository

윗부분과 겹치는 내용은 제외하고 나머지 코드를 살펴보자.

BoardTag

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "post_tag")
public class BoardTag {

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

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Board board;

    @ManyToOne
    @JoinColumn(name = "tag_id")
    private Tag tag;

    @Builder
    public BoardTag(Board board, Tag tag) {
        this.board = board;
        this.tag = tag;
    }
}
BoardTag id : BoardTag 객체의 id
board : Tag가 지정된 board의 정보
Tag : board에 지정된 Tag 정보

Tag는 미리 정해 서버 DB에 저장해놨으므로 사용자가 변경할 일이 없기 때문에 따로 Entity 객체와 Repository를 생성하지 않았다.

BoardTagRepository

public interface BoardTagRepository extends JpaRepository<BoardTag, Long>, BoardCustomRepository {

}
  • BoardRepository와 마찬가지로 JPA 레포지토리와 BoardCustomRepository를 상속받는 인터페이스 레포지토리이다.

 

1.3. Application & ApplicationRepository

Application

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "application")
public class Application {

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

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Board board;

    @Column(columnDefinition = "enum")
    private String status = "대기";

    @Builder
    public Application(User user, Board board, String status) {
        this.user = user;
        this.board = board;
        this.status = status;
    }
}
Application id : Application 객체의 id
user: 게시글을 신청하려는 사용자의 정보
board: 사용자가 신청하려는 게시글 정보
status: 사용자의 게시글 신청 상태. default 값은 "대기". 게시글 작성자가 참여 신청한 사용자를 수락하면 "수락"으로 변경하고, 거절하면 "거절"로 변경한다.

 

ApplicationRepositroy

public interface ApplicationRepository extends JpaRepository<Application, Long>, ApplicationCustomRepository {

}
  • JPA 레포지토리와 Application과 관련된 기능을 QueryDSL을 사용해 구현한 ApplicationCustomRepository를 상속받는 인터페이스 레포지토리

 

2. DTO 객체

BoardRequestDto

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BoardRequestDto {

    private Integer participantMax; // 최대 참여 인원 수
    private String category;    // 카테고리
    private String status;  // 모집 상태
    private Integer period;  // 진행 기간
    private String title;   // 제목
    private String content; // 내용
    private List<Long> tagIds;  // tag ID 리스트

    public Board toEntity(User user) {
        return Board.builder()
                .user(user)
                .participantMax(participantMax)
                .title(title)
                .status(status)
                .category(category)
                .period(period)
                .content(content)
                .build();
    }
}
  • 게시글 생성 요청이 들어오면 request body에 게시글 제목, 내용, 카테고리, 모집 상태, 진행 기간, 최대 참여 인원수, tagId 리스트가 들어온다.
  • request body에 있는 내용을 받아와 toEntity 메서드를 호출하면 BoardRequestDto로부터 Board Entity를 생성한다.

 

BoardResponseDto

@Getter
public class BoardResponseDto {

    private Long id;
    private String category;
    private String status;
    private Integer period;
    private String title;
    private String content;
    private Integer participantMax;
    private Integer participantNum;
    private Long viewCnt;
    private LocalDateTime createDate;
    private LocalDateTime updateDate;
    private char deleteYn;
    private List<String> tags;

    public BoardResponseDto(Board entity, Integer participantNum, List<String> tags) {
        this.id = entity.getId();
        this.category = entity.getCategory();
        this.status = entity.getStatus();
        this.period = entity.getPeriod();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.participantNum = participantNum;
        this.participantMax = entity.getParticipantMax();
        this.viewCnt = entity.getViewCnt();
        this.createDate = entity.getCreateDate();
        this.updateDate = entity.getUpdateDate();
        this.deleteYn = entity.getDeleteYn();
        this.tags = tags;
    }
  • 게시글 상세 정보 보기 요청이 들어오면 response body에 BoardResponseDto를 담아 반환한다.
  • BoardResponseDto의 생성자에 Board Entity와 현재 참여가 승인된 인원수, 게시글이 포함하고 있는 tag의 이름 리스트를 넣어주면, BoardResponseDto 객체가 생성된다.
  • BoardResponseDto는 Board Entity의 모든 정보에 현재 참여가 승인된 인원수 정보를 포함한 있는 객체이다.

 

BoardListResponseDto

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BoardListResponseDto {

    private Long id;
    private String category;
    private String status;
    private String title;
    private Integer participantMax;
    private Long viewCnt;
    private List<String> tags;
    private Integer participantNum;

    public BoardListResponseDto(Board entity, Integer participantNum, List<String> tagNames) {
        this.id = entity.getId();
        this.category = entity.getCategory();
        this.status = entity.getStatus();
        this.title = entity.getTitle();
        this.participantMax = entity.getParticipantMax();
        this.participantNum = participantNum;
        this.viewCnt = entity.getViewCnt();
        this.tags = tagNames;
    }
}
  • 게시글 전체 리스트 조회 요청이 들어오면 response body에 BoardListResponseDto를 담아 반환한다.
  • BoardListResponseDto의 생성자에 Board Entity와 최대 참여 가능한 인원수, 태그 이름 리스트를 넣어주면 BoardListResponseDto 객체가 생성된다.
  • BoardListResponseDto는 Board 객체 정보 중에서 id, category, status, title, viewCnt, participantMax 정보와 현재 게시글에 참여가 승인된 인원수 정보, 태그 이름 리스트를 포함한다.

 

UserBoardListResponseDto

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserBoardListResponseDto {

    List<BoardListResponseDto> write;
    List<BoardListResponseDto> comment;

    @Builder
    public UserBoardListResponseDto(List<BoardListResponseDto> write, List<BoardListResponseDto> comment) {
        this.write = write;
        this.comment = comment;
    }
}
  •  사용자 Id 기준 게시글 리스트 조회 요청이 들어오면 response body에 UserBoardListResponseDto를 담아 반환한다.
  • write 리스트에는 사용자가 작성한 게시글 리스트 정보가 포함되어 있고, comment 리스트에는 사용자가 댓글을 단 게시글 리스트 정보가 포함되어 있다.

 

ApplicationStatusRequestDto

@Getter
public class ApplicationStatusRequestDto {

    private String status;
    private String nickname;

    @Builder
    public ApplicationStatusRequestDto(String status, String nickname) {
        this.status = status;
        this.nickname = nickname;
    }
}
  • 게시글 신청 상태 업데이트 요청이 들어올 경우 request 정보에 담겨 있는 status 값과 사용자의 nickname을 가져와 ApplicationStatusRequestDto에 저장한다.

 

SearchRequestDto

@Getter
public class SearchRequestDto {

    private String searchType;
    private String keyword;
    private List<String> category;
    private List<String> status;
    private List<Integer> tagIds;
    private List<Integer> period;
    private List<Integer> participantMax;

    @Builder
    public SearchRequestDto(String searchType, String keyword, List<String> category, List<String> status, List<Integer> tagIds, List<Integer> period, List<Integer> participantMax) {
        this.searchType = searchType;
        this.keyword = keyword;
        this.category = category;
        this.status = status;
        this.tagIds = tagIds;
        this.period = period;
        this.participantMax = participantMax;
    }
}
  • 검색 요청이 들어오면 쿼리 파라미터로 전송된 검색 조건들을 받아와 SearchRequestDto 객체에 저장한다.

 

이번 글에서는 entity 패키지와 dto 패키지의 코드를 살펴보면서 궁금한 점과 리팩터링 할 부분을 정리해봤다. 다음 글부터는 기능 하나씩 살펴보며 궁금한 점과 리팩터링 할 부분을 정리해볼 것이다.

 

참고자료
1. [자바] 자주 사용되는 Lombok 어노테이션

2. [JPA] 엔티티와 매핑

 

728x90
LIST

댓글