우아한테크코스 7기/레벨2

[우아한테크코스 7기] 레벨2 미션4(방탈출 결제 / 배포) 회고

seaniiio 2025. 6. 16. 01:20

아주 혼란스러웠고 불안했던 레벨2의 마지막 회고글이다.

 

노랑이 주최한 블로그 포스팅 스터디에 참여하지 않았다면, 레벨2동안 단 한 개의 포스팅도 하지 못했을 것이라 확신한다... (고마워 노랑🐣)

 

레벨2 전체 회고는 별도 포스팅으로 작성할 예정이다 💪

👥 페어 프로그래밍

우테코에서의 마지막 페어는 서프였다. 서프는 똑똑하고, 두뇌 회전 속도가 빠르고, 설명도 잘하고, 착한 페어였다. 누구나 함께 일하고 싶은 개발자라고 생각할 것 같고, 닮고 싶은 부분이 정말 많았다.

 

서프와 함께 공부하는 과정에서, 핵심을 빠르게 찾고 모르는 부분을 파악해서 파고드는 능력을 키우고 싶다고 느꼈다. 그리고 나는 그냥 받아들이고 넘어갔던 부분도 서프는 궁금증을 갖고 여러 사람에게 의견을 물어보곤 했다. 의식적으로 "왜?"라는 질문을 날리면서 학습해야 하는데, 아직은 잘 되지 않는 것 같다.

예를 들어 토스 위젯을 가져올 때 필요한 customerKey와 clientKey의 차이가 뭔지, amount는 토스 서버에도 저장되는데 왜 우리 서버에서도 저장해야 하는지 등.. 그냥 납득하지 말자!!

 

지금까지 항상 페어의 코드에서 시작했어서, 나는 이번에 내 코드에서 시작해보고 싶었는데, 마침 서프도 이번에는 페어의 코드에서 시작하고 싶다고 했다. 덕분에 내 코드에서 시작할 수 있었다. 익숙한 코드라는 장점이 있었지만, 내가 생각보다 이전 미션까지의 코드를 제대로 파악하지 못하고 있었다는 것을 알게 됐다. 왜 이렇게 작성했는지 잘 설명하지 못하기도 했다. '전 페어의 코드니까..' 하고 대충 읽고 넘어갔던 부분이 많았고, 당장 현재 미션에 관련된 코드에만 집중했는데, 코드에 대한 파악은 기본적으로 필요하다는 것을 알게 됐다.

 

이번 페어 프로그래밍도 즐겁게 할 수 있었고, 서프 덕분에 마지막 페어 프로그래밍을 좋은 기억으로 남길 수 있었다. 🌞

🌱 페어 프로그래밍 회고

2월부터 총 8번의 페어 프로그래밍을 했고, 이 과정에서 알게 된 나의 특징을 정리해봤다.

  • 나의 장점은 분위기를 편하게 만들어주는 것이다. 나는 특히 일대일로 대화할 때 더 적극적으로 대화 주제를 찾는 것 같다고 느꼈다. 쉽게 공감하고, 리액션이 익숙한 것이 강점인 것 같다.
  • 주관이 부족하다. "내 생각"이라는 것이 뚜렷하게 잡혀 있지 않다. 혼자 고민하고 결정해 본 경험이 부족하기 때문이라 생각한다. 그리고 이런 약점은 내가 우테코에 들어와서 가장 뼈저리게 느끼고, 가장 고쳐야겠다 생각한 부분중 하나다. 
  • 나는 내 의견을 끝까지 고집하지 못하는데, 주관이 부족한 것과 더불어 "내 생각은 틀렸을 것"이라는 생각이 크기 때문이다. 
  • 눈치를 많이 본다. 그 이유는 다른 사람에 대한 평가가 두렵기 때문이다. 여기엔 자세히 적지 않겠다.

함께 협업하고 싶은 개발자가 되기 위해, 열심히 메타인지하고 행동하자 💪

 

📜 방탈출 결제 / 배포

이번 미션에서는 api를 연동하고, 배포하고, 협업을 위한 문서화를 하고, 오류를 찾기 위한 로깅을 연습했다.

 

본격적으로 "웹 프로젝트"를 만들기 위한 연습을 해본 것 같다.

 

나는 동아리 해커톤에서 외부 api 연동, 배포를 해보긴 했다. 그 당시에는 너무 뿌듯하고 내 힘으로 해낸 것 같고 그랬는데, 지금 와서 생각해보면 그땐 그저 클론코딩을 했던 것에 불과하다 생각한다. 주어진 대로 따라가고, 안 되면 다른 블로그에서 나온 방법 써보고, 정말 하나도 모르고 사용했던 것이라 지금은 하나도 기억나지 않는다.(이해하고 사용하자.. 정말..)

 

그래서 이번 미션에서 주어진 요구사항(api연동, 배포 등)도 처음 시도해보는 것과 마찬가지라 생각했다.

✏️ 외부 api 연동

이번 미션에서는 토스 결제 api를 사용했다.

 

✔️ 문서 읽고 파악

우선 해당 api를 제공해주는 문서를 읽고 동작을 파악했다. 결제 요청 - 결제 승인의 과정이 복잡하다고 느껴졌지만, 제공된 클라이언트 코드에서 상당수가 이미 구현되어 있었고, 내가 작업해야할 부분이 많지는 않았다. 문서에서 제시한 과정을 이해한 후 착실히 수행했다.

 

✔️ 외부 api를 트랜잭션에서 분리하기

처음에는 예약 생성 ~ 결제 승인 api 호출까지 한 트랜잭션 내에서 처리했다. 그런데 외부 api는 요청 - 응답 시간이 비교적 길고, 그 기간 동안 DB connection이 반납되지 않아 고갈 문제가 발생할 수 있다는 것을 알게 됐다. 이후 외부 api 호출부를 분리한 후, 그 부분은 트랜잭션 전파가 되지 않도록 설정해서 api를 트랜잭션에서 분리했다.

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void confirmPayment(TossPaymentConfirmCommand command) {
    PaymentConfirmRequest paymentRequest = new PaymentConfirmRequest(command.orderId(), command.amount(), command.paymentKey());
    tossPaymentClient.confirm(paymentRequest);
}

 

사실 이렇게 하면 분리된다길래 일단 사용해본건데, 트랜잭션 전파에 대해서는 잘 모른다. 방학동안 공부해봐야겠다.

 

✔️ 토스 에러 코드 처리

토스 결제 승인 api는 오류 응답을 내려줄 때 사용하는 다양한 에러 코드가 존재한다.

400	INVALID_REJECT_CARD	카드 사용이 거절되었습니다. 카드사 문의가 필요합니다.
400	INVALID_API_KEY	잘못된 시크릿키 연동 정보 입니다.

 

위의 실패 사유는 사용자에게 알려야 하고, 아래의 실패 사유는 알리지 않아야 한다고 생각했다. "사용자가 메시지를 읽고 대응을 할 수 있는지"를 기준으로 모든 에러 코드를 구분했고, 코드에 대한 Enum을 직접 관리했다.

public enum TossErrorCode {

    NOT_ALLOWED_POINT_USE(true),
    INVALID_API_KEY(false),
    ...   
}

 

다만 이렇게 관리할 경우, 항상 토스 api 에러 코드를 모니터링할 필요가 있고, 유지보수하기 어렵다는 단점이 있다.

 

실제로 관련 리뷰를 받았고, 여기에 완전히 동의한다. 그럼에도 사용자 입장에서 생각하면 이렇게 구분해서 관리하는 것이 최선이라 생각했다. 내가 만약 서비스 사용자인데 "잘못된 시크릿키 연동 정보 입니다"라는 메시지를 받으면 '뭐 어쩌라고..?'라는 생각이 들 것 같았다.

 

✔️ 외부 api 호출 테스트

나는 이번에 MockRestServiceServer를 통해 특정 요청 - 응답에 대해 원하는 시나리오대로 동작하는지 테스트했다. (ex. 토스에서 시크릿키 관련 에러 코드를 보내면, 공통된 메시지로 처리하는지 테스트) 즉 실제 토스 서버에 요청을 보내고 응답을 받아오는 것은 테스트하지 않았다.

 

또한 외부 api를 호출하는 부분은, 내 손을 벗어난 곳이고, 외부 서버의 상황에 따라 내 테스트 코드가 실패할 가능성이 있다는 것부터 문제라는 의견도 들었다.

 

사실 다른 api였으면 테스트했을 것 같다. 그런데 토스 결제 승인 api는, 위젯을 통해 결제 요청을 보내고 그 응답으로 받아온 paymentKey가 필요했다. paymentKey는 10분동안만 유효하고, 테스트 환경에서 위젯을 통해 결제 요청을 보내는 방법을 찾지 못했기 때문에 테스트하지 않았다.

 

그래서 제대로 동작하는지 확인하는 방법은 직접 서버를 띄우고, 예약을 생성하고 결제해보는 방법이었다. 자동 테스트로는 paymentKey가 잘못된 경우에 대해서만 테스트할 수 있다고 파악했는데, 테스트할 수 있는 방법이 있을까? 아직 찾지 못했다..

 

✏️ 배포

우테코에서 제공해준 AWS 계정으로 ec2를 만들고, ec2 서버에 방탈출 서비스를 배포했다. (이 ec2에 캠퍼스가 아니면 접속이 안 됐기 때문에 연휴~주말동안 매일 출근할 수 있었다.. ㅎ)

 

배포 스크립트를 작성해서, 스크립트를 실행했을 때 기존 프로세스를 종료하고, 최신 코드로 업데이트하고, 빌드 후 실행하는 과정을 자동화할 수 있었다.

 

인프라 수업과 강의 자료를 보고, 네트워크 공부의 필요성을 느꼈다. 사실 네트워크 수업은 들었지만 2년 전에 아무것도 모를 때 수강했고, 그냥 암기했어서 머리에 남은 게 없다.

 

배포 피드백 자료에 있는 AZ, VPC, CIDR 등 키워드에 대해서만 아주 얕게 공부하고 넘어갔는데, 방학동안 인프라도 더 공부해봐야겠다.(할 수 있을까?)

 

복학하면 네트워크 포함한 CS 공부도 열심히 하고, 리눅스 프로그래밍 수업도 들어야겠다.

 

✏️ 문서화

내가 생각한 api 문서의 목적은 요청, 응답의 형식을 제공하는 것이었고, 요청 데이터를 기반으로 실제 api 요청을 보내서 비즈니스 로직을 타고 응답하는 과정까지는 필요하지 않다고 판단했다. (대신 오류 상황에 대한 상세한 명세는 필요하다.)

 

그래서 나는 문서화에 대해 swagger, Spring REST docs 두 가지 방법을 고민했다.

 

두 문서화 방법을 비교하고 결론을 내린 과정은 아래와 같다.

 

✔️ swagger

  • 애노테이션 붙여주면 문서를 알아서 잘 만들어줘서 편하다.
  • 자세한 명세를 위해서는 프로덕션 코드에 애노테이션을 붙여줘야 한다는 것이 단점
  • 내가 생각한 “api 명세”의 목적이 그냥 요청 - 응답 형식 명세라서, 실제 로직까지 타버리면 과도할 수 있다.

✔️ Spring REST docs

  • 테스트 코드를 기반으로 문서가 생성되기 때문에 신뢰성이 높고, 프로덕션 코드는 건드리지 않아도 된다.
  • 테스트에 사용한 요청 - 응답 값을 캡쳐해서 그대로 문서에서 사용하기 때문에, 동적으로 요청을 보내지 못한다. api의 스펙을 나타내기 위한 용도
  • 못생겼다

✔️ 결론

  • Spring REST docs를 사용하자. 프론트 입장에서는 명세만 필요하지 않을까? 요청 형식 - 응답 형식. 로직을 탈 필요가 없다. 그리고 프로덕션도 깔끔하게 유지된다.
  • 그리고 마~ 침 MockMvc를 이용한 Controller 테스트가 있었기 때문에… ^^

✔️ 근데...

  • swagger를 사용하지 않은 가장 큰 이유는 프로덕션 코드에 붙는 애노테이션이었다. 비즈니스 로직만 순수하게 존재하는 것이 아니라, 문서화까지 다뤄서 SRP를 위반하게 된다. 그런데 REST docs를 사용해도 테스트 관점에서의 SRP를 지키지 못하는게 아닐까? 테스트에 문서화 과정이 포함되면, 문서화가 실패하면 테스트도 깨진다. 

✏️ 로깅

✔️ 내가 생각한 로깅의 목적

  • 모니터링 → 비정상 요청이 있는지 확인
  • 분석 → 어떤 요청이 많이 들어오는지 확인

✔️ 미션에서 로그 남긴 부분

  • api 요청, 응답에 대한 로그  기술적인 정보에 관한 로그. 어떤 요청이 많이 오는지 분석할 수 있고, 요청 패턴 분석할 수 있음
  • 비즈니스 이벤트 로그  비즈니스적 가치가 높은 로그를 제공, 사용자 패턴을 추적할 수 있음. “이 행위에 대한 로그가 있으면 내 서비스에 대해 분석할 때 좋겠다”싶은 부분. Ex) 회원 가입, 예약 생성, 예약 취소 …
  • 외부 api 사용에 대한 로그 → 내 애플리케이션을 벗어난 곳이니까, 문제 상황을 알기 위해 더 상세히 남기기

✔️ ExceptionHandler에서 로그 처리하기

비즈니스 이벤트 관련 로그는 service에서 남겨주도록 했다. 그리고 예외 상황에도 로그를 남기려 하다 보니 이런 코드가 생기게 됐다.

try {
    order.validateOrderAndPaymentRequest(request.amount(), member, schedule);
} catch (IllegalArgumentException e) {
    log.warn("EVENT: RESERVATION_CREATE_FAILED_ORDER_NOT_MATCH, orderId={}",
            order.getId());
    throw e;
}

 

도메인 엔티티에서 예외가 발생하는데, 로깅을 위해 catch를 하고 다시 그대로 던져주고 있다. 비즈니스 로직도 아니고, 로깅을 위해 서비스 코드에 불필요한 코드가 존재하는 것은 좋지 않다고 생각했고, 리뷰어께 여쭤봤는데 ExceptionHandler에서 처리하는 방법을 알려주셨다.

 

ExceptionHandler에서 처리하는 방법은 생각해보지 못했기 때문에, 이거다! 라는 생각으로 적용해봤다. 다만 로깅에 필요한 데이터와 이벤트 코드를 저장하는 커스텀 예외가 필요해졌고, 도메인에서 던지는 예외에 로깅을 위한 데이터를 담아주는 과정이 마음에 들지 않았다.

public void validateOrderAndPaymentRequest(final Long amount, final Member member, final Schedule schedule) {
    if (!isAmount(amount)) {
    	// 로깅 예외
        throw new OrderNotMatchException("주문 금액과 결제 금액이 일치하지 않아 결제를 할 수 없습니다.", id);
    }
...

 

✔️ warn, error 레벨

 

처음에는 예측 가능한 오류가 발생한 경우 warn 로그를 남기도록 했다. 그래서 비즈니스 예외 상황, 예측해서 특정 예외로 잡은 상황은 warn으로 처리했다. 그리고 예상하지 못한, Exception으로 잡아준 경우만 error로 남겨줬다. 그런데 예측 가능하더라도 당장 모니터링할 필요성이 있는 경우(예를 들어, 결제 데이터 요청 데이터의 조작 가능성이 있는 경우) error를 찍어주는 것이 좋겠다고 생각을 바꿨다.

 

 

프로젝트 규모가 커질수록 로깅이 복잡하고 관리하기 힘들겠다는 생각이 들었다. 배포한 프로젝트에 문제가 생겨서 로그를 뜯어볼 때, 원인을 빠르게 찾을 수 있게 하도록 하기 위해서는 로그를 어떻게 남기는게 가장 효과적일까?

 

이 외에도 로그 파일을 관리하는 방법, 로그 실제로 분석해보기 등등 로깅에 대해서도 더 공부할 내용이 많다.

 

✏️ 결제 도메인 설계

설계를 여러 번 바꿨는데, 결과적으로 예약, 결제 관련 도메인 설계는 이렇게 됐다. 

Order <- Reservation <- ReservationPayment

 

Order의 존재 이유는 "결제 승인 api 데이터 유효성 검증"이다. 즉, 주문서와 같은 존재다. 클라이언트에서 위젯을 통해 보내는 결제 요청 api의 데이터와, 서버에 전달한 데이터의 일치 여부를 판단하기 위해, 결제 요청 시점의 데이터를 저장해둔다. 그리고 ReservationPayment는 결제 관련 정보를 저장하는, 영수증같은 존재다. 

 

그런데 단지 무결성 검증을 위해 db에 데이터를 저장하는 것은 바람직하지 않기 때문에, 개선이 필요하다고 느낀다.


timeout 테스트, 결제 승인 api 성공 후 로직 실패로 상태 불일치 가능성 등등.. 이번 미션에서 고민한 부분도 많고, 해결되지 않은 부분도 많다. 오히려 이렇게 정리하다 보니 미해결된 고민이 많은 것 같다. 최종 미션, 글쓰기 회고 하고 방학이 코앞이라는 생각에 풀어져서 좀 놀았는데, 내일부터 다시 달려보자!! 🔥