반응형

Swagger를 자세하게 작성해보자.

사실 프로젝트를 진행해가며 이렇게 자세하게 Swagger를 적어놓았던 적은 없었다.

 

이렇게 자세하게 적지 않아도 어느정도 알았을 뿐 아니라, 어차피 보여지는 부분도 아니라서 그냥 어느정도 알아 볼 수 있게만 했었었다.

하지만 이번에는 프론트 하는 팀원 중에 프로젝트를 처음하는 사람도 있고, 프로젝트를 모두 Public으로 공개할 것이라서 보기도 좋게 자세하게 작성해보려 한다.

 

일단 Swagger 설정

당연히 gradle에 의존성을 추가 해야한다.

그리고 항상 까먹는 이것

spring.mvc.pathmatch.matching-strategy = ant_path_matcher

application.properties에 이렇게 작성을 해야 Swagger를 추가하고 실행 할 때에 에러가 발생하지 않는다.

 

그 다음은 Swagger의 Config 작성

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .produces(getProduceContentTypes())
                .useDefaultResponseMessages(true)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("cobo.blog"))
                .paths(PathSelectors.any())
                .build();
    }

    private Set<String> getProduceContentTypes() {
        Set<String> produces = new HashSet<>();
        produces.add("application/json;charset=UTF-8");
        return produces;
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("CoBo")
                .description("CoBo 기술 블로그 Swagger 입니다.")
                .version("1.0.0")
                .build();
    }
}

항상 이렇게 작성해서 쓰다보니 이 부분은 복붙해 가는 것 같다.

이번에 추가한 게 있다면, produces에 어떤 content-type으로 응답하는 지 명시한 것

이제 Swagger에 json으로 응답한다고 표시가 된다.

 

가장 큰 단위인 컨트롤러부터 명시를 해보자.

컨트롤러에

@Api(tags = {"01. Home 화면에 사용하는 API"})

이렇게 annotation을 달아준다.

여기에 description도 달아줄 수 있지만, 이번에는 그냥 제목에 해당하는 tag만 넣으면 될 거 같아 추가하지 않았다.

 

이렇게 작성을 하면 

이런 식으로 Controller에 이름이 달리게 된다.

이렇게 이름을 달아주면 페이지 단위로 API를 구분할 때 편하지 않을까?

 

그 다음은 API에 달아주는 annotation이다.

@GetMapping("/posts")
    @ApiOperation(
            value = "Tech Post 의 정보를 List 응답",
            notes = "현재 전부 넘기고 있어서 몇페이지로 자를 건지 협의가 필요",
            response = TechPostRes.class
    )
    @ApiImplicitParams({
            @ApiImplicitParam(name = "pageNum", value = "페이지 번호", example = "1", required = true)
    })
    @ApiResponses({
            @ApiResponse(code = 200, message = "응답 성공")
    })
    public ResponseEntity<List<TechPostRes>> getPosts(@RequestParam("pageNum") Integer pageNum){
        return techService.getPosts(pageNum);
    }

가장 위의 @GetMapping은 Get으로 호출한다이니까 그냥 pass

 

@ApiOperation의 value에 API의 제목?을 작성해준다. 이 부분이 가장 메인으로 나가게 되고, 그 다음에 무언가를 알려야 한다면 notes에 적어준다. 이 부분은 스웨거를 펼치면 나타나게 된다.

response는 어떤 DTO로 응답이 되는지를 알려주는 부분인데, 이게 Swagger의 어느 부분에 나타나는 지는 모르겠다...

 

@ApiImplicitParams는 필요한 Parameter에 대한 정보를 나타내는 부분이다.

@ApiImplicitParam로 하나만 작성할 수 있지만, 그래도 @ApiImplicitParams로 괄호를 열어서 사용한다.

name에는 어떤 parameter를 사용하는지, 그리고 value에는 그 파라미터에 대한 설명과 example로 데이터의 예시를 보여주고 required로 필수여부를 나타낸다.

 

@ApiResponses는 응답 코드에 대한 내용을 적어준다.

이부분도 @ApiResponse로 하나만 적을 수 있지만 그래도 s를 붙이고 괄호를 열어 사용한다.

code에 응답 코드를 작성하고, message에 해당 코드에 대한 정보를 나타낸다. 아직 예외처리를 하지 않아 현재는 200 하나만 작성이 되어 있다.

 

이렇게 작성을 하면

이런 식으로 Swagger가 보이게 된다.

 

마지막으로 DTO에 작성하는 부분이다.

@ApiModelProperty(
            value = "TechPost 작성일자",
            example = "20230723"
    )
    private String createdAt;

이런 식으로 value에는 해당하는 데이터가 어떤 데이터인지, example은 그 데이터에 대한 예시를 class의 멤버들에게 적어준다.

이렇게 작성을 하면 

예시에는 

이렇게 보이게 되고, Schema에는 

이런식으로 어떤 데이터가 응답이 되는지 설명까지 나오게 된다.

 

여기까지 내가 사용한 Swagger 정리였다.

물론 대표적인 것들만 사용해 아직 모르는 부분들도 있겠지만, 이정도면 충분하다고 생각해 글을 작성하게 되었다.

'블로그 개발 프로젝트' 카테고리의 다른 글

CORS Error  (0) 2023.07.28
JPA으로 Paging  (0) 2023.07.24
JPA TopBy  (0) 2023.07.24
@ManyToOne, @OneToMany  (0) 2023.07.24
스프링 초반 구조 설계  (0) 2023.07.20
반응형

이번에 프로젝트를 시작하기 전에도 항상 고민 했던 부분이 설계 관련 부분이다.

아마 보통 스프링 부트를 사용하여 개발하는 사람들은 이런 구조를 가지고 있을 것이다.

당연히 나도 이렇게 개발을 하고 있었다.

 

이렇게 개발을 하면 JPA를 사용하기에 Repository -> Service -> Controller로 오게 되는데

이 때 Entity 단위의 Repository에서 Service로 그 Service가 Controller로 오기 때문에 Controller가 이런 용도 혹은 Entity 단위로 구성이 된다.

그렇게 개발된 컨트롤러를 Swagger로 본 것이다.

물론 어차피 백엔드의 나는 내가 만든 것이기에 굳이 상관은 없다....

 

하지만 이걸 사용하는 프론트는 페이지를 만들기 위해 여러 컨트롤러들을 찾아서 그 중에서 자신이 사용하는 것을 찾아 사용을 해야한다.

이번 프로젝트는 프론트에 참여한 사람들도 친한 친구들이기에 이 부분을 배려해서 설계를 해보려고 한다.

 

사실 해보는 것이지, 그냥 요즘 유행하는 DDD를 살짝 배껴온 것이다.

 

Repository

일단 Repository를 어떻게 배치할 것인가.

이번에도 JPA를 사용하기에 Repository가 Entity 단위로 생성이 된다.

뭔가 QueryDSL을 사용할까? 했지만 이번에는 그냥 JPA와 JPQL만 사용하기로 나와 약속을 했다.

만약 하나의 Entity를 하나의 도메인에서만 사용한다면 고민을 했겠지만, 이번 프로젝트는 어쩔 수 없이 하나의 도메인에서 대부분의 Entity를 참조한다.

그렇기에 Repository는 Global하게 한 쪽으로 빼고 설계했다.

 

Service

아마 Service 부터는 기존과 좀 달라질 것이다.

Service도 뭔가 용도 단위로 나누기 보다, 그냥 도메인 단위로 나누고 각 서비스에 필요한 Repository를 주입하도록 설계해보았다.

어차피 어떻게 설계를 해도 하나의 Service가 많은 Repository를 사용할 것 같았고, 어차피 나도 도메인 설계에 참여 하기에 도메인들이 왜 어떻게 나뉘는 지 알고 있었다.

그렇기에 그냥 도메인 단위로 패키지를 구성을 했고, 그곳에 하나씩 Service를 넣어주었다.

 

Controller

Controller도 도메인 단위로 나눈 패키지에 하나씩 넣어주었다.

이렇게 하니 API들이 도메인 단위로 나뉘게 되었다.

 

패키지 구성을 보자면 이렇게 되어있다.

이렇게 구성을 하니 

이렇게 API들이 도메인 단위로 분리가 된다.

 

이번 설계는 그냥 짝퉁 DDD이다.

여름이 좀 지나 시간이 남으면 다음 프로젝트 전까지 DDD를 공부하고 들어갈 수 있도록 해야겠다.

물론.. 설계에 정답은 없다! 본인이 편하고 맞다고 생각하는 대로 가면 된다.

'블로그 개발 프로젝트' 카테고리의 다른 글

CORS Error  (0) 2023.07.28
JPA으로 Paging  (0) 2023.07.24
JPA TopBy  (0) 2023.07.24
@ManyToOne, @OneToMany  (0) 2023.07.24
Swagger 작성  (0) 2023.07.24
반응형

이번에 프로젝트를 하면서 File 테이블의 ID는 Long, Integer로 사용하는 것이 아닌 KeyManager라는 테이블을 참조해서 FI + (오늘의 날짜) + 인덱스 이렇게 문자열로 생성해서 넣어주게 되었다.

 

package NexaDs_Knu.practice.Data.Entity.Util;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Embeddable;
import java.io.Serializable;
import java.time.LocalDateTime;

@Embeddable
@Data
@AllArgsConstructor
@NoArgsConstructor
public class KeyManagerId implements Serializable {

    private String keyType;
    private String date;
}

이렇게 ID는 문자열로 타입과, 문자열로 변환된 날짜를 넣어준 KeyManagerId를 사용하고

package NexaDs_Knu.practice.Data.Entity;

import NexaDs_Knu.practice.Data.Entity.Util.KeyManagerId;
import lombok.*;

import javax.persistence.*;

@Entity
@Table(name = "KeyManager")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class KeyManagerEntity {

    @EmbeddedId
    private KeyManagerId id;


    private Long sequence;
}

이렇게 테이블을 작성하였다.

이렇게 되면 ID는 KeyType과 날짜가 되어 이 두가지가 중복된 데이터는 들어올 수 없게 되었다.

@Query("SELECT k FROM KeyManagerEntity k WHERE k.id.keyType = :keyType AND k.id.date = :date")
KeyManagerEntity findByKeyTypeAndDate(
	@RequestParam("keyType") String keyType,
    @RequestParam("date") String date);
)

이렇게 JPQL을 이용하여 KeyType과 Date에 맞는 KeyManagerEntity를 가져오고 

	public static String toPrimaryKeyId(String keyType){
        String today = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        KeyManagerEntity keyManager = keyManagerRepository.findByKeyTypeAndDate(keyType, today);
        Long sequence;
        if(keyManager == null)
        {
            keyManager = new KeyManagerEntity(new KeyManagerId(keyType, today), 1L);
            keyManagerRepository.save(keyManager);
            sequence = 1L;
        }
        else
        {
            keyManager.setSequence(keyManager.getSequence() + 1);
            sequence = keyManager.getSequence();
            keyManagerRepository.save(keyManager);
        }
        return keyType + today + String.format("%05d", sequence);
    }

이렇게 그에 따라 ID를 생성해주게 만들었다.

 

그리고 이 방법을 사용하던 도중에 에러가 발생했다.

 

자바스크립트에서 해당 방법을 사용하는 API를 반복문으로 실행하는 구문이 있었는데, 이 과정에서 

이런 에러가 발생한 것이다.

현재 toPrimaryKeyId에서 동일한 ID를 생성해주고 있는 것이었다.

 

왜 이런 일이 생겼는지 생각해보면, 스프링의 쓰레드에서 동시에 메서드에 접근하고 +1된 값을 넣어주기 전에 같은 ID를 만들어 버린 이유일 것이다.

 

어찌보면 당연한 일이었다.

이 부분을 critical section으로 만들었어야 했는데, '어차피 자바스크립트 반복문으로 들어오니 대충 순서대로 들어오겠지' 라고 생각한 나의 잘못일 것이다.

 

해결 방법은 이 부분을 atomic하게 바꿔주는 어떤 방법이든 될 수 있겠지만, 나는 가장 간단한 @Synchronized 어노테이션을 달아주었다.

이렇게만 바꿔주었더니, 더 이상 에러가 발생하지 않았다.

스프링이 쓰레드 기반으로 동작된다는 것을 다시 한 번 느낄 수 있는 에러였다.

 

 

'현장실습' 카테고리의 다른 글

Mariadb와 Springboot를 Docker에 올리기  (0) 2023.07.28

+ Recent posts