본문 바로가기
5.1 대외활동/Likelion(12기 세션자료)

[Eulji_LikeLion_2024_BackEnd] 4차 요구사항

by Dohi._. 2024. 4. 8.
728x90

4차 목차  

  4-1 요구사항?

    1)  요구사항

    2)  비지니스 요구사항 도출

  4-2 계층

    1)  계층

    2)  데이터 관련 용어

  4-3 도메인과 리포지토리 만들기

  4-4 리포지토리 테스트 케이스 작성

  4-5 서비스 개발

  4-6 서비스 테스트 작성

 

4-1.  요구사항?

1) 요구사항

어떠한 프로젝트를 만들기 전에 설계에서 필요한 요구사항에대해서 간단하게  알아보겠습니다

 

요구사항

시스템 개발 분야에서 어떤 과제를 수행하기 위하여 필요한 조건이나 능력 

즉, 기능과 제약조건입니다.

다시 말해, 요구 사항은 소프트웨어가 수행해야 하는 작업, 형태, 충족되어야 하는 조건을 정의합니다.

 

요구사항 수집 및 분석

요구사항 수집은 사용자, 클라이언트, 고객들의 요구에 맞는 제품을 만들기 위해서 필요한 과정이며 요구 사항은 프로젝트 과정 전반에 걸쳐 변경될 수 있으므로 이러한 변경 사항을 추적하고 관리해야 합니다.

 

요구사항 유형

요구 사항에는 크게 두 가지 유형이 있습니다. 

  1. 시스템 요구 사항 : 시스템의 서비스와 제약조건에 대한 디테일한 설명(전문용어)
    • 낮은 수준의 요구사항
    • 주로 개발자나 관계자을 위한 요구사항
  2. 사용자 요구 사항:  시스템의 서비스와 제약조건에 대한 추상적인 상태(자연어)
    • 높은 수준의 요구사항
    • 기술 지식이 없는 사용자도 쉽게 이해해야 한다
      •  따라서 간단한 표, 형식 및 다이어그램을 사용하여 자연어로 작성함
    • 사용자 요구사항은 기능적 요구사항비기능적 요구사항로 나눠진다.

기능적 요구사항과 비기능적 요구사항

기능 요구 사항: 설계할 시스템의 기능을 설명합니다.

즉, 시스템이 무엇이고 사용자 요구를 충족시키기 위해 어떻게 기능할지에 대한 설명입니다.

기능 및 사용자가 기대하는 것에 어떻게 응답해야 하는지에 대한 명확한 설명을 제공합니다.

예시) 고객이 고객ID를 입력하면 구매내역을 출력해준다.

 

비 기능적 요구 사항: 설계할 시스템의 한계와 제약을 설명합니다.

예시) 시스템이 Windows 및 Linux 시스템에서만 작동한다

비기능 요구 사항을 다음과 같은 범주로 분류하기도 합니다.

  • 신뢰성 
  • 보안
  • 퍼포먼스
  • 유지보수
  • 인터페이스 적용
  • 무결성

비기능 요구사항을 범주로 나누는 방식은 설계할 시스템에서 충족해야 하는 요구 사항을 명세할때 도움이 됩니다.

 

2) 비지니스 요구사항 도출

우리가  배워볼 프로젝트에 대해서 아래에 대해서 간단하게 요구사항을 분석해보죠 

 

요구사항

  • 데이터: 회원ID, 이름
  • 기능: 회원 등록, 조회
  • 아직 데이터 저장소가 선정되지 않음

직접 세션에서 각자 만든 요구사항을 한번 들어보겠습니다:)

 

예시) 아직 데이터 저장소가 선정되지 않음

→ 인터페이스으로 구현하여 언제나 클래스를 변경할수있도록 설계

개발을 진행하기위해서 초기개발단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

 

적용 된 의존관계

클래스 의존관계
merberService → (interface) MemberRepository  ←.. memoryMemberRepository

4-2. 계층

1) 계층

계층은 각자의 역할과 책임이 있는 어떤 소프트웨어의 구성요소를 의미합니다

그리고 각 계층은 서로 소통할 수 있지만 다른 계층에 직접 간섭하거나 영향을 미치지는 않습니다

 

 

 

스프링 부트는 다음 그림에서 보듯 각 계층이 양 옆의 계층과 통신하는 구조를 따릅니다

아래의 계층들이 서로 통신하며 프로그램을 구성합니다

 

프레젠테이션 계층

HTTP 요청을 받고 이 요청을 비지니스 계층으로 전송하는 역할을 합니다.

이전 포스팅에서 본 TestController와 같은 컨트롤러가 바로 프레젠테이션 계층의 역할을 합니다.

 

비지니스 계층

모든 비지니스 로직을 처리하는 계층입니다

비지니스 로직은 서비스를 만들기 위한 로직을 뜻합니다

 

퍼시스턴스 계층

모든 DB(데이터베이스) 관련 로직을 처리하는 곳으로써 이과정에서 DB에 접근하는 DAO객체를 사용할 수도 있습니다.

 

위에 그림에서 컨트롤러와 서비스와 리포지토리는 계층이라는 개념의 영역을 실제 구현을위한 영역입니다

차이를 간단하게만 이해하고 차차 알아가면 좋을 것 같습니다.

 

 

일반적인 웹 애플리케이션 구조 


컨트롤러 : 웹MVC의 컨트롤러 역할 
서비스 : 핵심 비지니스 로직 구현
리포지토리 데이터베이스에 접근 도메인 객체를 DB에 저장하고 관리
도메인: 비지니스 도메인 객체( 예시 회원 주문 쿠폰 등등 ) 주로 데이터베이스에 저장하고 관리됨

 

2) 데이터 관련된 용어

퍼시스턴스 계층에는 Repository가 있는데 그전에 데이터 관련된 객체 4가지를 먼저 알아가고자 합니다 

  • DAO(데이터 엑세스 객체/ Data Access Object) 
    • 위에서 언급한번 했던 객체인데 DB와의 상호작용을 관리하고 DB에서 데이터를 읽기위해 사용됩니다.
    • 즉, Repository로 실제로 DB에 접근하여 CRUD하는 객체로 Service와 DB를 연결해주는 역할까지 합니다.
    • Repository와 정확하게 따지면 조금 다른 개념이긴 하지만 DAL(DB관련 정보를 처리하는 계층/Data Access Layer)의 구현체인 것은 같다. 이후 포스팅에는 주로Repository로 설명할 예정이다.

 

  • DTO(데이터 전송 객체 / Data Tranfer Object)
    • 데이터를 전송하거나 전달하기 위해서 사용되는 객체이며 Getter,Setter 메서드만 존재합니다.
    • DB에서 데이터를 받고 Service이나 Controller등으로 넘겨줍니다.
    • 즉, 로직은 보유하지 않은 DB에서 받을 데이터방식 정의혹은 데이터 객체입니다.
    • VO(Value Object)와 혼용 되기도 합니다만 크게 VO는 불변객체, DTO는 가변 객체라는 차이점이 있습니다

 

  • VO(Value Object)
    • VO는 불편객체의 개념으로 Read-Only을 의미합니다 
    • Getter/Setter가 둘다 존재하는 DTO와 달리 VO는 Setter만 존재합니다.
    • 즉, DTO는 Layer간의 데이터가 오고 가고 하는 객체 , VO는 특정한 값을 담는 객체라고 생각하면 편합니다 

 

  • Domain(Entity)
    • 실제 DB의 테이블과 매핑되는 객체이다.
    • @Entity 애너테이션을 이용하여 DB테이블과 매칭되는 클래스임을 나타낸다.

 

가끔 어떤 설명에는 DTO와 domain을 합쳐서 설명하기도 한다.
DTO역할을 domain에 합쳐 버리는 경우도 있기 때문이다.

 

언제 이개념이 쓰는지 글로만 보면 어렵기 때문에 그림을 통해 이해하면 좀 수월 할 듯 합니다.

 


4-3. 회원 도메인과 리포지토리 만들기

  • 회원 객체
  • 회원 리포지토리 인터페이스
  • 회원 리포지토리 메모리 구현체

회원 객체(class Member)

domain이라는 package만든후 안에 생성

id(여기서는 고객이 만든 아이디가 아니라 임의의 값)

Getter Setter방식으로 주고 받을수 있게 메소드를 선언한다 (윈도우 기준 alt +Insert하면 편하게 가능)

package hello.hellospring.domain;
public class Member {

 private Long id;
  
 private String name;
  
 public Long getId() {
 	return id;
 }
  
 public void setId(Long id) {
 	this.id = id;
 }
  
 public String getName() {
 	return name;
 }
  
 public void setName(String name) {
 	this.name = name;
 }
  
}

 

회원 리포지토리 인터페이스

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;

public interface MemberRepository {
 Member save(Member member);
 Optional<Member> findById(Long id);
 Optional<Member> findByName(String name);
 List<Member> findAll();
}

class말고 Interface이다.

Optinal이란?

Optional은 Java 8에서 추가된 클래스로, null 값의 처리를 간편하게 할 수 있도록 도와주는 기능을 제공합니다.
Java에서 null 값은 오류를 발생시키는 원인 중 하나입니다. null 값이 반환될 수 있는 메소드를 호출하거나 null 값이 저장된 변수를 사용하면 NullPointerException과 같은 예외가 발생합니다.
Optional 클래스는 이러한 문제를 해결하기 위해 null 값 처리를 안전하게 수행할 수 있도록 도와줍니다. Optional 객체는 값이 존재할 수도 있고, 존재하지 않을 수도 있습니다. 값이 존재하는 경우에는 get() 메소드를 사용하여 값을 가져올 수 있고, 값이 존재하지 않는 경우에는 get() 메소드를 호출하면 예외가 발생하지 않고 빈 Optional 객체를 반환합니다.

public Optional<User> getUserById(Long id) {
}   // id에 해당하는 사용자 정보를 조회하여 반환하는 로직

Optional<User> user = getUserById(123L);

if (user.isPresent()) {
    User foundUser = user.get();
    // 사용자 정보가 존재하는 경우, 해당 정보를 사용하여 로직 수행
} else {
    // 사용자 정보가 존재하지 않는 경우, 로직 수행
}

위의 코드에서 getUserById 메소드는 id에 해당하는 사용자 정보를 조회하여 Optional<User> 객체를 반환합니다.
이후에는 Optional의 isPresent() 메소드를 사용하여 Optional 객체가 비어있지 않은지 확인하고,
get() 메소드를 사용하여 사용자 정보를 가져옵니다.
이를 통해 null 값 처리를 안전하게 수행할 수 있습니다

 

 

회원 리포지토리 메모리 구현체

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
public class MemoryMemberRepository implements MemberRepository {
 private static Map<Long, Member> store = new HashMap<>();
 private static long sequence = 0L;
 @Override
 public Member save(Member member) {
 member.setId(++sequence);
 store.put(member.getId(), member);
 return member;
 }
 @Override
 public Optional<Member> findById(Long id) {
 return Optional.ofNullable(store.get(id));
 }
 @Override
 public List<Member> findAll() {
 return new ArrayList<>(store.values());
 }
 @Override
 public Optional<Member> findByName(String name) {
 return store.values().stream()
 .filter(member -> member.getName().equals(name))
 .findAny();
 }
 public void clearStore() {
 store.clear();
 }
}

 

 

 

현재 HashMap을 사용하고 있다.  그리고 sequence를 Long타입으로 정하였다. 여기서는 단순한 예제이기 때문에 동시성 문제를 고려하지 않고, HashMap, Long을 사용하고 있다.

동시성 문제가 있을 수 있어서 공유되는 변수일 때는 ConcurrentHashMap와  AtomicLong을 고려하자.

 


4-4. 회원 리포지토리 테스트 케이스 작성

개발한 기능을 테스트 -> main으로 하거나 웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행.

하지만 준비하는데 오래걸리고 반복실행하기 어렵기때문에 여러번 테스트를 한번에 하기 어렵다는 단점이 있음
자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

 

회원 리포지토리 메모리 구현체 테스트

src/test/java 하위 폴더에 생성한다.

main이 아닌 test에서 진행한다.

 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {
 MemoryMemberRepository repository = new MemoryMemberRepository();
 
 @AfterEach
 public void afterEach() {
 repository.clearStore();
 }
 
 @Test
 public void save() {
 //given
 Member member = new Member();
 member.setName("spring");
 //when
 repository.save(member);
 //then
 Member result = repository.findById(member.getId()).get();
 assertThat(result).isEqualTo(member);
 }
 
 @Test
 public void findByName() {
 //given
 Member member1 = new Member();
 member1.setName("spring1");
 repository.save(member1);
 Member member2 = new Member();
 member2.setName("spring2");
 repository.save(member2);
 //when
 Member result = repository.findByName("spring1").get();
 //then
 assertThat(result).isEqualTo(member1);
 }
 
 @Test
 public void findAll() {
 //given
 Member member1 = new Member();
 member1.setName("spring1");
 repository.save(member1);
 Member member2 = new Member();
 member2.setName("spring2");
 repository.save(member2);
 //when
 List<Member> result = repository.findAll();
 //then
 assertThat(result.size()).isEqualTo(2);
 }
}

코드에 사용된 간단한 메소드/애노테이션

 

@Test

JUnit과 같은 자바 단위 테스트 프레임워크에서 사용되는 애노테이션입니다.

@Test는 테스트 메소드를 지정하는 데 사용됩니다.

@Test 애노테이션을 붙인 메소드는 JUnit 테스트 도구에서 실행되며,

해당 메소드의 실행 결과에 따라 테스트가 성공적으로 완료되었는지 여부를 판단합니다.


Assertions

JUnit 프레임워크는 assert를 포함한 다양한 Assertions 메소드를 제공합니다.

이를 사용하여 테스트 코드에서 예상한 결과가 실제 결과와 일치하는지를 검사할 수 있습니다.

JUnit에서 제공하는 대표적인 Assertions 메소드는 다음과 같습니다.

 

  • assertEquals(expected, actual) : expected와 actual 값이 동일한지를 비교합니다.
  • assertNotEquals(expected, actual) : expected와 actual 값이 서로 다른지를 비교합니다.
  • assertTrue(condition) : condition이 true인지 검사합니다.
  • assertFalse(condition) : condition이 false인지 검사합니다.
  • assertNull(object) : object가 null인지 검사합니다.
  • assertNotNull(object) : object가 null이 아닌지 검사합니다.
  • assertSame(expected, actual) : expected와 actual이 같은 객체를 참조하는지 검사합니다.
  • assertNotSame(expected, actual) : expected와 actual이 서로 다른 객체를 참조하는지 검사합니다.

assertThat()

AssertJ 라이브러리에서 제공하는 메소드 중 하나입니다.

assertThat() 메소드는 특정 값을 비교할 때 사용합니다.

일반적으로는 JUnit의 assertEquals()와 유사한 역할을 합니다.

그러나 assertThat()은 가독성이 높고, 테스트 코드 작성 시 자연어와 비슷한 문법을 사용하여 코드를 더 명확하고 이해하기 쉽게 만들어줍니다.

예를 들어, 다음과 같은 코드가 있다고 가정해봅시다.

int num = 5;
assertEquals(5, num);
 

위의 코드는 JUnit의 assertEquals()를 사용하여 num의 값이 5와 같은지를 검사합니다. 이를 AssertJ의 assertThat()을 사용하여 작성하면 다음과 같습니다.

int num = 5;
assertThat(num).isEqualTo(5);
 

위의 코드는 num의 값이 5와 같은지를 검사하는데, 이를 자연어처럼 표현하여 가독성을 높였습니다. 이외에도 assertThat() 메소드를 사용하여 다양한 비교 연산을 수행할 수 있습니다.

 


@AfterEach

각 테스트가 종료될 때마다 이 기능을 실행한다(각 메서드가 호출될 때마다 호출되는 콜백 메서드라고 생각하면 된다). 여기서는 메모리 DB에 저장된 데이터를 삭제한다.

 

@BeforeEach 

각 테스트 실행 전에 호출된다.

테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다

 

테스트에는 각각 독립적으로 실행되어야하고 

순서에 따라 의존관계가 있는 테스트는 좋은게 아닙니다.


4-5. 회원 서비스 개발 

memberservice

package hello.hellospring.service;

public class MemberService {

  private final MemberRepository memberRepository = new MemoryMemberRepository();

  /**
   * 회원 가입
   */
  public Long join(Member member) {
    validateDuplicateMember(member); // 중복 회원 검증
    memberRepository.save(member);
    return member.getId();
  }

  private void validateDuplicateMember(Member member) {
    memberRepository.findByName(member.getName())
            .ifPresent(m -> {
              throw new IllegalStateException("이미 존재하는 회원입니다.");
            });
  }

  /**
   * 전체 회원 조회
   */ 
  public List<Member> findMembers() {
    return memberRepository.findAll();
  }

  public Optional<Member> findOne(Long memberId) {
    return memberRepository.findById(memberId);
  }
}

validateDuplicateMember()

회원가입 하기 전에 중복 회원이 있는지 확인을 하고 중복 이름이 있다다면

ifPresent()메소드(Optional에서 ifPresent사용)를 사용하여 m 객체가 값이 있는 경우(Optional 객체가 비어있지 않은 경우)

IllegalStateException 예외를 던진다.

 

일단 Optional로 한 번 감싸면 Optional 안에 Member가 있기에

findByName의 리턴값이 Optional이기 때문에 가능한 것이다.

 

이러한 방식으로 Optional을 사용하면,

null체크나 if문을 사용하여 예외처리를 하는 등의 코드를 줄일 수 있으며, 가독성과 안정성을 높일 수 있다.

 

 

null일 가능성이 있을 경우에는 Optional로 감싸서 반환을 해주고, ifPresent를 사용한다. 

 

 


4-6.회원서비스 테스트

테스트 케이스 메서드명은 한글로 작성하는 경우도 종종 있다. 직관적이고 빌드에 포함되지도 않기 때문이다.

 

테스트를 작성할 때 given, when, then으로 나누어 작성하는 것이 좋다. 

//Given   테스트에서 구체화하고자 하는 행동을 시작하기 전에 테스트 상태를 설명하는 부분

//When   구체화하고자 하는 그 행동

//Then    어떤 특정한 행동 때문에 발생할거라고 예상되는 변화에 대한 설명

 

다음과 같이 주석으로 given, when, then으로 적어두고 해당하는 코드를 작성하면 직관적이다.

상황에 따라 이에 맞지 않을 때는 변형하여 작성하면 된다.

 

기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했다.

public class MemberService {
  private final MemberRepository memberRepository = new MemoryMemberRepository();
}
public class MemberServiceTest {
  MemberService memberService = new MemberService;
  MemberRepository memberRepository = new MemoryMemberRepository();
  //.. 
}


회원 리포지토리의 코드가

회원 서비스 코드를 DI 가능하게 변경한다.

public class MemberService {

  private final MemberRepository memberRepository;

  public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
  }
  ..
}
public class MemberServiceTest {
  MemberService memberService;
  MemberRepository memberRepository;
  
  @BeforeEach
  public void beforeEach() {
    memberRepository = new MemoryMemberRepository();
    memberService = new MemberService(memberRepository);
  }
}


지금 문제는 MemberService에서 생성한 memberRepository와 테스트 케이스에서 만든 memberRepository가

서로 다른 Instance라는 것이다.

지금은 memberRepository의 store 객체가 static이기 때문에 문제가 되지 않지만

만약 static이 아니라면 문제가 생길 것이다. 


그래서 같은 인스턴스를 사용하도록 바꿔야 한다. 

MemberRepository를 MemberService에서 직접 생성안하고 생성자 파라미터를 통해 외부로부터 주입받도록 바꿔준다.

이러한 것을 DI(의존성 주입,Dependency Injection)이라고 한다.

의존성 주입을 사용하면 MemberService가 MemberRepository를 직접 생성하지 않기 때문에,

MemberRepository가 변경되더라도 MemberService를 수정할 필요가 없습니다.

이는 유지보수성을 높이고, 코드의 결합도를 낮추는 효과가 있습니다.

 

Instance(인스턴스)

 객체 지향 프로그래밍에서 클래스로부터 생성된 구체적인 실체(객체)를 의미합니다.

클래스는 일종의 틀이며, 이를 이용해 여러 개의 인스턴스를 생성할 수 있습니다.

이러한 인스턴스는 서로 다른 상태를 가질 수 있으며, 메소드를 호출하여 특정 작업을 수행할 수 있습니다.


회원 서비스 테스트

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class MemberServiceTest {

  MemberService memberService;
  MemoryMemberRepository memberRepository;

  @AfterEach
  public void afterEach() {
    memberRepository = new MemoryMemberRepository();
    memberService = new MemberService(memberRepository);
    memberRepository.clearStore();
  }

  @Test
  void 회원가입() {
    // given
    Member member = new Member();
    member.setName("spring");

    // when
    Long saveId = memberService.join(member);

    // then
    Member findMember = memberService.findOne(saveId).get();
    assertThat(member.getName()).isEqualTo(findMember.getName());
  }

  @Test
  public void 중복_회원_예외() {
    //given
    Member member1 = new Member();
    member1.setName("spring");

    Member member2 = new Member();
    member2.setName("spring");

    //when
    memberService.join(member1);
    IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        /*
        try {
            memberService.join(member2);
            // Exception이 발생되지 않고 넘어가면 실패한 것이기 때문에 fail()
            fail();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage().isEqualTo("이미 존재하는 회원입니다."));
        }
         */
  }

  @Test
  void findMembers() {

  }

  @Test
  void findOne() {
  }
}

+)

테스트 클래스 자동 생성 단축키 Ctrl + Shift + T
이전 실행한 것 다시 실행 Shift + F10

 


이 글은 을지대학교 백엔드 세션 강의를 위해 제작된 게시글입니다 
언제나 조언부탁드립니다

 

 

728x90

댓글