반응형
웹 개발 기초
정적 컨텐츠
- 클라이언트에게 요청받은 파일을 서버의 처리 없이 웹 브라우저에 그대로 보여주는 것
- 우선적으로 컨트롤러에서 관련 메서드를 찾고, 없으면 static 내에서 해당 파일을 찾아 웹 브라우저에 반환
- 파일 위치 : /main/resources/static/hello-static.html
- 실행 : localhost:8080/hello-static.html
MVC와 템플릿 엔진
- url에서 파라미터를 받아 모델에 담고 viewResolver를 통해 템플릿 엔진이 렌더링하여 변환한 HTML을 웹 브라우저에 반환
- 예시 : localhost:8080/hello-mvc?name=spring&age=20 ('&'로 여러 개의 파라미터를 받을 수 있음)
- viewResolver : 리턴된 뷰를 찾아 템플릿과 연결하고 파라미터를 넘김
- @RequestParam : url로부터 받은 파라미터를 컨트롤러 메소드의 파라미터로 바인딩(파라미터가 없는 경우 예외 발생)
- Thymeleaf의 장점 : path를 통해 서버 연결 없이 파일 자체를 웹 브라우저에서 실행시킬 수 있음
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, @RequestParam("age") String age, Model model) {
model.addAttribute("name", name);
model.addAttribute("age", age);
return "hello-template";
}
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}"></p>
<p th:text="'your age is...' + ${age}"></p>
</body>
</html>
API
- 웹 브라우저에게 HTML을 반환하지 않고, 문자나 객체를 그대로 반환하는 것
- 따라서 viewResolver를 사용하지 않음(뷰 자체가 없기 때문에)
- viewResolver 대신에 HttpMessageConverter가 동작
- 기본 문자 처리 : StringHttpMessageConverter
- 기본 객체 처리 : MappingJackson2HttpMessageConverter (Jackson : JSON 형식으로 변환하는 라이브러리)
- @ResponseBody : HTTP의 바디에 리턴값을 직접 반환, 이 어노테이션이 있어야 HttpMessageConverter가 동작
// 문자 반환
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
// 객체 반환
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
회원 관리 예제 - 백엔드 개발
웹 애플리케이션 계층 구조
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체
회원 도메인과 리포지토리 구현
회원 객체
package hello.hello_spring.domain;
public class Member {
private Long id; // 시스템 상에서 회원 데이터를 구분하는 key
private String name;
// Getter, Setter
}
회원 리포지토리 인터페이스
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();
}
회원 리포지토리 메모리 구현체
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());
}
public void clearStore() {
store.clear();
}
}
회원 리포지터리 테스트 케이스 작성
- JUnit 프레임워크로 테스트를 실행하여 위에서 구현한 리포지터리의 메서드가 제대로 수행되는지 테스트
- 테스트 파일 위치 : src/test/java
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.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);
}
}
- 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트 결과가 남을 수 있다. 그러면 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다.
- 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.
- @AfterEach : 각 테스트가 종료될 때마다 이 어노테이션이 있는 메서드 자동 실행. 여기서는 메모리 DB에 저장된 데이터가 삭제된다.
- 전반적인 흐름은 대부분 비슷하다.
- Member 객체 생성 후 리포지토리에 저장
- 리포지토리로부터 저장된 데이터 조회
- 조회된 데이터가 저장한 데이터와 동일한지 테스트 --> Assertion.assertThat(result).isEqualTo(member)
회원 서비스 개발
- 파일 위치 : main/java/hello.hello-spring/service
- 회원 가입 및 회원 조회 등 핵심 비즈니스 로직 구현
- 실제 리포지토리 구현체로부터 객체를 불러오는 것이 아닌, 리포지토리 인터페이스로부터 객체를 불러옴
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 (윈도우 기준) --> new create Test -> 테스트할 메서드 모두 선택 --> 자동으로 Test 파일 생성
class MemberServiceTest {
MemoryMemberRepository memberRepository;
MemberService memberService;
// memberRepository와 memberService가 같은 인스턴스를 사용
// 의존성 주입(DI)
@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 savedID = memberService.join(member);
// then
Member findMember = memberService.findOne(savedID).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
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("이미 존재하는 이름입니다.");
}
}
- @BeforeEach : 각 테스트 실행 전에 호출. 여기서는 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, MemberService와 MemberRepository에 대한 객체가 따로인 것이 아니라 같은 인스턴스로 생성되도록 의존관계를 부여한다.
- 잘 실행되는 케이스도 확인해야 하지만, 더 중요한 것은 예외가 발생하는 케이스가 잘 처리되는지 테스트하는 것이다.
반응형
'WINK-(Web & App) > Spring Boot 스터디' 카테고리의 다른 글
[2024-2 SpringBoot 스터디] 윤성욱 #1주차 (0) | 2024.10.10 |
---|---|
[2024-2 SpringBoot 스터디] 탁태현 #1주차 (0) | 2024.10.10 |
[2024-2 Spring Boot 스터디] 조상혁 #1주차 (3) | 2024.10.09 |
[2024-2 Spring Boot 스터디] 김문기 #1주 (1) | 2024.10.09 |
[2024 Spring Boot 스터디] 유태근 #3 주차 - 컴포넌트 스캔과 의존관계 자동 주입 (0) | 2024.07.11 |