본문 바로가기
부트캠프 개발일기/Spring MVC

43일차: Spring MVC(서비스 계층, Mapper)

by shyun00 2023. 4. 13.

❯ 서비스 계층

Controller 가 전달받은 요청을 직접적으로 처리하는 계층을 말한다.

Spring의 DI를 통해 API 계층과 서비스 계층을 연동할 수 있다. (서비스 객체를 API 객체 생성자 파라미터로 주입)

API 계층에서 전달받은  DTO를 서비스 계층의 도메인인 엔티티(Entity) 객체로 변환해서 서비스 계층에 전달한다.

(반대로 비즈니스 로직을 거친 Entity는 DTO 객체로 변환되어 클라이언트에게 전달된다.)

DTO 클래스와 Entity 객체의 역할을 분리하는 이유는 크게 세가지로 나뉜다.

  • 계층별 관심사 분리 : DTO의 역할은 API 계층에서 요청 데이터를 받고, 응답 데이터를 전달하는 목적을 가진다.
          반면 Entity는 서비스 계층에서 데이터 엑세스 계층과 연동하여 비즈니스 로직 결과를 다루게 된다.
          이처럼 두개의 역할이 다르기 때문에 계층, 관심사에 따라 두개를 분리하는것이 좋다.
  • 코드 구성 단순화 : 위의 관심사 분리와 비슷한 역할을 한다.
          DTO 클래스와 Entity를 분리하지 않으면 데이터와 연동했을 때 JPA 의 애너테이션과 DTO의 유효성 검사 애너테이션 등이
          복잡하게 섞이는 형태가 될 수 있다. 따라서 역할에 맞게 구분해놓는것이 코드 단순화를 위해 꼭 필요한 작업이다.
  •  REST API 스펙 독립성 확보 : 데이터 액세스 계층에서 전달받은 데이터(Entity)를 클라이언트 응답으로 그대로 보내면
          불필요한 데이터까지 전송될 수 있다. 따라서 응답으로는 Entity를 DTO로 변환하여 원하는 정보만 전달할 수 있다. 

서비스 계층 구현

1. Service 클래스 작성

    Controller 클래스의 핸들러 메서드를 참고하여 구현되어야하는 기술이 어떤것이 있는지 결정하여 메서드를 작성한다.

    @Service 애너테이션을 추가한다. -> 이 과정을 통해 해당 클래스가 Spring Bean으로 등록된다.

    자동으로 생성되어 Spring Bean으로 등록되어야하기 때문에 혹시 생성자가 여러개라면 DI 적용에 사용될 생성자에
     @Autowired 애너테이션을 추가해주어야한다.

// Member entity 를 사용하는 Service 클래스 작성

@Service
public class MemberService {
    public Member createMember(Member member) { // 컨트롤러의 postMember 메서드와 매칭
        // 아직 DB 관련 작업은 진행 X, 비즈니스 로직은 제외

        Member createdMember = member;
        return createdMember;
    }

    public Member updateMember(Member member) { // 컨트롤러의 patchMember 메서드와 매칭
        // 아직 DB 관련 작업은 진행 X, 비즈니스 로직은 제외
        
        Member updatedMember = member;
        return updatedMember;
    }
}

 

2. Entity 클래스 작성

    DTO 클래스에서 사용한 멤버 변수들을 모두 포함한 클래스를 구현한다.

    @Getter, @Setter, @NoArgsConstructor(기본 생성자), @AllArgsConstructor(모든 멤버변수를 파라미터로 갖는 생성자) 등의
     애너테이션을 붙여준다.

// 지난 수업에 이어 Member 클래스 구현

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}

 

3. Mapper 클래스 작성

어제 작성했던 코드에서는 컨트롤러에서 DTO를 받아서 Entity 객체로 변환하고, Entity 객체 형태로 반환하는 형태였다.

위의 [DTO 클래스와 Entity 객체의 역할을 분리하는 이유]에서 언급했던바와 같이 컨트롤러는 DTO 를 받고 DTO를 전달하는 역할을 해야한다.

따라서 DTO -> Entity, Entity -> DTO 역할을 하는 매퍼를 별도로 작성하고 DI를 통해 이를 주입해준다.

(Mapper 클래스는 직접 구현할수도 있으나, MapStruct 기반의 Mapper를 사용하면 간단하게 생성할 수 있다.)

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

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

위와 같이 @Mapper를 사용하면 MapStruct가 자동으로 매퍼 클래스를 구현해준다. (MapStruct 의존 라이브러리 추가해야함)

애너테이션 안의 comonentModel은 해당 매퍼가 스프링 빈으로 등록됨을 의미한다.

 

4. 컨트롤러 수정

최종적으로 Mapper를 컨트롤러 안에 주입하여 아래와 같이 코드를 작성할 수 있다.

컨트롤러는 DTO 객체를 받아서 Mapper 에게 전달하고, Mapper가 해당 객체를 Entity로 변환하여 Service 에게 전달한다.

Service 의 비즈니스 로직을 통해 반환된 데이터를 Mapper를 이용해 다시 DTO 객체로 변환한다.

최종적으로 컨트롤러는 해당 DTO 객체를 리턴해주게 된다.

(결국, 컨트롤러는 전체 코드를 지휘하는 역할을 할 뿐 세부적인 동작은 Mapper와 Service 가 담당하게 된다.)

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

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

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {

        Member member = mapper.memberPostDtoToMember(memberDto);

        Member response = memberService.createMember(member);

        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);

        Member response = 
              memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));

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

점점 사용하는 애너테이션도 많아지고, 한 부분을 수정하면 거기에 따라 다른 부분들을 수정해야하는 경우들이 있어서 복잡한 부분이 많다.

강사님들의 얘기를 들어보니 데이터 액세스 계층까지 가면 더 많이 복잡해질거라고 하는데..ㅎㅎ

우선 이번 주말중에 이때까지 작성했던 코드들을 강의 자료를 최대한 참고하지 않고 작성해보는 연습을 해봐야겠다.