송민준의 개발노트
Spring DTO 관리 패턴에 대한 고민 본문
공부할 때나 실무에서나 개발을 하다보면 정신없게 늘어만 가는 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)));
}
혹시나 이 글을 보고 좋은 의견이 있다면 댓글을 남겨주세요
참고 블로그 및 사이트
'웹 > 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 |