[F-Lab 모각코 챌린지 34일차] 테스트 여행기

부추·2023년 7월 4일
0

F-Lab 모각코 챌린지

목록 보기
34/66

TIL

  1. Repository 구현체 여러개 만들기
  2. 테스트 코드를 작성해보기 위한 나의 여행기



1. Repository 구현체 여러개 만들기

기존의 domain 패키지에 있던 Repository 클래스는 아래같이 생겼다!

public interface UrlRepository {
    List<ShortenUrl> findAll();
    ShortenUrl findByShortenUrl(String shortenUrl);
    ShortenUrl save(ShortenUrl shortenUrl);
    int getTotalUrlSize();
}

메소드 이름만 봐도 해당 메소드들이 어떤 역할을 할지 대충 예상이 가쥬? 이걸 여러 구현체로 만들어보겠따!!!!!

1) List

기존에는 UrlRepositoryImpl이라는 클래스 내부에 List자료구조를 두고 아래와 같이 구현했다.

@Repository
public class UrlRepositoryListImpl implements UrlRepository {
    private final List<ShortenUrl> shortenUrls = new ArrayList<>();

    @Override
    public List<ShortenUrl> findAll() {
        // deep copy
        return new ArrayList<>(shortenUrls);
    }

    @Override
    public ShortenUrl findByShortenUrl(String shortenUrl) {
        int index = ShortenUrl.decodeUrlIndex(shortenUrl);
        if (index < 0 || index >= shortenUrls.size()) throw new UrlNotFoundException();
        return shortenUrls.get(index);
    }

    @Override
    public ShortenUrl save(ShortenUrl shortenUrl) {
        shortenUrls.add(shortenUrl);
        return shortenUrl;
    }

    @Override
    public int getTotalUrlSize() {
        return shortenUrls.size();
    }
}

특별한 부분은 없고, shortenUrl이 기존 사용자로부터 들어온 URL index를 62진법으로 그대로 변화시킨 URL이라고 보면 된다. 이는 decodeUrlIndex()를 통해 다시 가용한 index로 돌아온다.


2) Map

List 자료구조 대신 Map 자료구조를 쓰는 구현체도 만들었다.

@Repository
@Profile("map")
public class UrlRepositoryMapImpl implements UrlRepository {
    private final Map<String, String> urls = new HashMap<>();

    @Override
    public List<ShortenUrl> findAll() {
        return urls.keySet().stream()
                .map(key ->
                        ShortenUrl.builder()
                                .shortenUrl(key)
                                .originalUrl(urls.get(key))
                                .build())
                .collect(Collectors.toList());
    }

    @Override
    public ShortenUrl findByShortenUrl(String shortenUrl) {
        String originalUrl = urls.get(shortenUrl);
        if (originalUrl==null) throw new UrlNotFoundException();

        return ShortenUrl.builder()
                .originalUrl(originalUrl)
                .shortenUrl(shortenUrl)
                .build();
    }

    @Override
    public ShortenUrl save(ShortenUrl shortenUrl) {
        urls.put(shortenUrl.getShortenUrl(),shortenUrl.getOriginalUrl());
        return shortenUrl;
    }

    @Override
    public int getTotalUrlSize() {
        return urls.size();
    }
}

Map을 이용한 구현체에선 index를 이용해 base62로 decode, encode하는 과정 없이 그냥 put()get()메소드를 통해 ShortenUrl값을 불러온다.

2번째 줄의 @Profile("map") 부분이 중요하다! application.yml파일에 아래와 같이 spring profile을 설정하면 application context에 UrlRepositoryMapImpl 구현체가 등록된다.

spring:
  profiles:
    active: map

이렇게 프로퍼티 파일 내 프로파일을 설정함으로써 application context에 원하는 빈을 자유자재로 등록할 수 있다.


3) DB

H2 인메모리 DB를 이용했다. application.yaml파일에 이런저런 설정을 해주고~

spring:
  h2:
    console:
      enabled: true
      path: /h2-console
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:~/test
    username: sa
    password: 1234
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create

사용할 엔티티 클래스, 즉 ShortenUrl 클래스에 @Entity 어노테이션을 붙여주고 pk로 사용할 @Id역시 지정해주었다. 일단 여기선 단축된 URL인 shortenUrl을 pk로 사용하기로 했다!

@Entity
@Getter
public class ShortenUrl {
	@Id
    private String shortenUrl;
    
    // other stuffs..
}

그리고 Spring Data JPA Repository를 infrastructure 패키지에 생성.

@Repository
@Profile("jpa")
public interface UrlJpaRepository extends JpaRepository<ShortenUrl, String> {
    Optional<ShortenUrl> findByShortenUrl(String shortenUrl);
}

그리고 이를 이용해서 domain의 리포지토리를 구현하는 구현 객체까지 작성했다!

@Repository
@Profile("jpa")
@RequiredArgsConstructor
public class UrlRepositoryJpaImpl implements UrlRepository {
    private final UrlJpaRepository urlJpaRepository;

    @Override
    public List<ShortenUrl> findAll() {
        return urlJpaRepository.findAll();
    }

    @Override
    public ShortenUrl findByShortenUrl(String shortenUrl) {
        return urlJpaRepository.findByShortenUrl(shortenUrl)
                .orElseThrow(UrlNotFoundException::new);
    }

    @Override
    public ShortenUrl save(ShortenUrl shortenUrl) {
        return urlJpaRepository.save(shortenUrl);
    }

    @Override
    public int getTotalUrlSize() {
        return (int) urlJpaRepository.count();
    }
}

# 현타갑 삽질

부끄러워서 공개 안하려고 했는데.. 기존에 코드는 아래와 같았다.

@Repository
public interface UrlRepositoryJpa extends JpaRepository<ShortenUrl, String> {
    Optional<ShortenUrl> findByShortenUrl(String shortenUrl);
}
@Repository
@RequiredArgsConstructor
public class UrlRepositoryJpaImpl implements UrlRepository {
    private final JpaUrlRepository jpaUrlRepository;

    // UrlRepository 구현 내용..
}

그리고 의존성 cycle 에러 메세지를 쳐맞고 2시간동안 삽질했다.
사이클이 생길게 없는데 뭐가 문제지? 게다가 Impl클래스 안에서 자기참조라고? 덕분에 2시간동안 개난리난리쌩난리를 피웠다.. @Component 붙였다 뗐다.. @Repository 붙였다 뗐다.. @Profile 건드려보고.. @AutoWired 붙였다가 뗐다가, setter 사용했다가, @Configuration + @Bean 조합 사용하다가, @RequiredArgsConstructor 대신 생성자를 직접 만들기도 하다가 의존 객체를 final로 하니 마니 진짜 별의 별 짓을 다 했다.

Bean들의 연관관계를 아무리 그려도 circular dependency는 보이지 않았기 때문에 멘붕이 왔다. JpaRepository는 어디에도 의존하지 않아서 독자적으로 존재할 수 있는걸? 그러다 아무런 생각 없이 JpaRepository<ShortenUrl,String>을 상속한 인터페이스 이름을 UrlRepositoryJpa -> UrlJpaRepository로 바꿔봤다. 진짜 뭘 얼마나 시도했으면 클래스 이름까지 바꿔보는 시도를 했겠나? ㅋㅋ

결과는 성공.
에.............?


원인 - ,. -

Bean 이름에 컨벤션따윈 없는 줄 알았다.
그런데.. Repository에 대한 구현체는 해당 클래스 이름 뒤에 "Impl" 접미사를 붙여야 Spring Data JPA 측에서 구현된 빈으로 관리를 해준다고 한다.docs에서 "most important part"라고 강조까지 했다.

처음에 코드에서 나의 JpaRepository 빈 이름은 "UrlRepositoryJpa"였다. 만약 내가 이 객체를 Repository로 그냥 사용했다면, Spring Data JPA측에서 해당 인터페이스의 구현체로 "UrlRepositoryJpaImpl"를 만들고 이를 이용했을 것이다. 그.러.나. 내 쪽에서 UrlRepositoryJpaImpl 이름을 먼저 채가버렸으니 Spring Data JPA 쪽에서 "UrlRepositoryJpaImpl" 생성을 안했던 것이다.

JpaRepository가 구현체로 이용되려면 UrlRepositoryJpaImpl 클래스가 필요한데, 내가 선점한 UrlRepositoryJpaImpl에서 JpaRepository를 쓰려고 하니 circular dependency가 발생한것..!

그림을 이용해 직관적으로 표현해보겠다.

  • UrlRepositoryJpa 인터페이스에 대해, Spring Data JPA가 내부적으로 UrlRepositoryJpaImpl 이라는 클래스 구현체를 만들려고 시도한다.
  • 그러나 내가 UrlRepositoryJpaImpl을 이미 선점했기 때문에.. 내가 선점한 클래스가 UrlRepositoryJpa의 구현체로 작동한다.
  • 그런데? 내 UrlRepositoryJpaImpl은 UrlRepositoryJpa를 구성으로 두면서 의존한다. 여기서 circular dependency problem이 일어난 것이다...

그림에서도 화살표가 원을 그리는 것을 확인할 수 있따 TT


그렇기 때문에 인터페이스의 이름을 UrlJpaRepository로 바꿔주면, Spring Data JPA가 만든 구현체와 충돌하지 않는다.

내 2시간 돌려도!!!!!!!!




2. 테스트 코드 첫걸음

객체지향 프로그래밍의 장점엔 변경에 유연하다는 것 말고 테스트가 용이하다라는 장점이 있었다. 이는 프로그램의 동작이 객체 단위로 나눠지기 때문에, 다시 객체 단위로 프로그램 동작을 테스트 할 수 있기 때문이다.

어플리케이션 컨텍스트 내 모든 빈들과 구현체들을 띄워놓고 테스트를 하는 것은 힘들다. 우리의 프로그램은 내부의 service, controller 등의 클래스들만 있는게 아니라 JPA, MessageQ, 캐시서버 등등등 여러 외부 기술 구현체에 의존하고 있기 때문이다. 테스트 하겠다고 이 구현체들을 모두 띄우다간 시간도 오래걸리고, 실제 운영 서버와 겹쳐서 크고작은 문제를 일으킬 수도 있다.

그래서 이런 인프라 기술들을 제외하고 작성한 코드들이 있는 각 모듈들에 대해 기능이 잘 동작하는지 확인할 수 있는 단위 테스트가 진행된다. 단위 테스트를 잘 진행하면 앞서 언급한 시간이 오래 걸린다, 구현체를 준비하기 힘들다 라는 기존 테스트의 단점을 극복할 수 있다.

뭐.. 가볍게 단위 테스트가 가져야하는 원칙들을 살펴보자. 일명 FRIST라고 하는 원칙이다.

  1. Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
  2. Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.
  3. Repeatable: 어느 환경에서도 반복 가능해야 한다.
  4. Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.
  5. Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.

하아.. 원칙은 알겠는데 실제 테스트 코드 작성하는거 너무 힘들다. 아니 어렵다. 구문이 너무 많다; 오늘 몇 시간 삽질을 해서 단위테스트, mock, 통합테스트, assert와 MockMvc 등등에 대해 간단히 배우고 controller/service 유닛 테스트를 작성했다. 그런데 너무 힘들어.. 작성한 테스트 코드에 관한건 내일 자세히 정리하겠다!

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글