서비스 계층은 API 계층에서 전달 받은 클라이언트의 요청 데이터를 기반으로 실질적인 비즈니스 요구사항을 처리하는 계층이다.
DI를 이용해 API 계층과 비즈니스 계층을 연동하고, API 계층에서 전달받은 DTO 객체를 Entity 객체로 변환해 전달한다.
Service의 의미
도메인 업무 영역을 구현하는 비즈니스 로직과 관련이 있다. 지금 당장은 비즈니스 로직을 처리하는 Service 클래스라 생각하자.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
private long memberId;
private String email;
private String name;
private String phone;
}
@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, "hgd@gmail.com", "홍길동", "010-1234-5678");
return member;
}
public List<Member> findMembers() {
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) {
}
}
@RestController
@RequestMapping("/v2/members")
@Validated
public class MemberControllerV2 {
private final MemberService memberService;
public MemberControllerV2() {
this.memberService = new MemberService();
}
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = new Member();
member.setEmail(memberDto.getEmail());
member.setName(memberDto.getName());
member.setPhone(memberDto.getPhone());
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);
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);
return new ResponseEntity<>(response, HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
List<Member> response = memberService.findMembers();
return new ResponseEntity<>(response, HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
memberService.deleteMember(memberId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Member의 객체 member를 생성해 그곳에 정보를 담은 뒤 memberService의 createMember 등을 이용해 response를 리턴한다.
하지만 이는 DI가 적용되지 않았다. MemberService의 객체를 직접 생성하고있다. 이를 변경해보자.
public class MemberControllerV3 {
private final MemberService memberService;
public MemberControllerV3(MemberService memberService) {
this.memberService = memberService;
}
~~
MemberController의 생성자 파라미터로 MemberService의 객체를 주입받는다.
하지만 이 또한 문제점이 있다.
우선 Controller 핸들러 메서드의 책임과 역할에 관한 문제인데, 이는 전달 받은 요청 데이터를 Service 클래스로 전달하고, 다시 클라이언트로 전송해주는 단순한 역할만 하도록 바꿔주는 것이 좋을 것 같다.
또한 Entity 객체(Member)를 클라이언트의 응답으로 그대로 전송하고 있는데, 이것은 데이터를 처리하는 역할만 하게 하는 것이 좋다. 계층 간의 역할 분리를 시키는 것이 좋다.
@Mapper(componentModel = "spring")
public interface MemberMapper {
public Member memberPostDtoToMember(MemberPostDto memberPostDto);
public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
public MemberResponseDto memberToMemberResponseDto(Member member);
}
@Mapper 어노테이션을 이용해 mapper를 구현한다.
@Component
public class MemberMapperImpl implements MemberMapper {
@Override
public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
if ( memberPostDto == null ) {
return null;
}
Member member = new Member();
member.setEmail( memberPostDto.getEmail() );
member.setName( memberPostDto.getName() );
member.setPhone( memberPostDto.getPhone() );
return member;
}
~~~
}
위의 Mapper interface를 만든 이후 gradle -> Tasks -> build -> build.task 를 실행하면 위의 'MemberMapperImpl' 클래스가 생성된다.
원래의 Controller에서는 핸들러 메서드가 DTO 클래스를 Entity 클래스로 변환하는 작업까지 맡아서 했다. 하지만 mapper가 이제 그 역할을 담당할 수 있게 된다.
또한 문제점 중 하나였던 Entity 클래스 객체를 응답으로 전송하는 것을 해결하기 위해 따로 MemberResponseDto 클래스를 만든 뒤 그것 또한 mapper에 넣어 리턴값을 바꿀 수 있게 한다.
@RestController
@RequestMapping("/v5/members")
@Validated
public class MemberControllerV5 {
private final MemberService memberService;
private final MemberMapper mapper;
public MemberControllerV5(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);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
Member response = memberService.findMember(memberId);
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
List<Member> members = memberService.findMembers();
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) {
memberService.deleteMember(memberId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
위의 문제들을 모두 해결한 코드이다.