본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2024-2 Spring Boot 스터디] 김아리 #1 주차

반응형

웹 개발 기초

정적 컨텐츠

  • 클라이언트에게 요청받은 파일을 서버의 처리 없이 웹 브라우저에 그대로 보여주는 것
  • 우선적으로 컨트롤러에서 관련 메서드를 찾고, 없으면 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에 대한 객체가 따로인 것이 아니라 같은 인스턴스로 생성되도록 의존관계를 부여한다.
  • 잘 실행되는 케이스도 확인해야 하지만, 더 중요한 것은 예외가 발생하는 케이스가 잘 처리되는지 테스트하는 것이다.
반응형