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)를 전달 받을 때 MemberPostDtoMemberPatchDto 같은 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) 현재까지 작성한 코드의 문제점

  1. Controller 핸들러 메서드의 책임과 역할에 관한 문제
    - 핸들러 메서드의 역할은 클라이언트로부터 전달 받은 요청 데이터를 Service 클래스로 전달하고, 응답 데이터를 클라이언트로 다시 전송해주는 단순한 역할만을 하는 것이 좋다
    - 현재의 MemberController에서는 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 객체로 변환하는 작업까지 하고 있다
  2. 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

https://ko.wikipedia.org/wiki/%EB%B9%88%EC%95%BD%ED%95%9C_%EB%8F%84%EB%A9%94%EC%9D%B8_%EB%AA%A8%EB%8D%B8

 

빈약한 도메인 모델 - 위키백과, 우리 모두의 백과사전

 

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

 

1. IoC(Inversion of Control) / DI(Dependency Injection)

 1) 반전 제어 (IoC)

  • Library는 애플리케이션 흐름의 주도권이 개발자... Framework은 애플리케이션 흐름의 주도권이 Framework
  • 애플리케이션 흐름의 주도권이 뒤바뀌는 기술
  • Java 콘솔 애플리케이션의 일반적인 제어권을 아래 코드에 따라 순서대로 진행하여 보면...
    - 프로세스의 진행을 위해서 main() 메서드가 있어야 한다
    - main() 메서드가 호출되면 System 클래스를 통해서 static 멤버 변수인 out의 println()을 호출한다
    - 일반적인 애플리케이션 제어 흐름은 개발자가 작성한 코드를 순차적으로 실행하는 것이다
public class Ioc {
    public static void main(String[] args) {
        System.out.println("Hello IoC!");
    }
}
  • Java 웹 애플리케이션에서 IoC가 적용되는 경우 (반전 제어)
    - 별도의 main( ) 메서드가 존재하지 않는다
    - main( ) 메서드와 같이 애플리케이션이 시작되는 지점을 엔트리 포인트(Entry point)라고 한다
    - main( ) 메서드 대신 서블릿 컨테이너 내의 서블릿 로직이 서블릿을 직접 실행 시킨다
    - 서블릿 로직은 service( )  메서드이다
    - 웹 애플리케이션은 서블릿 컨테이너가 서블릿을 제어하므로 제어 흐름은 서블릿 컨테이너에 있다

2. DI(Dependency Injection)

  • Dependency(의존하는, 종속되는) 와 Injection(주입) 의 합성 의미로 보면 의존성 주입의 의미이다
  • IoC 개념을 구체화시킨 내용이다
  • 아래 그림에서 A클래스는 B클래스의 기능을 사용하고 있다
    - A클래스는 B클래스에 의존한다
    - A클래스의 프로그래밍 로직의 완성을 위하여 B클래스에게 의지한다

  • 클래스 다이어그램 작성을 자주 해 보는 것이 도움이 된다

※ 클래스 다이어그램 도구 링크 : https://online.visual-paradigm.com/diagrams/features/

 

Online Drawing Software

Diagram Templates Templates of UML, Flowchart, BPMN, ER diagrams, DFD, Mind Map and more.

online.visual-paradigm.com

 1) 클래스 간 의존 관계 성립

  • 아래 그림을 참조

 2) 의존성 주입

  • 클래스 간 의존 관계에서는 new 키워드를 사용하여 객체를 직접 생성했다
  • 의존성 주입에서는 외부의 객체를 전달받아 생성자의 파라미터로 주입한다

!!! 클래스의 생성자로 객체를 전달 받는 코드가 있다면

▶ 객체를 외부에서 주입 받고 있으며, 의존성 주입이 이루어 지고 있다!!!

 

 3) 의존성 주입의 필요성

  • 일반적으로 Java에서 new 키워드를 사용해서 객체를 생성한다
  • Reflection이라는 기법을 이용해서 Runtime시에 객체를 동적으로 생성할 수 있는 방법도 있다
  • Stub과 같은 코드를 작성할 경우 new 키워드로 작성하면 내부에서 연동되어 작동하는 클래스명을 모두 고쳐야 한다
  • new 키워드를 사용하여 의존 객체를 생성하면 클래스 간의 결합이 강하다고 한다 (Tight Coupling)
  • 강한 결합이 있으면 향후 수정, 보완하기가 어렵다

 4) 의존성 주입의 방향성

  • 의존성 주입은 느슨한 결합이 좋다 (Loose Coupling)
  • 느슨한 결합의 대표적인 방법은 인터페이스(Interface)를 사용하는 것이다
  • A클래스가 B클래스에 의존하고 있을 경우 B클래스의 구현체가 어떻게 생성되었는지는 중요하지 않다
  • A클래스는 의존하고 목적의 결과만 확인할 수 있으면 된다

  • 아래 그림에서 CafeClient 클래스에는 new 키워드로 MenuServiceStub 객체를 생성하고 있다
    - MenuServiceStub 객체는 MenuService 인터페이스에 할당된다
    - 인터페이스 타입의 변수에 구현 객체를 할당하는 것을 업캐스팅(Upcasting)이라고 한다
  • 아래 그림은 업캐스팅에 의한 느슨한 결합을 보여준다

 5) Spring 기반 의존성 주입

  • 기존의 new 키워드를 사용하여 객체를 생성하는 코드를 Spring에서 변경하여 보자

 

※ 참조 링크

▶ 객체지향 설계 : https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84) 

 

SOLID (객체 지향 설계) - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

다이어그램 설계 : https://online.visual-paradigm.com/diagrams/templates/class-diagram/

 

Class Diagram Templates

Class Diagram Templates by Visual Paradigm A class diagram is one of the most widely used UML diagram types. As a type of static structure diagram, class diagram describes the structure of an object-oriented system by showing the system's classes, their at

online.visual-paradigm.com

 

'Spring Framework' 카테고리의 다른 글

Spring Framework - AOP  (0) 2022.06.22
Spring Framework - DI - Spring Container  (0) 2022.06.19
Spring Framework 특징  (0) 2022.06.15
Spring Framework - 정의  (0) 2022.06.15
Spring Framework - 환경 구성  (0) 2022.06.14

+ Recent posts