본문 바로가기
Backend 개발자/Springboot

개방과페쇄 Springboot API @GetMapping 3가지 방법 @ModelAttribute, @RequestParam, 정적 메소드 static method 현실적인 고민

by by 앵과장 2022. 4. 9.
반응형

안녕하세요

앵과장입니다.

 

이전시간에 Springboot 에서 API 개발에 필요한 Annotation 들에 대해서 가볍게 알아보았습니다.

이번에는 API구현하면서 실제로 어떻게 하는게 좋을지 고민을 한번 해보도록 하겠습니다.

 

저도 오랜만에 API를 구현해보기도 하고 기억도 가물 가물한데 어떤방법이 좋은지 몰라서

삽질 하는 중 이라는점 참고하시기 바랍니다.

 

Request 에서 사용할때 미리 알아둬야하는 방법에 대해서 궁금하다면 클릭해보세요!!

 

Spring Request 처리 방법 ModelAttribute, RequestParam, RequestBody, ArgumentResolver

안녕하세요 앵과장입니다. 프로젝트를 신규로 개발하다보니 Server Api 오랜만에 직접 구현할일이 생기다보니 하면서 정리가 필요할것같아서 공유드립니다. Spring에서는 클라이언트 요청을 바인

angryfullstack.tistory.com

 

API GetMapping 방식 구현하는 3가지 방법

 

@RequestParam
GetMapping QueryString으로 처리하는 방법

 

아래코드처럼 가볍게 파라메타 들이 많지 않다면 이렇게 사용하시는것이 바람직한 방법인것 같습니다.

심플합니다. 

@Slf4j
@RequestMapping("/api")
@RestController
@Api(tags = "상품")
@RequiredArgsConstructor
public class GoodsController {
    private final GoodAppService goodAppService;

    @ApiOperation(value = "상품조회", notes = "상품조회", tags = "상품")
    @ApiResponses({
            @ApiResponse(code=200, message="goodsNo list로 전달받은 결과물을 리턴합니다.")
    })
    @GetMapping("/goods")
    public List<GoodsResponse> getGoodsByNo(@RequestParam(required=true) List<Long> goodsNumbers){
        return goodsAppService.getAllByGood(goodsNumbers);
    }

}

하지만 Request Parameta 가 너무 많아진다면 어떻게 해야될까요 

Get방식으로 구현하는게 방법일까요 ?

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodRequet {

    private String vendorId;
    private String nextToken;
    private int maxPerPage;
    private long sellerProductId;
    private String sellerProductName;
    private Boolean status;
    private String manufacture;
    private LocalDate createdAt;
    private int pageIndex;
    private int pageRowSize;
}
@GetMapping("/providers/seller_api/apis/api/marketplace/seller-products")
	public ResponseEntity<GoodResponse> getByGood(
			@RequestParam(value="vendorId", required = false) String vendorId,
			@RequestParam(value="nextToken", required = false) String nextToken,
			@RequestParam(value="maxPerPage", required = false) int maxPerPage,
			@RequestParam(value="sellerProductId", required = false) long sellerProductId,
			@RequestParam(value="sellerProductName", required = false) String sellerProductName,
			@RequestParam(value="status", required = false) String status,
			@RequestParam(value="manufacture", required = false) String manufacture,
			@RequestParam(value="createdAt", required = false) String createdAt,
			@RequestParam(value="pageIndex", required = false) int pageIndex,
			@RequestParam(value="pageRowSize", required = false) int pageRowSize
	){
		return ResponseEntity.ok(GoodService.getByGood(
        	GoodRequest.builder()
            .vendorId(vendorId)
            .nextToken(nextToken)
            .maxPerPage(maxPerPage)
            .sellerProductId(sellerProductId)
            .sellerProductName(sellerProductName)
            .status(status)
            .manufacture(manufacture)
            .createdAt(createdAt)
            .pageIndex(pageIndex)
            .pageRowSize(pageRowSize)
            .build();
        ));
	}

해당 내용 어떤방식으로 전달하는게 좋을까요

@RequestParam 으로 전달받아서 처리하는방법이 좋을까요?

이런방법도 있긴하지만 코드가 길어집니다. 

 

 

@ModelAttribute
QueryString 파라메타가 많다면 Setter 데이터 바인드 방식을 통한 심플한 방법

 

이럴땐 @ModelAttribute 방식으로 처리할수 있습니다.

QueryString 으로 처리하는 방식으로 ModelAttribute 로 전달받는 Request DTO 객체는 @Setter 가 필수로 존재해야합니다.

그래야 바인딩이 가능해집니다. 

@Getter
@Setter
public class GoodRequet {

    private String vendorId;
    private String nextToken;
    private int maxPerPage;
    private long sellerProductId;
    private String sellerProductName;
    private Boolean status;
    private String manufacture;
    private LocalDate createdAt;
    private int pageIndex;
    private int pageRowSize;
}
@GetMapping("/providers/seller_api/apis/api/marketplace/seller-products")
	public ResponseEntity<GoodResponse> getByGood(
			@Vaild @ModelAttribute Search request
	){
		return ResponseEntity.ok(GoodService.getByGood(request));
	}

하지만 @Setter 를 사용하게되면 운영을하면서

정책이 변경되고 담당개발자가 바뀌고 지속될때 어쩌다보니 실수로 코드가 오염될수 있는 상황이 발생합니다.

 

예를 들어 서비스 GoodService 에서 

requet 를 비지니스로 구현하면서 변조를 한다거나 할수 있습니다. 왜냐면 Setter가 열려있기때문인데요

자바에서는 되도록이면 Setter를 통해서 의도가 들어나지 않는 값에 대한 변경은 최소화 하는것이 좋습니다.

 

Solid 원칙에도 나오는 OCP (Open Closed Principle) 개방 폐쇄 원칙 에서도 의미하는것 처럼 확장에 대해서는 개방 되어있지만

수정에 대해선 페쇄 되게 해야 한다는 원칙 말입니다.

 

어떻게 해야되나요 ?!

 

 

 

@Getter
@Setter(AccessLevel.PROTECTED)
public class GoodRequet {
    private String vendorId;
    private String nextToken;
    private int maxPerPage;
    private long sellerProductId;
    private String sellerProductName;
    private Boolean status;
    private String manufacture;
    private LocalDate createdAt;
    private int pageIndex;
    private int pageRowSize;
}​

이렇게 @Setter 에 AccessLevel.PROTECTED 를 추가하게되면 

AccessLevel.PROTECTE 접근레벨이 없었다면

GoodService 에서 GoodRequest 에 값을 받아 특정 필드를 .set 으로 접근이 가능했지만 

 

AccessLevel.PROTECTE 추가 한 뒤에는

서비스 코드에서 .set 자체로 변경이 불가능 하게 됩니다. 

해당 접근레벨 변경으로 인해서 개방 폐쇄 원칙을 지킬수 있게되는겁니다.!!

 

다른 방법도 한번 가볍게 알아보도록 하겠습니다.

정적 메소드로 처리하는 방법

먼저 GoodRequest 에 위에서 선언했던

@Setter, @Builder, @AllArgsConstructor, @NoArgsConstructor 모두 지워버립니다.

항상 사용하지 않는 코드 나 @Lombok Annotaion 삭제 해주시는 습관 기억하세요

@Getter
public class GoodRequet {
    private String vendorId;
    private String nextToken;
    private int maxPerPage;
    private long sellerProductId;
    private String sellerProductName;
    private Boolean status;
    private String manufacture;
    private LocalDate createdAt;
    private int pageIndex;
    private int pageRowSize;
    
    public GoodRequest(String vendorId, String nextToken, int maxPerPage
    	long sellerProductId, String sellerProductName, Boolean status,
        String manufacture, LocalDate createdAt, int pageIndex
        int pageRowSize
    ){
    	this.vendorId = vendorId;
        this.nextToken = nextToken;
        this.maxPerPage = maxPerPage;
        this.sellerProductId = sellerProductId;
        this.sellerProductName = sellerProductName;
        this.status = status;
        this.manufacture = manufacture;
        this.createdAt = createdAt;
        this.pageIndex = pageIndex;
        this.pageRowSize = pageRowSize;
    }
    
    public static GoodRequest ofSearch(
    String vendorId, String nextToken, int maxPerPage
    	long sellerProductId, String sellerProductName, Boolean status,
        String manufacture, LocalDate createdAt, int pageIndex
        int pageRowSize
    ){
    	return new GoodRequest(vendorId, nextToken, maxPerPage,
        sellerProductId, sellerProductName, status,
        manufacture, createdAt, pageIndex, pageRowSize);
    }
    
    
}
@GetMapping("/providers/seller_api/apis/api/marketplace/seller-products")
	public ResponseEntity<GoodResponse> getByGood(
			@RequestParam(value="vendorId", required = false) String vendorId,
			@RequestParam(value="nextToken", required = false) String nextToken,
			@RequestParam(value="maxPerPage", required = false) int maxPerPage,
			@RequestParam(value="sellerProductId", required = false) long sellerProductId,
			@RequestParam(value="sellerProductName", required = false) String sellerProductName,
			@RequestParam(value="status", required = false) String status,
			@RequestParam(value="manufacture", required = false) String manufacture,
			@RequestParam(value="createdAt", required = false) String createdAt,
			@RequestParam(value="pageIndex", required = false) int pageIndex,
			@RequestParam(value="pageRowSize", required = false) int pageRowSize
	){
		return ResponseEntity.ok(GoodService.getByGood(
        	GoodRequest.ofSearch(
				vendorId, nextToken, maxPerPage,
				sellerProductId, sellerProductName, status,
				manufacture, createdAt, pageIndex, pageRowSize
            )
        ));
        
    }

이렇게 처리하면 @GetMapper 방식 QueryString 방식 유지하면서 @Setter 안쓰고 할수있게됩니다.

 

정적메소드를 처리하면 의도를 들어나게 구현하기때문에 운영서비스할때도 필요한 파라메타가 코드에 들어나기때문에 좋은방법인것 같아보이지만 필드가 많아지게되면 사용하는데 어떤방법이 좋을지는 고민해보고 함께 프로젝트를 진행하는 형태라면 코드컨벤션에 맞춰서 진행 하시는것을 추천드립니다.

 

이번장에 중요한 포인트는 

Solid 원칙에도 나오는 OCP (Open Closed Principle) 개방 폐쇄 원칙 이라는 점 참고하세요

 

@ModelAttribute 멀티모듈일경우 
@Setter(AccessLevel.PROTECTED) 데이터 바인드 안되는문제발생

@ModelAttribute 멀티모듈일 경우

Request 로 전달받은값을 service 까지 전달해야되는데 값이 Requst class로 전달되지 않는것을 확인하였습니다.


public 만 데이터를 정상적으로 전달하는것이 확인되어 

 

Get방식에서 파라메타 전달받는 수를 정해서 3-4개 이전이면 정적메소드를 사용하고

그이상이라면 Post 방식으로 변경후 Json Payload로 처리하는것이 효율적인 방법일것 같습니다.