[Spring] MVC - Service

zini9188·2023년 2월 21일
0

Spring

목록 보기
18/33

서비스란?

서비스란 도메인 업무 영역을 구현하는 비즈니스 로직과 관련이 있다. 애플리케이션의 비즈니스 로직을 처리하기 위한 서비스 계층은 대부분 도메인 모델을 포함한다.

도메인 모델은 빈약한 도메인 모델과 풍부한 도메인 모델로 구분할 수 있는데, 이러한 도메인 모델은 DDD(도메인 주도 설계, Domain Driven Design)와 관련이 깊다.

DDD는 클래스 설계 경험이 풍부해야 제대로 사용할 수 있으며, Spring Data JDBC의 경우 DDD와 밀접한 관련이 있다.

결론적으로 서비스란 애플리케이션 내 비즈니스 로직을 처리하는 클래스라고 볼 수 있다.

Service 클래스 작성 방법

서비스는 클래스 레벨에 @Service 어노테이션을 추가하여 스프링 빈으로 등록할 수 있다.

서비스 클래스는 Controller 클래스의 핸들러 메서드와 1:1 매칭되는 메서드들을 가진다.

@Service
public class MemberService {
}

도메인 엔티티 클래스

API 계층에서는 DTO가 클라이언트의 요청을 받고 응답 데이터를 담는 역할을 하였다.

서비스 계층에서는 도메인 엔티티 클래스가 API 계층에서 전달 받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달 받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할을 한다.

Member 클래스

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

DTO와 Entity를 분리하여 얻을 수 있는 이점

계층별 관심사의 분리

두 클래스는 사용되는 계층이 다르다.

API 계층에서는 DTO를 이용하여 요청 데이터를 전달 받고 응답 데이터를 전송하는 것이 목적이며, 서비스 계층에서는 Entity를 사용하여 데이터 액세스 계층과 연동하여 비즈니스 로직의 결과로 생성된 데이터를 다루는 것이 목적이다.

코드 구성의 단순화

DTO 클래스에서 사용하는 유효성 검사 어노테이션이 Entity 클래스에서 사용이 된다면, JPA에서 사용하는 어노테이션과 뒤섞여 유지보수의 어려움이 생긴다.

REST API 스펙의 독립성 확보

데이터 액세스 계층에서 전달 받은 데이터로 채워진 Entity 클래스를 클라이언트의 응답으로 그대로 보내면 클라이언트에 암호와 같은 원치 않거나 중요한 데이터를 전송할 수 있다. Dto 클래스는 이러한 원치 않는 노출을 방지할 수 있다.

DI를 적용한 서비스 계층과 API 계층의 연동

생성자 방식의 DI는 생성자가 하나인 경우 @Autowired 애너테이션을 추가하지 않아도 DI가 적용된다.

MemberController 클래스

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

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

매퍼를 이용한 DTO 클래스와 엔티티 클래스 매핑

매퍼를 사용하지 않는 경우 API 계층과 서비스 계층에서의 엔티티 <-> DTO 클래스 간에 변환하는 작업이 핸들러 메서드에서 필요하다. 하지만 매퍼 클래스를 정의하여 이를 분리할 수 있다.

MemberMapper 클래스

@Component  
public class MemberMapper {
    public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
        return new Member(0L,
                memberPostDto.getEmail(), 
                memberPostDto.getName(), 
                memberPostDto.getPhone());
    }
    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
        return new Member(memberPatchDto.getMemberId(),
                null, 
                memberPatchDto.getName(), 
                memberPatchDto.getPhone());
    }
    public MemberResponseDto memberToMemberResponseDto(Member member) {
        return new MemberResponseDto(member.getMemberId(),
                member.getEmail(), 
                member.getName(), 
                member.getPhone());
    }
}

MemberMapper를 스프링의 빈으로 등록하기 위해 @Component 어노테이션을 사용하였고 Controller에서 사용된다.

MemberController 클래스

@RestController
@RequestMapping("/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);
    }
}

위 코드처럼 API 계층에서 Entity <-> DTO 간에 변환을 할 수 있다.

MapStruct를 활용한 Mapper 자동 생성 설정

도메인 업무 기능이 늘어남에 따라 작성해야 하는 Mapper가 늘어난다. MapStruct를 활용하면 자동으로 Mapper를 생성해준다.

의존 라이브러리 추가

build.gradle 파일

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

Build Task를 통한 자동 생성

이제 매퍼를 작성할 때 다음과 작성한 후 gradle의 build task를 실행하면 자동으로 mapper 클래스가 작성된다.

MemberMapper 클래스

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

자동으로 완성된 MemberMapperImpl 클래스

@Component
public class MemberMapperImpl implements MemberMapper {
    public MemberMapperImpl() {
    }

    public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
        if (memberPostDto == null) {
            return null;
        } else {
            Member member = new Member();
            member.setEmail(memberPostDto.getEmail());
            member.setName(memberPostDto.getName());
            member.setPhone(memberPostDto.getPhone());
            return member;
        }
    }

    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
        if (memberPatchDto == null) {
            return null;
        } else {
            Member member = new Member();
            member.setMemberId(memberPatchDto.getMemberId());
            member.setName(memberPatchDto.getName());
            member.setPhone(memberPatchDto.getPhone());
            return member;
        }
    }

    public MemberResponseDto memberToMemberResponseDto(Member member) {
        if (member == null) {
            return null;
        } else {
            long memberId = 0L;
            String email = null;
            String name = null;
            String phone = null;
            memberId = member.getMemberId();
            email = member.getEmail();
            name = member.getName();
            phone = member.getPhone();
            MemberResponseDto memberResponseDto = 
											     new MemberResponseDto(memberId, email, name, phone);
            return memberResponseDto;
        }
    }
}
profile
백엔드를 지망하는 개발자

0개의 댓글