본문 바로가기

(11.14) Spring Boot - Swagger UI, Swagger API 문서화, Swagger 적용

@starweb2025. 11. 14. 18:03

[ 13주차 - 1114 ]

    금일 커리큘럼
        ├ 09:00 ~ 12:00 backend 프로그래밍 (Swagger API 문서화, Swagger 적용 방법)
        └ 13:00 ~ 18:00 backend 프로그래밍 (Swagger Code-First 방식 구현)

1. Swagger API 문서화

Swagger : RESTful API 문서를 자동으로 생성해주는 오픈소스 프레임워크

API 문서화 필요성

  • 백엔드 ↔ 프론트엔드 개발자 간의 원활한 소통
  • API 명세서를 수동으로 작성하면 오류 발생 가능 (사람에 의한 실수)
  • Swagger를 사용하면 API 문서를 자동 생성 + 유지보수 용이
  • 버전 관리, 팀 개발에 매우 중요한 요소임

API 문서화의 필수 요소

  • 엔드포인트(Endpoint) : API의 URL 경로 (예: /api/users)
  • HTTP 메서드(HTTP Method) : GET, POST, PUT, DELETE 등
  • 요청 파라미터(Request Parameters) : 쿼리 파라미터, 경로 변수, 요청 본문 등
  • 응답(Response) : 응답 본문, 상태 코드 등
  • 보안(Security) : 인증 및 권한 부여 정보

Swagger의 주요 기능

  • 자동 문서 생성 : 코드 주석을 기반으로 API 문서를 자동으로 생성
  • 인터랙티브 UI : Swagger UI를 통해 API를 테스트하고 상호작할 수 있는 웹 인터페이스 제공
  • 다양한 언어 및 프레임워크 지원 : 다양한 프로그래밍 언어와 프레임워크에서 사용 가능
  • 확장성 : 플러그인 및 확장 기능을 통해 기능 추가 가능

Swagger 데모로 확인


2. Swagger 적용 방식

Swagger 적용하기 앞서 code-first 방식과 design-first 방식이 존재함

  • Code-First 방식 : 기존에 작성된 코드를 기반으로 Swagger 문서를 생성
  • Design-First 방식 : Swagger 를 따로 명세파일로 작성 (YAML/JSON)

Code-First 방식

  • 이미 작성된 Spring 코드(Controller, DTO) 에 애노테이션을 붙여 Swagger 문서를 자동 생성하는 방식.
  • 코드와 문서가 항상 동일하게 유지됨
@Operation(summary = "회원가입")
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequestDto dto) { ... }

Design-First 방식 (openapi.yaml 기반)

  • Swagger 문서를 YAML/JSON 파일(OpenAPI 스펙) 로 먼저 설계하고, 그 문서를 기준으로 개발을 진행하는 방식.
  • API를 문서 중심으로 먼저 설계
  • 개발팀, 프론트엔드 팀이 문서만으로 API 계약을 미리 공유
  • 단, 문서와 코드가 따로 관리되므로 유지보수가 어려울 수 있음
  • 해당 에디터 참고 URL : https://swagger.io/tools/swagger-editor/
openapi: 3.0.4
info:
  title: Swagger Petstore - OpenAPI 3.0
  description: This is a sample Pet Store Server
  version: 1.0.12
paths:
  /pets:
    get:
      summary: List all pets
      responses:
        200:
          description: OK

정리 비교

구분 Code-First Design-First
문서 작성 시점 코드 작성 후 문서 먼저
문서 관리 위치 Controller 애노테이션 openapi.yaml
난이도 쉬움 중간~높음
기업 실무 사용 소규모 중소기업 대규모 서비스(카카오, 네이버 등)
Swagger UI O O
Mock Server 약함 매우 강력
자동 SDK 생성 X O

3. Swagger 적용하기 (code-first 방식)

3.1 프로젝트 의존성 추가 (gradle)

dependencies {
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5' // Swagger UI
    implementation 'com.auth0:java-jwt:4.4.0' // JWT 라이브러리


    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

3.2 Swagger 설정 파일 작성

  • 추천경로 : config/SwaggerConfig.java
@Configuration
@OpenAPIDefinition(
    info = @Info(
        title = "Swagger API 타이틀",
        version = "1.0",
        description = "API 문서입니다."
    )
)
@SecurityScheme(
    name = "bearerAuth",
    type = SecuritySchemeType.HTTP,
    scheme = "bearer",
    bearerFormat = "JWT"
)
public class SwaggerConfig { }

기능 설명

  • @OpenAPIDefinition : 화면의 전체 문서 정보 설정
  • @SecurityScheme : JWT 인증 스키마 설정
    • Swagger UI에 “Authorize” 버튼 자동 생성된다

3.3 Swagger UI 접속 하기

  • 프로젝트 실행 후 브라우저에서 아래 URL 접속
http://localhost:8080/swagger-ui/index.html

3.4 Swagger 애노테이션으로 문서화

1) API 그룹섹션 만들기 (@Tag)

  • @Tag : API 그룹 이름과 설명을 정의
  • 위치 : Controller 클래스
@Tag(name = "Auth", description = "회원 인증 관련 API")
@RestController
@RequestMapping("/auth")
public class AuthController { /** ... */ }

2) 엔드포인트 문서화 (@Operation)

  • @Operation : 해당 API의 요약(summary) , 설명(description) 을 설정
  • 위치 : Controller 내 메서드
// ... 생략
public class AuthController {
    @Operation(
        summary = "회원가입",
        description = "email, password 정보로 등록"
    )
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequestDto dto) { /** ... */ }
}

3) 응답 상태코드 문서화 (@ApiResponses)

  • 각 API가 어떤 결과를 반환하는지 문서로 남길 때 사용
  • @ApiResponses : 여러 응답
    • value 속성에 @ApiResponse 여러 응답 정의가능
  • @ApiResponse : 단일 응답
    • responseCode : HTTP 상태 코드
    • description : 응답 설명
    • content : 응답 본문 스키마 및 예시 정의
// ... 생략
public class AuthController {

    @Operation(
        summary = "회원가입",
        description = "email, password 정보로 등록"
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "200",
            description = "회원가입 성공시",
            content = @Content(
                mediaType = "text/plain",
                schema = @Schema(type = "String", example = "회원가입 성공")
            )
        ),
        @ApiResponse(
            responseCode = "400",
            description = "회원가입 실패시",
            content = @Content(
                mediaType = "text/plain",
                schema = @Schema(type = "string", example = "이미 존재하는 아이디")
            )
        )
    })
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequestDto dto) { /** ... */ }

}

4) 요청/응답 DTO 문서화 (@Schema)

  • @Schema : DTO 클래스 또는 필드에 적용하여 스키마 정보 설정
  • 위치 : DTO 클래스, 필드
@Getter
@Schema(description = "회원가입 요청 DTO (email, password)") // DTO 클래스 설명
public class RegisterRequestDto {

    @Schema(description = "유저 이메일", example = "user@test.com") // 필드 설명 및 예시
    private String email;

    @Schema(description = "유저 비밀번호", example = "1234")
    private String password;
}

5) 쿼리 파라미터 문서화 (@Parameter)

  • @Parameter : 쿼리 파라미터, 경로 변수, 요청 헤더 등을 문서화할 때 사용
  • 위치 : Controller 내 메서드의 파라미터 부분
// ... 생략
public class AuthController {

    @GetMapping("/users")
    @Operation(summary = "범위 조회" , description = "번호,크기로 조회 (/auth/users?page=1&size=10)")
    public ResponseEntity<?> getUsers(
            @Parameter(description = "페이지 번호", example = "1")
            @RequestParam int page,

            @Parameter(description = "페이지 크기", example = "10")
            @RequestParam int size
    ) {
        return ResponseEntity.ok(null);
    }

}

6) 보안 적용 문서화 (@SecurityRequirement)

  • @SecurityRequirement : 특정 API에 보안 요구사항을 명시
    • SwaggerConfig.java 에서 정의한 security scheme 이름과 동일해야 함
    • 설정시 Swagger UI에 "Authorize" 버튼이 활성화되고, 해당 API 호출 시 인증 정보 입력 가능
    • 해당 API 아코디언에도 자물쇠 아이콘이 표시됨
// ... 생략
public class AuthController {

    @Operation(summary = "단일 사용자 조회", description = "ID로 사용자 조회 (인증 필요)")
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/{id}")
    public ResponseEntity<?> findById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

}

4. Swagger 적용 심플 구현

4.1 프로젝트 구조

src/main/java/org/example/swaggerexam
├── config
│   └── SwaggerConfig.java            # Swagger OPEN API 설정
│
├── controller
│   ├── AuthController.java           # 인증/회원 기능 REST API 컨트롤러
│   └── ShopController.java           # 임시 섹션 늘리기용
│
├── domain
│   └── User.java                     # User 엔티티 (JPA 매핑)
│
├── dto
│   ├── LoginRequestDto.java          # 로그인 요청 DTO (email/password)
│   ├── LoginResponseDto.java         # 로그인 응답 DTO (accessToken)
│   ├── RegisterRequestDto.java       # 회원가입 요청 DTO
│   └── UserResponseDto.java          # 유저 정보 응답 DTO
│
├── repsoitory
│   └── UserRepository.java           # User JPA Repository
│
├── service
│   └── UserService.java              # User 비즈니스 로직 (회원가입/로그인/조회)
│
└── util
    └── JwtUtil.java                  # JWT 생성/검증/로그아웃(무효화) 유틸

4.2 Config Package

  • SwaggerConfig : Swagger OPEN API 설정
SwaggerConfig.java
@Configuration
@OpenAPIDefinition(
        info = @Info(
                title = "Swagger API 타이틀",
                version = "1.0",
                description = "제목하단 부연설명 - API 문서입니다."
        )
        // ,security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityScheme(
        name = "bearerAuth",
        type = SecuritySchemeType.HTTP,
        scheme = "bearer",
        bearerFormat = "JWT"
)
public class SwaggerConfig {
}

4.3 controller Package

  • AuthController : 인증/회원 기능 REST API 컨트롤러
  • ShopController : 임시 섹션 늘리기용
AuthController.java
// 섹션의 제목 + 부연설명 (기본은 클래스명으로 노출됨)
@Tag(name = "Auth", description = "회원 인증 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {
    public final UserService userService;

    /** 홈화면 */
    // 해당 api 아코디언 제목과 설명
    @Operation(
            summary="환영인사",
            description="인증하면 환영인사를 해줍니다."
    )
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/welcome")
    public String wecome() {
        return "welcome";
    }


    /** 회원가입 */
    @Operation(
            summary = "회원가입",
            description = "email, password 정보로 등록"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "회원가입 성공시",
                    content = @Content(
                            mediaType = "text/plain",
                            schema = @Schema(type = "String", example = "회원가입 성공")
                    )
            ),
            @ApiResponse(
                    responseCode = "400", 
                    description = "회원가입 실패시",
                    content = @Content(
                            mediaType = "text/plain",
                            schema = @Schema(type = "string", example = "이미 존재하는 아이디")
                    )
            )
    })
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequestDto requestDto) {
        try {
            String msg = userService.register(requestDto);
            return ResponseEntity.ok(msg);

        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }

    /** 로그인 */
    @Operation(summary = "로그인", description = "email, password로 로그인하고 JWT 발급")
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequestDto requestDto) {
        try {
            String token = userService.login(requestDto);
            return ResponseEntity.ok(
                        LoginResponseDto.builder()
                        .accessToken(token)
                        .build()
            );
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }

    /** 로그아웃 */
    @Operation(summary = "로그아웃", description = "JWT 무효화 처리 (JWT 인증 필요)")
    @SecurityRequirement(name = "bearerAuth")
    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) {
        // "Bearer xxxx" → 토큰만 추출
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        userService.logout(token);
        return ResponseEntity.ok("로그아웃 성공");
    }

    /** 전체 조회 */
    @Operation(summary = "전체 사용자 조회", description = "UserResponseDto로 반환 (JWT 인증 필요)")
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/users/all")
    public ResponseEntity<?> findAll() {
        return ResponseEntity.ok(userService.findAll());
    }
    /** 범위 조회 */
    @Operation(summary = "전체 사용자 범위 조회", description = "UserResponseDto로 반환 (JWT 인증 필요)")
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/users")
    public ResponseEntity<?> getUsers(
            @RequestParam(value = "page", required = false, defaultValue = "1") int page,
            @RequestParam(value = "size", required = false, defaultValue = "10") int size
    ) {
        return ResponseEntity.ok(userService.getUsers(page, size));
    }

    /** 단건 조회 */
    @Operation(summary = "단일 사용자 조회", description = "ID로 사용자 조회 (JWT 인증 필요)")
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/{id}")
    public ResponseEntity<?> findById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    /** 로그인한 회원 정보 */
    @Operation(summary = "내 정보", description = "토큰으로 자기자신 조회 (JWT 인증 필요)")
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/myinfo")
    public ResponseEntity<?> getMyInfo(@RequestHeader("Authorization") String token) {
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }

        Long userId = userService.getUserIdFromToken(token);

        return ResponseEntity.ok(userService.findById(userId));
    }
}
ShopController.java
@Tag(name = "Shop", description = "쇼핑 관련 API")
@RestController
@RequestMapping("/shop")
public class ShopController {
    @GetMapping
    public ResponseEntity<?> getShop() {
        return ResponseEntity.ok().build();
    }
}

4.4 domain Package

  • User : User 엔티티 (JPA 매핑)
User.java
@Entity
@Getter
@Setter
@Table(name="swagger_users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;                // 사용자 ID (PK)

    @Column(unique = true, nullable = false)
    private String email;           // 사용자 이메일 (UNIQUE)

    @Column(nullable = false)
    private String password;        // 비밀번호
}

4.5 dto Package

  • LoginRequestDto : 로그인 요청 DTO (email/password)
  • LoginResponseDto : 로그인 응답 DTO (accessToken)
  • RegisterRequestDto : 회원가입 요청 DTO
  • UserResponseDto : 유저 정보 응답 DTO
LoginRequestDto.java
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "로그인 요청 DTO (email, password)")
public class LoginRequestDto {
    private String email;
    private String password;
}
LoginResponseDto.java
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "로그인 응답 DTO (accessToken)")
public class LoginResponseDto {
    @Schema(description = "액세스 토큰")
    private String accessToken;
}
RegisterRequestDto.java
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "회원가입 요청 DTO (email, password)")
public class RegisterRequestDto {
    @Schema(description = "유저이메일", required = true, example = "example@example.com")
    private String email;
    @Schema(description = "유저비밀번호", example = "example123")
    private String password;
}
RegisterRequestDto.java
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "유저 정보 응답 DTO (id, email)")
public class UserResponseDto {
    private Long id;
    private String email;

    public static UserResponseDto from(User user) {
        return UserResponseDto.builder()
                .id(user.getId())
                .email(user.getEmail())
                .build();
    }
}

4.6 repository Package

  • UserRepository : User JPA Repository
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

4.7 service Package

  • UserService : User 비즈니스 로직 (회원가입/로그인/조회)
UserService.java
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

    /** 회원가입 */
    public String register(RegisterRequestDto dto) {
        //매개변수로 들어온 이메일이 이미 존재 하는지 체크
        if(userRepository.findByEmail(dto.getEmail()).isPresent()) {
            throw new IllegalArgumentException("이미 존재하는 아이디");
        } else {
            User user = new User();
            user.setEmail(dto.getEmail());
            user.setPassword(dto.getPassword());

            userRepository.save(user);
            return "회원가입 성공";
        }
    }
    /** 로그인 */
    @Transactional(readOnly = true)
    public String login(LoginRequestDto dto) {
        User user = userRepository.findByEmail(dto.getEmail())
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));

        if (!user.getPassword().equals(dto.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        // userId로 JWT 생성
        return jwtUtil.generateToken(user.getId());
    }

    /** 토큰으로 사용자 id 조회 */
    @Transactional(readOnly = true)
    public Long getUserIdFromToken(String token) {
        return jwtUtil.validateToken(token); // 유효하면 userId 반환 / 실패 시 null
    }

    /** 로그아웃: 토큰 무효화 */
    public void logout(String token) {
        jwtUtil.invalidateToken(token);
    }

    /** 전체 유저 조회 (DTO 반환) */
    @Transactional(readOnly = true)
    public List<UserResponseDto> findAll() {
        List<User> users = userRepository.findAll();
        return users.stream()
                .map(UserResponseDto::from)
                .toList();
    }

    /** 특정 목록 조회 (DTO 반환) */
    @Transactional(readOnly = true)
    public List<UserResponseDto> getUsers(int page, int size) {
        Pageable pageable = PageRequest.of(page - 1, size); // page-1 = 0부터 시작
        Page<User> userPage = userRepository.findAll(pageable);

        return userPage.getContent()
                .stream()
                .map(UserResponseDto::from)
                .toList();
    }

    /** 개별 유저 조회 (DTO 반환) */
    @Transactional(readOnly = true)
    public UserResponseDto findById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다."));

        return UserResponseDto.from(user);
    }

}

4.8 util Package

  • JwtUtil : JWT 생성/검증/로그아웃(무효화) 유틸 (테스트용)
JwtUtil.java
// 임시 테스트용
@Component
public class JwtUtil {
    private static final String SECRET = "my-secret-key";
    private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
    private final ConcurrentHashMap<String, Boolean> invalidTokens = new ConcurrentHashMap<>();

    // JWT 생성
    public String generateToken(Long userId) {
        return JWT.create()
                .withSubject(String.valueOf(userId))
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .sign(Algorithm.HMAC256(SECRET));
    }

    // JWT 검증 및 사용자 ID 반환
    public Long validateToken(String token) {
        try {
            if (invalidTokens.containsKey(token)) {  // 로그아웃된 토큰인지 확인
                return null;
            }
            DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
            return Long.parseLong(decodedJWT.getSubject()); // userId 반환
        } catch (JWTVerificationException | NumberFormatException e) {
            return null;
        }
    }

    // JWT 무효화 (로그아웃)
    public void invalidateToken(String token) {
        invalidTokens.put(token, true);
    }
}

4.9 Swagger UI 화면 확인

http://localhost:8080/swagger-ui/index.html

Swagger UI 화면

Swagger UI - API 확장 화면

swagger 관련 사용 방법

  1. 각 API 내 테스트
    • "Try it out" 버튼 클릭 → 요청 파라미터 입력 → "Execute" 버튼 클릭
    • 응답 상태코드, 응답 본문 확인 가능
  1. JWT 인증
    • 우측 상단 "Authorize" 버튼 클릭
    • "bearerAuth" 항목에 JWT 토큰 입력
    • (예: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...)

starweb
@starweb :: starweb 님의 블로그

starweb 님의 블로그 입니다.

공감하셨다면 구독도 환영합니다!

목차