[Spring] API 설계 정리 1편(시작 전 알면 좋은 지식)
서버개발

[Spring] API 설계 정리 1편(시작 전 알면 좋은 지식)

 

Repository vs Service

# Repository : DB에 직접적으로 접근하는 코드를 모아두는 곳

# Service : DB에 직접적으로 접근하는 것은 Repository에 맡기고 비즈니스 로직에 집중하는 것

- 구분 짓는 이유 : 비즈니스 로직과 관련된 부분에 문제가 생기면 service를 확인하고 DB 접근 관련 문제가 생기면 repository를 확인하기 위해 구분지은 것

 

비즈니스 로직이란?

 

비즈니스 로직(Business Logic)이란?

안녕하세요. Mommoo 입니다. 프로그래밍에 관한 일을 하다보면 많이 듣는 용어중 하나 인, 비즈니스 로직(Business Logic)에 대하여 포스팅 합니다. 영역 구분하기 홈페이지 회원가입으로 예를 들어봅

mommoo.tistory.com

 

GetMapping vs PostMapping

GET과 POST는 HTTP프로토콜을 이용해서 서버에 무언가를 전달할 때 사용하는 방식입니다.

# GetMapping

- Select 기능을 원한다면 Get 메서드를 사용한다

- 검색 결과 등 고정적은 주소 및 링크 주소로 사용될 수 있다면 Get 메서드 사용

- Get은 캐시가 남아있어 전송 속도가 빠르다

- Get은 브라우저 히스토리에 파라미터가 남고 Post는 저장되지 않는다

- Get은 아스키 캐릭터만 허용하나 Post는 한계가 없다

  @GetMapping("/order")
    public String createForm(Model model){
        List<Member> members = memberService.findMembers();
        List<Item> items = itemService.findItems();

        model.addAttribute("members", members);
        model.addAttribute("items",items);

        return "orders/createOrderForm";
    }

 

# PostMapping

- Update 기능을 원한다면 Post 메서드를 사용한다

- 정보로 담을 URL길이는 한계가 있기 때문에 이를 해결하고 싶다면 Post 메서드 사용

- Post는 정보를 숨길 수 있다. 하지만 SSL(Secure Sockets Layer)를 사용안하며 Get과 마찬가지

- Post는 캐시가 남지 않아 보안적인 면에서 유리하다

- Post는 바이너리 데이터가 허용된다. 파일 입출력을 위해서는 Post 메서드가 이용된다

  @PostMapping("/order")
    public String order(@RequestParam("memberId") Long memberId,
                        @RequestParam("itemId") Long itemId,
                        @RequestParam("count") int count){
        orderService.order(memberId, itemId, count);
        return "redirect:/orders";
    }

 

@RequestBody @ResponseBody

클라이언트에서 서버로 통신하는 메시지를 요청(request) 메시지라고 하며, 서버에서 클라이언트로 통신하는 메시지를 응답(response) 메시지라고 한다

 

비동기통신을 하기위해서는 클라이언트에서 서버로 요청 메세지를 보낼 때, 본문에 데이터를 담아서 보내야 하고, 서버에서 클라이언트로 응답을 보낼때에도 본문에 데이터를 담아서 보내야 한다

이 본문이 바로 body 이다.

 

즉, 요청본문 requestBody, 응답본문 responseBody 을 담아서 보내야 한다

 

# @RequestBody

- RequestBody에 담긴 값을 자바 객체로 변환

- RequestBody를 통해서 자바객체로 conversion하는데, 이때 HttpMessageConverter가 사용된다

- @RequestBody가 붙은 파라미터에는 HTTP 요청의 본문 body부분이 그대로 전달된다

- 일반적인 GET/POST의 요청 파라미터라면 @RequestBody를 사용할 일이 없을 것이다

- 그러나 xml이나 json기반의 메시지를 사용하는 요청의 경우에 이 방법이 매우 유용하다

 

# @ResponseBody

- 자바 객체를 HTTP요청의 바디내용으로 매핑하여 클라이언트로 전송한다

- @ResponseBody가 붙은 파라미터가 있으면 HTTP 요청의 미디어타입과 파라미터의 타입을 먼저 확인한다

- 메세지 변환기 중에서 해당 미디어타입과 파라미터 타입을 처리할 수 있다면, HTTP요청의 본문 부분을 통째로 변환해서 지정된 메소드 파라미터로 전달해준다.

@ResponseBody
@RequestMapping(value = "/ajaxTest.do")
public UserVO ajaxTest() throws Exception {

  UserVO userVO = new UserVO();
  userVO.setId("테스트");

  return userVO;
}

 

즉, @Responsebody 어노테이션을 사용하면 http요청 body를 자바 객체로 전달받을 수 있다.

 

@RestController

@Controller와는 다르게 @RestController는 리턴값에 자동으로 @ResponseBody가 붙게되어 별도 어노테이션을 명시해주지 않아도 HTTP 응답데이터(body)에 자바 객체가 매핑되어 전달 된다.

 

@Controller인 경우에 바디를 자바객체로 받기 위해서는 @ResponseBody 어노테이션을 반드시 명시해주어야한다. 

 

API 설계 시 주의 사항

API는 항상 주고 받는 정보를 Entity 원 클래스를 사용하지 않는다

@GetMapping("/api/v2/members")
    public List<Member> membersV1(){
        return memberService.findMembers();
    }

와 같이 Service에서 바로 Entity들의 리스트를 보내버릴경우 생기는 문제점

1. 필요없는 내부 필드들도 같이 제공된다 (이는 @JsonIgnore 필드를 붙이면 해결된다)

2. @JsonIgnore을 사용하여도 화면을 뿌리기 위한 로직이 들어가버린다

3. 엔티티의 필드 이름이 바뀌면 json 필드 이름도 바뀌어버려 모든 어플리케이션에 혼란이 온다

 

아래와 같이 해결

 @GetMapping("/api/v2/members")
    public Result membersV2(){
        List<Member> findMembers = memberService.findMembers();
        List<MemberDto> collect = findMembers.stream()
        	.map(m -> new MemberDto(m.getId(), m.getName(), m.getAddress()))
            	.collect(Collectors.toList());

        return new Result(collect);
    }

    @Data
    @AllArgsConstructor
    static class Result<T>{
        private T data;
    }

    @Data
    @Getter
    @AllArgsConstructor
    static class MemberDto{
        private Long id;
        private String name;
        private Address address;
    }

 

Java 람다식 및 stream()

List<OrderDto> collect = orders.stream().map(o -> new OrderDto(o)).collect(Collectors.toList());


List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
        return flats.stream().collect(Collectors.groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getLocalDateTime(), o.getOrderStatus(), o.getAddress()),
                Collectors.mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), Collectors.toList()))).entrySet().stream()
                .map(e -> new OrderQueryDto(e.getKey().getOrderId(), e.getKey().getName(), e.getKey().getLocalDateTime(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
                .collect(Collectors.toList());

# 람다식

- Java는 Java 8부터 람다식(Lambda Expression)을 지원하기 시작했다. 람다식은 함수적 프로그래밍 기법이라고 할 수 있으며, 익명 함수(anonymous function)을 생성하는 식이다.

위의 y = f(x) 라는 함수가 람다식에선 (타입 매개변수,...) → { 실행문; ... } 형식으로 바뀌는데 f(x)의 x가 타입 매개변수가 되고, y가 실행문이 된다고 생각하면 된다.

- 장점 :

1. 코드의 간결함

2. 컬렉션 요소를 필터링 혹은 매핑하여 쉽게 데이터의 집계 가능(stream())

3. 병렬처리의 가능과 안정적인 확장성

- 단점 :

1. 일정 수준을 넘어가면 가독성에 영향을 미침

2. 익숙하지 않는 사람에게 쉽지 않은 호출 방식

3. 함수적 인터페이스(인터페이스가 단 한개의 추상 메소드를 정의하고 있는 인터페이스)에서만 사용 가능

 

 

- 람다식의 생략 기법

1. 매개변수의 타입이 생략 가능하다

2. 매개변수가 한개인 경우 소괄호 생략이 가능하다

3. 코드 블록 내 실행하는 코드가 한줄인 경우 중괄호 생략이 가능하다

4. 코드 블록 내 실행하는 코드가 return문만 있는 경우 중괄호와 return의 생략이 가능하다

5. 매개변수가 없는 경우 소괄호만 표시해준다

// 같은 동작을 하는 코드이다.
List<OrderDto> collect = orders.stream()
	.map(o -> new OrderDto(o))
    	.collect(Collectors.toList());

List<OrderDto> collect2 = orders.stream()
	.map((Order o) -> { return new OrderDto(o); })
    		.collect(Collectors.toList());

 

# steam()

- 스트림은 자바8부터 추가된 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다. Iterator와 비슷한 역할을 하지만 람다식으로 요소 처리 코드를 제공하여 코드가 좀 더 간결하게 할 수 있다는 점과 내부 반복자를 사용하므로 병렬처리가 쉽다는 점에서 차이점이 있습니다. 

// Iterator와 Stream의 비교

// iterator
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1,2,3));
Iterator<Integer> iter = list.iterator();
while(iter.hasNext()) {
    int num = iter.next();
    System.out.println("값 : "+num);
}

// stream
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1,2,3));
Stream<Integer> stream = list.stream();
stream.forEach(num -> System.out.println("값 : "+num));

# stream() 연산

// 필터링 - Filter 
// Filter는 Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어내는 연산이다
Stream<String> stream = list.stream().filter(name -> name.contains("a"));

// 데이터변환 - Map
Stream<String> stream = names.stream().map(s -> s.toUpperCase());

// 데이터정렬 - Sorted
// Stream의 요소들을 정렬하기 위해서 sorted를 사용해야 하며, 파라미터로 Comparator를 넘길 수도 있다
// Comparator가 없으면 기본적으로 오름차순, 내림차순을 위해서는 reverseOrder를 이용하면 된다
Stream<String> stream = list.stream().sorted();
Stream<String> stream = list.stream() .sorted(Comparator.reverseOrder());

// 중복제거 - Distinct
// Stream의 요소들에 중복된 데이터가 존재하는 경우 중복을 제거한다
// 중복을 검사하기 위해 Object의 equals() 메서드를 사용한다.
Stream<String> stream = list.stream().distinct();

// 최댓값/최솟값/총합/평균/갯수 - Max/Min/Sum/Average/Count
long count = IntStream.of(1, 3, 5, 7, 9).count();

// 데이터 수집 - Collect
// Stream의 요소들을 List나 Set, Map, 등 다른 종류의 결과로 수집하고 싶은 경우 이용할 수 있다
> collect() : 스트림의 최종연산, 매개변수로 Collector를 필요로 한다. 
> Collector : 인터페이스, collect의 파라미터는 이 인터페이스를 구현해야한다. 
> Collectors : 클래스, static메소드로 미리 작성된 컬렉터를 제공한다.

// 1. Collectors.toList() - List로 반환받는다 (toSet() 등과 같은 것도 존재한다)
List<String> nameList = productList.stream() 
	.map(Product::getName) 
    	.collect(Collectors.toList());
        
// 2. Collectors.joining() - 작업한 결과를 1개의 String으로 이어붙여준다
// 3개의 파라미터를 받을 수 있다 ( 요소 중간 구분자, 결과 맨 앞 문자, 결과 맨 뒤 문자 )
String listToString = productList.stream()
	.map(Product::getName)
    	.collect(Collectors.joining()); 
        
// 4. Collectors.groupingBy() / partitioningBy()
// 작업한 결과를 특정 그룹으로 묶기 ( 결과는 map으로 반환된다 )
// 매개변수로 함수형 인터페이스 function을 필요로 한다, partitioningBy()는 predicate를 받는다

Map<Integer, List<Product>> collectorMapOfLists = productList.stream() 
	.collect(Collectors.groupingBy(Product::getAmount)); 
    
    /* {23=[Product{amount=23, name='potatoes'}, Product{amount=23, name='bread'}], 
    	13=[Product{amount=13, name='lemon'}, Product{amount=13, name='sugar'}], 
        14=[Product{amount=14, name='orange'}]} */

Map<Boolean, List<Product>> mapPartitioned = productList.stream() 
	.collect(Collectors.partitioningBy(p -> p.getAmount() > 15)); 
    
    /* {false=[Product{amount=14, name='orange'}, Product{amount=13, name='lemon'}, Product{amount=13, name='sugar'}], 
    	true=[Product{amount=23, name='potatoes'}, Product{amount=23, name='bread'}]} */
        
// 5. Match - 조건 검사
> anyMatch: 1개의 요소라도 해당 조건을 만족하는가
> allMatch: 모든 요소가 해당 조건을 만족하는가
> nonMatch: 모든 요소가 해당 조건을 만족하지 않는가
boolean anyMatch = names.stream().anyMatch(name -> name.contains("a"));

// 6. forEach - 특정 연산 수행
// Stream의 요소들을 대상으로 어떤 특정한 연산을 수행하고 싶은 경우에는 forEach 함수를 이용할 수 있다
// forEach()는 최종 연산으로써 실제 요소들에 영향을 줄 수 있으며, 반환값이 존재하지 않는다.
// 반환값이 존재하고 싶을 땐 peek
names.stream().forEach(System.out::println);

 

 

 

 

#스프링 프레임워크 #스프링 API 설계 #Spring 람다 #java 람다 #자바 람다 #자바 stream #stream() #RestController