비즈니스 요구사항 정리
데이터는 회원ID와 이름, 기능은 회원 등록과 조회만 있고 아직 DB가 선정되지 않은 가상의 시나리오이다.
일반적인 웹 애플리케이션의 계층 구조이다.
- Controller : 웹 MVC의 컨트롤러 역할을 한다.
- Service : 핵심 비즈니스 로직을 구현한다.
- Repository : DB에 접근하며, 도메인 객체를 DB에 저장하고 관리한다.
- Domain : 비즈니스 도메인 객체(예 : 회원, 주문, 쿠폰 등)로 주로 DB에 저장하고 관리한다.
클래스 의존 관계이다. 아직 DB가 선정되지 않아 인터페이스로 구현 클래스를 변경할 수 있도록 설계하며, DB는 RDB, NoSQL 등 다양한 저장소를 고민 중인 상황이다. 개발 진행을 위해 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다. 여기서 인터페이스는 '클래스가 따라야 할 설계도' 역할로 다음과 같은 특징을 가진다.
- 구성 요소
- 추상 메소드 : 인터페이스에 선언된 메소드는 기본적으로
abstract
이며, 구현 클래스에서 반드시 구현해야 한다. - 상수 : 인터페이스에 선언된 변수는 자동으로
public static final
이 된다. - 기타 메소드 : Java 8 부터
default
메소드와static
메소드를 포함할 수 있다.
- 추상 메소드 : 인터페이스에 선언된 메소드는 기본적으로
- 다중 상속 지원 : 클래스는 하나의 부모 클래스만 상속할 수 있지만, 인터페이는 여러개를 구현할 수 있다.
- 객체 생성 불가 : 인터페이스 자체로는 객체를 생성할 수 없으며, 이를 구현한 클래스만 인스턴스화가 가능하다.
- 유연한 참조 : 인터페이스 타입의 참조변수를 통해 구현 객체를 다룰 수 있어, 코드 변경 없이 다양한 객체를 교체할 수 있다.
회원 도메인과 리포지토리 만들기
Member.java
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;
}
}
MemberRepository.java
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);
//Id로 찾기
Optional<Member> findById(Long id);
//이름으로 찾기
Optional<Member> findByName(String name);
//모든 회원 찾기
List<Member> findAll();
}
MemoryMemberRepository.java
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;
@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());
}
}
회원 리포지토리 테스트 케이스 작성
MemoryMemberRepositoryTest.java
public 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(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring1");
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("spring1");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
JUnit을 활용한 테스트이다. main 디렉토리 아래가 아닌 test 디렉토리 아래에 테스트를 위한 클래스를 작성하면 된다. 테스트 메소드가 여러개 있을 때 코드에 작성된 대로 순차적으로 실행되지 않기 때문에 변수 등에 저장된 값에 의해 의도치 않은 실패가 발생할 수 있다. 이를 방지하기 위해 @AfterEach 어노테이션을 통해 각 테스트 메소드가 종료되면 실행하는 메소드를 작성하면 된다. 여기서는 HashMap을 clear한다.
테스트는 언제나 중요하며, 개발 → 테스트가 아닌 반대의 경우도 있다. 이를 테스트 주도 개발(Test-Driven Development, TDD)라고 한다. TDD는 테스트 코드를 먼저 작성한 후 이를 통과하는 실제 코드를 구현하는 방식이다.코드 품질 향상, 유지보수 용이성 등의 장점이 있으면서 초기 시간 투자 증가, 학습 곡선 등의 단점도 있다.
회원 서비스 개발
MemberService.java
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);
}
}
서비스는 실제 비즈니스 로직을 구현한다.
회원 서비스 테스트
클래스에 대해 테스트 클래스를 쉽게 생성하는 방법은 윈도우 기준 Ctrl + Shift + T 단축키를 누르면 된다.
MemberService.java
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.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복회원가입() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
}
@Test
void 전체회원조회() {
Member member1 = new Member();
member1.setName("spring1");
memberRepository.save(member1);
Member member2 = new Member();
member2.setName("spring1");
memberRepository.save(member2);
List<Member> result = memberRepository.findAll();
assertThat(result.size()).isEqualTo(2);
}
@Test
void 회원조회() {
Member member1 = new Member();
member1.setName("spring1");
memberRepository.save(member1);
Member result = memberRepository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
}
테스트를 작성할 때는 given(어떤 환경에서), when(어떤 조건이 발생했을때), then(어떤 결과가 나오는지)의 3단계로 작성하면 쉽다. @BeforeEach 어노테이션으로 테스트 시에도 같은 MemberRepository를 사용하기 위해 의존성 주입 형태로 변경했다. 의존성 주입(Dependency Injection, DI)은 객체 간의 의존 관계를 외부에서 주입하여 관리하는 설계 패턴이다. 의존성 주입에 대해서는 다음에 자세히 알아보겠다.
'스터디 > Spring' 카테고리의 다른 글
Spring 스터디 - 6 (0) | 2025.02.18 |
---|---|
Spring 스터디 - 5 (0) | 2025.02.17 |
Spring 스터디 - 4 (0) | 2025.02.16 |
Spring 스터디 - 2 (0) | 2025.02.10 |
Spring 스터디 - 1 (0) | 2025.02.04 |