템플릿 프로젝트 복제
https://github.com/codestates-seb/be-template-service-layer
클론 후 인텔리제이로 프로젝트 오픈
학습 목표
DI를 사용해서 API 계층과 서비스 계층을 연동할 수 있다
API 계층의 DTO 클래스와 서비스 계층의 엔티티 클래스를 매핑할 수 있다
(API 계층에서 전달받은 DTO 객체를 서비스 계층의 도메인 엔티티 객체로 변환할 수 있다)
서비스 계층 : API 계층에서 전달받은 클라이언트의 요청 데이터를 기반으로 실질적인 비지니스 요구사항을 처리하는 계층
API 계층과 서비스 계층 연동 : API 계층에서 구현한 Controller 클래스가 서비스 계층의 Service 클래스와 메서드 호출을 통해 상호 작용
[Service]
도메인 업무 영역 구현하는 비지니스 로직과 관련
도메인 모델 - 빈약한 도메인 모델, 풍부한 도메인 모델
[MemberController 코드]
package com.codestates.member;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {
@PostMapping
public ResponseEntity postMember(
// postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다
@Valid @RequestBody MemberPostDto memberDto) {
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(
// patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(
// getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId) {
System.out.println("# memberId: " + memberId);
// not implementation
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
// getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다
System.out.println("# get Members");
// not implementation
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(
// deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId) {
System.out.println("# deleted memberId: " + memberId);
// No need business logic
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
// 회원 정보는 구현해야 할 실습이 없습니다!
}
MemberController에 총 다섯개의 핸들러 메서드 존재
postMember()
patchMember()
getMember()
getMembers()
deleteMember()
[MemberService 클래스 기본 구조 작성]
package com.codestates.member;
import java.util.List;
public class MemeberService {
public Member createMember(Member member) { // 파라미터와 리턴값에 Member 타입 사용
return null;
}
public Member updateMember(Member member) { // 파라미터와 리턴값에 Member 타입 사용
return null;
}
public Member findMember(long memberID) {
return null;
}
public List<Member> findMembers() {
return null;
}
public void deleteMember(long memberId) {
}
}
[Member 클래스의 역할]
API 계층에서 전달 받은 요청 데이터를 기반으로 서비스 계층에서 비지니스 로직을 처리하기 위해 필요한 데이터를 전달 받고, 비지니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할
-> 도메인 엔티티(Entity) 클래스라고 부름
package com.codestates.member;
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;
}
@Getter, @Setter : lombok 라이브러리에서 제공하는 애너테이션. DTO 클래스 작성시 각 멤버 변수에 해당하는 getter/setter 메서드를 일일히 작성하는 수고를 덜어줌
@AllArgsConstructor : 현재 Member 클래스에 추가된 모든 멤버 변수를 파라미터를 갖는 Member 생서자를 자동으로 생성해줌
@NoArgsConstructor : 파라미터가 없는 기본 생성자를 자동으로 생성해줌
[MemberService 클래스 구현]
package com.codestates.member;
import java.util.List;
public class MemeberService {
public Member createMember(Member member) { // 파라미터와 리턴값에 Member 타입 사용
// TODO should business logic
// TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요
Member createMember = member;
return createMember;
}
public Member updateMember(Member member) { // 파라미터와 리턴값에 Member 타입 사용
// TODO should business logic
// member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요
Member updateMember = member;
return updateMember;
}
public Member findMember(long memberID) {
// TODO should business logic
// TODO member 객체는 나중에 DB에서 조회 하는 것으로 변경 필요
Member member = new member(memberID, "hgd@gmail.com", "홍길동", "010-1234-5678");
return member;
}
public List<Member> findMembers() {
// TODO should business logic
// TODO member 객체는 나중에 DB에서 조회 하는 것으로 변경 필요
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) {
// TODO should business logic
}
}
[DI 없이 비지니스 계층과 API 계층 연동]
다른 클래스 기능 사용 방법 : 객체 생성하여 해당 객체로 클래스의 메서드 호출해서 사용하기 (new 키워드)
package com.codestates.member;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
@RestController
@RequestMapping("/v2/members")
@Validated
public class MemberController {
private final MemberService memberService;
public MemberController() {
this.memberService = new MemberService();
// (1) MemberService 사용하기 위해 new 키워드 사용해 MemberService의 객체 생성
}
@PostMapping
public ResponseEntity postMember(
// postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다
@Valid @RequestBody MemberPostDto memberDto) {
// (2) 클라이언트에서 전달 받은 DTO 클래스의 정보를 MemberService의 updateMember() 메서드의 파라미터로 전달하기 위해
// MemberPostDto 클래스의 정보를 Member 클래스에 채워넣는 중
// -> 후에 Mapper를 이용한 DTO 객체 <-> 엔티티 객체 매핑
Member member = new Member();
member.setEmail(memberDto.getEmail());
member.setName(memberDto.getName());
member.setPhone(memberDto.getPhone());
// (3) 회원 정보 등록을 위해 MemberService 클래스의 createMember() 메서드 호출. 💖서비스 계층과의 연결 지점
Member reponse = memberService.createMember(member);
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(
// patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// (4) 클라이언트에서 전달 받은 DTO 클래스의 정보를 MemberService의 createMember() 메서드의 파라미터로 전달하기 위해
// MemberPatchDto 클래스의 정보를 Member 클래스에 채워넣고 있음
Member member = new Member();
memeber.SetMemberId(memberPatchDto.getMemberId());
memeber.SetName(memberPatchDto.getName());
memeber.SetPhone(memberPatchDto.getPhone());
// (5) 회원 정보 수정을 위해 MemberService 클래스의 updateMember 메서드를 호출. 💖서비스 계층과의 연결 지점
Member response = memberService.updateMember(member);
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(
// getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId) {
// (6) 한 명의 회원 정보 조회를 위해 MemberService 클래스의 findMember() 메서드 호출
// 특정 회원의 정보를 조회하는 기준인 memberId를 파라미터로 넘겨줌. 💖서비스 계층과의 연결 지점
Member response = memberService.findMember(memberId);
return new ResponseEntity<>(response, HttpStatus.OK);
// System.out.println("# memberId: " + memberId);
//
// // not implementation
// return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
// getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다
// (7) 모든 회원의 정보를 조회하기 위해 MemberService 클래스의 findMembers() 메서드를 호출
List<Member> response = memberService.findMembers();
return new ResponseEntity<>(response, HttpStatus.OK);
// System.out.println("# get Members");
//
// // not implementation
//
// return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(
// deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId) {
System.out.println("# deleted memberId: " + memberId);
// No need business logic
// (8) 한 명의 회원 정보를 삭제하기 위해 MemberService 클래스의 deleteMember() 메서드를 호출.
// 특정 회원의 정보를 삭제하는 기준인 memberId를 파라미터로 넘겨줌
memberService.deleteMember(memberId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
// 회원 정보는 구현해야 할 실습이 없습니다!
}
[DI를 적용한 비지니스 계층과 API 계층 연동]
Spring에서 지원하는 DI 기능 사용하지 않아서 MemberConroller와 MemberService가 강하게 결합되어 있는 상태
-> Spring의 DI 사용시 느슨한 결합으로 바꿀 수 있음 (의존성 주입을 위해. 후에 수정 용이)
@RestController
@RequestMapping("/v3/members")
@Validated
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) { // 약한 의존성 위해 MemberController 변경
this.memberService = new MemberService();
MemberController에 Spring DI 기능 적용
V2 : MemberController 생성자 내부에서 new 키워드 사용하여 MemberService 객체 생성
↓
V3 : Spring DI 기능 이용해서 MemberController 생성자 파라미터로 MemberService의 객체를 주입(Injection). (주입받기 위해 주입 받는, 하는 대상 클래스 모두 Spring Bean이어야)
MemberService 에도 @Service 애너테이션 추가해줘야 Spring Bean이 됨
package com.codestates.member;
import java.util.List;
@Service
public class MemberService {
public Member createMember(Member member) {
...
}
[MemberController(V3)의 문제점]
Controller 핸들러 메서드의 책임과 역할 : 핸들러 메서드는 클라이언트로부터 전달 받은 요청 데이터를 Service 클래스로 전달하고, 응답 데이터를 클리아언트로 다시 전송해주는 단순한 역할만 하는 것이 좋음. 현재는 엔티티 객체로 변환하는 작업까지 도맡아서 하는 중 -> 다른 클래스에게 변환해달라고 요청
Service 계층에서 사용되는 엔티티 객체를 클라이언트의 응답으로 전송중 : 계층 간의 역할 분리 이루어지지 않음 -> 앤티티 클래스 전송하지 않고, 엔티티 클래스의 객체를 DTO 클래스의 객체로 다시 바꿔주면 됨
∴DTO 클래스와 엔티티 클래스를 변환해주는 매퍼(Mapper) 필요
[Mapper 클래스 구현]
package com.codestates.member;
import org.springframework.stereotype.Component;
@Component // (1) MemberMapper를 Spring의 Bean으로 등록하기 위해 @Component 애터네이션 추가.
// 등록된 Bean은 MemberController에서 사용
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());
}
}
[MemberResponseDto 클래스 생성]
package com.codestates.member;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MemberResponseDto {
private long memberId;
private String email;
private String name;
private String phone;
}
[MemberController의 핸들러 메서드에 Mapper 클래스 적용]
package com.codestates.member;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/v4/members")
@Validated
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
// <1> MemberMapper DI
// Spring Bean에 등록된 MemberMapper 객체를 MemberController에서 사용하기 위해 DI로 주입받는 중
public MemberController(MemberService memberService, MemberMapper mapper) { // 약한 의존성 위해 MemberController 변경
// this.memberService = new MemberService();
// // (1) MemberService 사용하기 위해 new 키워드 사용해 MemberService의 객체 생성
this.memberService = memberService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postMember(
// postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다
@Valid @RequestBody MemberPostDto memberDto) {
// (2) 클라이언트에서 전달 받은 DTO 클래스의 정보를 MemberService의 updateMember() 메서드의 파라미터로 전달하기 위해
// MemberPostDto 클래스의 정보를 Member 클래스에 채워넣는 중
// -> 후에 Mapper를 이용한 DTO 객체 <-> 엔티티 객체 매핑
// <2> 매퍼 이용해서 MemberPostDto를 Member로 변환
Member member = mapper.memberPostDtoToMember(memberDto);
// Member member = new Member();
// member.setEmail(memberDto.getEmail());
// member.setName(memberDto.getName());
// member.setPhone(memberDto.getPhone());
// (3) 회원 정보 등록을 위해 MemberService 클래스의 createMember() 메서드 호출. 💖서비스 계층과의 연결 지점
Member response = memberService.createMember(member);
// return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
// <3> 매퍼 이용해서 Member를 MemberResponseDto로 변환
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(
// patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// // (4) 클라이언트에서 전달 받은 DTO 클래스의 정보를 MemberService의 createMember() 메서드의 파라미터로 전달하기 위해
// // MemberPatchDto 클래스의 정보를 Member 클래스에 채워넣고 있음
// Member member = new Member();
// memeber.SetMemberId(memberPatchDto.getMemberId());
// memeber.SetName(memberPatchDto.getName());
// memeber.SetPhone(memberPatchDto.getPhone());
// <4> 매퍼 이용해서 MemberPatchDto를 Member로 변환
Member response = memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));
// // (5) 회원 정보 수정을 위해 MemberService 클래스의 updateMember 메서드를 호출. 💖서비스 계층과의 연결 지점
// Member response = memberService.updateMember(member);
// <5> 매퍼 이용해서 Member를 MemberResponseDto로 변환
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
// return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(
// getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다
@PathVariable("member-id") @Positive long memberId) {
// (6) 한 명의 회원 정보 조회를 위해 MemberService 클래스의 findMember() 메서드 호출
// 특정 회원의 정보를 조회하는 기준인 memberId를 파라미터로 넘겨줌. 💖서비스 계층과의 연결 지점
Member response = memberService.findMember(memberId);
// <6> 매퍼 이용해서 Member를 MemberResponseDto로 변환
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
// return new ResponseEntity<>(response, HttpStatus.OK);
// System.out.println("# memberId: " + memberId);
//
// // not implementation
// return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
// getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다
// (7) 모든 회원의 정보를 조회하기 위해 MemberService 클래스의 findMembers() 메서드를 호출
List<Member> members = memberService.findMembers();
// <7> 매퍼 이용해서 List<Member>를 MemberResponseDto로 변환
// memberService.findMembers() 통해 리턴되는 값이 List이므로
// List 안의 Member 객체들을 하나씩 꺼내서 MemberResponseDto 객체로 변환
// -> Java의 Stream이 해주고 있음
List<MemberResponseDto> response =
members.stream()
.map(member -> mapper.memberToMemberResponseDto(member))
.collect(Collectors.toList());
return new ResponseEntity<>(response, HttpStatus.OK);
// System.out.println("# get Members");
//
// // not implementation
//
// return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{member-id}"
delete이하 동일
Mapper 클래스 사용함으로써 MemberController(V3)의 문제점 해결
MemberController의 핸들러 메서드가 DTO 클래스를 엔티티 클래스로 변환하는 작업까지 도맡아서 하는 문제
: MemberMapper에게 DTO 클래스 -> 엔티티 클래스로 변환하는 작업을 위임. 역할 분리로 코드 깔끔
엔티티 클래스의 객체를 클라이언트의 응답으로 전송하는 문제
: MemberMapper가 엔티티 클래스를 DTO클래스로 변환. 서비스 계층에 있는 엔티티 클래스를 API 계층에서 직접적으로 사용하는 문제 해결
[MapStruct 이용한 Mapper 자동 생성]
수작업으로 Mapper 클래스 만드는 것은 비효율적
-> MapStruct 사용하여 매퍼 클래스 자동 생성하기
(MapStruct 의존 라이브러리를 Gradle의 build.gradle파일에 추가하기)
dependencies {
...
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
추가 후 Gradle 프로젝트 reload
[MapStruct 기반의 Mapper 인터페이스 정의]
import com.codestates.member.mapstruct.mapper;
import com.codestates.member.dto.MemberPatchDto; // dto 패키지 새로 생성해서 Dto 파일들 새로 넣어줬음
import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.dto.MemberResponseDto;
import com.codestates.member.entity.Member; // entity 패키지 새로 생성하고 Member 파일 넣어주기
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring") // (1) @Mapper 애터테이션 추가 -> MapStruct의 매퍼 인터페이스로 정의
// componentModel = "spring" 지정시 Spring의 Bean으로 등록됨
public interface MemberMapper {
Member memberPostDtoToMember(MemberPostDto memberPostDto);
Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
MemberResponseDto memberToMemberResponseDto(Member member);
}
[자동 생성된 인터페이스 구현 클래스 확인]
Gradle -> 프로젝트명 -> Tasks -> build -> build (task 실행)
MemberMapperImpl 클래스 생성 확인 : 인텔리제이 좌측 Project -> 프로젝트명 -> build 내 MemberMapper 인터페이스 위치한 패키지 내 생성
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.codestates.member.mapstruct.mapper;
import com.codestates.member.Member;
import com.codestates.member.dto.MemberPatchDto;
import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.dto.MemberResponseDto;
import org.springframework.stereotype.Component;
@Component
public class MemberMapperImpl implements MemberMapper {
public MemberMapperImpl() {
}
public Member memberPostDtoToMember(MemberPostDto var1) {
throw new Error("Unresolved compilation problems: \n\tThe method setEmail(String) is undefined for the type Member\n\tThe method setName(String) is undefined for the type Member\n\tThe method setPhone(String) is undefined for the type Member\n");
}
public Member memberPatchDtoToMember(MemberPatchDto var1) {
throw new Error("Unresolved compilation problems: \n\tThe method setMemberId(long) is undefined for the type Member\n\tThe method setName(String) is undefined for the type Member\n\tThe method setPhone(String) is undefined for the type Member\n");
}
public MemberResponseDto memberToMemberResponseDto(Member var1) {
throw new Error("Unresolved compilation problems: \n\tThe method getMemberId() is undefined for the type Member\n\tThe method getEmail() is undefined for the type Member\n\tThe method getName() is undefined for the type Member\n\tThe method getPhone() is undefined for the type Member\n\tThe constructor MemberResponseDto(long, String, String, String) is undefined\n");
}
}
[MemberController 핸들러 메서드에서 MapStruct 적용]
package com.codestates.member;
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 com.codestates.member.mapstruct.mapper.MemberMapper; // Mapstruct Mapper 적용
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/v5/members") // URI 버전 변경
@Validated
// 이하 동일
[MapStruct vs ModelMapper]
MapStruct가 더 빠름. Mapper을 직접 구현하기 보다는 매핑 라이브러리를 사용하자!
[DTO 클래스와 엔티티 클래스의 역할 분리가 필요한 이유]
계층별 관심사의 분리 (객체지향코드 관점에서 리팩토링 대상)
코드 구성의 단순화 (유지보수)
REST API 스펙의 독립성 확보 (ex. 회원 로그인 패스워드)