본문 바로가기

우테코

프로젝트 API문서 작성을 위한 Swagger 도입기

목차

 

1. API 문서 선정 기준

 

2. Swagger 선정 이유

 

3. 프로젝트 API 작성 사이클

 

4. Swagger 단점 극복기

    4-1) 프로덕션 코드 침해 > interface 분리

    4-2)  중복 코드 > 커스텀 애너테이션

 

5. 느낀 점

 


프로젝트에 들어오기 전 방학 때 가장 신경썼던 것이 무엇이냐 묻는다면 단연코 프론트와의 협력에서 가장 중요한 API문서이다. 그래서 Lv2 방학 기간 동안 Rest Docs와 Swagger를 각각 사용해보며 장단점을 알아보는 시간을 가지기도 했다.

 

>> [Rest Docs vs Swagger] 시리즈

 

이번 글에서는 우리 팀이 API 문서 작성을 Swagger로 선정하게된 이유와 Swagger의 단점을 극복하기 위해 고민해본 몇가지 노력을 적어보고자 한다.

 


1. API 문서 툴 선정 기준

 

1) 효율성 : 적은 리소스(프론트 크루들의 익숙함 + 적은 코드 작성 시간)

2) 신속성 : 빠른 docs 배포를 통한 신속한 개발환경 마련

3) 테스트 가능성 : QA 이전까지 테스트 가능한 환경마련으로 프론트 코드 안전성 확보

4) 안전성 : API 문서 불일치 최소화

 

이렇게 4가지가 API 문서 툴 선정의 기준이었다. 전반적으로 빠른 Docs 배포와 안드로이드 크루들의 의견을 많이 반영하는 것에 의견이 맞추어 졌다. 

 


2. Swagger 선정 이유

총 4가지 대안이 나왔다

1) Swagger -> 채택
2) Rest Docs
3) Post Man
4) Notion

 

그럼 각각의 기준에서 Swagger 선정 이유를 살펴보자

 

1) 효율성 : 안드로이드 크루들의 익숙함

안드로이드 크루들이 이전 Lv1과 Lv2 미션과정에서 Swagger를 사용했었다. 


2) 신속성 : 빠른 API 문서 제작 가능

Swagger는 Rest Docs와 달리 테스트 코드 작성을 강제하지 않는다. 따라서 테스트 코드 작성 이전에 API 문서를 제작가능하며 안드로이드 측에게 빠른 코드 개발 환경을 제공해줄 수 있었다.

 

물론 이 특징은 양날의 검이다. 신속성이 올라간 만큼 테스트 코드가 없는 상황에서 API 안전성을 보장하지 못할 수 있고 실제 작동 코드와의 일치하는지도 보장할 수 없다. 그러나, 프로젝트가 2주 단위의 스프린트로 진행되는 만큼 신속성 > 안전성이라는데 어느정도 의견이 맞추어졌다.


3) 테스트 가능성 : OpenApi UI를 통한 테스트 가능

 

Swagger의 OpenApi UI를 통해 안드로이드 측에서 테스트를 진행 가능했다.


4) 안전성 : API 문서 불일치 최소화

 

Post Man과 Notion을 통한 API 문서를 제공하지 않게 된 이유기도 하다. API는 개발 과정에서 유동성이 크다. 따라서 변경이 있을 때마다 문서와 실제 API가 같이 변동되어야 한다. 그러나 PostMan과 Notion의 경우 사람이 직접 수기로 API 하나하나를 변경해주어야 하다보니 휴먼에러의 가능성이 있었다.

 

Swagger의 경우 @Schema 설정을 통해 Request / Response 객체의 변화나 API 변동 사안들을 바로바로 반영 가능했다.

 


3. 오디 팀의 API 문서 작성 사이클

1단계 - API 협의 : 안드로이드 크루들과 노션에서  API의 기본적 틀 합의

2단계  - 임시 문서 제공 : 더미 데이터가 반환되는 임시 API 문서를 제공해 빠른 안드로이드 개발 환경을 제공

3단계 - 실제 로직 대체 : API 구현과 동시에 실제 로직을 연결

 

각 단계를 살펴보자


1단계 : API 문서 협의

 

먼저 안드로이드 크루들과 API의 기본적 틀을 합의한다. 여기서 기본적 틀이라 함은 Header, Request / Response 객체의 형태, Restful API, 에러 상황 등등을 의미한다.

 

> 예시 API

더보기

Request Body

 

이름 타입 설명 필수
isMissing Boolean 위치추적 불가 여부 O
currentLatitude String 현재 위도 X
currentLongitude String 현재 경도 X
{
	"isMissing": false,
	"currentLatitude": "39.123345",
	"currentLongitude": "126.234524"
}

Response Body


이름   형식 설명 부연 필수
ownerNickname   String 기기 사용자 닉네임    
mateEtas   List<MateEtaResponse> 참여자 도착 정보 리스트   O
  nickname String 참여자 닉네임   O
  status String 참여자 ETA에 따른 상태 지각 위기: LATE_WARNING  
도착 예정: ARRIVAL_SOON          
도착: ARRIVED          
지각: LATE          
추적 불가: MISSING O        
  durationMinutes Long 도착지까지 남은 소요시간   O

예시

성공 - 200

예시1) 약속 시간 전
{
  "ownerNickname" : "카키공주"
	"mateEtas": [
		{
			"nickname": "콜리",
			"status": "LATE_WARNING",
			"durationMinutes": 83
		},
		{
			"nickname": "올리브",
			"status": "ARRIVAL_SOON",
			"durationMinutes": 10
		},
		{
			"nickname": "해음",
			"status": "ARRIVED",
			"durationMinutes": 0
		},
		{
			"nickname": "카키공주",
			"status": "MISSING",
			"durationMinutes": -1
		}
	]
}

 

예시2) 약속 시간 후
{
  "ownerNickname" : "카키공주",
	"mateEtas": [
		{
			"nickname": "콜리",
			"status": "LATE"
			"durationMinutes": 30
		},
		{
			"nickname": "올리브",
			"status": "ARRIVED"
			"durationMinutes": 0
		},
		{
			"nickname": "해음",
			"status": "ARRIVED"
			"durationMinutes": 0
		},
		{
			"nickname": "카키공주",
			"status": "MISSING"
			"durationMinutes": -1
		}
	]
}

실패

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 401,
  "detail": "에러 설명 메시지",
  "instance": "/problemDetailTest"
}

 


400 클라이언트 입력 오류 또는 허용되지 않는 요청 시간
401 인증되지 않은 사용자
404 존재하지 않는 모임이거나 해당 모임 참여자가 아닌 경우
500 서버 오류

 


2단계 : 임시 API 문서 작성

  • 합의된 API에 기재된 더미 데이터를 하드 코딩하여 제공한다
  • 빠르게 만들어진 Swagger API Docs로 신속한 개발 환경을 구축한다

 

더미 데이터를 반환하는 API의 예시를 보면 다음과 같다

    @Override
    @PostMapping("/meetings")
    public ResponseEntity<MeetingSaveResponse> save(
            @AuthMember Member member,
            @Valid @RequestBody MeetingSaveRequest meetingSaveRequest
    ) {
        MeetingSaveResponse meetingSaveResponse = new MeetingSaveResponse(
                1L,
                "우테코 16조",
                LocalDate.parse("2024-07-15"),
                LocalTime.parse("14:00"),
                "서울 송파구 올림픽로35다길 42",
                "37.515298",
                "127.103113",
                1,
                List.of(new MateResponse("오디")),
                "초대코드"
        );
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(meetingSaveResponse);
    }

 

이렇게 임시 API문서를 작성한 상황에서 안드로이드 크루들은 빠르게 테스트 가능한 개발 환경을 보장받게 된다.

 

실제로 Swagger에서 Test했을 때 하드코딩된 더미 데이터가 반환되는 모습을 볼 수 있다.


3단계 : 실제 API 문서 연결

  • 실제 로직을 개발한다
  • 더미 데이터가 반환되던 API를 실제 로직으로 대체한다

즉, API 하나하나를 개발 완료할 때마다 더미 데이터 대신 실제 로직을 이어주는 식으로 문서를 실체화 해나가는 것이다.

 

앞서 더미데이터를 반환하던 controller는 실제 로직 대체 후에는 다음과 같은 모습이 될 것이다.

    @Override
    @PostMapping("/meetings")
    public ResponseEntity<MeetingSaveResponse> save(
            @AuthMember Member member,
            @Valid @RequestBody MeetingSaveRequest meetingSaveRequest
    ) {
        MeetingSaveResponse meetingSaveResponse = meetingService.save(member, meetingSaveRequest)
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(meetingSaveResponse);
    }

 

 


사이클 정리

API 협의 (by Notion)   >>   임시 API 문서 제공(더미 데이터 반환)   >>   실제 로직 대체 후 문서 제공

 

이 3가지 사이클을 통해 현 프로젝트에서는

안드로이드 크루들에게 빠른 개발 환경을 제공함과 동시에

변동에 따른 API 문서 불일치를 줄이고 있다.

 

그러나, Swagger를 사용하면서 겪게 되는 본질적인 한계는 어떨까? 감수할 수 밖에 없는 걸까?

[Swagger 의 큰 단점 2가지]
- 프로덕션 코드에 Swagger 코드가 혼재된다
- 반복되는 애너테이션/ 코드가 많다

 

백엔드 크루들끼리 다른 활용 사례를 살펴보며 이 단점을 조금이나마 극복할 수 있는 방안을 찾아냈다.


4. Swagger 단점 극복하기

 

단점1 : 프로덕션 코드 침범 > interface로 분리

 

controller의 public interface들을 따로 분리하여 swagger 관련 설정만을 따로 관리하게 해주었다.

 

예를 들어 memberController에 대한 swagger 설정은 MemberControllerSwagger interface에서만 관리한다. MemberController는 애플리케이션 코드만 관리할 수 있도록 관심사를 나누었다.

 

자세히 살펴보면, MemberControllerSwagger의 경우 Swagger 관련 설정만 담겨있다

@Tag(name = "Member API")
@SecurityRequirement(name = "Authorization")
public interface MemberControllerSwagger {

    @Operation(
            summary = "회원 추가",
            responses = @ApiResponse(responseCode = "201", description = "회원 추가 성공")
    )
    @ErrorCode500
    ResponseEntity<Void> save(@Parameter(hidden = true) String authorization);
}

 

MemberController는 MemberControllerSwagger를 구현하여 관련 Swagger 설정을 오버라이딩 한다. 그렇게 되면 Controller 단에서는 실제 애플리케이션 코드만 존재한다.

@Slf4j
@RestController
@RequiredArgsConstructor
public class MemberController implements MemberControllerSwagger {

    private final MemberService memberService;

    @Override
    @PostMapping("/members")
    public ResponseEntity<Void> save(@RequestHeader("Authorization") String authorization) {
        memberService.save(new DeviceToken(authorization));
        return ResponseEntity.status(HttpStatus.CREATED)
                .build();
    }
}

 

이 구조의 장점은 '관심사의 분리' 이다. API 문서를 위한 코드와 애플리케이션 로직 코드를 구분하게 해줌으로써 수정포인트에 따라 유지 보수를 원활하게 해준다.

 

그러나 단점도 존재한다. OOP에서 규정한 interface와는 다른 용도로 interface를 활용하고 있다는 것이며,  그 결과 두 클래스간 결합도가 커서 controller API 수정 시 각각의 클래스를 모두 수정해주어야 한다.

 


단점2 : 중복되는 코드 > 커스텀 에너테이션 선언

Swagger는 애너테이션을 활용하여 선언적으로 문서를 구성하다 보니 각각의 API마다 중복되는 코드가 필연적으로 발생하게 된다. 이 경우 커스텀 에너테이션을 활용해 중복코드를 최대한 줄여주었다.

 

예를 들어 반복되는 에러코드의 경우 다음과 같이 각 커스텀 애너테이션을 선언했다.

 

@ErrorCode400은 다음과 같이 구현되어 있다

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ApiResponse(
        responseCode = "400",
        description = "클라이언트 입력 오류",
        content = @Content(schema = @Schema(implementation = ProblemDetail.class))
)
public @interface ErrorCode400 {

    @AliasFor(annotation = ApiResponse.class, attribute = "description")
    String description() default "클라이언트 입력 오류";
}

 

이렇게 커스텀 에너테이션을 설정해놓으면 Swagger API 문서 설정에서 중복되는 에러 코드를 애너테이션으로 대체할 수 있게 된다.

 

예를 들어 다음과 같은 형태로 말이다.

@Operation(
            summary = "참여자 재촉하기",
            requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = NudgeRequest.class))),
            responses = {@ApiResponse(responseCode = "200", description = "재촉하기 성공")}
    )
    @ErrorCode400(description = "유효하지 않은 mateId")
    @ErrorCode401
    @ErrorCode500
    ResponseEntity<Void> nudgeMate(NudgeRequest nudgeRequest);

5. 느낀 점

결국, 사용자의 입장이 중요하다

 

사실 API 문서의 경우 백엔드보다 안드로이드/프론트 를 위해 작성하는 만큼, 사용자가 익숙한 툴이 무엇인지 또, 어떤 ui를 선호하는 지가 우선적으로 고려될 필요가 있다고 생각했다. 실제로 프론트 크루들이 Swagger를 사용해달라고 백엔드 측에 강력히 요청했다는 팀도 있었다.

 

무언가 불안정한 Swagger, Rest Docs와의 융합은 어떨까?

 

이 사이클이 빠른 건 좋았다. 그런데 무언가 불안했다. Swagger가 문자열에 많이 의존하는 만큼 실수에 취약하고 또 API 일관성이 보장되지 않는 부분들이 기시감으로 계속 존재했다. 다른 팀에서는 OpenApi Ui와 Rest Docs를 융합하여 각 툴의 장점만을 극대화하여 사용한다고 들었다. 어차피 테스트 커버리지를 높일 것이라면 혹은 테스트의 가치를 높게 볼수록 Rest Docs와 함께 사용하는 방안도 상당히 매력적으로 느껴졌다.

 

단점은 개선하면 되는 것이지 근본적인 한계가 아니다.

 

커스텀 애너테이션을 미리 학습하고 문서 작성 시, 제안할 수 있어서 조금 뿌듯했다. 방학 때 swagger를 미리 알아본 효과가 있었던 느낌? 이후에도 조조가 interface 구조를 제안해주었을 때도 내가 생각치 못한 방안이라 놀랐다. 결국 머리를 맞대면 방법은 나온다고 일전에 내가 swagger의 단점이라 못박았던 것들이 조금은 부끄러워졌다. 개선책을 고민해보지도 않고 극복하지 못하는 문제라 규정하는 순간 코드는 그곳에서 머물렀다. 그 이상의 발전은 없었다.

 

기억하자. 단점은 스스로 던져야 할 질문이지, 누군가 우리에게 내리는 선언이 아니다.