본문 바로가기

Spring Boot(스프링 부트)

Spring Boot(스프링 부트) > 회원 관리 예제

이번 주는 김영한 선생님의 회원 관리 예제 강의를 들었다

동아리 활동 스타트!

 

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

도메인: 데이터베이스에 저장하고 관리하는 데이터들. ex) 회원, 주문, 쿠폰 등
컨트롤러: 웹 MVC의 컨트롤러 역할. ex) html 매핑
서비스: 실질적인 핵심 비즈니스 로직, 리포지토리를 활용 ex) CRUD
리포지토리: 데이터베이스에 접근, 저장, 관리 등. ex) 저장, 조회 등
                    인터페이스와, 구현 클래스 두 가지로 나뉨

 

리포지토리의 인터페이스와 구현 클래스는 다음 관계를 따른다!

 

 

MemberRepository의 인터페이스를 만들고 그것을 구현하는 클래스인 MemoryMemberRepository가 있다

이때 서비스(MemberService)에서는 인터페이스(MemberRepository)를 활용한다.

 

인터페이스(MemberRepository)의 코드는 다음과 같다.

package hello.hello_spring.repository;
import hello.hello_spring.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();
}

 

인터페이스를 구현한 클래스(MemoryMemberRepository) 코드는 다음과 같다

package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.*;

public class MemoryMemberRepository implements MemberRepository{
      private static Map<Long, Member> store = new HashMap<>();
      private static long sequence = 0L;

      // id는 자동 증가
      @Override public Member save(Member member) {
            member.setId(++sequence);
            store.put(member.getId(), member); return member;
      }

      // Optional: NULL 가능성이 있는 데이터를 Optional이라는 주머니에 감싸는 형태
      @Override public Optional<Member> findById(Long id) {
            return Optional.ofNullable(store.get(id));
      }

      // Map(store)의 값들을 쭉 돌려서 member의 이름이 파라미터 name과 같으면 그 객체를 리턴!
      @Override public Optional<Member> findByName(String name) {
            return store.values().stream()
                  .filter(member -> member.getName().equals(name))
                        .findAny();
      }
   
      @Override public List<Member> findAll() {
            return new ArrayList<>(store.values());
      }

      // clear -> Map의 요소를 지운다, Test 때 사용할 것!
      public void clearStore() {
            store.clear();
      }
}

 

여기서 만능 꿀팁!

오버라이딩 하나하나 입력할 필요없이 클래스를 생성하면

public class MemoryMemberRepository {
}

 

이렇게 단순히 뜰텐데 여기서

public class MemoryMemberRepository implements MemberRepository {
}

 

이렇게만 입력한 후에 "Alt + Enter"를 입력해주면

 

 

이렇게 창을 선택할 수 있는데 바로 "OK"만 눌러주면

이렇게 모든 메서드가 불러와진다!

그 후 바로 오버라이딩할 코드만 작성해주면 끝!

 

강의에서는 "테스트"라는 것을 굉장히 강조를 하는 느낌이었다

만약 테스트 기능을 활용하지 않으면 복잡할 것이라고 생각이 들긴 했다

어떤 기능(메서드)를 만들었는데 테스트 해보려면 일반적으로 메인 메서드를 수정하겠으나, 혹시나 오류가 생기거나 그러면?

굉장히 번거로워질 것이고, 코드가 점점 길어지면 테스트 하려고 코드를 조금 수정하려고 하는 과정이 굉장히 복잡할 것이다

그래서 요즘에는 "테스트" 기능이 강조가 많이 되고 많이 쓰는 추세라고 한다!

 

"테스트" 기능을 사용하려면 test 폴더를 활용하면 되는데 폴더를 또 만들고 클래스 만들고 이름 만들고...

이런 과정이 복잡하지 않은가!

단축키는 만능이다! "Ctrl + Shift + T" 버튼으로 단순하게 테스트 창을 자동 생성할 수 있다!

 

요 버튼을 눌러주면 자동으로 생성이 완료된다!

 

그 이후 코드를 다음과 같이 작성하였다

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

class MemoryMemberRepositoryTest {
      MemoryMemberRepository repository = new MemoryMemberRepository();

      // 각 테스트 구문 이후에 자동으로 실행시키는 구문
      @AfterEach
      public void afterEach(){
            repository.clearStore(); // Map 요소 초기화
      }
   
      // 한 멤버를 저장을 한 후, 아이디로 객체를 불러온 후 서로 비교
      @Test
      public void save() {
            Member member = new Member();
            member.setName("spring");
            repository.save(member);

            Member result = repository.findById(member.getId()).get();
            assertThat(member).isEqualTo(result);
      }

      @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();
            assertThat(result).isEqualTo(member1);
      }

      // 멤버를 두 명 저장하고 리스트 사이즈가 2가 맞는지 비교
      @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();
            assertThat(result.size()).isEqualTo(2);
      }
}

 

 

여기서 @AfterEach를 실행시키는 이유가 궁금할 것이다!

테스트를 각각 실행하면 문제가 없지만, 테스트 클래스를 한번에 실행할 때 객체가 겹치고 데이터가 겹치는 경우가 생길 수 있는데 그렇게 되면 각각 올바른 테스트여도 오류가 날 수 있는 것이다

따라서 테스트가 각각 끝날 때마다 clear를 해줘야만 한다

 

테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니기 때문에 각각 독립적으로 만들어야 하고 독립적으로 실행되어야 한다.

 

또한 Test 코드에서 Assertions를 사용하였는데 다음의 코드를 보자

assertThat(member).isEqualTo(result);

 

원래 사용 코드는 Assertions.asserThat(member).is EqualTo(result); 이다

선생님께서는 먼저 Assertions를 쓰고 "Alt + Enter" 단축키를 활용하여 Assertions 구문을 import하여 생략할 수 있었지만, 시도해보니 그냥 위에 처럼 쓰고 "Alt + Enter" 를 쓰면 자동으로 import가 되어서 더 편리했던 것 같다!

나에겐 이런 방식이 더 편리한 것 같아 선호하지 않을까 싶었다

 

다음은 회원 서비스(MemberService) 클래스 코드이다.

package hello.hello_spring.service;
import hello.hello_spring.domain.Member;import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;

public class MemberService {
      private final MemberRepository memberRepository;
     
      // DI(Dependency Injection): 의존성 주입
      public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
      }

      /**
      * 회원 가입
      */
      public Long join(Member member) {
            // 같은 이름이 있는 중복 회원 X
            validateDuplicateMember(member); // 중복 회원 검증

            memberRepository.save(member);
            return member.getId();
      }

      // 중복 회원 검증 메서드, Optional 형태인 경우 ifPresent와 같은 속성 사용 가능(람다 형식)
      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);
      }
}

 

위의 코드를 보고 처음에 나오는 코드

// DI(Dependency Injection): 의존성 주입
      public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
      }

에서 잠깐 멈칫할 것이다!

 

의존성 주입(DI: Dependency Injection): 객체가 필요한 의존성을 외부에서 주입받는 패턴
                                                                  객체 간의 결합도를 낮추고 유연성을 높임

 

아래에 Service의 Test 코드를 만들 것인데 거기(외부)에서 객체를 생성해서 Service 클래스에 넣어주는 형식!

이렇게 하지 않으면 객체가 서로 달라져서 Test 시 다른 값을 가리킬 수도 있게 되기 때문에 의존성 주입을 활용해야 한다!

 

그리고 회원가입 기능에서

 // 중복 회원 검증 메서드, Optional 형태인 경우 ifPresent와 같은 속성 사용 가능(람다 형식)
      private void validateDuplicateMember(Member member) {
            memberRepository.findByName(member.getName())
                  .ifPresent(m -> {
                        throw new IllegalStateException("이미 존재하는 회원입니다.");
            });
      }

이런 메서드를 만들었는데 처음에 작성할 때는 join 메서드 안에 존재했지만, 코드가 메서드 형태로 되어서 메서드 형태로 뽑아주었다

단축키는 "Ctrl + Alt + M" 정말 효율적인 것 같다

 

여기서 Service와 Repository와 헷갈릴 수도 있을 것인데

Repository는 DB와 관련하여 저장하고 관리하는 기능을 수행하는 것이고, Service는 실질적으로 구현해야 할 기능을 말한다.

그래서 메서드 이름도 Service 클래스가 더 비즈니스 적인 용어이기도 하고 이런게 룰이라고 한다!

 

이제 Service 클래스를 테스트 해보자!

단축키 "Ctrl + Shift + T" 를 눌러서 테스트 클래스를 자동 생성하고 아래 코드를 입력하였다

package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
      MemberService memberService;
      MemoryMemberRepository memberRepository;

      @BeforeEach
      public void beforeEach() {
            memberRepository = new MemoryMemberRepository();
            memberService = new MemberService(memberRepository);
      }

      @AfterEach
      public void afterEach() {
            memberRepository.clearStore();
      }

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

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

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

      @Test
      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); fail();
            } catch(IllegalStateException e) {
                  assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
            } */

            //then
}

 

Service Test 코드에는 @BeforeEach문을 추가하여 테스트 구문이 실행되기 전에 Repository와 Service 객체를 생성하여 생성자로 불러 의존성 주입을 사용하였다

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

 

코드를 보면 알겠지만, 테스트 코드는 영어나 한글 상관 없다고 한다!

실제 코드에 영향을 미치는 것이 아니기 때문!

 

그리고 Test 구문 작성시 Given, When, Then 구문을 활용하면 좋다고 하셨다.

Given: 어느 데이터가 주어졌을 때

When: 이러한 조건일 때,

Then: 
이렇게 되어야 한다!

 

이 구문은 다음과 같은 구성이다!

 

또한 테스트 코드 작성 시 try-catch문을 Assertion을 이용하여 수정할 수 있다.

assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

            /* try {
                  memberService.join(member2); fail();
            } catch(IllegalStateException e) {
                  assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
            } 
*/

 

주석 처리한 try-catch문을 assertThat()과 같이 매우 심플하게 한 줄로 작성할 수 있다!

 


 

마치며... 매우 단순한 DB도 사용하지 않는 회원 개발 예제를 만들어보았다!

강의 Q&A를 봐도 이 강의부터 상당수가 이해하지 못하는 글들이 많았다.

물론 처음 듣는 나도 그랬지만 지금은 놀랍게도 이해가 되는 것이 아무리 생각해도 신기하다...

처음 들었을 때 단축키는 필요없고 이해한다는 마인드로만 강의를 시청했는데 오히려 단축키를 활용하니 이해도 더 잘 되는 거 같은 은 느낌이었다!

아래에 마지막으로 새로운 단축키를 정리할테니 참고하면 좋을 것 같다!

그럼 이만! 다음주에 또 밤샘 포스팅을 진행해보겠다

화면(탭) 이동 : Alt + ← or →

단어 이동(한 칸씩 이동이 불편할 때) : Ctrl + ← or →

커서는 그대로 화면만 이동 : Ctrl + ↑ or ↓

동일한 변수 이름 동시에 변경 : Shift + F6

반환 타입 보기 : Ctrl + Alt + V

테스트 창 자동 생성 : Ctrl + Shift + T

코드 -> 메서드로 변환(뽑아내기) : Ctrl + Alt + M