5.9 KiB
5.9 KiB
API Controller 예시
좋은 예시
예시 1. controller는 request DTO를 받아 use case를 호출하고 표준 응답을 반환한다
@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 제어가 필요할 때만 사용한다
@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. 입력 출처를 시그니처에 명시한다
@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는 예외를 직접 잡지 않는다
@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를 직접 호출한다
@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로 직접 받는다
@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로 감싼다
@RestController
@RequestMapping("/api/health")
public class BadHealthController {
@GetMapping
public ResponseEntity<ApiResult<String>> health() {
return ResponseEntity.ok(ApiResult.success("ok"));
}
}
나쁜 이유:
- 추가로 제어할 status/header가 없다
- 불필요한 ceremony만 늘어난다
예시 4. controller 안에서 공통 예외를 직접 처리한다
@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 기준과 충돌한다
- 정상 흐름과 에러 흐름이 한 메서드에 뒤섞인다