Taeyoung Kim

Engineering

3. 회원 관리 예제 - 백엔드 개발

3. 회원 관리 예제 - 백엔드 개발 학습 내용을 정리한 백필 노트입니다.

이 글은 2025년 학습 기록을 블로그 형식으로 정리한 백필 노트입니다.

1. 비즈니스 요구사항 정리

데이터 항목

  • 회원 ID
  • 이름

기능

  • 회원 등록
  • 회원 조회

가정

  • 데이터 저장소는 아직 정해지지 않은 상태 (RDB, NoSQL 등 고려 중)
  • 초기 개발 단계에서는 메모리 기반 데이터 저장소 사용

웹 애플리케이션 계층 구조

| 계층 | 역할 | | --- | --- | | 컨트롤러 | 웹 MVC의 컨트롤러 역할 | | 서비스 | 핵심 비즈니스 로직 구현 | | 리포지토리 | DB 접근 및 도메인 객체 관리 | | 도메인 | 비즈니스 도메인 객체 (예: 회원, 주문, 쿠폰 등) |

설계 원칙

  • 저장소 변경(RDB, NoSQL 등)을 고려해 인터페이스 기반 설계
  • 초기 구현은 Memory 기반 Repository로 시작
  • 동시성 문제는 고려되지 않음 (실무에서는 ConcurrentHashMap, AtomicLong 사용 권장)

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

✅ 회원 객체 (Member.java)

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

✅ 리포지토리 인터페이스 (MemberRepository.java)

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import java.util.*;

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

✅ 메모리 구현체 (MemoryMemberRepository.java)

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import java.util.*;

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

    public void clearStore() {
        store.clear();
    }
}

⚠️ 주의: 예제는 동시성 문제를 고려하지 않음.

실제 서비스에서는 ConcurrentHashMap, AtomicLong 사용 필요.


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

문제점

  • main 메서드나 컨트롤러를 통한 테스트는 비효율적이고 반복 어려움.

해결책

  • JUnit 프레임워크를 사용하여 자동화 테스트 수행.

✅ MemoryMemberRepositoryTest.java

package hello.hellospring.repository;

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

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");
        repository.save(member);
        Member result = repository.findById(member.getId()).get();
        assertThat(result).isEqualTo(member);
    }

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

    @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 : 각 테스트 후 메모리 DB 초기화

테스트는 독립적으로 실행되어야 하며 순서에 의존하지 않아야 함.


4. 회원 서비스 개발

✅ MemberService.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import java.util.*;

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


5. 회원 서비스 테스트

✅ DI(의존성 주입) 구조 변경 전

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
}

✅ DI 구조 변경 후

public class MemberService {
    private final MemberRepository memberRepository;
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

✅ MemberServiceTest.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.*;
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
    public void 회원가입() {
        Member member = new Member();
        member.setName("hello");
        Long saveId = memberService.join(member);
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

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

        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

@BeforeEach: 테스트 전마다 새로운 객체 생성

테스트 간 의존성 제거독립성 확보 목적.


📑 전체 목차 요약

  1. 비즈니스 요구사항 정리
  2. 회원 도메인과 리포지토리 만들기
  3. 회원 리포지토리 테스트 케이스 작성
  4. 회원 서비스 개발
  5. 회원 서비스 테스트