Skip to content

[그리디] 김하은 SpringCore 7~9단계 제출합니다. - #273

Open
haeun92e0 wants to merge 103 commits into
next-step:haeun92e0from
haeun92e0:final9
Open

[그리디] 김하은 SpringCore 7~9단계 제출합니다.#273
haeun92e0 wants to merge 103 commits into
next-step:haeun92e0from
haeun92e0:final9

Conversation

@haeun92e0

@haeun92e0 haeun92e0 commented Jul 26, 2026

Copy link
Copy Markdown

🙋‍♂️ 인사
수민님, 안녕하세요! 그리디 4기 김하은입니다. 저번 미션때 너무 번거롭게 해드린 것 같아 마음이 좋지 않습니다😥 그만큼 이번 미션 리뷰에서 열심히 공부하고 리뷰 반영하고 리팩토링해보겠습니다!!
리뷰 남겨주셔서 감사합니다! 부족한 점이 많겠지만 잘 부탁드립니다!

📝 미션 내용 설명
7단계: @configuration 기반 빈 설정

  • JwtUtils에서 @component 어노테이션을 제거하고, auth 패키지로 역할을 분리했습니다.

  • 별도의 AuthConfig 클래스를 생성하여 @configuration@bean을 통해 수동으로 빈을 등록하도록 수정했습니다.

  • LoginMemberArgumentResolver에서 토큰 검증 후 매번 MemberRepository.findById()를 호출해 DB를 조회하던 방식을 개선했습니다.

  • JWT Claim에 저장된 sub(id)와 role 정보만 활용하여 new Member(id, role) 객체를 직접 생성 및 반환하도록 수정했습니다.

8단계: Profile과 Resource 분리

  • application.properties 파일에 JWT 비밀키(roomescape.auth.jwt.secret)를 분리하여 외부에서 관리하도록 설정했습니다.

  • SQL INSERT 구문 대신 CommandLineRunner 기반의 DataLoader 클래스들을 구현했습니다.

  • DataLoader: 기본 환경(!test)에서 회원 및 기초 데이터 초기화

  • TestDataLoader: 테스트 환경(test)에서 테스트 동작에 필요한 사전 데이터 초기화

9단계: 배포 스크립트 작성

🤔 고민한 내용

  1. 기존에는 schema.sql이나 data.sql로 데이터를 넣었는데, CommandLineRunner를 활용한 자바 코드로 초기화해 보면서 실행 시점에 따른 데이터 생성 순서나 외래키 연관관계를 고려해야 한다는 점을 배웠습니다.

  2. 테스트를 돌릴 때 기존 운영 데이터와 충돌이 일어나 실패하는 문제를 경험했습니다. @Profile("test")와 @activeprofiles("test")를 활용해 테스트용 프로파일을 따로 분리했고, 그 결과 환경의 영향을 받지 않고 테스트를 안정적으로 통과시킬 수 있었습니다.

❓ 질문사항

  1. 초기 데이터를 구성할 때 data.sql 방식과 CommandLineRunner를 이용한 자바 코드 저장 방식 중, 실제 프로젝트나 실무에서는 어떤 방식을 더 선호하거나 권장하는지 궁금합니다!

  2. 이번 미션에서는 DB 접근을 줄이는 것이 항상 좋은 방향일지에 대한 고민을 했던 것 같습니다. DB 조회를 줄이면 성능상 장점은 있지만, 코드가 조금 복잡해지는 경우도 있는 것 같습니다. 실제로 이런 상황에서 개발을 할 때 성능 최적화와 코드 단순성 중 어떤 것을 택하는지, 기준이 있는지 궁금합니다.

  3. 이번 미션에서는 ArgumentResolver에서 매 요청마다 MemberRepository를 조회하지 않고, JWT Claim에 담긴 id와 role 정보만으로 Member 객체를 생성하도록 변경했습니다. 성능 측면에서는 DB 조회가 없어지는 장점이 있다고 생각했는데, 반대로 회원 권한이 변경되거나 탈퇴한 경우처럼 DB와 JWT 정보가 달라질 수도 있을 것 같다는 생각도 들었습니다. 보통 어느 정도까지 JWT 정보만 신뢰하고 사용하는 편인지, 아니면 매 요청마다 DB 조회를 하는 경우도 있는지 궁금합니다!

  4. 배포 스크립트를 어떻게 작성하는건지 아직 잘 모르겠습니다ㅠㅠ 제가 제대로 작성한게 맞는지도 잘 모르겠습니다. 한번 피드백 부탁드립니다!!

haeun92e0 added 30 commits June 29, 2026 17:57

@boyekim boyekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

안녕하세요 하은님!~ 처음 만나는 것 같네요!!!
양해 구해주셔서 감사합니다 ㅎㅎ
함께 화이팅해봐요!!!
우선 리뷰 포인트인데 이번 pr에서 변경이 없었어서 file change되지 않은 것은 여기에 작성해두겠습니다.

ReservationController에서 경로를 포함해서 선언된 것을 볼 수 있었습니다. dao에서 변경하는 과정에서 생긴 일 같네요~ import를 사용해주세요. 다른 부분도 이렇게 되어있는 경우가 있다면 정리해주세요.

private final roomescape.theme.ThemeRepository themeRepository;

대기를 생성하는 기능을 생각해봅시다. Waiting 도메인에서, 사용자가 date, time, theme, id를 요청보내서 대기 생성 요청을 보냈습니다. 하지만 이게 존재할 경우, isAlreadyWaiting이 false임을 확인하는 테스트를 작성해주세요.

잘 테스트가 작성 되시나요? 테스트 작성하기 어려웠다고 느끼셨나요? 하은님 경험 공유를 우선 해주세요. 그리고 이어서 코멘트 남기겠습니다~

Repository 코드에서 대개 코드 길이 컨벤션이 지켜지지 않고 있네요.

@Query("""

""")

텍스트 블록을 활용해봅시다.


기존에는 schema.sql이나 data.sql로 데이터를 넣었는데, CommandLineRunner를 활용한 자바 코드로 초기화해 보면서 실행 시점에 따른 데이터 생성 순서나 외래키 연관관계를 고려해야 한다는 점을 배웠습니다.

sql로 생성하더라도 외래키 연관관계를 고려해야합니다! sql 파일은 순서와 관계 없을 것이라고 생각하실까봐 정정해봅니다~


초기 데이터를 구성할 때 data.sql 방식과 CommandLineRunner를 이용한 자바 코드 저장 방식 중, 실제 프로젝트나 실무에서는 어떤 방식을 더 선호하거나 권장하는지 궁금합니다!

둘이 선택하게 되는 상황이 다르긴 할 것 같아요.
프로젝트를 하다보면 DB 마이그레이션 도구(flyway 등)를 쉽게 접하게 될텐데 이때에는 코드로 제어하는 것이 아닌 파일로 제어합니다.
저라면 CommandLineRunner는 암호화를 적용하여 비밀번호를 만드는 등, sql로 처리하기 복잡한 상황을 구현할 때 쓰게될 것 같아요. 하지만 CommandLineRunner는 아무래도 자바에만 국한된 구현이라는 점도 염두에 둬야겠죠?!
간단한 데이터 삽입은 보통 sql파일을 씁니다. 하지만 CommandLineRunner로 운영 db에 값을 넣는다던가, 운영 환경에서 sql을 애플리케이션 시작마다 실행하는 것은 잘 쓰지 않아요. 재배포할때 db가 오염될 가능성이 있기 때문입니다!!


이번 미션에서는 DB 접근을 줄이는 것이 항상 좋은 방향일지에 대한 고민을 했던 것 같습니다. DB 조회를 줄이면 성능상 장점은 있지만, 코드가 조금 복잡해지는 경우도 있는 것 같습니다. 실제로 이런 상황에서 개발을 할 때 성능 최적화와 코드 단순성 중 어떤 것을 택하는지, 기준이 있는지 궁금합니다.

너무 좋은 고민이네요!!! 이런 고민을 하게 된 상황이 더 듣고싶어요. 어떤 때에 코드가 복잡하다고 느껴졌나요? 둘이 항상 정반대의 상황이라고 느껴졌나요?
뭐든 트레이드오프를 고민하면서 더 나아보이는 선택지를 선택하기 때문에, 하은님의 상황을 더 듣고 고민해보고싶네요~


이번 미션에서는 ArgumentResolver에서 매 요청마다 MemberRepository를 조회하지 않고, JWT Claim에 담긴 id와 role 정보만으로 Member 객체를 생성하도록 변경했습니다. 성능 측면에서는 DB 조회가 없어지는 장점이 있다고 생각했는데, 반대로 회원 권한이 변경되거나 탈퇴한 경우처럼 DB와 JWT 정보가 달라질 수도 있을 것 같다는 생각도 들었습니다. 보통 어느 정도까지 JWT 정보만 신뢰하고 사용하는 편인지, 아니면 매 요청마다 DB 조회를 하는 경우도 있는지 궁금합니다!

결론을 말하자면 'access token 만료' 시간까지 신뢰합니다.
토큰 로그인과 세션 로그인에 대해서 학습해보면 너무 좋을 것 같아요.(인증/인가에 대해서 학습해봅시다~)

access token 만료 이전에 권한이 변경되거나 탈퇴하게 되는 경우도 있을 수 있어요!!!
만약, 결제와 관련된 민감한 사항인데, 토큰 만료 이전에 이미 탈퇴한 사용자가 결제 요청을 보내면 이는 막아줘야겠지요? 그럴때는 토큰을 100퍼센트 신뢰하기보다는 한번 더 사용자 상태를 체크해줘야겠네요. 즉, 토큰 구현으로는 토큰 만료 시간까지는 신뢰하게 되니, 꼭 체크해줘야하는 상황에서는 따로 상태 체크가 필요할 수 있다는 말이에요. 뭐든 어떤 역할인지 알고 상황에 맞게 사용해주시면 됩니다 ㅎㅎ


이미 코멘트 볼륨이 좀 있을 것 같아, 배포 스크립트 관련해서는 1차 리뷰 답변 이후 이어가면 좋을 것 같아요!

Claims claims = jwtUtils.parseToken(token);
Long id = Long.parseLong(claims.getSubject());
String role = claims.get("role", String.class);
return new Member(id, role); // 필요한 id와 role만 가진 객체 반환

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

현재 로그인한 회원도 예약 생성이 되지 않습니다. 왜일까요?
해당 부분을 시작으로 한번 찾아봅시다~!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

앗... new Member(id, role) 객체를 직접 생성했더니, name이나 email이 null 상태가 되어 예약 생성 시 에러가 발생한 것 같습니다.
LoginMemberArgumentResolver에 MemberRepository 의존성을 다시 주입받아 DB를 통해 Member 엔티티를 반환하도록 수정했습니다.
감사합니다!

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver(jwtTokenProvider, memberRepository));
resolvers.add(new LoginMemberArgumentResolver(jwtUtils));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Component로 관리되고 있는 LoginMemberArgumentResolver를 new로 새로 생성할 필요가 있을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

오마이갓 수정했습니다!! 감사합니다!!

@haeun92e0

haeun92e0 commented Jul 29, 2026

Copy link
Copy Markdown
Author

대기를 생성하는 기능을 생각해봅시다. Waiting 도메인에서, 사용자가 date, time, theme, id를 요청보내서 대기 생성 요청을 보냈습니다. 하지만 이게 존재할 경우, isAlreadyWaiting이 false임을 확인하는 테스트를 작성해주세요.

잘 테스트가 작성 되시나요? 테스트 작성하기 어려웠다고 느끼셨나요? 하은님 경험 공유를 우선 해주세요. 그리고 이어서 코멘트 남기겠습니다~

말씀해주신 부분 테스트를 작성하려고 했는데, Waiting 도메인에서는 자기 자신의 정보만 알 수 있고 실제 중복 여부는 WaitingRepository를 조회해야 알 수 있어서 말씀해주신 상황을 어떻게 테스트 코드로 짜야할 지 감이 잘 오지 않는 것 같습니다..

그래서 현재 커밋한 테스트 코드는 Waiting의 isSameWaiting()을 테스트하는 걸로 짰는데 수민님이 원하시는 방향이 아닌 것 같다는 생각이 듭니다. 어떻게 리팩토링 하면 좋을지 조언 부탁드립니다!

이번 미션에서는 DB 접근을 줄이는 것이 항상 좋은 방향일지에 대한 고민을 했던 것 같습니다. DB 조회를 줄이면 성능상 장점은 있지만, 코드가 조금 복잡해지는 경우도 있는 것 같습니다. 실제로 이런 상황에서 개발을 할 때 성능 최적화와 코드 단순성 중 어떤 것을 택하는지, 기준이 있는지 궁금합니다.

너무 좋은 고민이네요!!! 이런 고민을 하게 된 상황이 더 듣고싶어요. 어떤 때에 코드가 복잡하다고 느껴졌나요? 둘이 항상 정반대의 상황이라고 느껴졌나요? 뭐든 트레이드오프를 고민하면서 더 나아보이는 선택지를 선택하기 때문에, 하은님의 상황을 더 듣고 고민해보고싶네요~

이번 7단계 미션을 진행하면서 LoginMemberArgumentResolver 로직을 수정할 때 DB 접근에 대한 생각을 했던 것 같습니다. 이전에는 memberRepository.findById(id)로 회원 엔티티를 조회해 넘겨주는 방식이었는데 이번에는 DB 조회를 줄이기 위해서 JWT claim에 담긴 id와 role로 member 객체를 생성해 반환하도록 변경했습니다.

이 과정에서 DB를 조회하지 않는 대신에 코드가 길어졌다는 생각이 들었고 이런 상황이 DB를 조회하는 방식과 DB 조회를 더는 방식이 서로 장단점이 분명해 개발을 할 때의 중요한 선택지처럼 느껴졌습니다! 그래서 수민님의 의견이 궁금해 질문을 남겼습니다!

@boyekim boyekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

말씀해주신 부분 테스트를 작성하려고 했는데, Waiting 도메인에서는 자기 자신의 정보만 알 수 있고 실제 중복 여부는 WaitingRepository를 조회해야 알 수 있어서 말씀해주신 상황을 어떻게 테스트 코드로 짜야할 지 감이 잘 오지 않는 것 같습니다..
그래서 현재 커밋한 테스트 코드는 Waiting의 isSameWaiting()을 테스트하는 걸로 짰는데 수민님이 원하시는 방향이 아닌 것 같다는 생각이 듭니다. 어떻게 리팩토링 하면 좋을지 조언 부탁드립니다!

이에 대해서, Service 계층 도입을 우선 해봅시다. 그리고 메서드의 역할분리도 한번 해볼까요?

이 과정에서 DB를 조회하지 않는 대신에 코드가 길어졌다는 생각이 들었고 이런 상황이 DB를 조회하는 방식과 DB 조회를 더는 방식이 서로 장단점이 분명해 개발을 할 때의 중요한 선택지처럼 느껴졌습니다! 그래서 수민님의 의견이 궁금해 질문을 남겼습니다!

둘의 판단 근거는 '코드 길이'가 되면 안될 것 같아요!
말씀주신 상황에서 근거는 '해당 요청에서 최신 회원 정보가 필요한가'일 것 같아요.
예를 들어 단순히 로그인 사용자의 아이디만 필요하다면 JWT claim의 id를 활용해 조회를 생략할 것 같아요. 하지만 이름, 탈퇴 여부, 현재 권한처럼 DB의 최신 상태가 필요한 요청은 조회가 필요할 것 같습니다!


추가 리뷰

배포스크립트에서, 실패해도 배포 절차가 계속 진행될 것으로 보여요~
git pull 또는 빌드 실패 후에도 이전 프로세스를 종료하고 배포를 계속할 수 있을 것 같아요. set -euo pipefail이나 pgrep ... || true 같은 명령어를 한번 추가해볼 수 있을 것 같네요(명령어 한번 재확인해주세요 ㅎㅎ...)

sleep 5를 하는 이유가 어떤 것일까요? 포트 충돌의 가능성은 없을까요?

Comment on lines +32 to +33
private final MemberRepository memberRepository;
private final ThemeRepository themeRepository;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

추가로, 모든 Controller에서 Respository를 의존하고 있는데요,
Controller의 역할을 어떤 것으로 생각하고 계신가요?
Controller와 Service, Repository의 역할을 말씀해주세요.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

아직 계층별로 알맞게 분리하여 코드를 짜는 것에 익숙하지 못한 것 같습니다. 컨트롤러는 HTTP 요청을 받아 service로 전달한 뒤 결과를 HTTP 응답 형태로 변환하여 반환하는 역할입니다. Service는 비지니스 로직을 수행하는 계층으로 서비스 계층입니다. Repository는 데이터베이스에 접근하여 데이터 CRUD를 담당합니다.

Controller가 Service만 의존하도록 수정하여 리팩토링 하겠습니다!

@haeun92e0

Copy link
Copy Markdown
Author

sleep 5를 하는 이유가 어떤 것일까요? 포트 충돌의 가능성은 없을까요?

사실 아직 배포 스크립트의 문법을 잘 숙지하지 못해 스터디에서 배포를 해보면서 사용했던 스크립트를 그대로 썼습니다! 공부해보니, sleep 5는 애플리케이션이 정상적으로 종료될 시간을 주기 위해 사용하는 것인데 말씀해주신 것처럼 종료가 5초 이상 걸리면 새 애플리케이션이 먼저 실행되어 포트 충돌이 발생할 수도 있는 것 같습니다. 그래서 sleep으로 시간을 고정하는 방식말고 프로세스가 실제로 종료되었는지 확인한 다음 작업을 진행하도록 개선해보겠습니다!

@boyekim boyekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

하은님!
이전 범위에서 말씀드리고 싶었던 것까지 포함하다보니 리뷰가 길어지고있네요..!!

레이어를 분리하며 코드가 훨씬 깔끔해진 것 같아 너무 보기 좋습니다.
Controller에서도 어떤 요청으로 어떤 응답이 가는지 깔끔하게 볼 수 있고, 서비스에서 비즈니스 로직이 분리되어 있어 비즈니스로직이 어떤 것지 훨씬 명확해진 것 같아요.

추가 코멘트 달았으니 확인 부탁드려요! 마지막까지 조금만 더 화이팅해봅시다 ㅎㅎ

Comment on lines +30 to +40
public ReservationService(ReservationRepository reservationRepository,
MemberRepository memberRepository,
ThemeRepository themeRepository,
TimeRepository timeRepository,
WaitingRepository waitingRepository) {
this.reservationRepository = reservationRepository;
this.memberRepository = memberRepository;
this.themeRepository = themeRepository;
this.timeRepository = timeRepository;
this.waitingRepository = waitingRepository;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

대체할 수 있는 어노테이션이 있을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

우왓 이런 어노테이션이 있는지 몰랐습니다! 찾아보니, final 키워드가 붙은 필드들의 생성자 주입 코드를 대체할 수 있는 @requiredargsconstructor 어노테이션이 있어 적용해봤습니다. 감사합니다!

import java.util.List;

@Service
@Transactional(readOnly = true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 readOnly=true 옵션의 역할은 어떤 것일까요!?
다른 메서드에 @Transactional을 붙이신 것도 있고, 아닌 것도 있는데, 어떤 것을 기준으로 구분하셨나요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

readOnly=true 옵션은 기본값을 읽기 전용으로 두고 실수로 데이터를 수정해도 DB에 반영되지 않는 특징을 가지고 있는 것으로 알고 있습니다. 주로 Service 계층에서는 대부분의 메서드가 단순 조회일 가능성이 높다고 생각해 readOnly = true 옵션을 적용했고 데이터 수정이 일어나는 메서드인 경우에는 @transactional만 붙였습니다.

return reservationRepository.findAllWithFetchJoin();
}

@Transactional

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 어노테이션의 역할은 어떤 것일까요?!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

createReservation 메서드는 예약을 생성하는 작업을 해줍니다! @transactional 어노테이션은 회원이나 테마, 시간을 조회 중 예외가 발생하거나 DB 저장 중에 에러가 발생할 경우 메서드 내부를 롤백해줍니다.

List<Time> times = timeRepository.findByDeletedFalse();
List<AvailableTime> result = new ArrayList<>();

for (Time t : times) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

t와 같이 의미 없는 변수명은 지양해주세요!


@Query("SELECT COUNT(w) > 0 FROM Waiting w WHERE w.date = :date AND w.time.id = :timeId AND w.theme.id = :themeId AND w.member.id = :memberId")
boolean existsByDateAndTimeAndThemeAndMemberId(@Param("date") String date, @Param("timeId") Long timeId, @Param("themeId") Long themeId, @Param("memberId") Long memberId);
@Query("""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

쿼리 어노테이션을 사용하신 이유가 있나요?!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

수민님의 리뷰를 받고 제가 쿼리 메서드로 구현 가능한 단순 조회에 @query를 썼다는 것을 알게 되었습니다. 미션 가이드에 있던 JPQL 예시를 보고 무작정 쓴 것 같습니다. 아래 리뷰 코멘트까지 반영하여 불필요한 어노테이션을 제거했습니다!

AND w.member.id = :memberId
""")

boolean existsByDateAndTimeAndThemeAndMember(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

해당 메서드명으로는 어떤 쿼리가 만들어질까요?? JPA의 쿼리 메서드는 어떤 것일까요?!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Spring JPA는 existsByDateAndTimeAndThemeAndMember 키워드와 메서드명의 필드들을 해석해주며 존재 확인 쿼리를 자동으로 생성해줍니다. findWaitingsWithRankByMemberId 메서드에는 쿼리 어노테이션을 남기고 existsByDateAndTimeAndThemeAndMember메서드는 존재 확인 메서드이므로 쿼리 메서드로 변경했습니다.

}

@Transactional
public WaitingResponse createWaiting(ReservationRequest request, Member loginMember) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이제 서비스 레이어를 분리했으니
isAlreadyWaiting이 false인 것에 대한 테스트를 짜볼까요?
Controller 에 이 로직이 존재했다면 보통 HTTP 요청을 보내는 Controller 통합 테스트까지 해야 했을거에요.
하지만 지금은 레이어를 분리한 덕에 Http 요청을 보내지 않아도, 테스트를 작성할 수 있을 것 같네요!
(예외가 발생하는지에 대해 테스트를 작성해볼 수 있을 것 같아요~)

추가로 하은님이 더 필요하다고 느껴지는 테스트를 짜봐도 좋을 것 같네요 ㅎㅎ

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.

2 participants