송민준의 개발노트

Spring DTO 관리 패턴에 대한 고민 본문

웹/Spring boot

Spring DTO 관리 패턴에 대한 고민

송민준 2021. 8. 6. 00:13

공부할 때나 실무에서나 개발을 하다보면 정신없게 늘어만 가는 DTO를 보고 어떻게 줄일 수 없을까라는 생각을 하게 된다. 너무 많아져 IDE의 도움 없이는 찾지도 못할 수준이 되는 프로젝트도 있다. 그래서 이번 기회에 DTO에 관한 부분을 고민하고 개선해보기로 했다.

 

우선 내가 생각하기에 충족되어야 할 조건들은... 아래와 같았다.

1. 패키지 구조에서 보다 간단하게

2. API에 필요한 데이터들만 뽑아낼 DTO

3. Swagger에 명시가 가능하게

4. 필요에 따라 페이징도 적용 가능하게

 

이것저것 찾아보고 고민해본 결과 inner class 패턴이 보기에 좋고 관리하기도 좋아보였다.

 

Account(계정)을 예로 남겨보겠다. swagger 및 valid 관련 코드는 가독성을 위해 제거했으니 필요하다면 추가하면 된다.

 

우선 AccountDto의 기본 구조는 아래와 같다.

PageDto라는 페이징 처리 관련 Dto를 상속 받은 것을 볼 수 있다.

실질적으로 AccountDto에 있는 필드들은 실질적으로 안쓰겠지만 어떤 필드들이 있는지 명세적으로 알기 위해 entity와 동일하게 적용해주고 있다.

@Data
@Builder
@ToString(callSuper=true, includeFieldNames=true)
@NoArgsConstructor
@AllArgsConstructor
public class AccountDto extends PageDto {
    private Long id;
    private String username;
    private String password;
    private String email;
    private int age;
    private Set<Role> userRoles = new HashSet<>();
}

 

추가적으로 Account 리스트 조회를 위한 요청 Dto를 생성해보겠다.

AccountDto 내부에 Inner Class 형태로 추가해준다. 페이징 처리를 위해 PageDto를 상속 받으며 필요한 필드들만 선언해준다. static으로 선언한 이유는 아래에서 설명하겠다.

@Data
@ToString(callSuper=true, includeFieldNames=true)
public static class ReqList extends PageDto {
  private String email;
  private String username;
  private List<String> roles = new ArrayList<>();
}

 

다음은 Account 리스트 조회를 위한 응답 Dto를 생성해보겠다.

마찬가지로 반환할 필드를 선언해주고 생성자를 선언해준다. 생성자를 보면 role에서 RoleDto의 inner class로 변환해주는 것을 볼 수 있다.

@Data
public static class ResList {
  private Long id;
  private String username;
  private String email;
  private int age;
  private Set<RoleDto.ResAccountRole> userRoles = new HashSet<>();

  public ResList(Account account){
    this.id = account.getId();
    this.username = account.getUsername();
    this.email = account.getEmail();
    this.age = account.getAge();
    this.userRoles = account.getUserRoles().stream()
      .map(RoleDto.ResAccountRole::new)
      .collect(Collectors.toSet());
  }
}

완성된 형태는 아래와 같다...

@Data
@Builder
@ToString(callSuper=true, includeFieldNames=true)
@NoArgsConstructor
@AllArgsConstructor
public class AccountDto extends PageDto {
    private Long id;
    private String username;
    private String password;
    private String email;
    private int age;
    private Set<Role> userRoles = new HashSet<>();

    @Data
    @ToString(callSuper=true, includeFieldNames=true)
    public static class ReqList extends PageDto {
        private String email;
        private String username;
        private List<String> roles = new ArrayList<>();
    }

    @Data
    public static class ResList {
        private Long id;
        private String username;
        private String email;
        private int age;
        private Set<RoleDto.ResAccountRole> userRoles = new HashSet<>();

        public ResList(Account account){
            this.id = account.getId();
            this.username = account.getUsername();
            this.email = account.getEmail();
            this.age = account.getAge();
            this.userRoles = account.getUserRoles().stream()
                .map(RoleDto.ResAccountRole::new)
                .collect(Collectors.toSet());
        }
    }
    
    @Data
    public static class Res {
        private Long id;
        private String email;
        private String username;
        private int age;
        private List<String> roles = new ArrayList<>();
    }
    
    // 추가적으로 필요한 Dto 생성...

}

 

자 그렇다면 이제 실질적으로 사용해보자.

예시는 JPA로 구성되어 있고 mybatis에서도 정상적으로 작동하는 것을 확인했다.

Inner class에서 선언된 Dto를 반환타입과 파라미터로 각각 선언해서 받아온 것을 볼 수 있다.

쿼리 생성 시 Dto로 바로 주입받는 방법도 고민해봤지만 나는 이 방법이 좀 더 간결하고 보기 좋은 것 같다.

    @Override
    public Page<AccountDto.ResList> findAllToDtoPage(Pageable pageable, AccountDto.ReqList reqDto) {

        QueryResults<Account> result = query
                .select(account)
                .from(account)
                .leftJoin(account.userRoles, role)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<AccountDto.ResList> content = result.getResults().stream()
                .map(AccountDto.ResList::new)
                .collect(Collectors.toList());

        long total = result.getTotal();

        return new PageImpl<>(content, pageable, total);
    }

 

추가적으로 내부 클래스는 static으로 만든 이유는 다음과 같다.

- 외부 참조가 유지된다는 것은 메모리에 대한 참조가 유지되고 있다는 뜻이고 GC가 메모리를 회수할 수가 없다. 이는 메모리 누수를 부르는 치명적인 단점이다.

- 항상 외부 인스턴스의 참조를 통해야 하므로 성능 상 비효율적이다.

 

 

++++ controller에서 데이터를 Body에 반환 시에는 아래와 같이 구현했다. ++++

제네릭 타입을 이용해 반환하고자 하는 데이터 타입을 정의한다.

이는 swagger에서 반환타입을 정확하게 나타내주기 위한 방법이다.

(처음엔 Object로 받았다가... swagger에 안나와서 고민을 많이 했다...)

@Data
@AllArgsConstructor
@Builder
@ApiModel
public class ResultDto<T> {
    @ApiModelProperty(value = "결과 코드")
    private HttpStatus statusCode;
    @ApiModelProperty(value = "결과 메시지")
    private String resultMsg;
    @ApiModelProperty(value = "결과 데이터")
    private T resultData;

    public ResultDto(final HttpStatus statusCode, final String resultMsg) {
        this.statusCode = statusCode;
        this.resultMsg = resultMsg;
        this.resultData = null;
    }

    public static<T> ResultDto<T> res(final HttpStatus statusCode, final String resultMsg) {
        return res(statusCode, resultMsg, null);
    }

    public static<T> ResultDto<T> res(final HttpStatus statusCode, final String resultMsg, final T t) {
        return ResultDto.<T>builder()
                .resultData(t)
                .statusCode(statusCode)
                .resultMsg(resultMsg)
                .build();
    }
}

컨트롤러에선!?

@GetMapping
public ResponseEntity<ResultDto<Page<AccountDto.ResList>>> findAll(@Valid @ModelAttribute AccountDto.ReqList requestDto) {
	return ResponseEntity.ok(ResultDto.res(HttpStatus.OK, HttpCode.getMessage(HttpStatus.OK),accountService.findAll(requestDto)));
}

 

 

혹시나 이 글을 보고 좋은 의견이 있다면 댓글을 남겨주세요

 

 

참고 블로그 및 사이트

https://velog.io/@agugu95/%EC%99%9C-Inner-class%EC%97%90-Static%EC%9D%84-%EB%B6%99%EC%9D%B4%EB%8A%94%EA%B1%B0%EC%A7%80

https://velog.io/@ausg/Spring-Boot%EC%97%90%EC%84%9C-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-DTO-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0

https://codinghack.tistory.com/78

' > Spring boot' 카테고리의 다른 글

h2 database 사용법(spring boot)  (0) 2020.10.25
YAML  (0) 2020.07.12
테스트코드에 시큐리티 적용하기  (0) 2020.03.19
네이버 간편로그인(SSO, oauth2)  (1) 2020.03.17
DB에 세션 저장을 위한 의존성 등록  (0) 2020.03.17