seonggoc

3. 회원 관리 예제 (김영한 스프링 입문 강의 정리) 본문

Spring

3. 회원 관리 예제 (김영한 스프링 입문 강의 정리)

seonggoc 2025. 2. 11. 14:32

강의에서 회원 관리 예제 프로젝트를 만들기를 진행한다.
예제 프로젝트는 회원 관리 프로젝트로 비즈니스 요구사항을 정리하고 진행한다.

1. 비즈니스 요구사항

웹 어플리케이션 계층 구조

  • 컨트롤러 : MVC 컨트롤러와 API 만드는 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직이 들어가 있음 (회원 가입 중복이나 비즈니스에서 설정한 로직)
  • 도메인 : DB에서 관리되는 비즈니스 도메인 객체
  • 레파지토리 : 비즈니스 도메인 객체를 가지고 동작하는 객체

클래스 의존관계

비즈니스 요구 사항에서 회원 ID, 회원 이름을 사용하고, 회원가입, 조회 기능을 요구한다.

회원 비즈니스 로직에 MemberService가 존재하고, MemberRepository는 Interface로 설계한다.
DB를 정하지않아 단순하게 메모리에 저장하고 나중에 DB가 선정되면 저장된 데이터를 DB로 바꿔야 하기 때문에 Interface로 진행한다.

2. 회원 도메인과 레파지토리 만들기

회원 도메인 만들기

회원 도메인은 회원이 가진 정보이다. 내가 이해한 바로는 Django의 Model과 유사하다고 느꼈다.

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

DB Table을 만들 때 class 형식으로 만들어주면 그에 따라 DB처럼 사용하게 되는 것 같다.

회원 레파지토리

회원에 해당하는 객체를 만들었으면 레파지토리를 만들어야한다.
C언어로 링크드리스트 구현시 그에 맞는 함수를 별도로 만들고 함수포인터로 구조체에 넣어주는데, 그것처럼 도메인을 만들었으면, 그에 맞는 메서드가 필요한 것 같다.

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();
}

Optional 객체의 경우 반환 값이 Null인 경우에 알 수 없는 오류를 일으키므로 이 상황을 대비한 객체이다.
ex C) C언어에서 Null Guard를 해주는 이유
ex C++) nullptr?

회원 레파지토리 메서드

위에 부분은 Interface로 설정하였기 때문에 선언부만 존재한다.
아래에서처럼 상속을 받아서 구현이 필요하다.

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();
    }
}

3. 회원 레파지토리 테스트 케이스

위에서 만든 도메인과 레파지토리가 정상적으로 작동하는지 확인하기 위한 테스트 코드를 작성해야 한다.
자바의 JUnit을 사용하면 테스트할 수 있다.

인텔리제이는 레파지토리 코드에서 시프트 + 커맨드 + T를 누르면 간편하게 테스트 케이스 틀을 만들어준다.

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.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach // 각 테스트 실행 전에 실행되는 메서드
    public void beforeEach() {
        // 새로운 MemoryMemberRepository 인스턴스를 생성하여 memberRepository에 할당
        memberRepository = new MemoryMemberRepository();
        // MemberService 인스턴스를 생성하면서 memberRepository를 주입
        memberService = new MemberService(memberRepository);
    }

    @AfterEach // 각 테스트 실행 후 실행되는 메서드
    public void afterEach() {
        // 테스트가 끝난 후, 저장소를 초기화하여 데이터가 남지 않도록 함
        memberRepository.clearStore();
    }

    @Test // 회원가입 테스트
    public void 회원가입() throws Exception {
        // Given (테스트 준비)
        Member member = new Member();
        member.setName("hello");

        // When (테스트 수행)
        Long saveId = memberService.join(member);

        // Then (테스트 검증)
        // 저장된 회원을 조회하고, 입력한 이름과 저장된 회원의 이름이 같은지 확인
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    @Test // 중복 회원 예외 테스트
    public void 중복_회원_예외() throws Exception {
        // Given (테스트 준비)
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");

        // When (테스트 수행)
        memberService.join(member1); // 첫 번째 회원 가입

        // 두 번째 회원을 가입 시도할 때, IllegalStateException 예외가 발생해야 함
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2)); // 예외 발생 예상

        // Then (테스트 검증)
        // 예외 메시지가 기대한 메시지와 일치하는지 확인
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

위에서 메서드가 시작되기 전에 memberRepository와 memeberService를 생성해 주면 같은 memeberRepository가 생성되고, 끝나면 다시 memberRepository에서 clearStore를 하게 된다. 이를 의존성 주입 (DI)라고 한다.