웹 계층 구조

- Controller: HTTP 요청 검증 후 Service 계층에 전달
- Service: 비즈니스 로직 처리, 트랜잭션 관리
- Repository(데이터베이스 통신 계층)
- JPA 기반 Repository: SQL 작성없이 JPA가 자동처리 (@Entity와 동작) //findById, findAll, save 메서드통해 데이터처리
- DAO (Data Access Object): SQL을 직접 다뤄 DB와 매핑(MyBatis 또는 JDBC)
- Domain(데이터 모델 정의 계층)
- Entity: DB 테이블과 매핑되는 객체 (데이터 저장, 수정 등 가능, @Entity통해 DB 테이블과 매핑) -> 데이터베이스 저장용
- DTO: 데이터 전송을 위한 객체 (클라이언트 API 응답, 보안 목적) -> 데이터 전송용
데이터 저장소 미선정시 초기 개발
인터페이스 통해 구현클래스 변경가능하도록 설계, 가벼운 메모리 기반 데이터 저장소 사용.

-java/hello.hello_spring/repository/MemberRepository
public interface MemberRepository {
Member save(Member member);
//null이 반환될 가능성이 있는 경우, Optional<>로 감싸면 클라이언트 측에서 안전하게 처리 가능
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
-MemoryMemberRepository
@Repository
public class MemoryMemberRepository implements MemberRepository {
//동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
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(m -> m.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clear () {
store.clear();
}
}
데이터 저장소가 선정되면 구현 클래스만 변경
(ex. 인터페이스 (MemberRepository) 유지 새로운 JpaMemberRepository만들어 구현)
테스트 케이스
기능 테스트 시 main 메서드나 컨트롤러 통해 실행 -> 오래걸리고 반복실행 어려움. => JUnit 프레임워크 사용
src/test/java 하위 폴더에 생성 or ⌘⇧T
-src/test/java/hello.hello_spring/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.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach //메서드 실행 끝날때마다 동작
public void afterEach() {
repository.clear(); //테스트끝나면 지워지게 만들어
}
@Test
public void save() {
//given
Member member = new Member();
member.setName("spring");
//when
repository.save(member);
//then
Member result = repository.findById(member.getId()).get();//Optional.get() 테스트에선 ㅇㅋ
//Assertions.assertThat(result).isEqualTo(member); //org.assertj.core.api
assertThat(result).isEqualTo(member); //opt+enter
}
@Test
public void findByName() {
//given
Member member = new Member();
member.setName("spring1");
repository.save(member);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
Member result = repository.findByName("spring1").get();
//then
assertThat(result).isEqualTo(member);
}
@Test
public void findAll() {
//given
Member member = new Member();
member.setName("spring1");
repository.save(member);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
List<Member> result = repository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
}
}
* given(주어짐), when(실행), then(결과) 구조 나누면 좋음
Service
-MemberService
public class MemberService {
private final MemberRepository memberRepository;
public MemberService (MemberRepository memberRepository) {/////
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
public Long join(Member member) {
//같은 이름 있는 중복 회원x (메서드추출)
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);
}
}
-MemberServiceTest
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach //각 테스트 실행 전 호출
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach //각 테스트 실행 후 호출
public void clear() {
memberRepository.clear();
}
@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 중복_회원_예외() {
//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("이미 존재하는 회원입니다.");
}
}
통합테스트
db연동 후 통합 테스트할때
@SpringBootTest //스프링 컨테이너와 테스트를 함께 실행
@Transactional //테스트 시작 전 트랜잭션 시작, 완료 후에 롤백-> DB데이터 남지않아 다음테스트 영향x
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
...
//@BeforeEach@AfterEach 안해도됨'Spring' 카테고리의 다른 글
| [Spring] AOP(Aspect-Oriented Programming) (0) | 2025.01.22 |
|---|---|
| [Spring] 스프링 DB 접근(JDBC,JdbcTemplate,JPA,스프링 데이터 JPA) (0) | 2025.01.12 |
| [Spring] From 입력, 조회 (0) | 2024.12.22 |
| [Spring] 스프링 빈 등록과 의존관계 (0) | 2024.12.19 |
| [Spring] 클라이언트-서버 통신 방식 (0) | 2024.12.10 |
