코드스테이츠 백엔드 부트캠프 43일차 - [Spring MVC] 서비스 계층

wish17·2023년 2월 14일
0
post-thumbnail

Daily Coding - 22번

sort 사용 안하고 오름차순으로 정렬하시오.
(버블 정렬 알고리즘 참고)

public class BubbleSort {
    public int[] bubbleSort(int[] arr) { // sort 사용 안하고 오름차순으로 정렬하는 메서드
        int[] result = arr.clone();
        for(int j=0; j<arr.length; j++) {
            for (int i = 0; i < arr.length - 1 - j; i++) {
                if (result[i] > result[i + 1]) { //앞요소가 뒷요소보다 크면 // 부등호만 바꾸면 내림차순 됨
                    int small = result[i + 1];
                    result[i + 1] = result[i]; // 두 위치 바뀌기
                    result[i] = small;
                }
            }
        }
        return result;
    }
}

시간복잡도가 너무 크다.
줄여보자

public int[] bubbleSort(int[] arr) { // sort 사용 안하고 오름차순으로 정렬하는 메서드
        int[] result = arr.clone();
        for(int j=0; j<arr.length; j++) {
            boolean stillNotSort = false;
            for (int i = 0; i < arr.length - 1 - j; i++) {
                if (result[i] > result[i + 1]) { //앞요소가 뒷요소보다 크면 // 부등호만 바꾸면 내림차순 됨
                    int small = result[i + 1];
                    result[i + 1] = result[i]; // 두 위치 바뀌기
                    result[i] = small;
                    stillNotSort =true;
                }
            }
            if(!stillNotSort) break;
        }
        return result;
    }

어떤 요소도 위치가 바뀌지 않는 경우, 배열이 정렬된 상태이기 때문에
boolean을 이용해서 필요없는 수행을 중단시키도록 했다.


[Spring MVC] 서비스 계층

서비스 계층

  • API 계층에서 전달 받은 클라이언트의 요청 데이터를 기반으로 실질적인 비즈니스 요구사항을 처리하는 계층

DI를 통한 서비스 계층 ↔ API 계층 연동

API 계층에서 구현한 Controller 클래스가 서비스 계층의 Service 클래스와 메서드 호출을 통해 상호 작용한다는 것

개념정리

Service

  • 도메인 업무 영역을 구현하는 비즈니스 로직을 처리하는 것

도메인 엔티티(Entity) 클래스

  • 서비스 계층에서 데이터 액세스 계층과 연동하면서 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스
  • API 계층에서 전달 받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 데이터 액세스 계층으로부터 전달 받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할
  • API계층에서 DTO클래스 같은 느낌

기능정리

@Getter, @Setter

  • getter/setter 메서드를 일일이 작성하는 수고를 덜어주는 편리한 유틸리티성 라이브러리
  • lombok이라는 라이브러리에서 제공하는 애너테이션

@AllArgsConstructor

  • (모든 멤버 변수를 파라미터로 갖는) 생성자를 자동으로 생성

@NoArgsConstructor

  • 파라미터가 없는 기본 생성자를 자동으로 생성

@RestController

  • 해당 컨트롤러에서 return 하는 값을 그대로 클라이언트에게 전달해준다.
  • 즉, 페이지가 아닌 데이터 자체를 반환할 때 사용하는 어노테이션
  • Spring Bean 등록

@Service

  • Spring에서 관리되는 객체임을 표시하기 위해 사용하는 애너테이션
  • 비즈니스 로직이나 respository layer 호출하는 함수에 사용
  • @Component 어노테이션과 거의 차이가 없지만 비즈니스 로직을 수행하는 서비스 레이어 클래스임을 나타낸다.
  • Spring Bean 등록
  • Controller 클래스에 @RestController 애너테이션을 추가하면 Spring Bean으로 등록된다.
  • Service 클래스에 @Service 애너테이션을 추가하면 Spring Bean으로 등록된다.
  • 생성자 방식의 의존성주입(DI)은 생성자가 하나일 경우에는 @Autowired 애너테이션을 추가하지 않아도 DI가 적용된다.

실습적용

Member 클래스

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter // getter메서드 대신 만들어 줌
@Setter // setter메서드 대신 만들어 줌
@NoArgsConstructor // 파라미터가 없는 기본 생성자를 자동으로 생성
@AllArgsConstructor // (모든 멤버 변수를 파라미터로 갖는) 생성자를 자동으로 생성
public class Member {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}

MemberService 클래스

import org.springframework.stereotype.Service;

import java.util.List;

@Service // Spring Bean 등록
public class MemberService {
    public Member createMember(Member member){ // 아직 DB연결 안했으니 대충 쓴거
        Member createMember = member;
        return createMember;
    }

    public Member updateMember(Member member) { // 아직 DB연결 안했으니 대충 쓴거
        Member updatedMember = member;
        return updatedMember;
    }

    public Member findMember(long memberId) { // 아직 DB연결 안했으니 대충 쓴거
        Member member = new Member(memberId, "hgd@gmail.com", "홍길동", "010-1234-5678");
        return member;
    }

    public List<Member> findMembers() { // 아직 DB연결 안했으니 대충 쓴거
        List<Member> members = List.of(
                new Member(1, "hgd@gmail.com", "홍길동", "010-1234-5678"),
                new Member(2, "lml@gmail.com", "이몽룡", "010-1111-2222")
        );
        return members;
    }

    public void deleteMember(long memberId) {

    }
}

MemberController 클래스

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;

// 현재 상태로는 2가지 문제점이 있다.
// MemberController의 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 클래스로 변환하는 작업까지 도맡아서 하고 있다.
// 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송함으로써 계층 간의 역할 분리가 이루어지지 않았다.

@RestController // Spring Bean 자동등록 기능 포함
@RequestMapping("/v1/members")
@Validated // @PathVariable이 추가된 변수에 유효성 검증하려면 붙여야함
//클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해준다.
public class MemberController { // 회원 관리를 위한 클래스
    private final MemberService memberService;

    public MemberController(MemberService memberService) { // 느슨한결합  // Spring이 애플리케이션 로드시 자동으로 주입해줌
        this.memberService = memberService;
    }


    @PostMapping // 클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberPostDto){

        Member member = new Member();
        member.setEmail(memberPostDto.getEmail());
        member.setName(memberPostDto.getName());
        member.setPhone(memberPostDto.getPhone());

        Member response = memberService.createMember(member);

        return new ResponseEntity<>(response, HttpStatus.CREATED); // Map 객체를 리턴하게 되면  JSON 형식으로 자동 변환
        // ResponseEntity 객체로 데이터를 래핑함으로써 HTTP 응답 상태를 함께 전달
    }

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Positive long memberId, // @Min(1) =  1 이상의 숫자일 경우에만 유효성 검증 통과
                                      @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        Member member = new Member();
        member.setMemberId(memberPatchDto.getMemberId());
        member.setName(memberPatchDto.getName());
        member.setPhone(memberPatchDto.getPhone());

        Member response = memberService.updateMember(member);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping("/{member-id}") // 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션
    public  ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId){ // 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드
        Member response = memberService.findMember(memberId);
        System.out.println("# get Member");
        return new ResponseEntity<>(response,HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() { // 모든회원 정보 목록을 클라이언트에게 제공하는 핸들러 메서드
        List<Member> response = memberService.findMembers();
        System.out.println("# get Members List");
        return new ResponseEntity<>(response,HttpStatus.OK);
    }

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
        memberService.deleteMember(memberId);
        System.out.println("# delete member");
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

현재 상태로는 2가지 문제점이 있다.

  • MemberController의 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 클래스로 변환하는 작업까지 도맡아서 하고 있다.
  • 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송함으로써 계층 간의 역할 분리가 이루어지지 않았다.

DTO 클래스와 엔티티(Entity) 클래스를 서로 변환해주는 매퍼(Mapper)를 만들어서 해결 가능하다.


매퍼(Mapper)를 이용한 DTO 클래스 ↔ 엔티티(Entity) 클래스 매핑

Mapper적용 전

  • DTO 클래스-> MemberController -> 엔티티(Entity)
  • 엔티티(Entity) -> 응답

Mapper적용

  • DTO 클래스-> MemberMapper -> 엔티티(Entity)
  • 엔티티(Entity) -> MemberMapper -> DTO 클래스 -> 응답

Mapper적용 실습코드

MemberResponseDto 클래스

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class MemberResponseDto { // 응답 데이터의 역할을 해주는 DTO 클래스
    private long memberId;

    private String email;

    private String name;

    private String phone;
}

MemberMapper 클래스

import org.springframework.stereotype.Component;

@Component // 빈 등록
public class MemberMapper {
    public Member memberPostDtoToMember(MemberPostDto memberPostDto) { // MemberPostDto를 Member로 변환
        return new Member(0L,
                memberPostDto.getEmail(),
                memberPostDto.getName(),
                memberPostDto.getPhone());
    }

    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) { // MemberPatchDto를 Member로 변환
        return new Member(memberPatchDto.getMemberId(),
                null, // MemberPatchDto객체에는 email 없으니까 null (이메일 수정 안되게 해뒀음)
                memberPatchDto.getName(),
                memberPatchDto.getPhone());
    }

    public MemberResponseDto memberToMemberResponseDto(Member member) { // Member를 MemberResponseDto로 변환
        return new MemberResponseDto(member.getMemberId(),
                member.getEmail(),
                member.getName(),
                member.getPhone());
    }
}

MemberController 클래스

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
import java.util.stream.Collectors;

// 이전버전 상태로는 2가지 문제점이 있다.
// MemberController의 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 클래스로 변환하는 작업까지 도맡아서 하고 있다.
// DTO 클래스-> MemberController -> 엔티티(Entity)
// 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송함으로써 계층 간의 역할 분리가 이루어지지 않았다.
// 엔티티(Entity) -> 응답

// 현재 상태는 Mapper 클래스를 이용해 위 문제점을 해결했다.
// DTO 클래스-> MemberMapper -> 엔티티(Entity)
// 엔티티(Entity) -> MemberMapper -> DTO 클래스 -> 응답

@RestController // Spring Bean 자동등록 기능 포함
@RequestMapping("/v1/members")
@Validated // @PathVariable이 추가된 변수에 유효성 검증하려면 붙여야함
//클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해준다.
public class MemberController { // 회원 관리를 위한 클래스
    private final MemberService memberService;
    private final MemberMapper memberMapper;

    public MemberController(MemberService memberService, MemberMapper memberMapper) { // 느슨한결합 // Spring이 애플리케이션 로드시 자동으로 주입해줌
        this.memberService = memberService;
        this.memberMapper = memberMapper;
    }


    @PostMapping // 클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberPostDto){

        Member member = memberMapper.memberPostDtoToMember(memberPostDto);
        Member response = memberService.createMember(member);

        return new ResponseEntity<>(memberMapper.memberToMemberResponseDto(response),
                HttpStatus.CREATED);
    }

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Positive long memberId,
                                      @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        Member member = memberMapper.memberPatchDtoToMember(memberPatchDto);
        Member response = memberService.updateMember(member);

        return new ResponseEntity<>(memberMapper.memberToMemberResponseDto(response), HttpStatus.OK);
    }

    @GetMapping("/{member-id}") // 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션
    public  ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId){ // 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드
        Member response = memberService.findMember(memberId);
        System.out.println("# get Member");
        return new ResponseEntity<>(memberMapper.memberToMemberResponseDto(response),HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() { // 모든회원 정보 목록을 클라이언트에게 제공하는 핸들러 메서드
        List<Member> members = memberService.findMembers();
        List<MemberResponseDto> response = members.stream()
                    .map(member -> memberMapper.memberToMemberResponseDto(member))
                    .collect(Collectors.toList());

        System.out.println("# get Members List");
        return new ResponseEntity<>(response,HttpStatus.OK);
    }

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
        memberService.deleteMember(memberId);
        System.out.println("# delete member");
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

List 타입변환을 잘못해서 오류가 났었는데 stream 메서드 import 잘못한줄 알고 헛짓 좀 했다.
기본인데... 타입 생각 항상 하자.


MapStruct를 이용한 Mapper 자동 생성

  • 위 Meeper 적용 실습과 같이 개발자가 일일이 수작업으로 매퍼(Mapper) 클래스를 만드는 것은 비효율적이다.

MapStruct

  • 매퍼(Mapper) 클래스를 자동으로 구현

MapStruct 기반의 매퍼(Mapper)를 자동 생성하기 위해서 아래와 같이 build.gradle 파일에 MapStruct 의존 라이브러리 추가를 해줘야 한다.

dependencies {
	...
	...
	implementation 'org.mapstruct:mapstruct:1.4.2.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

@Mapper

  • 해당 인터페이스를 MapStruct의 매퍼 인터페이스로 정의
  • 애트리뷰트로 componentModel = "spring"을 지정해주면 Spring의 Bean으로 등록이 된다.
    • @Mapper(componentModel = "spring")

매퍼(Mapper) 인터페이스

  • MapStruct 기반의 매퍼(Mapper) 인터페이스 정의한거
package com.codestates.section3week1.member.mapstruct.mapper;

import com.codestates.section3week1.member.Member;
import com.codestates.section3week1.member.MemberPatchDto;
import com.codestates.section3week1.member.MemberPostDto;
import com.codestates.section3week1.member.MemberResponseDto;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface MemberMapper {
    Member memberPostDtoToMember(MemberPostDto memberPostDto); // MemberPostDto를 Member로 변환
    Member memberPatchDtoToMember(MemberPatchDto memberPatchDto); // MemberPatchDto를 Member로 변환
    MemberResponseDto memberToMemberResponseDto(Member member); // Member를 MemberResponseDto로 변환
}

DTO 클래스와 엔티티 클래스의 역할 분리가 필요한 이유

  • 계층별 관심사의 분리
  • 코드 구성의 단순화
  • REST API 스펙의 독립성 확보

정리

  • Mapper를 사용해서 DTO 클래스와 Entity 클래스 간의 관심사를 분리할 수 있다.
  • Mapper를 개발자가 직접 구현하기 보다는 MapStruct 같은 매핑 라이브러리를 사용하는 것이 생산성 측면에서 더 나은 선택이다.

학습내용 복습

CoffeeController 서비스계층 추가하기

위 과정을 한번 더 반복해서 최대한 참고하지 않고 coffee패키지에 적용해 봤다.

0개의 댓글