Files

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 기준과 충돌한다
  • 정상 흐름과 에러 흐름이 한 메서드에 뒤섞인다