본문 바로가기
Back-End/Spring

[Spring] pet clinic - rest api

by hongdor 2023. 5. 2.
728x90

GitHub - spring-petclinic/spring-petclinic-rest: REST version of the Spring Petclinic sample application

 

GitHub - spring-petclinic/spring-petclinic-rest: REST version of the Spring Petclinic sample application

REST version of the Spring Petclinic sample application - GitHub - spring-petclinic/spring-petclinic-rest: REST version of the Spring Petclinic sample application

github.com

 

petclinic을 예제 REST API로 만든 git이 있어서 살펴봤다.

 

위 git 사이트에서 project를 받아서 intellij로 실행했다.

 

java: package org.springframework.samples.petclinic.rest.dto does not exist

위와 같은 에러가 발생했다.

 

 

코드를 다시 받아 아래와 같은 방법으로 해결했다.

 

해결방법

  1. 터미널에 nvm install 치면 target > generated-sources > openapi > src > … > api, dto 폴더 생김
  2. target 폴더 하위 폴더 중 java 폴더를 우 클릭후 Mark directory as > sources root 로 설정
  3. File > restart IDE 로 재시작 해줌
  4. 다시 PetClinicApplication 우클릭 Run 실행
  5. http://localhost:9966/petclinic 접속해서 API 스웨거 페이지가 잘 나오는지 확인

 

 

눈에 띄는 특징 정리

 

특징

  • Entity → Dto 맵핑을 위해 mapstruct 라이브러리를 사용했다.
  • RepositoryImpl 마다 @Profile 을 통해 jdbc, jpa, springdatajpa 3개의 구현체의 예시를 만들어 놓았다.

 

 

Entity 에서 눈에 띄는 점

 

entity 내부에서 field 접근 시 처리해야할 로직이 있으면 protected 메서드를 사용

protected Set<Pet> getPetsInternal() {
        if (this.pets == null) {
            this.pets = new HashSet<>();
        }
        return this.pets;
    }

 

 

외부로 노출 시 수정 불가한 unmodifiableList 

public List<Pet> getPets() {
        List<Pet> sortedPets = new ArrayList<>(getPetsInternal());
        PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true));
        return Collections.unmodifiableList(sortedPets);
    }

 

 

Entity 내부 collection에 새로운 object 추가 시 해당 object에 자기 자신도 등록

public void addPet(Pet pet) {
        getPetsInternal().add(pet);
        pet.setOwner(this);
    }

 

 

spring-data-jpa에서 눈에 띄는 점

  • (자동구현될) 필요한 메서드 Interface / 사용자가 구현할 override Interface /
    Spring Data JPA 를 활용해 구현할 Interface 로 나눴음
  • @Query를 사용
public interface PetRepository {

	Collection<Pet> findAll() throws DataAccessException;

	void delete(Pet pet) throws DataAccessException;

}

@Profile("spring-data-jpa")
public interface PetRepositoryOverride {
	
	void delete(Pet pet);
}

@Profile("spring-data-jpa")
public class SpringDataPetRepositoryImpl implements PetRepositoryOverride {

	@PersistenceContext
    private EntityManager em;

	@Override
	public void delete(Pet pet) {
		String petId = pet.getId().toString();
		this.em.createQuery("DELETE FROM Visit visit WHERE pet.id=" + petId).executeUpdate();
		this.em.createQuery("DELETE FROM Pet pet WHERE id=" + petId).executeUpdate();
        if (em.contains(pet)) {
            em.remove(pet);
        }
	}

}

@Profile("spring-data-jpa")
public interface SpringDataPetRepository extends PetRepository, Repository<Pet, Integer>, PetRepositoryOverride {

    @Override
    @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
    List<PetType> findPetTypes() throws DataAccessException;
}
  • 2개 이상의 쿼리 작성 필요로 jpa entitymanager.createQuery 사용
  • 연관 entity 관련 동작 로직을 repository 단에서 처리
  • ex. pet을 삭제하면 pet이 방문한 장소도 함께 삭제

 

Controller에서 눈에 띄는 점

  • ControllerAdvice 사용
  • @PreAuthorize 으로 권한 확인
  • readme에 있는 것처럼 API First 접근 방식을 사용한다.
    • build를 하면 openapi.yml에 작성한대로 API template interface가 생성된다.
    • 인터페이스를 Controller에서 상속받아 구현한다.
    • 처음 보는 방식이었는데, Controller 메서드에 어노테이션 url이 나와있지 않아서 불편했다.

 

생성된 API template interface

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-04-29T02:13:49.482423300+09:00[Asia/Seoul]")
@Validated
@Tag(name = "owners", description = "Endpoints related to pet owners.")
public interface OwnersApi {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /owners : Adds a pet owner
     * Records the details of a new pet owner.
     *
     * @param ownerFieldsDto The pet owner (required)
     * @return The pet owner was sucessfully added. (status code 201)
     *         or Bad request. (status code 400)
     *         or Server error. (status code 500)
     */
    @Operation(
        operationId = "addOwner",
        summary = "Adds a pet owner",
        description = "Records the details of a new pet owner.",
        tags = { "owner" },
        responses = {
            @ApiResponse(responseCode = "201", description = "The pet owner was sucessfully added.", content = {
                @Content(mediaType = "application/json", schema = @Schema(implementation = OwnerDto.class))
            }),
            @ApiResponse(responseCode = "400", description = "Bad request.", content = {
                @Content(mediaType = "application/json", schema = @Schema(implementation = RestErrorDto.class))
            }),
            @ApiResponse(responseCode = "500", description = "Server error.", content = {
                @Content(mediaType = "application/json", schema = @Schema(implementation = RestErrorDto.class))
            })
        }
    )
    @RequestMapping(
        method = RequestMethod.POST,
        value = "/owners",
        produces = { "application/json" },
        consumes = { "application/json" }
    )
    default ResponseEntity<OwnerDto> addOwner(
        @Parameter(name = "OwnerFieldsDto", description = "The pet owner", required = true) @Valid @RequestBody OwnerFieldsDto ownerFieldsDto
    ) {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                    String exampleString = "null";
                    ApiUtil.setExampleResponse(request, "application/json", exampleString);
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

 

 

컨트롤러

@RestController
@CrossOrigin(exposedHeaders = "errors, content-type")
@RequestMapping("/api")
public class OwnerRestController implements OwnersApi {

    private final ClinicService clinicService;

    private final OwnerMapper ownerMapper;

    private final PetMapper petMapper;

    private final VisitMapper visitMapper;

    public OwnerRestController(ClinicService clinicService,
                               OwnerMapper ownerMapper,
                               PetMapper petMapper,
                               VisitMapper visitMapper) {
        this.clinicService = clinicService;
        this.ownerMapper = ownerMapper;
        this.petMapper = petMapper;
        this.visitMapper = visitMapper;
    }
    
    @PreAuthorize("hasRole(@roles.OWNER_ADMIN)")
    @Override
    public ResponseEntity<OwnerDto> addOwner(OwnerFieldsDto ownerFieldsDto) {
        HttpHeaders headers = new HttpHeaders();
        Owner owner = ownerMapper.toOwner(ownerFieldsDto);
        this.clinicService.saveOwner(owner);
        OwnerDto ownerDto = ownerMapper.toOwnerDto(owner);
        headers.setLocation(UriComponentsBuilder.newInstance()
            .path("/api/owners/{id}").buildAndExpand(owner.getId()).toUri());
        return new ResponseEntity<>(ownerDto, headers, HttpStatus.CREATED);
    }
}

 

그 외

 

간단한 security 적용

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // Enable @PreAuthorize method-level security
@ConditionalOnProperty(name = "petclinic.security.enable", havingValue = "true")
public class BasicAuthenticationConfig  {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
                )
                .httpBasic()
                    .and()
                .csrf()
                    .disable();
        // @formatter:on
        return http.build();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        // @formatter:off
        auth
            .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery("select username,password,enabled from users where username=?")
                .authoritiesByUsernameQuery("select username,role from roles where username=?");
        // @formatter:on
    }
}

 

간단한 AOP 적용

  • repository 메서드 호출 횟수 및 동작 시간 기록
  • @Around는 메서드 호출을 감싼다.
  • joinPoint.proceed() 는 메서드의 실행을 의미
@ManagedResource("petclinic:type=CallMonitor")
@Aspect
public class CallMonitoringAspect {

    private boolean enabled = true;

    private int callCount = 0;

    private long accumulatedCallTime = 0;

    @ManagedAttribute
    public boolean isEnabled() {
        return enabled;
    }

    @ManagedAttribute
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @ManagedOperation
    public void reset() {
        this.callCount = 0;
        this.accumulatedCallTime = 0;
    }

    @ManagedAttribute
    public int getCallCount() {
        return callCount;
    }

    @ManagedAttribute
    public long getCallTime() {
        if (this.callCount > 0)
            return this.accumulatedCallTime / this.callCount;
        else
            return 0;
    }

    @Around("within(@org.springframework.stereotype.Repository *)")
    public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable {
        if (this.enabled) {
            StopWatch sw = new StopWatch(joinPoint.toShortString());

            sw.start("invoke");
            try {
                return joinPoint.proceed();
            } finally {
                sw.stop();
                synchronized (this) {
                    this.callCount++;
                    this.accumulatedCallTime += sw.getTotalTimeMillis();
                }
            }
        } else {
            return joinPoint.proceed();
        }
    }

}
728x90

댓글