init: 클린 기반 auth 서버 설계
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# API Controller 예시
|
||||
|
||||
## 좋은 예시
|
||||
|
||||
### 예시 1. controller는 request DTO를 받아 use case를 호출하고 표준 응답을 반환한다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/sessions")
|
||||
public class SessionCommandController {
|
||||
|
||||
private final CreateSessionUseCase createSessionUseCase;
|
||||
|
||||
@PostMapping
|
||||
public ApiResult<CreateSessionResponse> create(
|
||||
@Valid @RequestBody CreateSessionRequest request
|
||||
) {
|
||||
CreateSessionResult result = createSessionUseCase.create(
|
||||
request.email(),
|
||||
request.password(),
|
||||
request.loginType()
|
||||
);
|
||||
|
||||
return ApiResult.success(CreateSessionResponse.from(result));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**좋은 이유:**
|
||||
|
||||
- @RestController가 API 용도와 맞다
|
||||
- JSON body를 전용 request DTO로 받는다
|
||||
- controller가 use case 호출과 응답 반환에 집중한다
|
||||
|
||||
### 예시 2. ResponseEntity는 HTTP 제어가 필요할 때만 사용한다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/users")
|
||||
public class UserCommandController {
|
||||
|
||||
private final RegisterUserUseCase registerUserUseCase;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResult<UserCreatedResponse>> register(
|
||||
@Valid @RequestBody RegisterUserRequest request
|
||||
) {
|
||||
UserCreatedResult result = registerUserUseCase.register(request.email(), request.password());
|
||||
UserCreatedResponse response = UserCreatedResponse.from(result);
|
||||
|
||||
URI location = URI.create("/api/users/" + response.userId());
|
||||
|
||||
return ResponseEntity.created(location)
|
||||
.body(ApiResult.success(response));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**좋은 이유:**
|
||||
|
||||
- 201 Created와 Location 헤더가 필요한 경우에만 ResponseEntity를 사용한다
|
||||
- 모든 endpoint를 습관적으로 ResponseEntity로 감싸지 않는다
|
||||
|
||||
### 예시 3. 입력 출처를 시그니처에 명시한다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/users")
|
||||
public class UserQueryController {
|
||||
|
||||
private final UserQueryUseCase userQueryUseCase;
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ApiResult<UserResponse> getUser(
|
||||
@PathVariable String userId,
|
||||
@RequestParam(defaultValue = "false") boolean includeInactive
|
||||
) {
|
||||
UserResult result = userQueryUseCase.getUser(userId, includeInactive);
|
||||
return ApiResult.success(UserResponse.from(result));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**좋은 이유:**
|
||||
|
||||
- path와 query 입력 출처가 시그니처에서 구분된다
|
||||
- HttpServletRequest 전체를 들고 오지 않아도 되는 입력은 annotation으로 처리한다
|
||||
- request id 같은 관측용 헤더는 use case 입력으로 섞지 않는다
|
||||
|
||||
### 예시 4. controller는 예외를 직접 잡지 않는다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/password")
|
||||
public class PasswordController {
|
||||
|
||||
private final ChangePasswordUseCase changePasswordUseCase;
|
||||
|
||||
@PostMapping("/change")
|
||||
public ApiResult<Void> changePassword(
|
||||
@Valid @RequestBody ChangePasswordRequest request
|
||||
) {
|
||||
changePasswordUseCase.change(
|
||||
request.userId(),
|
||||
request.currentPassword(),
|
||||
request.newPassword()
|
||||
);
|
||||
|
||||
return ApiResult.success(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**좋은 이유:**
|
||||
|
||||
- 예외는 @RestControllerAdvice에서 통합 처리할 수 있다
|
||||
- controller가 공통 에러 응답 정책을 직접 품지 않는다
|
||||
|
||||
## 나쁜 예시
|
||||
|
||||
### 예시 1. controller가 repository를 직접 호출한다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/users")
|
||||
public class BadUserController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ApiResult<User> getUser(@PathVariable Long userId) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
return ApiResult.success(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**나쁜 이유:**
|
||||
|
||||
- controller가 persistence access를 직접 수행한다
|
||||
- domain/entity가 외부 응답 모델로 직접 노출된다
|
||||
- application boundary가 사라진다
|
||||
|
||||
### 예시 2. entity를 request body로 직접 받는다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class BadUserCommandController {
|
||||
|
||||
@PostMapping
|
||||
public ApiResult<Void> create(@Valid @RequestBody User user) {
|
||||
return ApiResult.success(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**나쁜 이유:**
|
||||
|
||||
- request model과 domain/persistence model이 섞인다
|
||||
- 웹 입력 변경이 domain/entity 구조에 직접 번진다
|
||||
|
||||
### 예시 3. 모든 응답을 습관적으로 ResponseEntity로 감싼다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/health")
|
||||
public class BadHealthController {
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResult<String>> health() {
|
||||
return ResponseEntity.ok(ApiResult.success("ok"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**나쁜 이유:**
|
||||
|
||||
- 추가로 제어할 status/header가 없다
|
||||
- 불필요한 ceremony만 늘어난다
|
||||
|
||||
### 예시 4. controller 안에서 공통 예외를 직접 처리한다
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/sessions")
|
||||
public class BadSessionController {
|
||||
|
||||
private final CreateSessionUseCase createSessionUseCase;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResult<?>> create(@RequestBody CreateSessionRequest request) {
|
||||
try {
|
||||
return ResponseEntity.ok(ApiResult.success(
|
||||
createSessionUseCase.create(request.email(), request.password(), request.loginType())
|
||||
));
|
||||
} catch (InvalidCredentialException ex) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResult.fail(ErrorCode.INVALID_CREDENTIAL));
|
||||
} catch (Exception ex) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResult.fail(ErrorCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**나쁜 이유:**
|
||||
|
||||
- controller마다 예외 정책이 중복된다
|
||||
- 전역 advice 기준과 충돌한다
|
||||
- 정상 흐름과 에러 흐름이 한 메서드에 뒤섞인다
|
||||
Reference in New Issue
Block a user