1. 서비스 계층 연동
- 예제 코드를 활용하여 서비스 계층의 연동을 진행한다
- API계층과 서비스계층의 연동은 API계층의 클래스가 서비스계층의 클래스와 메서드를 호출하여 상호 작용한다는 의미이다
1) 예제 코드
2) 학습 방향
- Spring의 DI(Dependency Injection)를 이용해서 API 계층과 비즈니스 계층을 연동한다
- API 계층에서 전달받은 DTO 객체를 비즈니스 계층의 도메인 엔티티(Entity) 객체로 변환해서 전달하는 방법을 알아본다
- Controller에 많은 개선이 일어나기 때문에 Controller의 요청 URI가 “/v1/xxxx”에서 “/v2/xxxx”와 같이 지속적으로 API 버전이 바뀌는 점에 유의한다
- 서비스 계층에서 샘플 응답 데이터를 클라이언트에 전송하는 로직을 확인할 수 있다
2. 서비스(Service)
- 애플리케이션에서 Service는 도메인 업무 영역을 구현하는 비지니스 로직과 연관이 있다
- 비지니스 로직을 처리하기 위한 서비스 계층은 도메인 모델을 포함하고 있다
- 도메인 모델은 DDD(Domain Driven Design / 도메인 주도 설계)와 관련성이 높다
- 빈약한 도메인 모델(anemic domain model) 과 풍부한 도메인 모델(roch domain model)로 구분된다
3. Service 클래스 작성
1) Controller 클래스와 연동하는 Service 클래스의 틀을 작성한다
package com.codestates.member;
import java.lang.reflect.Member;
import java.util.List;
public class MemberService {
//MemberController 클래스의 postMember
public Member saveMember(Member member) {
return null;
}
//MemberController 클래스의 patchMember
public Member updateMember(Member member) {
return null;
}
//MemberController 클래스의 getMember
public Member findMember(Member member) {
return null;
}
//MemberController 클래스의 getMembers
public List<Member> findMember() {
return null;
}
//MemberController 클래스의 deleteMember
public void deleteMember(long memberId) {
}
}
- MemberController 클래스에는 핸들러 메서드가 5개 있다
- postMember() , patchMember() , getMember() , getMembers() , deleteMember()
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 {
//postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다.
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
//patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다.
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
//getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다.
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
System.out.println("# memberId: " + memberId);
// not implementation
return new ResponseEntity<>(HttpStatus.OK);
}
//getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다.
@GetMapping
public ResponseEntity getMembers() {
System.out.println("# get Members");
// not implementation
return new ResponseEntity<>(HttpStatus.OK);
}
//deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달 받는다.
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
System.out.println("# deleted memberId: " + memberId);
// No need business logic
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
- MemberController의 핸들러 메서드에서 클라이언트의 요청 데이터(Request Body)를 전달 받을 때 MemberPostDto나 MemberPatchDto 같은 DTO 클래스를 사용한다
- DTO가 API 계층에서 클라이언트의 Request Body를 전달 받고 클라이언트에게 되돌려 줄 응답 데이터를 담는 역할을 한다 - MemberService에서는 saveMember() 메서드와 updateMember() 메서드의 파라미터와 리턴값에 Member라는 타입을 사용했다
- API 계층에서 전달 받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달 받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할을 한다
2) Member 클래스 구현
- 아노테이션을 적용하면 자동으로 import 된다
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;
}
3) MemberService 클래스 세부 구현
package com.codestates.Member;
import java.lang.reflect.Member;
import java.util.List;
public class MemberService {
//MemberController 클래스의 postMember
public Member createMember(Member member) {
// TODO should business logic
// TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
Member createdMember = member;
// DB 데이터가 없으므로 Member 객체를 그대로 리턴
return createdMember;
}
//MemberController 클래스의 patchMember
public Member updateMember(Member member) {
// TODO should business logic
// TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
Member updatedMember = member;
// DB 데이터가 없으므로 Member 객체를 그대로 리턴
return updatedMember;
}
//MemberController 클래스의 getMember
public Member findMember(Member member) {
// TODO should business logic
// TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
// 현재는 stub 데이터로 사용
Member member =
new Member(memberId, "hgd@gmail.com", "홍길동", "010-1234-5678");
return member;
}
//MemberController 클래스의 getMembers
public List<Member> findMember() {
// 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;
}
//MemberController 클래스의 deleteMember
public void deleteMember(long memberId) {
// TODO should business logic
}
}
4) DI(Dependency Injection)없이 비즈니스 계층과 API 계층 연동
- 어떤 클래스가 다른 클래스의 기능을 사용하고자 한다면 객체를 새로 생성하여 다른 클래스의 메서드를 호출하면 된다
- 객체의 생성은 new 키워드를 사용한다
- MemberController에서 MemberService의 기능을 사용하도록 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;
import java.util.List;
@RestController
@RequestMapping("/v2/members")
@Validated
public class MemberController {
private final MemberService memberService;
private class MemberService memberService;
//에러로 인하여 생긴 class - 질문 요함...???
public MemberController() {
this.memberService = new MemberService();
//MembeService 클래스를 사용하기 위하여 객체를 생성한다
}
//postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다.
@PostMapping
public ResponseEntity postMember(
@Valid @RequestBody MemberPostDto memberDto) {
//클라이언트에서 전달 받은 DTO 클래스의 정보를
// MemberService의 updateMember() 메서드의 파라미터로 전달하기 위해
// MemberPostDto 클래스의 정보를 Member 클래스에 채워준다
Member member = new Member();
member.setEmail(memberDto.getEmail());
member.setName(memberDto.getName());
member.setPhone(memberDto.getPhone());
//회원 정보 등록을 위해 MemberService 클래스의 createMember() 메서드를 호출한다
Member response = memberService.createMember(member);
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
//patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다.
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(
@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
//클라이언트에서 전달 받은 DTO 클래스의 정보를
//MemberService의 createMember() 메서드의 파라미터로 전달하기 위해
//MemberPatchDto 클래스의 정보를 Member 클래스에 채워준다
Member member = new Member();
member.setMemberId(memberPatchDto.getMemberId());
member.setName(memberPatchDto.getName());
member.setPhone(memberDto.getPhone());
//회원 정보 수정을 위해 MemberService 클래스의 updateMember() 메서드를 호출한다
Member response = memberService.updateMember(member);
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
//getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다.
@GetMapping("/{member-id}")
public ResponseEntity getMember(
@PathVariable("member-id") @Positive long memberId) {
//한 명의 회원 정보 조회를 위해 MemberService 클래스의 findMember() 메서드를 호출한다
//특정 회원의 정보를 조회하는 기준인 memberId를 파라미터로 넘겨준다
Member response = memberService.fineMember(memberId);
System.out.println("# memberId: " + memberId);
// not implementation
return new ResponseEntity<>(HttpStatus.OK);
}
//getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다.
@GetMapping
public ResponseEntity getMembers() {
//모든 회원의 정보를 조회하기 위해 MemberService 클래스의 findMembers() 메서드를 호출한다
List<Member> response = memberService.fineMembers();
System.out.println("# get Members");
// not implementation
return new ResponseEntity<>(HttpStatus.OK);
}
//deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달 받는다.
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(
@PathVariable("member-id") @Positive long memberId) {
System.out.println("# deleted memberId: " + memberId);
//한 명의 회원 정보를 삭제하기 위해 MemberService 클래스의 deleteMember() 메서드를 호출한다
//특정 회원의 정보를 삭제하는 기준인 memberId를 파라미터로 넘겨준다
memberService.deleteMember(memberId);
// No need business logic
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
5) DI를 적용한 비즈니스 계층과 API 계층 연동
- MemberController v2 코드는 Spring에서 지원하는 DI 기능을 사용하지 않았기 때문에 MemberController와 MemberService가 강하게 결합(Tight Coupling)되어 있는 상태이다
- Spring의 DI를 사용하면 클래스 간의 결합을 느슨한 결합(Loose Coupling)으로 만들 수 있다
- Spring의 DI를 사용하도록 MemberController를 수정해 본다
//DI를 적용하기 위한 코드 수정, 비어있던 (파라미터)에 객체를 추가한다
public MemberController(MemberService memberService) {
// Spring이 애플리케이션 로드시, ApplicationContext에 있는 MemberService 객체를 주입하여 준다
this.memberService = memberService();
//DI를 적용하여 MembeService 클래스를 사용하기 위하여 객체를 지정한다
}
- Spring에서 DI를 통해서 어떤 객체를 주입 받기 위해서는 주입을 받는 클래스와 주입 대상 클래스 모두 Spring Bean이어야 한다
- MemberService 클래스에 @Service 애너테이션을 추가하여 MemberService 클래스를 Spring Bean으로 변경한다
@Service
public class MemberService {
- 생성자가 하나 이상일 경우에는 DI를 적용하기 위해 생성자에 반드시 @Autowired 애너테이션을 붙여야 한다
6) 현재까지 작성한 코드의 문제점
- Controller 핸들러 메서드의 책임과 역할에 관한 문제
- 핸들러 메서드의 역할은 클라이언트로부터 전달 받은 요청 데이터를 Service 클래스로 전달하고, 응답 데이터를 클라이언트로 다시 전송해주는 단순한 역할만을 하는 것이 좋다
- 현재의 MemberController에서는 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 객체로 변환하는 작업까지 하고 있다 - Service 계층에서 사용되는 엔티티(Entity) 객체를 클라이언트의 응답으로 전송하고 있다
- DTO 클래스는 API 계층에서만 데이터를 처리하는 역할을 하고 엔티티(Entity) 클래스는 서비스 계층에서만 데이터를 처리하는 역할을 해야 한다
- 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송함으로써 계층 간의 역할 분리가 되어있지 않다
7) 적용한 아노테이션
- @Valid
- 필드에 사용된 모든 아노테이션에 대한 유효성을 검증할 때 사용한다 - @REquestBody
- @REquestBody를 사용하면 파라메터에 HTTP요청 본문이 그대로 전달된다
- 일반적으로 GET/POST 요청 파라미터는 @REquestBody를 사용하지 않는다
- xml 또는 Json 파라미터를 요청할 경우에 @REquestBody를 사용한다 - @ResponseBody
- @ResponseBody를 사용하면 HTTP요청의 Body 내용으로 변환하여 전송한다 - @Getter, @Setter
- lombok 라이브러리에서 제공하는 애너테이션이다
- DTO 클래스를 작성하면서 각 멤버 변수에 해당하는 getter/setter 메서드를 일일이 작성하지 않아도 된다 - @AllArgsConstructor
- 현재 Member 클래스에 추가된 모든 멤버 변수를 파라미터로 갖는 Member 생성자를 자동으로 생성해 준다 - @NoArgsConstructor
- 파라미터가 없는 기본 생성자를 자동으로 생성해 준다
@Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor, @Data, @ToString
※ 참조 링크
▶ DDD : https://ko.wikipedia.org/wiki/%EB%8F%84%EB%A9%94%EC%9D%B8_%EC%A3%BC%EB%8F%84_%EC%84%A4%EA%B3%84
도메인 주도 설계 - 위키백과, 우리 모두의 백과사전
ko.wikipedia.org
빈약한 도메인 모델 - 위키백과, 우리 모두의 백과사전
ko.wikipedia.org
https://martinfowler.com/eaaCatalog/domainModel.html
P of EAA: Domain Model
martinfowler.com
https://martinfowler.com/tags/domain%20driven%20design.html
domain driven design
When programming, I often find it's useful to represent things as a compound. A 2D coordinate consists of an x value and y value. An amount of money consists of a number and a currency. A date range consists of start and end dates, which themselves can be
martinfowler.com
▶ Lombok : https://projectlombok.org/features/all
Stable
projectlombok.org
'Spring MVC' 카테고리의 다른 글
Spring MVC - 데이터 액서스 계층(Data Access Layer) - DDD (0) | 2022.07.03 |
---|---|
Spring MVC - 데이터 액서스 계층(Data Access Layer) - Spring Data JDBC(샘플 구현) (0) | 2022.07.03 |
Spring MVC - 데이터 액서스 계층(Data Access Layer) - Spring Data JDBC(개요) (0) | 2022.07.03 |
Spring MVC - 데이터 액서스 계층(Data Access Layer) - JDBC (0) | 2022.07.03 |
Spring MVC - 예외 처리(Handling Exception) - @ExceptionHandler (0) | 2022.06.28 |