서비스 계층

jungseo·2023년 6월 13일
0

Spring

목록 보기
5/23
post-thumbnail

API 계층과 서비스 계층 연동

  • API 계층의 Controller 클래스와 서비스 계층의 Service 클래스가 메서드 호출을 통해 상호작용

1. Domain Entity class

  • 서비스 계층에서 데이터 액세스 계층과 연동해 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 클래스

  • Member class

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

@Getter
@Setter
@NoArgsConstructor // 파라미터가 없는 기본 생성자를 자동 생성
@AllArgsConstructor // 모든 멤버 변수를 파라미터로 갖는 생성자를 자동 생성
public class Member {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}
  • lombok 라이브러리 제공 애너테이션 사용

2. Service class

  • Controller 클래스의 메서드와 1:1 매칭
  • API 계층에서 전달 받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달 받고 비즈니스 로직을 처리한 후 결과 값을 다시 API 계층으로 리턴
  • MemberController class
@RestController
@RequestMapping("/v3/members")
@Validated
public class MemberController {
    private MemberService memberService;

//    (1)
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
//        (2)
        Member member = new Member();
        member.setEmail(memberDto.getEmail());
        member.setName(memberDto.getName());
        member.setPhone(memberDto.getPhone());

//        (3)
        Member response = memberService.createMember(member);
        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

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

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

//        (5)
        Member response = memberService.updateMember(member);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
        System.out.println("# memberId: " + memberId);

//        (6)
        Member response = memberService.findMember(memberId);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");

//        (7)
        List<Member> response = memberService.findMembers();
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
        System.out.println("# deleted memberId: " + memberId);

//        (8)
        memberService.deleteMember(memberId);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

  • MemberService class
@Service
public class MemberService {
    public Member createMember(Member member) {
        Member createdMember = member;
        return createdMember;
    }

    public Member updateMember(Member member) {
        Member updatedMember = member;
        return updatedMember;
    }

    public Member findMember(long memberId) {
        Member member = new Member(memberId, "jungseo@gmail.com", "jungseo", "010-1234-5678");
        return member;
    }

    public List<Member> findMembers() {
        List<Member> members = List.of(
                new Member(1, "jungseo@gmail.com", "jungseo", "010-1234-5678"),
                new Member(2, "hgd@gmail.com", "길동", "010-1111-5555")
        );
        return members;
    }

    public void deleteMember(long memberId) {
    }
}
  • @Service : Spring Bean에 추가
  • 생성자가 하나일 경우 @Autowired 생략 가능

3. Mapper class

  • 현재 문제점

    • Controller에서 DTO 클래스를 Entity 클래스로 변환
    • Entity 클래스 객체를 클라이언트 응답으로 전송함으로써 계층 간 역할 분리 제한
  • 매퍼(Mapper)를 사용하여 해결 가능

    • Mapper 클래스에서 DTO 클래스를 Entity 클래스로 변환
    • 클라이언트 응답으로 Entity 클래스의 객체가 아닌 DTO 클래스의 객체로 변환하여 응답
  • DTO 클래스와 Entity 클래스를 서로 변환

  • 응답 데이터 DTO class

@Getter
@AllArgsConstructor
public class MemberResponseDto {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}

  • Mapper class 구현
// (1)
@Component
public class MemberMapper {
//    (2) MemberPostDto를 Member로 변환
    public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
        return new Member(0L,
                memberPostDto.getEmail(),
                memberPostDto.getName(),
                memberPostDto.getPhone());
    }

//    (3) MemberPatchDto를 Member로 변환
    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
        return new Member(
                memberPatchDto.getMemberId(),
                null,
                memberPatchDto.getName(),
                memberPatchDto.getPhone());
    }

//    (4) Member를 MemberResponseDto로 변환
    public MemberResponseDto memberToMemberResponseDto(Member member) {
        return new MemberResponseDto(
                member.getMemberId(),
                member.getEmail(),
                member.getName(),
                member.getPhone()
        );
    }
}
  • 스프링 빈으로 등록하기 위해 @Component 애너테이션 사용

  • Mapper class를 적용한 MemberController class

@RestController
@RequestMapping("/v4/members")
@Validated
public class MemberController {
    private MemberService memberService;
    private MemberMapper mapper;

//    (1) DI
    public MemberController(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberPostDto) {
//        (2) MemberPostDto를 Member로 변환
        Member member = mapper.memberPostDtoToMember(memberPostDto);

        Member response = memberService.createMember(member);

//        (3) Member를 MemberResponseDto로 변환
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.CREATED);
    }

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

//        (4) MemberPatchDto를 Member로 변환
        Member response =
                memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));

//        (5) Member를 MemberResponseDto로 변환
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
        Member response = memberService.findMember(memberId);

//        (6) Member를 MemberResponseDto로 변환
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        List<Member> members = memberService.findMembers();

//        (7) List<Member>를 MemberResponseDto로 변환
        List<MemberResponseDto> response = members.stream()
                .map(member -> mapper.memberToMemberResponseDto(member))
                .collect(Collectors.toList());

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

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
        System.out.println("# deleted memberId: " + memberId);

        memberService.deleteMember(memberId);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

4. MapStruct interface로 Mapper 자동 생성

  • MemberMapper interface
@Component
@Mapper(componentModel = "spring")
public interface MemberMapper {
    Member memberPostDtoToMember(MemberPostDto memberPostDto);
    Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
    MemberResponseDto memberToMemberResponseDto(Member member);
}
  • @Component 애너테이션을 사용하지 않아도
    @Mapper(componentModel = "spring")를 추가하면 자동으로 빈으로 등록
    - IntelliJ에서 @Component 없이 정상 작동하나 빨간줄로 오류로 처리되어 추가함
  • 구현 클래스는 프로젝트 빌드 시 build 디렉토리에 생성
  • 적용은 위에서 구현했던 Mapper class와 동일

DTO 클래스와 Entity 클래스의 역할 분리

1. Entity class

  • DB의 테이블과 매핑
  • DB에서 읽고 쓰는 작업을 위해 사용

2. DTO class

  • 다른 계층 간 데이터 전달을 위한 객체
  • 주로 API 계층과 서비스 계층 사이에서 데이터 전송을 담당
  • 특정 계층에 필요한 데이터만을 가지고 필요에 따라 형태나 구조를 변환

3. DTO class와 Entity 클래스를 매핑하는 이유

1) 계층별 관심사 분리

  • DTO는 API 계층에서 데이터를 전달 받고 응답 데이터를 전송
  • Entity는 서비스 계층에서 데이터 액세스 계층과 연동해 비즈니스 로직의 결과로 생성된 데이터를 다룸

2) 코드 구성의 단순화

  • DTO 클래스에서의 유효성 검사 애너테이션과 Entity 클래스의 JPA에서 사용되는 애너테이션 분리
  • 유지보수 용이

3) REST API 스펙의 독립성 확보

  • 데이터 엑세스 계층에서 전달받은 데이터로 채워진 Entity 클래스를 그대로 클라이언트 응답으로 전달하면 원치 않는 데이터까지 전송 가능 ex)PW
  • 필요한 정보만 제공하기 위해

0개의 댓글