들어가기 전에
프로젝트를 진행하면 할 수록 쌓여만 가는 DTO 클래스를 보면서 관리의 필요성을 느끼게 되었다. 같은 도메인을 조회하더라도 API 마다 요구하는 데이터 필드가 다르기 때문에 불필요한 데이터 전송을 최소화하기 위해 API 마다 DTO 클래스를 생성하다 보니 유지보수하기 힘든 지경에까지 이르게 되었다. DTO 클래스 관리를 위한 리팩토링이 필요하다는 결론에 다달았고 상속 구조와 Static Inner 클래스를 사용하여 DTO 클래스를 관리했던 경험을 기록하고자 한다.
상속 구조를 이용한 DTO 관리
API 마다 데이터 구조가 다르다하더라도 공통적으로 조회하는 필드는 존재한다. 이 때 공통 필드를 모아놓은 DTO 클래스를 생성하고 새로운 API를 개발할 때 공통 DTO를 상속받아 새 DTO 클래스를 생성하면 같은 필드를 DTO 마다 작성할 필요가 없기 때문에 DTO 클래스를 관리하기가 쉬워진다.
FolderReseponse
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FolderResponse {
private Long id;
private String name;
private int count;
}
Folder 엔티티에 대한 ResponseDto 클래스이다.
PK, 이름, Folder에 포함된 데이터 수를 전송하며 Folder 데이터를 조회할 때 공통적으로 조회하는 필드이다.
만약 폴더에 포함된 데이터 정보까지 같이 조회해야하는 API를 개발해야한다고 가정해보자.
데이터 정보를 포함한 DTO 클래스를 아래와 같이 작성했다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FolderDetailResponse {
private Long id;
private String name;
private int count;
private List<ArchiveResponse> archiveResponses;
}
id, name, count 필드가 FolderResponse 클래스와 중복되는데 위와 같은 경우 문제점은 만약 Folder 데이터를 조회하는 API를 여러 사람이 개발할 때 필드명이 제각각일 수 있다는 점이다. 또한 응답 데이터 구조에 변동이 생겼을 때 모든 DTO의 응답 구조를 수정해야한다는 점이다. 이를 방지하기 위해 상속 구조를 사용하여 아래와 같이 수정했다.
FolderResponse
@Getter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class FolderResponse {
private Long id;
private String name;
private int count;
public static FolderResponse fromEntity(Folder folder) {
return fromEntity(folder, builder());
}
public static <T extends FolderResponse> T fromEntity(Folder folder, FolderResponse.FolderResponseBuilder<T, ?> builder) {
return builder
.id(folder.getId())
.name(folder.getName())
.count(folder.getFolderArchives().size())
.build();
}
}
FolderDetailResponse
@Getter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class FolderDetailResponse extends FolderResponse {
private List<ArchiveResponse> archiveResponses;
public static FolderDetailResponse fromEntity(Folder folder) {
return fromEntity(
folder,
builder().archiveResponses(folder.getFolderArchives().stream().map(fa -> ArchiveResponse.fromEntity(fa.getArchive())).toList()));
}
}
부모 클래스의 필드를 빌더 패턴으로 생성하기 위해 @SuperBuilder 어노테이션을 사용했다.
또한 FolderResponse 클래스를 상속 받은 자식 클래스에서 부모 클래스 필드를 포함한 응답 데이터를 생성하기 위해 FolderResponse 클레스에 제네릭 빌더 메서드를 구현했다.
위와 같은 방법으로 상속 구조를 이용해 DTO를 관리하면 공통 필드가 많을수록 중복된 코드를 줄일 수 있고, 유지보수성을 크게 향상시킬 수 있다. 또한, 새로운 API 개발 시 공통 필드를 재사용함으로써 일관된 응답 구조를 유지할 수 있고, 구조 변경이 있을 때도 부모 DTO만 수정하면 되기 때문에 수정 범위가 최소화된다.
Static Inner Class
상속 구조를 사용하더라도 API 마다 Dto 클래스를 생성해야 하는 것은 변함없다. FolderDetailResponse 클래스의 ArchiveResponse Dto 클래스의 경우 다른 API에서도 단독으로 사용되기 때문에 상관없지만 해당 API에서만 사용하는 Response 클래스의 경우 전부 따로 생성하면 관리해야 할 클래스 파일이 늘어나게 된다.
하지만 아래와 같이 FolderResponse 클래스 내부에서 Inner 클래스를 사용하면 별도의 Dto 클래스 파일을 생성할 필요없이 FolderResponse 클래스 안에서 전부 관리할 수 있다.
FolderResponse
@Getter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class FolderResponse {
private Long id;
private String name;
private int count;
private OwnerResponse ownerResponse;
public static FolderResponse fromEntity(Folder folder, OwnerResponse ownerResponse) {
return fromEntity(folder, ownerResponse, builder());
}
public static <T extends FolderResponse> T fromEntity(Folder folder, OwnerResponse ownerResponse, FolderResponse.FolderResponseBuilder<T, ?> builder) {
return builder
.id(folder.getId())
.name(folder.getName())
.count(folder.getFolderArchives().size())
.ownerResponse(ownerResponse)
.build();
}
@Builder
public static class OwnerResponse {
private long userId;
private String name;
public OwnerResponse from(User user) {
return builder()
.userId(user.getId())
.name(user.getName())
.build();
}
}
}
폴더의 소유자 정보를 갖고 있는 OwnerResponse 클래스를 FolderResponse 클래스에서 static inner 클래스로 생성하였다.
이제 OwnerResponse 클래스에 대한 정보를 보고싶다면 클래스를 이동할 필요없이 FolderResponse에서 확인할 수 있으며 만약 OwnerResponse 클래스를 다른 API에서도 공용으로 사용하게 된다면 따로 클래스 파일로 분리하여 공용 DTO 패키지에서 관리하면 훨씬 수월할 것이다.
물론 가장 중요한 점은 체계적인 패키지 구조로 Dto 클래스를 구분하고 명확한 클래스 명명 규칙으로 한눈에 Dto 클래스를 식별할 수 있어야 한다는 점이다.
'Spring' 카테고리의 다른 글
[Spring] Spring AOP를 사용하여 사용자 권한 체크하기 (0) | 2024.09.17 |
---|---|
[Spring] 추상 팩토리 패턴을 사용하여 소셜 로그인 구현하기 (1) | 2024.09.16 |
헥사고날 아키텍처(Hexagonal Architecture) (0) | 2024.01.19 |
[Spring] Master, Slave 데이터베이스 구조로 쓰기, 읽기 연산 나누기 (0) | 2023.07.28 |
[Spring] 디자인 패턴 - 전략 패턴(Strategy Pattern) (0) | 2023.07.01 |