JAVA & SPRING/Spring 입문

스프링 입문 강의 - 2일차 (스프링 계층 구조)

눈오는1월 2023. 7. 14. 17:49
728x90

이번 강의에서는 비지니스 요구사항 및 회원 도메인을 간략하게 개발 하는식으로 진행했다.(아직 내가 자바와 스프링의 개념이 중요해서 이번 강의는 좀 어렵게 느껴짐.. 분발하자)

 

일반적인 웹 애플리케이션 계층 구조에 대해서 먼저 말하자면

웹 애플리케이션 계층 구조

컨트롤러 -> 웹 MVC의 컨트롤러 역할을 한다(말 그대로 컨트롤 데이터를 준다던지)

서비스 -> 핵심 비즈니스를 로직으로 구현을 한다

레포지토리 -> DB에 접근하고, 도메인 객체를 DB에 저장하고 관리한다.

도메인 -> 비즈니스 도메인 객체로 회원,주문,쿠폰 등등 주로 데이터베이스에 저장하고관리한다.

 

이번에 강의들으면서 어떠한 상황에서 개발할건지에 대한 상황을 먼저 말하자면

아직 데이터 저장소를 선정하지 않는다는 가정하에 개발을 진행한다. 메모리에 저장한다고 보면됨 ( 이로 인해서 인터페이스가 필요함)

 

일단 회원 객체를 만든다.

도메인이라는 패키지를 만든 후에 Member라는 클래스를 만들었다. Member의 변수로는 id와 name을 가지게 됨

 package hello.hellospring.domain;
  public class Member {
      private Long id;
      private String name;
      public Long getId() {
          return id;
}
      public void setId(Long id) {
          this.id = id;
}
      public String getName() {
          return name;
}
      public void setName(String name) {
          this.name = name;
} }

그리고 나서 레퍼지토리라는 패키지를 하나 더 만든 후에 회원 레퍼지토리 인터페이스를 만든다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;
public interface MemberRepository {
    Member save(Member member);  // 회원을 저장하는 메소드
    Optional<Member> findById(Long id); //id를 찾는 메소드
    Optional<Member> findByName(String name); //이름을 찾는 메소드
    List<Member> findAll(); //저장된 회원의 리스트를 모두 가져오는 메소드

}

/*
Optional 이란 개발할때 가장 많이 발생하는 NPE(NullPointerException)을 방지해줄 수 있다 Optional 은 클래스 이기에 각종 메소드 역시 제공해준다.
 */

그후 이 인터페이스를 상속받는 MemoryMemberRepository class를 repository 패키지 안에 생성한다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements  MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L; // 동시성 고려 x 단순하게
    @Override
    public Member save(Member member) {
        member.setId(++sequence); // id값을 세팅
        store.put(member.getId(), member); //getId와 member를 store에 저장
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        // store.get(id)가 null인지 알 수 없기에 ofNullable을 사용 null 일경우 NPE를 던지지 않고 Optional.empty()와 동일하게 비어있는 Optional 객체를 얻어옴
        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() {  //test할때 초기화될때 이용
        store.clear();
    }
}

이렇게 클래스까지 만들어준다음 이 코드가 문제가 있나 없나를 확인을 해야한다. 스프링을 하는 개발자는 테스트를 System.out.println()이나 직접 스프링을 실행하면서 확인하는것이 아닌 test를 통해서 문제가 있는지 없는지 확인한다.

스프링으로 실행하면서 테스트한다는 것은 오래 걸리기도하고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하는데 어려움이 있다.

자바는 Junit이라는 프레임워크로 테스트를 한다.

 

테스트에 들어가서 repository라는 패키지를 하나 더 만든다. 그 후 MemoryMemberRepositoryTest 라는 클래스를 생성한다.

Test 클래스는 위처럼 테스트하려는 클래스의 이름 뒤에 Test를 붙인다(코드에 테스트하려는 클래스 이름에 마우스 커서 놓고 command + shift + t 누르면 테스크 클래스를 바로 만들 수 있다)

 

package hello.hellospring.repository;

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

import javax.swing.text.html.Option;
import java.util.List;

import java.util.Optional;

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

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();
    @AfterEach // 각 테스트가 종료될때 작동
    public void afterEach() {  // 테스트는 서로 의존관계 없이 설계해야하기떄문에 데이터를 깔끔하게 지워야함
        repository.clearStore();
    }

    @Test
    public void save() {
        //given
        Member member = new Member();
        member.setName("spring");

        //when
        repository.save(member);

        //then
//        Optional<Member> optionalMember = repository.findById(member.getId()); 이게 더 좋은방법 아래 get() 하는것보다
//        Member result = optionalMember.orElse(null);
        Member result = repository.findById(member.getId()).get();
        //Assertions.assertEquals(member,result);
        assertThat(member).isEqualTo(result);
        //assertj의 assertThat 사용 member와 result를 비교 태스트에서 많이 사용함
    }

    @Test
    public void findByname(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();  // shift f6을 하면 같은 글자 한번에 고치기 가능
        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("spring");
        repository.save(member2);

        List<Member> result = repository.findAll(); 

        assertThat(result.size()).isEqualTo(2); // test에서 2개를 만들었으니 2개 가 맞는지 확인



    }
}

  이렇게 해서 테스트를 할 수 있다. 또한 save() 테스트 처럼 김영한님께서  추천해주신 방식으로 //given //when //then 이 3개의 주석을 적어서 테스트하는것이 좋다고 추천했다

테스트는 거의 같은 흐름으로 테스트하는데-> 뭔가를 줬을때(given) 이거를 실행했을때(when) 이게 나와야해(then)!!  이 흐름이기때문에 주석처리를 한 후 테스트를 진행하면 좀 더 편하다는 것

또한 테스트를 하면 위 코드처럼 @AfterEach 어노테이션을 통해 값을을 초기화 해야지 각 테스트의 영향이 없게끔 해야한다.

또한 테스트(툴)을 먼저 만들고 구현하는 것을 TDD(test-driven development) 라고 한다. ( 아직 어색하긴하지만 좋은 백엔드 개발자가 되려면 많은 연습을 해야할듯)

또 테스트에 메소드들은 한글이름으로 해도 된다.(오히려 직관적이여서 좋을 수 있다. 테스트는 빌드하는데 영향을끼치지않으니까) 외국인들과 같이 개발하는게 아니라면 사용 가능 또 이 밑에서 서비스 클래스도 만들건데 서비스 클래스 같은 경우 메소드를 서비스 이름과 관련돼서 만드는게 좋다고 한다. ( 기획자의 요구사항에 바로바로 대응할 수 있게끔)

 

이제 회원 도메인을 만들었으니, 회원 서비스를 개발해야한다.

 

다시 main으로 넘어와서 service라는 패키지를 만들고 Memberservice라는 클래스를 만든다.

package hello.hellospring.service;

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

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

public class Memberservice {

    private final MemberRepository memberRepository; // final은 한번만 사용할 수 있는 엔티티(데이터베이스 테이블과 매핑되는 자바 클래스) 정의할 때 사용

    //private final MemberRepository memberRepository = new MemoryMemberRepository();

    public Memberservice(MemberRepository memberRepository) {  // 직접생성하는것이 아닌 넣어주는 식으로
        this.memberRepository = memberRepository; 
    } // 회원 리포지토리 코드가 회원 서비스 코드를 DI 가능하게 변경한다.

    /*
    * 회원가입
     */
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원 x

        // Optional 안에 객체를 감쌈 null 일경우 Optional로 감쌈 꺼내고싶으면 get()쓰면됨
        // Optional로 바로 반환하는게 별로 좋지 않음
//        Optional<Member> result = memberRepository.findByName(member.getName());
//        result.ifPresent(m -> {
//            throw new IllegalStateException("이미 존재하는 회원입니다.");
//        })
        validateDuplicateMember(member);  // 중복회원검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) { // 위 중복회원 검증 메소드
        memberRepository.findByName(member.getName())  //결과가 바로 Optional 임
                        .ifPresent(m -> {
                            throw new IllegalStateException("이미 존재하는 회원입니다.");  // try catch 말고 이런식으로 예외처리가 가능
                        });
    }

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

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }

}

* 위 주석에 적힌 DI란 Dependency Injection의 줄임말로 의존관계 주입이라고 한다. * 

쉽게 설명하자면 A가 B를 의존한다고 하면 B의 기능이  추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에게 미친다

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.repository.MemoryMemberRepositoryTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

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();
        //Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
        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 = assertThrows(IllegalStateException.class, () -> memberService.join(member2)); // command + option + v 단축키 사기
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//            memberService.join(member1); // 중복되는 것을 try catch로 잡을 수도 있음
//            //fail();
//        } catch (IllegalStateException e) {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 사람입니다.");
//        }

        // then
    }

    @Test
    void findMember() {
    }

    @Test
    void findOne() {
    }
}

 

강의들으면서 자바에 기초가 좀 부족한 것 그리고 스프링이 Django 보다 어렵게 느껴진것(아무래도 자바가 파이썬보다도 어려운데..당연한거일수도..) 블로그에 정리하기 전까진 좀 어렵게 생각했으나 정리하니까 100%는 아니더라도 조금은 정리가 되었다.

이 강의는 스프링의 전반적인 생태계? 를 맛보는 강의이기에 모르더라도 이렇게 정리하면서 완강하는게 중요한거같다

 

 

728x90