Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spring 체스 - 2단계] 오리(최진영) 미션 제출합니다. #425

Merged
merged 35 commits into from Apr 30, 2022

Conversation

jinyoungchoi95
Copy link
Member

@jinyoungchoi95 jinyoungchoi95 commented Apr 27, 2022

안녕하세요 소니 :)

해보고 싶은 부분들은 많지만 욕심에 비해서 공부를 하는 속도가 느리다보니 아직까지 부족한게 많아보이는 이번 미션인 것 같아요 😢
스프링 사용해보기가 이번 미션의 목적인만큼 그래도 기본에 충실하자라는 마음으로 미션을 진행했습니다 :)

개발하면서 느꼈던 내용 comment 더해서 풀리퀘 드려요 😄

1. 이전 피드백

  • 금방 머지가 되어버려서 코멘트는 이전 pr에 담아두었어요.
  • 자바때도 그랬지만 "왜" 쓰게되었는지를 계속해서 머릿속에 되뇌이면서 생각할 수 있었던 좋은 시간이었던 것 같습니다 :)

2. 통합 ? 단위?

  • 스프링을 진행하게되면서 단위테스트만 진행했던 1레벨와는 다른 고민이 생기는 것 같아요.
  • 가장 마음 편한건 @SpringBootTest를 사용해서 스프링 컨텍스트를 모두 올리기 때문에 실제 환경과 완전히 비슷한 환경에서 진행하는 방식이긴해요.
  • 다만 말한대로 스프링 컨텍스트를 모두 올리기 때문에 실제 서버를 올릴때와 마찬가지로 로딩 시간이 오래걸린다라는 문제가 있는거죠. 오래 걸리는 이유조차도 굳이 필요없는 컨텍스트도 같이 올라오니까 더 문제가 된다라고 생각하구요.
  • 그래서 크게보았을때 Controller, Service, Dao에 대해서 어떤 테스트를 진행해야하는지 고민을 했었어요. 테스트를 조금 더 효율적으로 할 수 있는 방법이 없을까하구요.
  • 개인적으로 정리한건 아래와 같았는데요.
controller - 통합 테스트
service - 단위 테스트
dao - JdbcTest 슬라이싱 테스트
  • Dao의 경우 다른 기능이 전부 필요없이 Jdbc에 관련된 빈들만 필요하다라고 생각했어요. 또한 DB와 가장 밀접하여 존재하는 객체이기 때문에 DB에 대한 의존성없이 단위테스트만 진행하는 것에 대한 의미를 찾지 못했어요. 실제 DB에 접속해서 테스트가 잘되는지를 테스트해야 Dao에 대한 안정성을 보장한다라고 생각했었기 때문이에요.

  • Controller의 경우 어떻게보면 Dao랑 마찬가지로 @WebMvcTest 슬라이싱 테스트를 고민을 했었는데요. 결국은 아무리 슬라이싱테스트를 진행한다라고하더라도 실제 환경이랑 완전히 유사한 통합테스트가 필요하다라고 생각했고, 거기에 가장 적합한 것이 외부와 가장 밀접하게 기능이 추상화된 controller라고 생각했어요. 실제 사용자가 사용하는 관점에서 테스트할 수 있는 계층인거죠.

  • 아직 인수테스트나 통합테스트의 범위가 어디까지인지는 미션을 진행하면서 또 고민해봐야할 문제이고, 컨텍스트 로딩이 많아질수록 더 느려진다는걸 감안하면 슬라이싱 테스트를 생각하겠지만 지금 범위 내에서는 @SrpingBootTest까지도 충분하다고 생각했어요.

  • Service는 지금도 많이 고민되는데요. 결국 각각의 계층에 비해서 Service는 연결다리로써의 역할만 한다라고 생각했어요.

  • Controller는 웹과의 통신, Dao는 DB와의 연결, 그리고 도메인은 각각의 비즈니스 로직을 갖는데에 비해 Service는 단순히 필요한 도메인 객체들을 호출해서 기능을 사용했었거든요. 그래서 Service는 Spring에 종속적이기보다는 객체들을 에러없이 잘 호출하고 있는지만 테스트하면 되지 않을까라는 생각이에요.

  • 현재는 아직 익숙치 않아서 @JdbcTest를 사용해서 의존관계를 맺어두었는데 앞서 말한 내용대로라면 Service는 Spring에 대한 의존성 없이 단위테스트로 두고, Dao에 대한 의존성 주입은 실제 Dao가 아닌 Fake 혹은 Mock Dao를 두어서 Service에 대한 연결 기능에 대한 테스트만 진행하는게 좋을 것 같다라는게 제가 내린 결론이었어요.

  • 조금 장황하게 이야기한 것 같은데... 소니는 어떤식으로 통합, 단위테스트를 진행하시는편이신지 궁금합니다 :)

감사합니다 🙇‍♂️

@sonypark
Copy link

sonypark commented Apr 28, 2022

안녕하세요 소니 :)

해보고 싶은 부분들은 많지만 욕심에 비해서 공부를 하는 속도가 느리다보니 아직까지 부족한게 많아보이는 이번 미션인 것 같아요 😢 스프링 사용해보기가 이번 미션의 목적인만큼 그래도 기본에 충실하자라는 마음으로 미션을 진행했습니다 :)

개발하면서 느꼈던 내용 comment 더해서 풀리퀘 드려요 😄

1. 이전 피드백

  • 금방 머지가 되어버려서 코멘트는 이전 pr에 담아두었어요.
  • 자바때도 그랬지만 "왜" 쓰게되었는지를 계속해서 머릿속에 되뇌이면서 생각할 수 있었던 좋은 시간이었던 것 같습니다 :)

2. 통합 ? 단위?

  • 스프링을 진행하게되면서 단위테스트만 진행했던 1레벨와는 다른 고민이 생기는 것 같아요.
  • 가장 마음 편한건 @SpringBootTest를 사용해서 스프링 컨텍스트를 모두 올리기 때문에 실제 환경과 완전히 비슷한 환경에서 진행하는 방식이긴해요.
  • 다만 말한대로 스프링 컨텍스트를 모두 올리기 때문에 실제 서버를 올릴때와 마찬가지로 로딩 시간이 오래걸린다라는 문제가 있는거죠. 오래 걸리는 이유조차도 굳이 필요없는 컨텍스트도 같이 올라오니까 더 문제가 된다라고 생각하구요.
  • 그래서 크게보았을때 Controller, Service, Dao에 대해서 어떤 테스트를 진행해야하는지 고민을 했었어요. 테스트를 조금 더 효율적으로 할 수 있는 방법이 없을까하구요.
  • 개인적으로 정리한건 아래와 같았는데요.
controller - 통합 테스트
service - 단위 테스트
dao - JdbcTest 슬라이싱 테스트
  • Dao의 경우 다른 기능이 전부 필요없이 Jdbc에 관련된 빈들만 필요하다라고 생각했어요. 또한 DB와 가장 밀접하여 존재하는 객체이기 때문에 DB에 대한 의존성없이 단위테스트만 진행하는 것에 대한 의미를 찾지 못했어요. 실제 DB에 접속해서 테스트가 잘되는지를 테스트해야 Dao에 대한 안정성을 보장한다라고 생각했었기 때문이에요.
  • Controller의 경우 어떻게보면 Dao랑 마찬가지로 @WebMvcTest 슬라이싱 테스트를 고민을 했었는데요. 결국은 아무리 슬라이싱테스트를 진행한다라고하더라도 실제 환경이랑 완전히 유사한 통합테스트가 필요하다라고 생각했고, 거기에 가장 적합한 것이 외부와 가장 밀접하게 기능이 추상화된 controller라고 생각했어요. 실제 사용자가 사용하는 관점에서 테스트할 수 있는 계층인거죠.
  • 아직 인수테스트나 통합테스트의 범위가 어디까지인지는 미션을 진행하면서 또 고민해봐야할 문제이고, 컨텍스트 로딩이 많아질수록 더 느려진다는걸 감안하면 슬라이싱 테스트를 생각하겠지만 지금 범위 내에서는 @SrpingBootTest까지도 충분하다고 생각했어요.
  • Service는 지금도 많이 고민되는데요. 결국 각각의 계층에 비해서 Service는 연결다리로써의 역할만 한다라고 생각했어요.
  • Controller는 웹과의 통신, Dao는 DB와의 연결, 그리고 도메인은 각각의 비즈니스 로직을 갖는데에 비해 Service는 단순히 필요한 도메인 객체들을 호출해서 기능을 사용했었거든요. 그래서 Service는 Spring에 종속적이기보다는 객체들을 에러없이 잘 호출하고 있는지만 테스트하면 되지 않을까라는 생각이에요.
  • 현재는 아직 익숙치 않아서 @JdbcTest를 사용해서 의존관계를 맺어두었는데 앞서 말한 내용대로라면 Service는 Spring에 대한 의존성 없이 단위테스트로 두고, Dao에 대한 의존성 주입은 실제 Dao가 아닌 Fake 혹은 Mock Dao를 두어서 Service에 대한 연결 기능에 대한 테스트만 진행하는게 좋을 것 같다라는게 제가 내린 결론이었어요.
  • 조금 장황하게 이야기한 것 같은데... 소니는 어떤식으로 통합, 단위테스트를 진행하시는편이신지 궁금합니다 :)

감사합니다 🙇‍♂️

네 아주 좋은 고민이네요. 저도 테스트 관련한 부분이 아직도 가장 어렵고 고민되는 부분인데요.

  • 통합 테스트 vs 단위 테스트
  • 값 검증vs 행위(verify) 검증
  • mock vs 실제 객체

위와 같은 고민은 항상 하고 있고 그에 따라 현재는 아래와 같은 선택을 하고 있습니다.

  • 단위 테스트는 비즈니스 로직이 있는 Entity, DTO, Utils에서 잘 작성해놔야 한다. → 값에 대한 검증
  • core 부분의 단위테스트가 잘 작성 되어 있으면 그걸 가져다 쓰는 서비스 단에서는 mocking으로 호출여부(verify)만 판단해도 테스트가 가능하다. → 행위에 대한 검증
  • 내부에 단위테스트가 잘 작성되어 있으면 컨트롤러에서는 정상 경우 응답, 비정상인 경우 응답에 대한 검증 테스트만 하면 된다.

그런데 서비스 테스트에서 저장소를 Mocking 할지에 대한건 더 깊이 고민을 해봐야 할 것 같습니다.
관련해서 참고할만한 링크를 남길테니 읽어보시고 오리의 생각도 공유해주세요🙏🏻

Copy link

@sonypark sonypark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오리 안녕하세요.
2단계 미션 잘 구현해주셨네요!
테스트 코드도 꼼꼼하게 잘 작성해주신게 인상적이었습니다!
그리고 테스트 코드를 작성하며 생긴 고민과 오리의 생각을 잘 정리해주셔서 좋았습니다.
해당 부분에 대해 코멘트 남겼으니 확인해주세요~ (테스트에 대한 오리의 생각을 듣고 싶어 아직 merge는 하지 않을게요!😁)
(+ 현재 체스말을 움직이면 화면이 깜박이는데 왜 그런걸까요? 움직일때마다 화면을 재로드 해오는건가요?)

Comment on lines +27 to +29
long chessGameId = chessGameRoomService.createNewChessGame(request.toNewChessGameRoom());
return ResponseEntity.created(URI.create("/chessgames/" + chessGameId)).build();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP 규약을 꼼꼼히 보신게 느껴지네요! 201 응답 👍

Comment on lines +5 to +22

public class ChessGameRoom {

private final Long id;
private final String title;
private final String password;
private final Turn turn;

public ChessGameRoom(Long id, String title, String password, Turn turn) {
Objects.requireNonNull(title, "title은 null이 들어올 수 없습니다.");
Objects.requireNonNull(password, "password는 null이 들어올 수 없습니다.");
Objects.requireNonNull(turn, "turn은 null이 들어올 수 없습니다.");
this.id = id;
this.title = title;
this.password = password;
this.turn = turn;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB에서 ChessGameRoom을 조회해서 도메인 로직을 수행하는 객체를 만드셨네요 👍

Comment on lines +17 to +40

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ChessGameRoomControllerTest {

@Autowired
private ChessGameDao chessGameDao;

@LocalServerPort
private int port;

@BeforeEach
void setUp() {
RestAssured.port = port;
}

@Test
@DisplayName("모든 체스방의 리스트 반환")
void findAllChessGameRoom() {
RestAssured.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when().get("/chessgames")
.then().log().all()
.statusCode(HttpStatus.OK.value());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨트롤러 테스트까지 꼼꼼하게 작성해주신 것 좋네요💯

@jinyoungchoi95
Copy link
Member Author

안녕하세요 소니 :)

주셨던 코멘트에 대한 제 생각 comment 더해서 풀리퀘 드려요 😄

피드백 주셨던 내용과 공유해주신 글, 그리고 관련해서 여러 글을 보고 고민했던 부분들을 조금이나마 다시 정리할 수 있었어요 :)

사실 아래 부분에 대해서는 이견이 없는 것 같아요.

  • domain과 같은 비즈니스 로직이 담긴 객체들은 단위 테스트를 통해 안정적인 객체를 보장한다.
  • Dao의 관심사는 DB와 잘 연결되며 쿼리를 잘 날려보냈는지이다. 단위테스트만으로는 의미가 없고 슬라이싱 테스트를 통해서 실제 DB와 연결을 했을때만이 안정적인 Dao를 보장한다.
  • Controller는 웹과의 통신이 원할한지를 확인해야한다. 인수테스트를 할 수 있는 웹의 end 계층이고, 통합 테스트를 통해 실제 만든 코드들이 전체적으로 잘 동작하는지 확인해야한다.

그럼 문제 상황은 다시 Service는 어떻게 할 것이냐로 돌아오는데요.

총 4가지로 분류할 수 있을 것 같아요.

  • 통합 테스트 (@SpringBootTest)
  • 슬라이싱 테스트 (@JdbcTest, @DataJpaTest)
  • Mock 테스트
  • Fake 테스트

1. 통합 테스트

  • 장점
    • 실제 환경에 대한 테스트
    • 모든 의존성을 받아오기 때문에 실제 환경과 가장 근접하게 테스트할 수 있다.
  • 단점
    • 가장 느리다
일단 통합 테스트는 아예 고려점이 안되는 것 같아요.
“테스트는 실제 상황에 대한 커버를 해주어야한다”기 때문에 실제 환경에 가장 흡사한 통합 테스트가 좋아보일 때도 있어요.

하지만 의미없는 시간을 날리는 경우가 너무 많고,
통합 테스트가 아니더라도 실제 환경과 유사한 상황을 만들 수 있기 때문이에요.

controller에서 통합테스트를 해준다라는 상황을 가정하고 있기도 하구요.

2. 슬라이싱 테스트

  • 장점
    • 통합 테스트보다 필요한 의존성만 불러오기 때문에 완전히 느리지는 않다.
    • 필요한 의존성에 대해 받아오기 때문에 실제 환경과 근사하다고 볼 수 있다.
  • 단점
    • jdbc 관련된 빈을 제외하고는 스프링에서 제공하는 슬라이싱 테스트가 따로 없다. 따라서 외부 의존성이 다르게 필요할 경우 슬라이싱 테스트로만으로는 해결 불가능하다.
Service는 이전에도 말씀드렸던 것처럼 
"요청을 받고, 필요한 도메인을 db에서 불러와서, 도메인에게 메시지를 보내 필요한 행위를하고, 결과를 반환하는"
역할을 가지고 있다라고 생각해요.

물론 실제 DB를 가져와서 행위에 대한 검증을 해도 좋다라고 할 수도 있지만,
행위에 대한 검증은 굳이 DB 연결에 대한 의존성을 받아오지 않아도 할 수 있다고 생각해요.

그게 아래에서 이야기할 Fake 혹은 Mock 테스트라는 대체품이 있기 때문이기도하구요.

또한 단점에도 적은 것처럼 결국 단순한 서비스가 아닌 경우 또다른 외부 의존이 걸린 객체가 필요해지고,
스프링에서 제공하는 슬라이싱 테스트로는 한계점이 있다라고 생각하게 되었어요.

3. Mock vs Fake

그럼 필요한 의존성 주입을 어떤 객체로 하느냐에 대한 건 이 두 방법에 갈리게 되었어요.

Mock의 경우 사실 예전에 한참 단위테스트에 미쳐(?)있을 때 모든 테스트를 단위테스트로 커버리지를 채우고자했을 때 사용했던 방법이었어요 ㅎㅎ….
필요한 컨텍스트들을 불러오지도 않고, 단순하게 내가 원하는 행위만을 동작시키게끔 정의를 하면 필요한 테스트에 대해서 단위테스트를 손쉽게 만들 수 있었거든요. 당시에는 Spring에서 TDD를 하기 위한 최적화된 방법이라고 생각했어요.
다만 프로젝트를 진행하면서 결국 갖가지 문제점이 있어서 억지로 사용했던 경험이 있는데 이 기회에 여러가지 글을 보면서 생각을 조금 정리했어요.

  1. 다른 객체의 구현 내용(행위)에 대해서 알아야한다.
결국 mocking된 객체가 어떤 행위를 할 때 `어떤 값을 받으면 어떤 값을 반환해야한다`라는 어떻게보면 구현체 내용까지 정의해야해요.

저는 현재 테스트에서 Service에 대한 행위만 궁금하고,
다른 객체는 협력관계를 통해 Service와 연결되어있어야하는데 다른 객체의 구현 내용을 알아야하기 때문에
Service만을 위한 테스트는 아니게 되어버리는 문제가 발생합니다.

또한 mocking된 해당 객체의 구현 내용이 변경될 경우 mock한 행위를 또 고쳐줘야하기 때문에
변경점에 대한 전파가 관련되지 않은 테스트까지 영향이 가는 문제가 발생해요.
  1. 너무 추상적이기 때문에 실제 행위를 반영한다라고 믿기가 어렵다.
프로젝트를 진행했을 때 가장 크게 와닿았던 문제점이에요.
결국 구현체없이 내가 생각한대로 예상결과를 반환하다보니 실제 구현 내용에서는 예상과는 다르게 결과를 반환할 수가 있어져버려요.
그래서 테스트는 통과했지만 실제 동작시에는 에러가 발생하는 기이한 구조를 가졌었던 경험이 있어요.
객체의 mock행위 자체를 믿을 수 있다라는 근거 또한 없구요.

남은 방법은 이제 Fake객체 한가지 입니다.

실제 환경은 아니지만 실제 환경과 최대한 맞게 구현할 수 있는 테스트 방법이에요. 개발자가 구현한 내용이 실제 환경과 최대한 맞다면 mock과는 다르게 믿을 수는 있거든요.
위에서 이야기했던 실제 행위를 반영하는 것을 믿을 수 없다라는 단점 또한 Dao에 대한 테스트가 있을 것이니 Fake객체 또한 해당 Dao 테스트를 똑같이 반복해서 테스트 그린을 만든다면 믿을 수 있는 객체가 될 수 있다라고 생각합니다. 구현 내용에 대해서도 Service는 이제 알지 않아도 되기도 하구요.

정리를 하자면 이런 내용들때문에 대부분의 상황에서는 Fake를 고려하게 될 것 같아요. 테스트는 “속도”보다는 “실패 케이스에 대한 보장”이 먼저이긴 하지만 Fake도 충분히 이야기한것을 바탕으로 실패 케이스에 대한 보장을 해주고, Service는 연결과 로직에 대한 테스트만 해주면되기 때문에 통합테스트하기 아깝기도 하구요.

다만 위에도 이야기한 만약 Fake 객체도 만들기 힘든 상황이라면 어쩔수 없이(ㅜㅜ) 해당 객체에 대해서만 Mocking을 할 것 같아요.

감사합니다 🙇‍♂️

Copy link

@sonypark sonypark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오리 안녕하세요.
테스트 관련해서 오리의 생각을 잘 정리해주셨네요!💯
앞으로의 미션에서 오리가 생각한 기준대로 테스트를 작성해보면 좋을 것 같아요~
이번 미션은 여기서 Approve 하겠습니다. 하나 더 선택 미션을 드리자면, 현재 구현해주신 api 이외의 경로로 조회 요청이 들어왔을 때에 대한 처리는 안 되어 있는 것 같은데요. 404 ERROR에 대한 처리를 어떻게 할지, 커스텀 페이지를 어떻게 보여줄지에 대한 고민과 적용도 한 번 해보시면 좋을 것 같아요 :) 이 부분은 다음 미션을 진행할 때 적용하셔도 됩니다. 이번 미션도 수고 하셨어요! 👍

@sonypark sonypark merged commit 249ad21 into woowacourse:jinyoungchoi95 Apr 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants