Development/Spring

[Spring] 스프링 입문 - 강의 정리 3

kangkyunghyun 2026. 2. 8. 14:46
728x90
반응형

본 게시물은 김영한님의 “스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술” 강의를 바탕으로 작성했습니다.

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

비즈니스 요구사항 정리

회원 관리 예제 프로젝트를 만들기 위해서 몇 가지 요구사항을 먼저 정리해봅시다. 각 회원은 ID이름이라는 데이터를 갖고 있습니다. 회원을 등록하고 조회하는 기능도 필요합니다. 그리고 아직 데이터를 저장할 데이터베이스가 선정되지 않았다는 가상 시나리오를 바탕으로 진행합니다.

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

일반적인 웹 애플리케이션 계층 구조는 컨트롤러, 서비스, 리포지토리, 도메인, DB로 구성되어 있다.

  • 컨트롤러웹 MVC를 컨트롤러 역할과 API를 만드는 컨트롤러 역할을 합니다.
  • 서비스에서는 핵심 비즈니스 로직을 구현합니다.
  • 리포지토리는 DB에 접근, 도메인 객체를 DB에 저장하고 관리하는 역할을 합니다.
  • 도메인은 회원, 주문, 쿠폰 등 주로 DB에 저장하고 관리되는 비즈니스 도메인 객체입니다.

클래스 의존관계

  • 회원 관리의 비즈니스 로직을 다루는 MemberService가 있습니다.
  • MemberRepositoryinterface로 설계합니다. 아직 DB가 선정되지 않았기 때문에 단순히 메모리에 데이터를 저장합니다. DB가 선정되면 메모리에 저장된 데이터를 DB로 옮겨야하기 때문에 interface가 필요합니다.
  • MemoryMemberRepository는 MemberRepository 인터페이스를 구현한 구현체입니다.

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

회원 도메인

회원 도메인을 만들기 위해 src/main/java/hello.hello_spring 에 domain이라는 패키지를 추가하고 Member 클래스를 생성합니다.

회원 ID와 이름과 이에 대한 Getter와 Setter를 작성합니다.

package hello.hello_spring.domain;

public class Member {
    private Long id;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

회원 리포지토리 interface

위에서 만든 회원 객체를 저장할 리포지토리를 만듭니다. src/main/java/hello.hello_spring 폴더에 repository라는 패키지를 추가하고 MemberRepository라는 interface 파일을 생성합니다.

회원 저장 및 조회 기능을 추가합니다.

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();
}
  • Optional은 Java 문법으로 값을 반환할 때 반환값이 null인 경우를 대비하기 위해 사용합니다.

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

interface를 만들었으니 구현체를 만듭니다. interface와 같은 패키지 안에 MemoryMemberRepository를 추가합니다. 위에서 만든 interface를 구현하기 위해 implements 메서드를 사용합니다.

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<>(); // Java의 자료구조 중 하나로 key:value 형태로 데이터를 저장합니다.
    private static long sequence = 0L; // id로 사용할 값입니다.

    @Override
    public Member save(Member member) {
        member.setId(++sequence); // id를 1 증가시키고 설정합니다.
        store.put(member.getId(), member); // id와 객체를 함께 저장합니다.
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
		    // 해당하는 id를 가진 회원이 없는 경우(null)를 대비해 Optional.ofNullable()로 감쌉니다.
        return Optional.ofNullable(store.get(id)); // id로 store에서 꺼내서 return합니다.
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream() // store의 values를 stream을 통해 돌면서
                .filter(m -> m.getName().equals(name)) // member의 이름이 name과 같은지 filtering합니다.
                .findAny(); // filter를 통과한 첫 번째 요소를 return합니다.
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values()); // store의 values를 List형태로 return합니다.
    }

    public void clearStore() {
        store.clear(); // store를 초기화합니다.
    }
}

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

개발한 기능이 의도한대로 동작하는지 확인하기 위해서 테스트 케이스를 작성합니다. Java에서는 JUnit이라는 프레임워크로 테스트를 실행합니다.

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

src/test/java 폴더에 repository라는 패키지를 생성하고 MemoryMemberRepositoryTest 클래스를 만듭니다.

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class MemoryMemberRepositoryTest { // public일 필요가 없으므로 지워줍니다.

		// Test를 위한 저장소를 생성합니다.
    MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository();

    @AfterEach // 각각의 테스트 이후에
    public void afterEach() {
        memoryMemberRepository.clearStore(); // 저장소를 초기화합니다.
        // 파일 단위로 테스트를 실행할 경우 필요합니다.
    }

    @Test
    public void save() {
		    // given
		    // name이 "spring"인 member 객체를 생성합니다.
        Member member = new Member();
        member.setName("spring");
				// 저장소에 member를 저장합니다.
        memoryMemberRepository.save(member);
        
        // when
				// 저장소에서 id를 통해 member를 저장소에서 찾습니다.
        Member result = memoryMemberRepository.findById(member.getId()).get();
				
				// then
        // 저장한 값과 찾은 값을 비교합니다.
        assertThat(member).isEqualTo(result);
    }

    @Test
    public void findByName() {
		    // given
		    // name이 "spring1"과 "spring2"인 member 객체를 생성하여 저장소에 저장합니다.
        Member member1 = new Member();
        member1.setName("spring1");
        memoryMemberRepository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        memoryMemberRepository.save(member2);
				
				// when
				// name이 "spring1"인 member를 저장소에서 찾습니다.
        Member result = memoryMemberRepository.findByName("spring1").get();
				
				// then
				// 저장한 값과 찾은 값을 비교합니다.
        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll() {
		    // given
		    // name이 "spring1"과 "spring2"인 member 객체를 생성하여 저장소에 저장합니다.
        Member member1 = new Member();
        member1.setName("spring1");
        memoryMemberRepository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        memoryMemberRepository.save(member2);
				
				// when
				// 저장소의 모든 member 객체를 찾습니다.
        List<Member> result = memoryMemberRepository.findAll();
				
				// then
				// 찾은 member의 수가 저장한 member의 수와 같은지 비교합니다.
        assertThat(result.size()).isEqualTo(2);
    }
}
  • 각 함수를 실행하여 테스트를 수행하거나 파일, 패키지 단위로도 테스트를 실행할 수 있습니다.
  • ⌥⌘V 으로 return 값의 자료형으로 변수에 저장할 수 있습니다.
  • ⌃R 으로 가장 최근 실행했던 함수, 파일 또는 패키지를 다시 실행할 수 있습니다.

좌측 사진 버튼을 눌러 실행하면 우측 사진과 같은 결과를 볼 수 있습니다.

회원 서비스 개발

회원 서비스에는 회원 domain과 회원 repository를 활용하여 비즈니스 로직을 작성합니다. src/main/java/hello.hello_spring 에 service 패키지를 새로 생성하고 그 안에 MemberService 클래스를 생성합니다. 클래스 내부는 아래와 같이 작성합니다.

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
		// 저장소
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    public Long join(Member member) { // 회원가입

        validateDuplicateMember(member); // 중복 회원 검증
        memberRepository.save(member); // 저장소에 저장
        return member.getId(); // member id 반환
    }

    private void validateDuplicateMember(Member member) { // 중복 회원 검증 메소드
        memberRepository.findByName(member.getName()) // 저장소에서 이름이 같은 member 객체가
                .ifPresent(m -> { // 존재한다면
                    throw new IllegalStateException("이미 존재하는 회원입니다."); // 예외발생
                });
    }

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

    public Optional<Member> findOne(Long memberId) { // id로 회원 조회
        return memberRepository.findById(memberId);
    }
}
  • ⌃T로 Refactoring 관련 항목을 볼 수 있습니다.
  • Repository 클래스는 기계적/개발스러운 용어를, Service 클래스는 비즈니스 용어를 사용합니다.

회원 서비스 테스트

MemberService 클래스의 테스트 클래스를 만들어봅시다. MemberService 클래스에 커서를 두고 ⇧⌘T으로 간편하게 Test를 만들 수 있습니다. 자동으로 main과 동일한 패키지/클래스 구조로 만들고 아래 코드를 작성합니다.

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

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

class MemberServiceTest {

    MemberService memberService = new MemberService();

    @Test
    void 회원가입() { // Test 코드는 빌드에 포함되지 않기 때문에 한글을 사용해도 됩니다.
        // given
        Member member = new Member(); // member 객체 생성
        member.setName("spring"); // name을 "spring"으로 설정

        // when
        // member 객체를 회원가입하고 반환된 id를 저장
        Long saveId = memberService.join(member);

        // then
        // 저장소에서 회원가입한 member의 id가 있으면 객체로 저장
        Member findMember = memberService.findOne(saveId).get();
        // 회원가입한 member와 저장소에서 가져온 member의 name이 같은지 검증
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복_회원_예외() {
        // given
        // 이름이 같은 두 member 객체 생성
        Member member1 = new Member();
        member1.setName("spring");

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

        // when
        // member1 회원가입
        memberService.join(member1);
        
        // member2가 회원가입할 때 예외 발생 여부
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        // 앞서 발생한 예외의 메시지가 의도한 메시지와 같은지 검증
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

위와 같이 작성하고 실행하면 모든 테스트가 정상적으로 작동합니다.

회원 서비스 테스트 DI

앞서 회원 리포지토리 테스트 케이스 작성처럼 각 메서드의 실행이 끝날 때마다 저장소에 있는 내용을 다 지우는 메서드를 추가해야합니다.

MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();

@AfterEach // 메서드 실행이 끝날 때마다
public void afterEach() {
	memberRepository.clearStore(); // 저장소 초기화
}

그러나 이렇게 작성하면 문제가 발생할 수 있습니다. memberService 객체가 아닌 새로운 memoryMemberRepository 객체이기 때문에 다른 저장소를 초기화할 수 있습니다. 지금은 MemoryMemberRepository의 저장소인 HashMapstatic으로 선언되어 있기에 문제가 없지만, static이 아닌 경우 다른 저장소에 작업을 수행하게 됩니다.

그래서 이런 경우 MemberServiceMemoryMemberRepository를 외부에서 주입하는, **의존성 주입(DI, Dependency Injection)**을 합니다. 아래와 같이 수정합니다.

private final MemberRepository memberRepository;

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

이제 MemberServiceTest를 아래와 같이 수정해보겠습니다.

MemberService memberService;
MemoryMemberRepository memoryMemberRepository;

@BeforeEach // 메서드 시작 전
public void beforeEach() {
    memberRepository = new MemoryMemberRepository(); // 새로운 저장소를 만들고
    memberService = new MemberService(memoryMemberRepository); // 서비스에 주입
}

@AfterEach // 메서드 종료 후
public void afterEach() {
    memberRepository.clearStore(); // 저장소 초기화
}

이제 실행해 보면 문제 없이 실행되는 것을 확인할 수 있습니다.

728x90
반응형