Spring boot MVC 패턴과 Test case

강정우·2022년 10월 25일
0

spring

목록 보기
17/25
post-thumbnail

1. 일반적인 웹 애플리케이션 계층 구조

  • 컨트롤러 : 웹 MVC의 컨트롤러 역할
  • 서비스 : 핵심 비즈니스 로직 구현 예) 회원은 중복가입이 안 된다.
  • 리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리 즉, 핵심 비스니스 로직이 구동이 되도록 구현한 계층
  • 도메인 : 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 DB에 저장하고 관리됨

2. 회원 도메인과 테스트용 리포지토리 만들기

ctrl + n => getter setter 쉽게 만들기
alt + enter => implements 상황에서 즉시 method 구현하기
ctrl + → => 단어단위 이동
ctrl + del => 단어단위 삭제

ctrl + i => implement 가능한 메서드 확인 (opt + enter)

private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
  • 실무에서는 동시성문제가 있을 수 이썽서 이렇게 공유되는 변수일 경우에는 conquer를 대신 쓴다.
@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());
}
  • 예전엔 if문을 써서 null 객체를 생성하였지만 요즘엔 null이 반환될 가능성이 있다면 이렇게 optional 키워드로 예외처리를 할 수 있다. 그래서 null일때도 감싸서 보낼 수 있다. 이렇게 하면 client에서도 뭘 할수가 있다.

  • stream : 이건 data와 POJO의 data관을 만들어준다고 생각하면 된다. 또한 stream() 은 마치 반복문처럼 data 하나하나를 돌며 어떠한 로직을 실행한다.

  • findAny() : 얘는 찾다가 찾아지면 그냥 바로 반환한다.
    그럼 findany vs findfist 는 무슨 차이냐? 라고 할 수 있는데
    stream의 순서를 고려하냐 안 하냐의 차이이다. 즉, findAny() 는 조건에 부합하는 결과값이 여러개라면 결과값이 실행할 때마다 달라질 수 있다.

3. 테스트 케이스 작성해보기

  • 개발한 기능을 실행해서 테스트할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 분비하고 실행하는데 오래걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

test case 작성

1. package 작성

  • main과 동일한 위치에 package를 작성해야한다.

2. class 작성

  • 다른 곳에서 import를 해서 쓸 것이 아니기 때문에 앞에 굳이 public을 작성할 필요가 없다.
  • class의 이름은 main에서 테스트 하고자하는 이름과 동일하게 한 뒤 뒤에 Test를 붙여준다.

3. method 작성

  • @Test annotation만 작성해주면 된다.

  • 요 refactoring 기능을 보고 optional이니까 get으로 꺼낸다. 사실 get으로 바로 꺼내는 것은 좋은 방법은 아니나 testcase에서는 편의상 가능하다.
  • ctrl + alt + V(mac cmd+opt+v) : 값 refactoring
  • ctrl + p : 메서드 상세정보

4. 결과값 검증

@Test
public void save(){
	Member member = new Member();
	member.setName("Spring");
	repository.save(member);
	Member result = repository.findById(member.getId()).get();// refactoring가능 ctrl + alt + V
    // 결과값 code
}

1. Assertions(org.junit.jupiter.api)

  • 출력되는 것은 없지만 녹색불이 떴다.

2. Assertions(org.assertj.core.api)

  • 만약 값이 다르다면 다음과 같이 주황등이 들어온다.
  • alt + enter : static import가 뜬다. 이걸 이렇게 쓴다면 앞의 Assertions 객체가 static memory에 올라가 객체를 쓰지 않고도 method를 바로 쓸 수 있다는 편리한 점이 있따.

findByName()

  • ctrl + d : 복사 붙여넣기 즉시 실행
  • shift + F6 : 해당 class내의 변수 이름 동시 변경

@AfterEach

  • 보니까 각각이 잘 test가 되던 것을 한 큐에 돌리니까 error가 났다
    원인은 추가된 id를 또 추가였기 때문이다. => test methods는 위에서 아래로 prompt 식이아닌 동시다발적으로 각각의 test 코드를 실행하다보니 충돌이 날 수 있다.

  • 그래서 test를 한 후 항상 비워줘야하는데 이를 error 처리에 final 처럼 어떠한 함수가 돌더라도 항상 실행되는 @AfterEach 문으로 비워주면 된다.
    즉, test는 서로 의존관계없이 설계가 되어야 한다.
  • 그래서 원래는 main을 작성 후 test로 넘어오는 것이 정석이지만 반대로 틀을 먼저 만들어놓고 틀에 맞춰서 main을 작성할 수 있는데 이를 tdd (test 주도 개발)이라 한다.

4. optional vs stream

  • 테케를 작성할 때 물론 위 사진처럼 Optional을 반환할 수 있지만 별로 권장하진 않는다.
    그래서 아래 사진처럼 ifPresent라는 메서드를 작성하여 stream 형태로 처리하고 또 이를 하나의 메서드로 묶어서 사용할 수도 있다.


ctrl + alt + shift + T : Refactoring
(메서드에 커서 올려놓고) ctrl + B : 해당 매서드가 사용되는 곳 보기
ctrl + shift + m : method로 Refactoring

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 회원가입
    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);
    }
}

5. 더 간단한 테스트 작성법

  • test code는 한글로 적어도 무방하다. 또한 build될때 test code는 포함되지 않기에 직관적으로 보기 위해서 한글로 작성하는 경우도 있다.
  • 앞서 package를 만들고 따로 코드를 쳤지만 편하게 하는 방법이 있다. 바로 테스트 코드 생성을 원하는 패키지에서
    ctrl + shift + T : test를 위한 package와 code를 알아서 만들어준다.

gwt (given, when, then)

  • 사실 영어를 조금만 할 줄 아면 무슨 뜻인지 금방 느낄 것이다.
    given : 검증할 데이터가 들어감
    when : 실제 검증할 코드
    then : 검증부가 들어갈 부분
    으로 나누어 잡고 테스트 케이스를 작성하면 매우 긴 테스트 케이스도 조금 수월하게 작성할 수 있다.

  • 초급 개발자는 이런식으로 깔아놓고 하면 굉장히 도움이 된다.

예외 flow (assertThrow)

  • test내부에서 그게 given when then 으로 주석으로 나누어 단계별로 작성한다면 협업할 때 훨씬 좋다.
  • test는 정상 flow도 중요한데 예외 flow가 훨씬 더 중요하다.
memberService.join(member1);
try {
	memberService.join(member2);
	fail();
} catch (IllegalStateException e) {
	assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
  • 예전엔 try-catch문을 주로 사용했지만
memberService.join(member1);
assertThrows(IllegalStateException.class,()-> memberService.join(member2));
  • 요즘은 assertThrow를 이용한다.
    위 코드의 뜻은 assetThrows(다음 lambda 함수를 실행했을 때 예외가 터져야한다.),
    콜백람다함수(예외가 터질 코드 작성)

assertThrow

  • assertThrow 함수바로 앞에 발생할 exception을고 화살표 함수로 조건을 설정한다. "이 logic을 태울때"
    그래서 만약 다른 예외가 발생한다면 바로 error를 띄워준다.

shift + F10 : 이전에 실행했던 것 그대로 다시 실행 (mac opt + r)

DI

  • 또 한가지 테케 작성할 때 주의할 점이 다른 테케에 있는 class나 다른 테케class 자체를 접근할 때 instance를 또 새로 생성해버리면 같은 곳을 바라보지 않기 때문에 분명 의도와른 다른 결과가 생길 수 있다. 이를 방지하고자 각 클래스 마다 생성하는 것이 아닌 외부에서 주입방식으로 가면 된다.

  • 전체적으로 보면 MemberService가 MemberRepository의 의존성을 주입 받았다. 이것도 DI라고도 한다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글