본문 바로가기

Spring Boot(스프링 부트)

Spring Boot(스프링 부트) > 백엔드 개발(회원 관리 예제)

김영한 스프링 입문 3주차를 수강하고 남기는 포스팅이다!

이번에는 매우매우 간단한 회원 서비스를 만들어 본다고 하였다.

매우 <- 라는 단어를 강조해서 긴장이 풀렸지만.. 처음 보는 입장이어서 그런가

복잡 그 자체였다.. 아직 감도 안 잡히고 큰일이다!

열심히 수강하면 조금씩 발전하는 나를 볼 수 있지 않을까 하는 요즘이다..

 

다음은 일반적인 웹 애플리케이션 계층 구조이다.

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

컨트롤러: 웹 MVC의 컨트롤러 역할

서비스: 핵심 비즈니스 로직 구현

리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리

도메인: 비즈니스 도메인 객체, ex) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리됨

 

부가 설명을 하자면 컨트롤러는 이전 포스팅에 다루었던(ex. API) 내용들을 의미하고

서비스는 핵심 비즈니스 로직, 서비스는 비즈니스 도메인 객체를 가지고 비즈니스 로직이 동작하도록 구현한 것을 의미한다.

 

이번에 만드는 회원 관리 예제는 데이터 저장소를 어떤 것을 쓸 것인지 결정되지 않았다는 상황을 간주하였다.

그래서 인터페이스로 구현 클래스를 변경할 수 있도록 설계할 것이다.

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

 

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

 

hello-spring -> src -> main -> java -> hello.hellospring에 domain이라는 패키지를 만들고 Member이름의 클래스를 만든다.

그런 후 회원 정보인 id와 name을 private로 생성하고 getter setter를 선언해 준다.

코드는 다음과 같다.

package 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;
   }
}

 

회원 정보를 가져오고 설정하는 클래스를 만들었다면 그 정보들을 저장하는 interface를 만들어야 하는데

hello-spring -> src -> main -> java -> hello.hellospring에 repository 이름의 패키지를 만들고 인터페이스로 MemberRepository를 만든다.

그런 이후, 아래 내용을 입력한다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optaional;

public interface MemberRepository {
   Member save(Member member); // return 형태가 Member이고 입력 값이 Member 형태인 함수 save
   Optional<Member> findById(Long id); // id 값으로 회원 정보를 찾는 함수
   Optional<Member> findByName(String name); // name 값으로 회원 정보를 찾는 함수
   List<Member> findAll(); // 멤버의 모든 값을 리스트로 받아오는 함
}

 

만약 값이 null일 수 있는 값이면 Optional 키워드를 사용하 감싼다!

 

interface를 구현하기 위해 같은 패키지 내에 MemoryMemberRepository 클래스를 만든다

그리고 다음 코드를 작성한다.

 

import java.util.optional;

public class MemoryMemberRepository implements MemberRepository {
   private static Map<Long, Member> store = new HashMap<>(); // Key: Long, Values: Member 인 HashMap 객체 store 생성
private static long sequence = 0L; // member의 id를 지정해주기 위한 변수

@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)); // Optional.ofNullable(): store.get(id)가 널일 경우에도 리턴
}

@Override
public Optional<Member> findByName(String name) {
// store.values().stream: store의 모든 값을 한바퀴 돌리며 Check
// .filter(member -> member.getName().equals(name)): member의 이름과 입력 name이 같으면 필터링!
// .findAny(): 하나라도!
   return store.values().stream()
      .filter(member -> member.getName().equals(name))
      .findAny();
}

@Override
public List<Member> findAll() {
   return new ArrayList<>(store.values()); // store의 있는 모든 값을 리스트로 반환
}

 

리턴값이 클래스이고, 널일 가능성이 있으면

Member에서 <> 기호로 묶어 <Member>이라고 쓰고 앞에 Optional의 키워드를 붙이는 것 같다.

그리고 store.get(id)는 id에 해당하는 값(Member 클래스)를 리턴하는데 리턴할 때에는 Optional.ofNullable() 키워드를 붙이는 것 같다!

 

또 궁금한 점은

return store.values().stream()
      .filter(member -> member.getName().equals(name))
      .findAny();

이 코드였는데 왜 이렇게 코드를 작성했는지 이해가 되지 않았다..

열심히 알아본 결과

 

store.values() 값은 Member 형태일 것이고 이것을 filter 함수를 사용하여 람다 함수로 표현하였다.

이 값은 Member 클래스 형이 추론되므로 클래스 타입은 생략 후 변수 명만 임시로 사용한 것이라고 한다.

.filter(member -> member.getName().equlas(name))에서 member가 아닌 다른 변수명으로 통일하여도 정상 작동하는 것이다!

 

100% 이해가 되지는 않았지만 그래도 계속 보면 익숙해지지 않을까 생각이 든다..!

.

.

.

spring boot에서는 자바의 JUnit이라는 프레임 워크로 테스트를 실행하여 테스트 케이스를 작성할 수 있다!

테스트 케이스가 없다면, 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어려우며 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다.

 

테스트 케이스를 만들어보자!

 

hello-spring -> src -> test -> java -> hello.hellospring에 패키지 repository를 만들고, 클래스 MemoryMemberRepositoryTest를 만든다.

 

그 이후 다음과 같이 테스트 케이스를 작성한다.

package hello.hellospring.repository;

import org.junit.jupiter.api.Test;

class MemoryMemberRepositoryTest {
   MemberMemberRepository repository = new MemoryMemberRepository();

   @AfterEach
   public void afterEach() { // 각 Test가 끝날 때마다 실행...
      repository.clearStore(); // 초기화 함수 시
   }

   @Test
   public void save() {
      Member member = new Member();
      member.setName("spring");

      repository.save(member);

      Member result = repository.findById(member.getId()).get(); // member.getId()는 Optional<Member> 타입인데, .get()을 붙이게 되면 순수 Member로 타입 선언 가능
      assertThat(member).isEqualTo(result); // member와 result와 같다면 Check
   }

   @Test
   public void findByName() {
      Member member1 = new Member();
      member1.setName("spring1");
      repository.save(member1);

      Member member2 = new Member();
      member2.setName("spring2");
      repository.save(member2);

      Member result = repository.findByName("spring1").get(); // spring1 name을 가진 Member 객체를 result에 저장

      assertThat(result).isEqualTo(member1); // Check
   }

   @Test
   public void findAll() {
      Member member1 = new Member();
      member1.setName("spring1");
      repository.save(member1);

      Member member2 = new Member();
      member2.setName("spring2");
      repository.save(member2);

      List<Member> result = repository.findAll(); // 모든 객체를 List<Member>에 저

      assertThat(result.size()).isEqualTo(2); // result 객체의 크기가 2라면(2개라면) Check
   }

   public void clearStore() {
      store.clear(); // store을 모두 초기화, repository 초기화
   }
}

 

이렇게 까지 끝내면 간...단한 회원정보 저장 기능과, 정보들을 한 공간에 저장하는 기능을 만들고 테스트까지 끝난 것이다!

 

비즈니스 로직과 테스트는 다음 포스팅에 써야겠다

 

회원가입 기능과 비즈니스 로직 합해서 30분정도의 강의인데, 노베이스여서 그런지 100프로 이해하는 것도 아닌데

매우 많은 시간을 소요하고 있다.

계속 반복해서 보면서 이해하고, 거기에 코드도 작성해보고 주석도 달고...

앞으로에 있어서 분명 도움이 되겠지만 이해를 잘 못하는 내가 좀 주눅들게 되는 것 같다

앞으로 학교 수업도 들으면서 다시 수강할 때면 조금은 더 익숙해지겠지? 라는 마인드로 듣고 있다

화이티잉!!