Project/9uin

spring boot 사이드 프로젝트[4] : 도메인 엔티티, DTO 설계 및 특별한 패키지 구조

attlet 2023. 5. 22. 15:16

데이터베이스를 설계하기 위해 E-R다이어그램을 기반한 도메인 설계를 시작했다. 보통 스프링 부트 프로젝트들을 보면 service , controller, repository 이렇게 패키지를 나눠서 진행하는 경우가 많은데, 우리는 조금 다른 방식으로 진행했다.

 

 

domain에 속하는 user, comment, post, profile을 먼저 만들고, 그 안에 controller, service , repository등을 만드는 것이다. 이 패턴의 장점은 각 엔티티와 연관된 레이어를 찾는 것이 굉장히 쉽다는 점이다. 단점은 필요 이상으로 패키지 개수가 늘어날 수 있다는 점인데, 나는 이 패턴을 통해 파일을 찾는 것이 더 유용하다 판단해서 이 방법을 선택했다.

 

도메인은 크게 User, profile, post, comment로 나눴다. 그 중 profile은 학력, 자격증 등 다수의 객체를 보유한다.

 

만약 User의 DTO(Data Transfer Object)를 찾고 싶다면 User패키지로 들어가 dto로 가면 바로 찾을 수 있다. 

 

 

 

 

각 도메인 별 DTO는 반환용 DTO도 같이 만들었다. 이는 비즈니스 로직이 동작한 후 그에 대한 결과 객체를 반환하는 용도이다. 

 

그리고 중요한 점은 각 entity에는 데이터 생성, 수정 일자 필드도 포함되어야한다는 점이다. 이를 위해 BaseEntity를 만들어 모든 entity가 이 클래스를 상속하도록 지정했다.

 

 

BaseEntity는 global패키지에 속하는 클래스이다. 엔티티리스너 어노테이션을 이용해 엔티티가 데이터베이스에 적용되기 전, 후로 콜백이 요청되게 할 수 있다. 콜백은 auditing기능을 부르도록 설정해 엔티티를 기반한 데이터가 생성될 때 마다 auditing 정보를 주입한다. mappedsuperclass를 이용해 모든 엔티티에게 공통된 매핑 정보를 제공한다. 즉 이걸 상속하는 엔티티에 createAt, updateAt을 제공한다. 

 

 

 

 

 

Entity 구조


 

프로젝트가 진행되면서, 도메인도 늘어나고 그만큼 엔티티 코드도 늘어났다. 

 

먼저, 가장 중요한 board엔티티를 통해 엔티티들이 어떻게 작성되었는지 확인해보자.

 

 

board.java

@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "board") //테이블과 매핑
@SQLDelete(sql = "UPDATE board SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class Board extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;
    @Column(nullable = false)
    private String type;
    @Column(nullable = false)
    private String title;
    @Column(nullable = false)
    private String text;
    @Column(nullable = false)
    private String proceed_method; //진행 방식
    @Column(nullable = false)
    private LocalDateTime period; //예상 기간
    @Column
    private int comment_cnt;  //댓글 개수

    @Column
    private boolean deleted = Boolean.FALSE;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User author;               //작성자 정보 접근

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
    @ToString.Exclude
    private List<ApplicantBoardRelation> applicantBoardRelationList;     //게시글에 지원서를 제출한 유저에 대한 정보

    @OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
    @ToString.Exclude
    private List<Comment> commentList = new ArrayList<>();    //게시글에 작성된 댓글들

    @OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
    @ToString.Exclude
    private List<TagBoardRelation> tagBoardRelationList;   //태그

    @OneToMany(mappedBy = "clipedBoard", cascade = CascadeType.REMOVE)
    @ToString.Exclude
    private List<ClipBoardRelation> clipBoardRelationList; //관심 클립으로 지정한 유저들

    @OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
    @ToString.Exclude
    private List<RoleBoardRelation> roleBoardRelationList;   //직군

    public void updateBoard(RequestUpdateBoardDto requestUpdateBoardDto){
        this.type = requestUpdateBoardDto.getType();
        this.title = requestUpdateBoardDto.getTitle();
        this.text = requestUpdateBoardDto.getText();
        this.proceed_method = requestUpdateBoardDto.getProceed_method();
        this.period = requestUpdateBoardDto.getPeriod();
//        this.tagBoardRelationList = requestBoardDto.getTagBoardRelationList();
//        this.roleBoardRelationList = requestBoardDto.getRoleBoardRelationList();
    }
}

 

 

id필드는 board엔티티들을 식별하기 위한 식별자라 할 수 있다. DB에 board가 생성될 때 자동으로 값이 생성된다. 

 

@Column 어노테이션 옵션으로 그 컬럼의 속성을 정할 수 있다. 만약 사용하지 않는다면, 디폴트 값으로 필드 이름이 그대로 DB 컬럼 이름으로 들어가게 된다.  nullable을 통해 null값을 허용할지 말지 결정할 수 있다.

 

 

@ManyToOne, @OneToMany 등은 연관관계 매핑을 위한 어노테이션이다. 일대다, 다대일 연관관계를 의미한다. 가령 예를 들자면, 유저 한 명은 게시글 여러개를 작성할 수 있으니 일대다 관계이다. 

 

 

@OneToMany에서 mappedBy를 통해 다른 엔티티에서 어떤 필드로 board를 다룰 지 정할 수 있다. mappedBy가 사용된 엔티티는 하위엔티티에게 자신의 id를 외래키로 전달한다.  예로 commentList부분을 보면 board 하나에 여러 댓글이 연관되고, 댓글들은 board의 id 값을 외래키로 갖는다. board는 comment의 상위 엔티티느낌이 되는 것이다.

 

fetch 옵션을 통해서 board가 영속화 될 때 하위 엔티티들도 같이 로딩되도록 할지 정할 수 있다. EAGER는 즉시 로딩, LAZY는 지연 로딩 방식이다. 

 

cascade 옵션은 board의 상태에 따라 하위 엔티티에 줄 변화를 적용할 수 있다. board가 삭제되면, board에 연관된 댓글들도 삭제되는 것이다.

 

 

 

마지막으로 update메서드를 통해 update로직을 구현할 때 편하게 작성할 수 있도록 만들었다. 

 

 

 

 

DTO 


dto들은 사용처에 따라 모두 다르게 작성되었다.

 

 

 

가장 많은 dto를 사용하는 board도메인이다. request와 response를 구분해 만드는 것이 중요했었다.

 

 

 

RequestCreateBoardDto.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class RequestCreateBoardDto {   //게시글 생성 시 필요한 정보들.

//    private Long user_id;
    private String type;
    private String title;
    private String text;
    private String proceed_method;
    private LocalDateTime period;

    //태그
//    private List<String> tagNames;

    //직군
    private List<RequestUsingInBoardDto> roleNeededDtoList;

    //태그
    private List<RequestSkillTagDto> tagDtoList;

    public RequestBoardDto toBoardDto(){
        return RequestBoardDto.builder()
                .title(title)
                .text(text)
                .type(type)
                .period(period)
                .proceed_method(proceed_method)
                .build();
    }

}

 

게시글을 생성할 때 사용하는 요청 dto이다. 게시글 제목, 내용, 기간 등을 받아오면 이 정보를 이용해 게시글을 생성한다.  

 

 

 

여기서 특이한 건 dto안에 다른 dto들을 가져온다는 점이다. 처음에는 별로 좋지 않은 구조라 생각했지만, 결국 각 dto가 코드 내에서 사용되어야 관리하기 편해졌고 깔끔해졌다.

 

 

request 메시지 형태는 다음과 같이 이루어진다.

 

 

 

swagger에서 확인한 RequestCreateDto

 

 

 

 

또 다른 예시로 게시글을 검색할 때 사용하는 dto이다.

 

 

RequestSearchBoardDto.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RequestSearchBoardDto {
    @Builder.Default
    private String username = "";
    @Builder.Default
    private String title = "";
    @Builder.Default
    private String type = "";
    @Builder.Default
    private List<String> tags = new ArrayList<>();  //url에 작성안하면 디폴트로 적용되는 값들 작성. 필터링할 때.
}

 

게시글을 get할 때, 필터링 하는 조건들을 담은 dto이다. builder 어노테이션의 default옵션을 이용해 아무 값도 보내지 않으면 필터링 안 하고 출력하도록 한다.

 

 

가령 게시글 작성자 이름만으로 게시글을 검색하려면, username만 값을 넣고 나머지 필드는 아무 값도 넣지 않고 보내는 것이다. 이러면 username으로만 필터링 한다.

 

 

이렇게 용도별로 dto들을 만들어 사용했다.