Files
project-auth-server/docs/examples/web/request-response-dto.md
T

203 lines
5.0 KiB
Markdown

# Request / Response DTO 예시
## 좋은 예시
### 예시 1. request와 response를 명확히 분리한다
```java
public record CreateUserRequest(
@NotBlank String email,
@NotBlank String password,
@NotBlank String displayName
) {
}
public record CreateUserResponse(
String userId,
String email,
String displayName
) {
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserCommandController {
private final RegisterUserUseCase registerUserUseCase;
@PostMapping
public ApiResult<CreateUserResponse> register(@Valid @RequestBody CreateUserRequest request) {
RegisteredUser registeredUser = registerUserUseCase.register(
request.email(),
request.password(),
request.displayName()
);
return ApiResult.success(new CreateUserResponse(
registeredUser.userId(),
registeredUser.email(),
registeredUser.displayName()
));
}
}
```
**좋은 이유:**
- request와 response 역할이 분리된다
- request DTO가 그대로 내부 모델처럼 전파되지 않는다
- 응답이 entity 구조가 아니라 API 계약 중심으로 표현된다
### 예시 2. query/form 입력은 전용 @ModelAttribute DTO로 받는다
```java
public record UserSearchRequest(
@NotBlank String keyword,
@Min(1) int page,
@Min(1) @Max(100) int size
) {
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserQueryController {
private final SearchUsersUseCase searchUsersUseCase;
@GetMapping
public ApiResult<UserSearchResponse> search(@Valid @ModelAttribute UserSearchRequest request) {
UserSearchResult result = searchUsersUseCase.search(
request.keyword(),
request.page(),
request.size()
);
return ApiResult.success(UserSearchResponse.from(result));
}
}
```
**좋은 이유:**
- query 입력도 전용 web model로 분리된다
- @ModelAttribute 대상이 domain/entity가 아니다
- 검색 조건과 응답 모델이 분리된다
### 예시 3. ResponseEntity는 HTTP 제어가 필요할 때만 사용한다
```java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserCommandController {
private final RegisterUserUseCase registerUserUseCase;
@PostMapping
public ResponseEntity<ApiResult<CreateUserResponse>> register(
@Valid @RequestBody CreateUserRequest request
) {
RegisteredUser registeredUser = registerUserUseCase.register(
request.email(),
request.password(),
request.displayName()
);
CreateUserResponse response = new CreateUserResponse(
registeredUser.userId(),
registeredUser.email(),
registeredUser.displayName()
);
return ResponseEntity.created(URI.create("/api/users/" + response.userId()))
.body(ApiResult.success(response));
}
}
```
**좋은 이유:**
- 201 Created와 Location 제어가 필요해 ResponseEntity 사용 이유가 분명하다
## 나쁜 예시
### 예시 1. entity를 request body로 직접 받는다
```java
@Entity
public class User {
@Id
private Long id;
private String email;
private String role;
}
@PostMapping("/api/users")
public ApiResult<Void> create(@Valid @RequestBody User user) {
...
}
```
**나쁜 이유:**
- web input model과 persistence/domain model이 섞인다
- 바인딩 범위가 불필요하게 넓다
- API 변경이 entity 구조에 직접 번진다
### 예시 2. request와 response를 하나의 DTO로 재사용한다
```java
public record UserDto(
String userId,
String email,
String password,
String displayName,
String role
) {
}
@PostMapping("/api/users")
public ApiResult<UserDto> create(@RequestBody UserDto request) {
...
}
```
**나쁜 이유:**
- 요청과 응답의 책임이 섞인다
- 응답에 불필요하거나 민감한 필드가 섞이기 쉽다
- write model과 read model이 분리되지 않는다
### 예시 3. DTO를 내부 모델처럼 그대로 넘긴다
```java
@PostMapping("/api/users")
public ApiResult<Void> create(@Valid @RequestBody CreateUserRequest request) {
registerUserUseCase.register(request);
return ApiResult.success(null);
}
```
**나쁜 이유:**
- request DTO가 presentation 경계를 넘어 application 시그니처로 새어 나간다
- 내부 use case가 transport model에 결합된다
### 예시 4. 응답으로 entity를 직접 반환한다
```java
@GetMapping("/api/users/{userId}")
public ApiResult<User> get(@PathVariable Long userId) {
User user = userRepository.findById(userId).orElseThrow();
return ApiResult.success(user);
}
```
**나쁜 이유:**
- persistence/domain 구조가 외부 계약이 된다
- 내부 필드가 의도치 않게 노출되기 쉽다
- controller가 repository와 entity에 직접 결합된다