도메인 업무 영역을 구현하는 비즈니스 로직과 관련 있다.
아직 코린이기에 Service라는 용어를 접하게 된다면 지금은 단순히 비즈니스 로직을 처리하는 Service 클래스라고 생각하자.
Service Class는 Controller Class와 1대1 매칭이 된다.
ex)

파라미터와 리턴값에 Member라는 타입을 사용했고 member Class는 DTO가 API 계층에서 클라이언트의 Request Body를 전달받고 클라이언트에게 되돌려 줄 응답 데이터를 담는 역할을 한다면, Member 클래스는 API 계층에서 전달받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할를 한다.
'Member' 클래스처럼 서비스 계층에서 데이터 액세스 계층과 연동하면서 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스를 도메인 엔티티(Entity) 클래스라고 한다.
핵심 포인트
애플리케이션에 있어 Service는 도메인 업무 영역을 구현하는 비즈니스 로직을 처리하는 것을 의미한다.
Controller 클래스에 @RestController 애너테이션을 추가하면 Spring Bean으로 등록된다.
Service 클래스에 @Service 애너테이션을 추가하면 Spring Bean으로 등록된다.
생성자 방식의 DI는 생성자가 하나일 경우에는 @Autowired 애너테이션을 추가하지 않아도 DI가 적용된다.
Mapper를 사용해서 DTO 클래스와 Entity 클래스 간의 관심사를 분리할 수 있다.
Mapper를 개발자가 직접 구현하기보다는 MapStruct 같은 매핑 라이브러리를 사용하는 것이 생산성 측면에서 더 나은 선택이다.
@Component //
public class MemberMapper {
// MemberPostDto를 Member로 변환
public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
return new Member(0L,
memberPostDto.getEmail(),
memberPostDto.getName(),
memberPostDto.getPhone());
}
// MemberPatchDto를 Member로 변환
public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
return new Member(memberPatchDto.getMemberId(),
null,
memberPatchDto.getName(),
memberPatchDto.getPhone());
}
// Member를 MemberResponseDto로 변환
public MemberResponseDto memberToMemberResponseDto(Member member) {
return new MemberResponseDto(member.getMemberId(),
member.getEmail(),
member.getName(),
member.getPhone());
}
}
MemberResponseDto 클래스는 응답 데이터의 역할을 해주는 DTO 클래스가 필요
@Getter
@AllArgsConstructor
public class MemberResponseDto {
private long memberId;
private String email;
private String name;
private String phone;
}
이를 Controller에 적용 시
이전 Class
@RestController
@RequestMapping("/v2/members")
@Validated
public class MemberController {
private final MemberService memberService;
public MemberController() {
this.memberService = new MemberService(); // (1)
}
@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) {
// (6)
Member response = memberService.findMember(memberId);
return new ResponseEntity<>(response, HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
// (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("# delete member");
// (8)
memberService.deleteMember(memberId);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Mapper를 적용한 Class
@RestController
@RequestMapping("/v4/members")
@Validated
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
// (1) MemberMapper DI
public MemberController(MemberService memberService, MemberMapper mapper) {
this.memberService = memberService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
// (2) 매퍼를 이용해서 MemberPostDto를 Member로 변환
Member member = mapper.memberPostDtoToMember(memberDto);
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("# delete member");
memberService.deleteMember(memberId);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
매우 깔끔해졌다.
하지만 계속 계속 이렇게 만드는건 귀찮기에 MapStruct을 사용하면 된다.
MapStruct는 DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는 매퍼(Mapper) 구현 클래스를 자동으로 생성해 주는 코드 자동 생성기이다.
MapStruct 관련 의존 라이브러리를 Gradle의 build.gradle 파일에 아래와 같이 추가.
dependencies {
...
...
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
build.gradle 파일에 의존 라이브러리를 추가한 후에는 아래와 같이 Gradle 프로젝트를 reload해야 한다.
1. IntelliJ IDE 우측의 [Gradle] 탭을 클릭한다.
2. IntelliJ 프로젝트명 위에서 마우스 우클릭
3. 컨텍스트 메뉴에서 [Reload Gradle Project] 또는 [Refresh Gradle Dependencies]를 클릭해서 프로젝트 또는 의존 라이브러리를 갱신해야한다.
package com.codestates.member.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") // (1)
public interface MemberMapper {
Member memberPostDtoToMember(MemberPostDto memberPostDto);
Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
MemberResponseDto memberToMemberResponseDto(Member member);
}
기가 막히다.
Mapper를 사용해서 DTO 클래스와 Entity 클래스 간의 관심사를 분리할 수 있다.
Mapper를 개발자가 직접 구현하기보다는 MapStruct 같은 매핑 라이브러리를 사용하는 것이 생산성 측면에서 더 나은 선택이다.
@Mapper 애너테이션의 애트리뷰트로 componentModel = "spring"을 지정해 주면 Spring의 Bean으로 등록이 된다는 사실을 꼭 기억하자.
복습할 때 다시 정독해야겠다 했는데 토나오는줄 알았다...
아직도 잘모르겠으니 질문은 받지 않도록하자...